├── COPYRIGHT.md
├── src
├── Exception
│ ├── ExceptionInterface.php
│ ├── RuntimeException.php
│ ├── InvalidArgumentException.php
│ └── NotFoundException.php
├── Password
│ ├── Exception
│ │ ├── ExceptionInterface.php
│ │ ├── RuntimeException.php
│ │ └── InvalidArgumentException.php
│ ├── PasswordInterface.php
│ ├── BcryptSha.php
│ ├── Bcrypt.php
│ └── Apache.php
├── Symmetric
│ ├── Exception
│ │ ├── ExceptionInterface.php
│ │ ├── NotFoundException.php
│ │ ├── RuntimeException.php
│ │ └── InvalidArgumentException.php
│ ├── Padding
│ │ ├── NoPadding.php
│ │ ├── PaddingInterface.php
│ │ └── Pkcs7.php
│ ├── SymmetricInterface.php
│ ├── PaddingPluginManager.php
│ └── Openssl.php
├── PublicKey
│ ├── Rsa
│ │ ├── Exception
│ │ │ ├── ExceptionInterface.php
│ │ │ ├── RuntimeException.php
│ │ │ └── InvalidArgumentException.php
│ │ ├── AbstractKey.php
│ │ ├── PrivateKey.php
│ │ └── PublicKey.php
│ ├── RsaOptions.php
│ ├── Rsa.php
│ └── DiffieHellman.php
├── Key
│ └── Derivation
│ │ ├── Exception
│ │ ├── ExceptionInterface.php
│ │ ├── RuntimeException.php
│ │ └── InvalidArgumentException.php
│ │ ├── Pbkdf2.php
│ │ ├── SaltedS2k.php
│ │ └── Scrypt.php
├── Utils.php
├── SymmetricPluginManager.php
├── Hash.php
├── Hmac.php
├── Hybrid.php
├── FileCipher.php
└── BlockCipher.php
├── phpcs.xml.dist
├── LICENSE.md
├── composer.json
└── README.md
/COPYRIGHT.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC. (https://getlaminas.org/)
2 |
--------------------------------------------------------------------------------
/src/Exception/ExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | src
18 | test
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/src/Password/BcryptSha.php:
--------------------------------------------------------------------------------
1 | 72 characters.
12 | */
13 | class BcryptSha extends Bcrypt
14 | {
15 | /**
16 | * BcryptSha
17 | *
18 | * @throws Exception\RuntimeException
19 | */
20 | public function create(string $password): string
21 | {
22 | return parent::create(Hash::compute('sha256', $password));
23 | }
24 |
25 | /**
26 | * Verify if a password is correct against a hash value
27 | *
28 | * @throws Exception\RuntimeException When the hash is unable to be processed.
29 | */
30 | public function verify(string $password, string $hash): bool
31 | {
32 | return parent::verify(Hash::compute('sha256', $password), $hash);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Symmetric/SymmetricInterface.php:
--------------------------------------------------------------------------------
1 | opensslKeyResource;
38 | }
39 |
40 | /**
41 | * Encrypt using this key
42 | *
43 | * @abstract
44 | */
45 | abstract public function encrypt(string $data): string;
46 |
47 | /**
48 | * Decrypt using this key
49 | *
50 | * @abstract
51 | */
52 | abstract public function decrypt(string $data): string;
53 |
54 | /**
55 | * Get string representation of this key
56 | *
57 | * @abstract
58 | */
59 | abstract public function toString(): string;
60 |
61 | public function __toString(): string
62 | {
63 | return $this->toString();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Symmetric/Padding/Pkcs7.php:
--------------------------------------------------------------------------------
1 | */
22 | private array $paddings = [
23 | 'pkcs7' => Padding\Pkcs7::class,
24 | 'nopadding' => Padding\NoPadding::class,
25 | 'null' => Padding\NoPadding::class,
26 | ];
27 |
28 | /**
29 | * Do we have the padding plugin?
30 | */
31 | public function has(string $id): bool
32 | {
33 | return array_key_exists($id, $this->paddings);
34 | }
35 |
36 | /**
37 | * Retrieve the padding plugin
38 | *
39 | * @return Padding\PaddingInterface
40 | */
41 | public function get(string $id)
42 | {
43 | if (! $this->has($id)) {
44 | throw new Exception\NotFoundException(sprintf(
45 | "The padding adapter %s does not exist",
46 | $id
47 | ));
48 | }
49 | $class = $this->paddings[$id];
50 | return new $class();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/SymmetricPluginManager.php:
--------------------------------------------------------------------------------
1 | Symmetric\Mcrypt::class,
28 | 'openssl' => Symmetric\Openssl::class,
29 | ];
30 |
31 | /**
32 | * Do we have the symmetric plugin?
33 | */
34 | public function has(string $id): bool
35 | {
36 | return array_key_exists($id, $this->symmetric);
37 | }
38 |
39 | /**
40 | * Retrieve the symmetric plugin
41 | *
42 | * @return Symmetric\SymmetricInterface
43 | */
44 | public function get(string $id)
45 | {
46 | if (! $this->has($id)) {
47 | throw new Exception\NotFoundException(sprintf(
48 | 'The symmetric adapter %s does not exist',
49 | $id
50 | ));
51 | }
52 | $class = $this->symmetric[$id];
53 | return new $class();
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Laminas Project a Series of LF Projects, LLC.
2 |
3 | Redistribution and use in source and binary forms, with or without
4 | modification, are permitted provided that the following conditions are met:
5 |
6 | - Redistributions of source code must retain the above copyright notice, this
7 | list of conditions and the following disclaimer.
8 |
9 | - Redistributions in binary form must reproduce the above copyright notice,
10 | this list of conditions and the following disclaimer in the documentation
11 | and/or other materials provided with the distribution.
12 |
13 | - Neither the name of Laminas Foundation nor the names of its contributors may
14 | be used to endorse or promote products derived from this software without
15 | specific prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 |
--------------------------------------------------------------------------------
/src/Key/Derivation/Pbkdf2.php:
--------------------------------------------------------------------------------
1 | ## Abandoned
4 | >
5 | > This package is **abandoned** and will receive no further development!
6 | >
7 | > See the Technical Steering Committee [meeting minutes](https://github.com/laminas/technical-steering-committee/blob/main/meetings/minutes/2023-11-06-TSC-Minutes.md#abandon-laminas-crypt)
8 |
9 | > ## 🇷🇺 Русским гражданам
10 | >
11 | > Мы, участники Laminas, родились и живем в разных странах. У многих из нас есть друзья, родственники и коллеги как в России, так и в Украине. Некоторые из нас родились в России. Некоторые из нас живут в России. У некоторых бабушки и дедушки сражались с фашистами во Второй мировой войне. Здесь никто не поддерживает фашизм.
12 | >
13 | > У одного из нас есть украинская родственница, которая спаслась из дома вместе с сыном. Поезд задержался из-за бомбежки на дороге впереди. У нас есть друзья, которые прячутся в бомбоубежищах. Мы с тревогой ждем весточки от них после воздушных налетов, которые беспорядочно наносят удары и попадают по больницам, школам, детским садам и домам. Мы не берем это из каких-либо СМИ. Мы наблюдаем это напрямую.
14 | >
15 | > Вы доверяете нам достаточно, чтоб использовать наши программы, и мы просим вас довериться нам вновь. Мы нуждаемся в помощи. Выходите и протестуйте против этой бесполезной войны. Остановите кровопролитие. Скажите "Нет войне!"
16 | >
17 | > ## 🇺🇸 To Citizens of Russia
18 | >
19 | > We at Laminas come from all over the world. Many of us have friends, family and colleagues in both Russia and Ukraine. Some of us were born in Russia. Some of us currently live in Russia. Some have grandparents who fought Nazis in World War II. Nobody here supports fascism.
20 | >
21 | > One team member has a Ukrainian relative who fled her home with her son. The train was delayed due to bombing on the road ahead. We have friends who are hiding in bomb shelters. We anxiously follow up on them after the air raids, which indiscriminately fire at hospitals, schools, kindergartens and houses. We're not taking this from any media. These are our actual experiences.
22 | >
23 | > You trust us enough to use our software. We ask that you trust us to say the truth on this. We need your help. Go out and protest this unnecessary war. Stop the bloodshed. Say "stop the war!"
24 |
25 | `Laminas\Crypt` provides support of some cryptographic tools.
26 | Some of the available features are:
27 |
28 | - encrypt-then-authenticate using symmetric ciphers (the authentication step
29 | is provided using HMAC);
30 | - encrypt/decrypt using symmetric and public key algorithm (e.g. RSA algorithm);
31 | - encrypt/decrypt using hybrid mode (OpenPGP like);
32 | - generate digital sign using public key algorithm (e.g. RSA algorithm);
33 | - key exchange using the Diffie-Hellman method;
34 | - key derivation function (e.g. using PBKDF2 algorithm);
35 | - secure password hash (e.g. using Bcrypt algorithm);
36 | - generate Hash values;
37 | - generate HMAC values;
38 |
39 | The main scope of this component is to offer an easy and secure way to protect
40 | and authenticate sensitive data in PHP.
41 |
42 | - File issues at https://github.com/laminas/laminas-crypt/issues
43 | - Documentation is at https://docs.laminas.dev/laminas-crypt
44 |
--------------------------------------------------------------------------------
/src/Password/Bcrypt.php:
--------------------------------------------------------------------------------
1 | $value) {
49 | switch (strtolower($key)) {
50 | case 'salt':
51 | $this->setSalt($value);
52 | break;
53 | case 'cost':
54 | $this->setCost($value);
55 | break;
56 | }
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * Bcrypt
63 | *
64 | * @throws Exception\RuntimeException
65 | */
66 | public function create(string $password): string
67 | {
68 | $options = ['cost' => (int) $this->cost];
69 | return password_hash($password, PASSWORD_BCRYPT, $options);
70 | }
71 |
72 | /**
73 | * Verify if a password is correct against a hash value
74 | */
75 | public function verify(string $password, string $hash): bool
76 | {
77 | return password_verify($password, $hash);
78 | }
79 |
80 | /**
81 | * Set the cost parameter
82 | *
83 | * @throws Exception\InvalidArgumentException
84 | */
85 | public function setCost(int|string $cost): static
86 | {
87 | if ($cost !== 0 && ($cost !== '' && $cost !== '0')) {
88 | $cost = (int) $cost;
89 | if ($cost < 4 || $cost > 31) {
90 | throw new Exception\InvalidArgumentException(
91 | 'The cost parameter of bcrypt must be in range 04-31'
92 | );
93 | }
94 | $this->cost = sprintf('%1$02d', $cost);
95 | }
96 | return $this;
97 | }
98 |
99 | /**
100 | * Get the cost parameter
101 | */
102 | public function getCost(): string
103 | {
104 | return $this->cost;
105 | }
106 |
107 | /**
108 | * Benchmark the bcrypt hash generation to determine the cost parameter based on time to target.
109 | *
110 | * The default time to test is 50 milliseconds which is a good baseline for
111 | * systems handling interactive logins. If you increase the time, you will
112 | * get high cost with better security, but potentially expose your system
113 | * to DoS attacks.
114 | *
115 | * @see php.net/manual/en/function.password-hash.php#refsect1-function.password-hash-examples
116 | *
117 | * @param float $timeTarget Defaults to 50ms (0.05)
118 | * @return int Maximum cost value that falls within the time to target.
119 | */
120 | public function benchmarkCost(float $timeTarget = 0.05): int
121 | {
122 | $cost = 8;
123 |
124 | do {
125 | $cost++;
126 | $start = microtime(true);
127 | password_hash('test', PASSWORD_BCRYPT, ['cost' => $cost]);
128 | $end = microtime(true);
129 | } while (($end - $start) < $timeTarget);
130 |
131 | return $cost;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/Key/Derivation/SaltedS2k.php:
--------------------------------------------------------------------------------
1 | */
52 | protected static $supportedMhashAlgos = [
53 | 'adler32' => MHASH_ADLER32,
54 | 'md2' => MHASH_MD2,
55 | 'md4' => MHASH_MD4,
56 | 'md5' => MHASH_MD5,
57 | 'sha1' => MHASH_SHA1,
58 | 'sha224' => MHASH_SHA224,
59 | 'sha256' => MHASH_SHA256,
60 | 'sha384' => MHASH_SHA384,
61 | 'sha512' => MHASH_SHA512,
62 | 'ripemd128' => MHASH_RIPEMD128,
63 | 'ripemd256' => MHASH_RIPEMD256,
64 | 'ripemd320' => MHASH_RIPEMD320,
65 | 'haval128,3' => MHASH_HAVAL128, // @deprecated use haval128 instead
66 | 'haval128' => MHASH_HAVAL128,
67 | 'haval160,3' => MHASH_HAVAL160, // @deprecated use haval160 instead
68 | 'haval160' => MHASH_HAVAL160,
69 | 'haval192,3' => MHASH_HAVAL192, // @deprecated use haval192 instead
70 | 'haval192' => MHASH_HAVAL192,
71 | 'haval224,3' => MHASH_HAVAL224, // @deprecated use haval224 instead
72 | 'haval224' => MHASH_HAVAL224,
73 | 'haval256,3' => MHASH_HAVAL256, // @deprecated use haval256 instead
74 | 'haval256' => MHASH_HAVAL256,
75 | 'tiger' => MHASH_TIGER,
76 | 'tiger128,3' => MHASH_TIGER128, // @deprecated use tiger128 instead
77 | 'tiger128' => MHASH_TIGER128,
78 | 'tiger160,3' => MHASH_TIGER160, // @deprecated use tiger160 instead
79 | 'tiger160' => MHASH_TIGER160,
80 | 'whirpool' => MHASH_WHIRLPOOL,
81 | 'snefru256' => MHASH_SNEFRU256,
82 | 'gost' => MHASH_GOST,
83 | 'crc32' => MHASH_CRC32,
84 | 'crc32b' => MHASH_CRC32B,
85 | ];
86 |
87 | /**
88 | * Generate the new key
89 | *
90 | * @param string $hash The hash algorithm to be used by HMAC
91 | * @param string $password The source password/key
92 | * @param string $salt The salt of the algorithm
93 | * @param int $bytes The output size in bytes
94 | * @throws Exception\InvalidArgumentException
95 | */
96 | public static function calc(string $hash, string $password, string $salt, int $bytes): string
97 | {
98 | if (! in_array($hash, array_keys(static::$supportedMhashAlgos))) {
99 | throw new Exception\InvalidArgumentException("The hash algorithm $hash is not supported by " . self::class);
100 | }
101 | if (mb_strlen($salt, '8bit') < 8) {
102 | throw new Exception\InvalidArgumentException('The salt size must be at least of 8 bytes');
103 | }
104 |
105 | $result = '';
106 |
107 | foreach (range(0, ceil($bytes / strlen(hash($hash, '', true))) - 1) as $i) {
108 | $result .= hash(
109 | $hash,
110 | str_repeat("\0", (int) $i) . str_pad(
111 | substr($salt, 0, 8),
112 | 8,
113 | "\0",
114 | STR_PAD_RIGHT
115 | ) . $password,
116 | true
117 | );
118 | }
119 |
120 | return substr($result, 0, intval($bytes));
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/PublicKey/Rsa/PrivateKey.php:
--------------------------------------------------------------------------------
1 | pemString = $pemString;
60 | $this->opensslKeyResource = $result;
61 | $this->details = openssl_pkey_get_details($this->opensslKeyResource);
62 | }
63 |
64 | /**
65 | * Get the public key
66 | */
67 | public function getPublicKey(): PublicKey|null
68 | {
69 | if ($this->publicKey === null) {
70 | $this->publicKey = new PublicKey($this->details['key']);
71 | }
72 |
73 | return $this->publicKey;
74 | }
75 |
76 | /**
77 | * Encrypt using this key
78 | *
79 | * @throws Exception\RuntimeException
80 | * @throws Exception\InvalidArgumentException
81 | */
82 | public function encrypt(string $data, int $padding = OPENSSL_PKCS1_PADDING): string
83 | {
84 | if ($data === '' || $data === '0') {
85 | throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty');
86 | }
87 |
88 | $encrypted = '';
89 | $result = openssl_private_encrypt($data, $encrypted, $this->getOpensslKeyResource(), $padding);
90 | if (false === $result) {
91 | throw new Exception\RuntimeException(
92 | 'Can not encrypt; openssl ' . openssl_error_string()
93 | );
94 | }
95 |
96 | return $encrypted;
97 | }
98 |
99 | /**
100 | * Decrypt using this key
101 | *
102 | * Starting in 2.4.9/2.5.2, we changed the default padding to
103 | * OPENSSL_PKCS1_OAEP_PADDING to prevent Bleichenbacher's chosen-ciphertext
104 | * attack.
105 | *
106 | * @see http://archiv.infsec.ethz.ch/education/fs08/secsem/bleichenbacher98.pdf
107 | *
108 | * @throws Exception\RuntimeException
109 | * @throws Exception\InvalidArgumentException
110 | */
111 | public function decrypt(string $data, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string
112 | {
113 | if (! is_string($data)) {
114 | throw new Exception\InvalidArgumentException('The data to decrypt must be a string');
115 | }
116 | if ('' === $data) {
117 | throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty');
118 | }
119 |
120 | $decrypted = '';
121 | $result = openssl_private_decrypt($data, $decrypted, $this->getOpensslKeyResource(), $padding);
122 | if (false === $result) {
123 | throw new Exception\RuntimeException(
124 | 'Can not decrypt; openssl ' . openssl_error_string()
125 | );
126 | }
127 |
128 | return $decrypted;
129 | }
130 |
131 | public function toString(): string
132 | {
133 | return $this->pemString;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/Hybrid.php:
--------------------------------------------------------------------------------
1 | bCipher = $bCipher ?? BlockCipher::factory('openssl');
40 | }
41 |
42 | /**
43 | * Encrypt using a keyrings
44 | *
45 | * @throws RuntimeException
46 | */
47 | public function encrypt(string $plaintext, array|string|Stringable|null $keys = null): string
48 | {
49 | // generate a random session key
50 | $sessionKey = Rand::getBytes($this->bCipher->getCipher()->getKeySize());
51 |
52 | // encrypt the plaintext with blockcipher algorithm
53 | $this->bCipher->setKey($sessionKey);
54 | $ciphertext = $this->bCipher->encrypt($plaintext);
55 |
56 | if (! is_array($keys)) {
57 | $keys = ['' => $keys];
58 | }
59 |
60 | $encKeys = '';
61 | // encrypt the session key with public keys
62 | foreach ($keys as $id => $pubkey) {
63 | if (! $pubkey instanceof PubKey && ! is_string($pubkey)) {
64 | throw new Exception\RuntimeException(sprintf(
65 | "The public key must be a string in PEM format or an instance of %s",
66 | PubKey::class
67 | ));
68 | }
69 | $pubkey = is_string($pubkey) ? new PubKey($pubkey) : $pubkey;
70 | $encKeys .= sprintf(
71 | "%s:%s:",
72 | base64_encode((string) $id),
73 | base64_encode($this->rsa->encrypt($sessionKey, $pubkey))
74 | );
75 | }
76 | return $encKeys . ';' . $ciphertext;
77 | }
78 |
79 | /**
80 | * Decrypt using a private key
81 | *
82 | * @throws RuntimeException
83 | */
84 | public function decrypt(
85 | string $msg,
86 | string|PrivateKey|null $privateKey = null,
87 | ?string $passPhrase = null,
88 | string $id = ""
89 | ): string|false {
90 | // get the session key
91 | [$encKeys, $ciphertext] = explode(';', $msg, 2);
92 |
93 | $keys = explode(':', $encKeys);
94 | $pos = array_search(base64_encode($id), $keys);
95 | if (false === $pos) {
96 | throw new Exception\RuntimeException(
97 | "This private key cannot be used for decryption"
98 | );
99 | }
100 |
101 | if (! $privateKey instanceof PrivateKey && ! is_string($privateKey)) {
102 | throw new Exception\RuntimeException(sprintf(
103 | "The private key must be a string in PEM format or an instance of %s",
104 | PrivateKey::class
105 | ));
106 | }
107 | $privateKey = is_string($privateKey) ? new PrivateKey($privateKey, $passPhrase) : $privateKey;
108 |
109 | // decrypt the session key with privateKey
110 | $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privateKey);
111 |
112 | // decrypt the plaintext with the blockcipher algorithm
113 | $this->bCipher->setKey($sessionKey);
114 | return $this->bCipher->decrypt($ciphertext);
115 | }
116 |
117 | /**
118 | * Get the BlockCipher adapter
119 | */
120 | public function getBlockCipherInstance(): BlockCipher
121 | {
122 | return $this->bCipher;
123 | }
124 |
125 | /**
126 | * Get the Rsa instance
127 | */
128 | public function getRsaInstance(): Rsa
129 | {
130 | return $this->rsa;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/PublicKey/Rsa/PublicKey.php:
--------------------------------------------------------------------------------
1 | certificateString = $pemStringOrCertificate;
62 | } else {
63 | $this->pemString = $pemStringOrCertificate;
64 | }
65 |
66 | $this->opensslKeyResource = $result;
67 | $this->details = openssl_pkey_get_details($this->opensslKeyResource);
68 | }
69 |
70 | /**
71 | * Encrypt using this key
72 | *
73 | * Starting in 2.4.9/2.5.2, we changed the default padding to
74 | * OPENSSL_PKCS1_OAEP_PADDING to prevent Bleichenbacher's chosen-ciphertext
75 | * attack.
76 | *
77 | * @see http://archiv.infsec.ethz.ch/education/fs08/secsem/bleichenbacher98.pdf
78 | *
79 | * @throws Exception\InvalidArgumentException
80 | * @throws Exception\RuntimeException
81 | */
82 | public function encrypt(string $data, int $padding = OPENSSL_PKCS1_OAEP_PADDING): string
83 | {
84 | if ($data === '' || $data === '0') {
85 | throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty');
86 | }
87 |
88 | $encrypted = '';
89 | $result = openssl_public_encrypt($data, $encrypted, $this->getOpensslKeyResource(), $padding);
90 | if (false === $result) {
91 | throw new Exception\RuntimeException(
92 | 'Can not encrypt; openssl ' . openssl_error_string()
93 | );
94 | }
95 |
96 | return $encrypted;
97 | }
98 |
99 | /**
100 | * Decrypt using this key
101 | *
102 | * @throws Exception\RuntimeException
103 | * @throws Exception\InvalidArgumentException
104 | */
105 | public function decrypt(string $data, int $padding = OPENSSL_PKCS1_PADDING): string
106 | {
107 | if (! is_string($data)) {
108 | throw new Exception\InvalidArgumentException('The data to decrypt must be a string');
109 | }
110 | if ('' === $data) {
111 | throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty');
112 | }
113 |
114 | $decrypted = '';
115 | $result = openssl_public_decrypt($data, $decrypted, $this->getOpensslKeyResource(), $padding);
116 | if (false === $result) {
117 | throw new Exception\RuntimeException(
118 | 'Can not decrypt; openssl ' . openssl_error_string()
119 | );
120 | }
121 |
122 | return $decrypted;
123 | }
124 |
125 | /**
126 | * To string
127 | *
128 | * @throws Exception\RuntimeException
129 | */
130 | public function toString(): string
131 | {
132 | if (isset($this->certificateString) && ($this->certificateString !== '' && $this->certificateString !== '0')) {
133 | return $this->certificateString;
134 | } elseif ($this->pemString !== '' && $this->pemString !== '0') {
135 | return $this->pemString;
136 | }
137 | throw new Exception\RuntimeException('No public key string representation is available');
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/PublicKey/RsaOptions.php:
--------------------------------------------------------------------------------
1 | privateKey = $key;
56 | $this->publicKey = $this->privateKey->getPublicKey();
57 | return $this;
58 | }
59 |
60 | /**
61 | * Get private key
62 | */
63 | public function getPrivateKey(): Rsa\PrivateKey|null
64 | {
65 | return $this->privateKey;
66 | }
67 |
68 | /**
69 | * Set public key
70 | */
71 | public function setPublicKey(Rsa\PublicKey $key): static
72 | {
73 | $this->publicKey = $key;
74 | return $this;
75 | }
76 |
77 | /**
78 | * Get public key
79 | */
80 | public function getPublicKey(): Rsa\PublicKey|null
81 | {
82 | return $this->publicKey;
83 | }
84 |
85 | /**
86 | * Set pass phrase
87 | */
88 | public function setPassPhrase(string $phrase): static
89 | {
90 | $this->passPhrase = $phrase;
91 | return $this;
92 | }
93 |
94 | /**
95 | * Get pass phrase
96 | *
97 | * @return string
98 | */
99 | public function getPassPhrase(): string|null
100 | {
101 | return $this->passPhrase;
102 | }
103 |
104 | /**
105 | * Set hash algorithm
106 | *
107 | * @throws Rsa\Exception\RuntimeException
108 | * @throws Rsa\Exception\InvalidArgumentException
109 | */
110 | public function setHashAlgorithm(string $hash): static
111 | {
112 | $hashUpper = strtoupper($hash);
113 | if (! defined('OPENSSL_ALGO_' . $hashUpper)) {
114 | throw new Exception\InvalidArgumentException(
115 | "Hash algorithm '{$hash}' is not supported"
116 | );
117 | }
118 |
119 | $this->hashAlgorithm = strtolower($hash);
120 | $this->opensslSignatureAlgorithm = constant('OPENSSL_ALGO_' . $hashUpper);
121 | return $this;
122 | }
123 |
124 | /**
125 | * Get hash algorithm
126 | */
127 | public function getHashAlgorithm(): string
128 | {
129 | return $this->hashAlgorithm;
130 | }
131 |
132 | public function getOpensslSignatureAlgorithm(): int
133 | {
134 | if ($this->opensslSignatureAlgorithm === null) {
135 | $this->opensslSignatureAlgorithm = constant('OPENSSL_ALGO_' . strtoupper($this->hashAlgorithm));
136 | }
137 | return $this->opensslSignatureAlgorithm;
138 | }
139 |
140 | /**
141 | * Enable/disable the binary output
142 | */
143 | public function setBinaryOutput(bool $value): static
144 | {
145 | $this->binaryOutput = $value;
146 | return $this;
147 | }
148 |
149 | /**
150 | * Get the value of binary output
151 | */
152 | public function getBinaryOutput(): bool
153 | {
154 | return $this->binaryOutput;
155 | }
156 |
157 | /**
158 | * Get the OPENSSL padding
159 | */
160 | public function getOpensslPadding(): int|null
161 | {
162 | return $this->opensslPadding;
163 | }
164 |
165 | /**
166 | * Set the OPENSSL padding
167 | */
168 | public function setOpensslPadding(int $opensslPadding): static
169 | {
170 | $this->opensslPadding = $opensslPadding;
171 | return $this;
172 | }
173 |
174 | /**
175 | * Generate new private/public key pair
176 | *
177 | * @throws Rsa\Exception\RuntimeException
178 | */
179 | public function generateKeys(array $opensslConfig = []): static
180 | {
181 | $opensslConfig = array_replace(
182 | [
183 | 'private_key_type' => OPENSSL_KEYTYPE_RSA,
184 | 'private_key_bits' => Rsa\PrivateKey::DEFAULT_KEY_SIZE,
185 | 'digest_alg' => $this->getHashAlgorithm(),
186 | ],
187 | $opensslConfig
188 | );
189 |
190 | // generate
191 | $resource = openssl_pkey_new($opensslConfig);
192 | if (false === $resource) {
193 | throw new Exception\RuntimeException(
194 | 'Can not generate keys; openssl ' . openssl_error_string()
195 | );
196 | }
197 |
198 | // export key
199 | $passPhrase = $this->getPassPhrase();
200 | $result = openssl_pkey_export($resource, $private, $passPhrase, $opensslConfig);
201 | if (false === $result) {
202 | throw new Exception\RuntimeException(
203 | 'Can not export key; openssl ' . openssl_error_string()
204 | );
205 | }
206 |
207 | $details = openssl_pkey_get_details($resource);
208 | $this->privateKey = new Rsa\PrivateKey($private, $passPhrase);
209 | $this->publicKey = new Rsa\PublicKey($details['key']);
210 |
211 | return $this;
212 | }
213 | }
214 |
--------------------------------------------------------------------------------
/src/Password/Apache.php:
--------------------------------------------------------------------------------
1 | $value) {
73 | switch (strtolower((string) $key)) {
74 | case 'format':
75 | $this->setFormat($value);
76 | break;
77 | case 'authname':
78 | $this->setAuthName($value);
79 | break;
80 | case 'username':
81 | $this->setUserName($value);
82 | break;
83 | }
84 | }
85 | }
86 |
87 | /**
88 | * Generate the hash of a password
89 | *
90 | * @throws Exception\RuntimeException
91 | */
92 | public function create(string $password): string
93 | {
94 | if (! isset($this->format) || ($this->format === '' || $this->format === '0')) {
95 | throw new Exception\RuntimeException(
96 | 'You must specify a password format'
97 | );
98 | }
99 | switch ($this->format) {
100 | case 'crypt':
101 | $hash = crypt($password, Rand::getString(2, self::ALPHA64));
102 | break;
103 | case 'sha1':
104 | $hash = '{SHA}' . base64_encode(sha1($password, true));
105 | break;
106 | case 'md5':
107 | $hash = $this->apr1Md5($password);
108 | break;
109 | case 'digest':
110 | if (
111 | ! isset($this->userName)
112 | || ($this->userName === '' || $this->userName === '0')
113 | || (! isset($this->authName) || ($this->authName === '' || $this->authName === '0'))
114 | ) {
115 | throw new Exception\RuntimeException(
116 | 'You must specify UserName and AuthName (realm) to generate the digest'
117 | );
118 | }
119 | $hash = md5($this->userName . ':' . $this->authName . ':' . $password);
120 | break;
121 | }
122 |
123 | return $hash;
124 | }
125 |
126 | /**
127 | * Verify if a password is correct against a hash value
128 | */
129 | public function verify(string $password, string $hash): bool
130 | {
131 | if (mb_substr($hash, 0, 5, '8bit') === '{SHA}') {
132 | $hash2 = '{SHA}' . base64_encode(sha1($password, true));
133 | return Utils::compareStrings($hash, $hash2);
134 | }
135 |
136 | if (mb_substr($hash, 0, 6, '8bit') === '$apr1$') {
137 | $token = explode('$', $hash);
138 | if (empty($token[2])) {
139 | throw new Exception\InvalidArgumentException(
140 | 'The APR1 password format is not valid'
141 | );
142 | }
143 | $hash2 = $this->apr1Md5($password, $token[2]);
144 | return Utils::compareStrings($hash, $hash2);
145 | }
146 |
147 | $bcryptPattern = '/\$2[ay]?\$[0-9]{2}\$[' . addcslashes((string) static::BASE64, '+/') . '\.]{53}/';
148 |
149 | if (mb_strlen($hash, '8bit') > 13 && ! preg_match($bcryptPattern, $hash)) { // digest
150 | if (
151 | ! isset($this->userName)
152 | || ($this->userName === '' || $this->userName === '0')
153 | || (! isset($this->authName) || ($this->authName === '' || $this->authName === '0'))
154 | ) {
155 | throw new Exception\RuntimeException(
156 | 'You must specify UserName and AuthName (realm) to verify the digest'
157 | );
158 | }
159 | $hash2 = md5($this->userName . ':' . $this->authName . ':' . $password);
160 | return Utils::compareStrings($hash, $hash2);
161 | }
162 |
163 | return Utils::compareStrings($hash, crypt($password, $hash));
164 | }
165 |
166 | /**
167 | * Set the format of the password
168 | *
169 | * @throws Exception\InvalidArgumentException
170 | * @return Apache Provides a fluent interface
171 | */
172 | public function setFormat(string $format): Apache
173 | {
174 | $format = strtolower($format);
175 | if (! in_array($format, $this->supportedFormat)) {
176 | throw new Exception\InvalidArgumentException(sprintf(
177 | 'The format %s specified is not valid. The supported formats are: %s',
178 | $format,
179 | implode(',', $this->supportedFormat)
180 | ));
181 | }
182 | $this->format = $format;
183 |
184 | return $this;
185 | }
186 |
187 | /**
188 | * Get the format of the password
189 | */
190 | public function getFormat(): string
191 | {
192 | return $this->format;
193 | }
194 |
195 | /**
196 | * Set the AuthName (for digest authentication)
197 | */
198 | public function setAuthName(string $name): static
199 | {
200 | $this->authName = $name;
201 |
202 | return $this;
203 | }
204 |
205 | /**
206 | * Get the AuthName (for digest authentication)
207 | */
208 | public function getAuthName(): string
209 | {
210 | return $this->authName;
211 | }
212 |
213 | /**
214 | * Set the username
215 | */
216 | public function setUserName(string $name): static
217 | {
218 | $this->userName = $name;
219 |
220 | return $this;
221 | }
222 |
223 | /**
224 | * Get the username
225 | */
226 | public function getUserName(): string
227 | {
228 | return $this->userName;
229 | }
230 |
231 | /**
232 | * Convert a binary string using the alphabet "./0-9A-Za-z"
233 | */
234 | protected function toAlphabet64(string $value): string
235 | {
236 | return strtr(strrev(mb_substr(base64_encode($value), 2, null, '8bit')), self::BASE64, self::ALPHA64);
237 | }
238 |
239 | /**
240 | * APR1 MD5 algorithm
241 | */
242 | protected function apr1Md5(string $password, string|null $salt = null): string
243 | {
244 | if (null === $salt) {
245 | $salt = Rand::getString(8, self::ALPHA64);
246 | } else {
247 | if (mb_strlen($salt, '8bit') !== 8) {
248 | throw new Exception\InvalidArgumentException(
249 | 'The salt value for APR1 algorithm must be 8 characters long'
250 | );
251 | }
252 | for ($i = 0; $i < 8; $i++) {
253 | if (! str_contains(self::ALPHA64, $salt[$i])) {
254 | throw new Exception\InvalidArgumentException(
255 | 'The salt value must be a string in the alphabet "./0-9A-Za-z"'
256 | );
257 | }
258 | }
259 | }
260 | $len = mb_strlen($password, '8bit');
261 | $text = $password . '$apr1$' . $salt;
262 | $bin = pack("H32", md5($password . $salt . $password));
263 | for ($i = $len; $i > 0; $i -= 16) {
264 | $text .= mb_substr($bin, 0, min(16, $i), '8bit');
265 | }
266 | for ($i = $len; $i > 0; $i >>= 1) {
267 | $text .= ($i & 1) !== 0 ? chr(0) : $password[0];
268 | }
269 | $bin = pack("H32", md5($text));
270 | for ($i = 0; $i < 1000; $i++) {
271 | $new = ($i & 1) !== 0 ? $password : $bin;
272 | if ($i % 3 !== 0) {
273 | $new .= $salt;
274 | }
275 | if ($i % 7 !== 0) {
276 | $new .= $password;
277 | }
278 | $new .= ($i & 1) !== 0 ? $bin : $password;
279 | $bin = pack("H32", md5($new));
280 | }
281 | $tmp = '';
282 | for ($i = 0; $i < 5; $i++) {
283 | $k = $i + 6;
284 | $j = $i + 12;
285 | if ($j === 16) {
286 | $j = 5;
287 | }
288 | $tmp = $bin[$i] . $bin[$k] . $bin[$j] . $tmp;
289 | }
290 | $tmp = chr(0) . chr(0) . $bin[11] . $tmp;
291 |
292 | return '$apr1$' . $salt . '$' . $this->toAlphabet64($tmp);
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/src/PublicKey/Rsa.php:
--------------------------------------------------------------------------------
1 | setPrivateKey($privateKey);
89 | }
90 | if ($publicKey instanceof Rsa\PublicKey) {
91 | $options->setPublicKey($publicKey);
92 | }
93 |
94 | return new Rsa($options);
95 | }
96 |
97 | /**
98 | * @throws Rsa\Exception\RuntimeException
99 | */
100 | public function __construct(protected RsaOptions $options = new RsaOptions())
101 | {
102 | if (! extension_loaded('openssl')) {
103 | throw new Exception\RuntimeException(
104 | 'Laminas\Crypt\PublicKey\Rsa requires openssl extension to be loaded'
105 | );
106 | }
107 | }
108 |
109 | /**
110 | * Set options
111 | */
112 | public function setOptions(RsaOptions $options): static
113 | {
114 | $this->options = $options;
115 | return $this;
116 | }
117 |
118 | /**
119 | * Get options
120 | */
121 | public function getOptions(): RsaOptions
122 | {
123 | return $this->options;
124 | }
125 |
126 | /**
127 | * Return last openssl error(s)
128 | */
129 | public function getOpensslErrorString(): string
130 | {
131 | $message = '';
132 | while (false !== ($error = openssl_error_string())) {
133 | $message .= $error . "\n";
134 | }
135 | return trim($message);
136 | }
137 |
138 | /**
139 | * Sign with private key
140 | *
141 | * @throws Rsa\Exception\RuntimeException
142 | */
143 | public function sign(string $data, ?Rsa\PrivateKey $privateKey = null): string
144 | {
145 | $signature = '';
146 | if (! $privateKey instanceof PrivateKey) {
147 | $privateKey = $this->options->getPrivateKey();
148 | }
149 |
150 | $result = openssl_sign(
151 | $data,
152 | $signature,
153 | $privateKey->getOpensslKeyResource(),
154 | $this->options->getOpensslSignatureAlgorithm()
155 | );
156 | if (false === $result) {
157 | throw new Exception\RuntimeException(
158 | 'Can not generate signature; openssl ' . $this->getOpensslErrorString()
159 | );
160 | }
161 |
162 | if ($this->options->getBinaryOutput()) {
163 | return $signature;
164 | }
165 |
166 | return base64_encode((string) $signature);
167 | }
168 |
169 | /**
170 | * Verify signature with public key
171 | *
172 | * $signature can be encoded in base64 or not. $mode sets how the input must be processed:
173 | * - MODE_AUTO: Check if the $signature is encoded in base64. Not recommended for performance.
174 | * - MODE_BASE64: Decode $signature using base64 algorithm.
175 | * - MODE_RAW: $signature is not encoded.
176 | *
177 | * @see Rsa::MODE_AUTO
178 | * @see Rsa::MODE_BASE64
179 | * @see Rsa::MODE_RAW
180 | *
181 | * @param int $mode Input encoding
182 | * @throws Rsa\Exception\RuntimeException
183 | */
184 | public function verify(
185 | string $data,
186 | string $signature,
187 | ?Rsa\PublicKey $publicKey = null,
188 | int $mode = self::MODE_AUTO
189 | ): bool {
190 | if (! $publicKey instanceof PublicKey) {
191 | $publicKey = $this->options->getPublicKey();
192 | }
193 |
194 | switch ($mode) {
195 | case self::MODE_AUTO:
196 | // check if data is encoded in Base64
197 | $output = base64_decode($signature, true);
198 | if ((false !== $output) && ($signature === base64_encode($output))) {
199 | $signature = $output;
200 | }
201 | break;
202 | case self::MODE_BASE64:
203 | $signature = base64_decode($signature);
204 | break;
205 | case self::MODE_RAW:
206 | default:
207 | break;
208 | }
209 |
210 | $result = openssl_verify(
211 | $data,
212 | $signature,
213 | $publicKey->getOpensslKeyResource(),
214 | $this->options->getOpensslSignatureAlgorithm()
215 | );
216 | if (-1 === $result) {
217 | throw new Exception\RuntimeException(
218 | 'Can not verify signature; openssl ' . $this->getOpensslErrorString()
219 | );
220 | }
221 |
222 | return $result === 1;
223 | }
224 |
225 | /**
226 | * Encrypt with private/public key
227 | *
228 | * @throws Rsa\Exception\InvalidArgumentException
229 | */
230 | public function encrypt(string $data, ?Rsa\AbstractKey $key = null): string
231 | {
232 | if (! $key instanceof AbstractKey) {
233 | $key = $this->options->getPublicKey();
234 | }
235 |
236 | if (! $key instanceof AbstractKey) {
237 | throw new Exception\InvalidArgumentException('No key specified for the decryption');
238 | }
239 |
240 | $padding = $this->getOptions()->getOpensslPadding();
241 | $encrypted = null === $padding ? $key->encrypt($data) : $key->encrypt($data, $padding);
242 |
243 | if ($this->options->getBinaryOutput()) {
244 | return $encrypted;
245 | }
246 |
247 | return base64_encode($encrypted);
248 | }
249 |
250 | /**
251 | * Decrypt with private/public key
252 | *
253 | * $data can be encoded in base64 or not. $mode sets how the input must be processed:
254 | * - MODE_AUTO: Check if the $signature is encoded in base64. Not recommended for performance.
255 | * - MODE_BASE64: Decode $data using base64 algorithm.
256 | * - MODE_RAW: $data is not encoded.
257 | *
258 | * @see Rsa::MODE_AUTO
259 | * @see Rsa::MODE_BASE64
260 | * @see Rsa::MODE_RAW
261 | *
262 | * @param int $mode Input encoding
263 | * @throws Rsa\Exception\InvalidArgumentException
264 | */
265 | public function decrypt(
266 | string $data,
267 | ?Rsa\AbstractKey $key = null,
268 | int $mode = self::MODE_AUTO
269 | ): string {
270 | if (! $key instanceof AbstractKey) {
271 | $key = $this->options->getPrivateKey();
272 | }
273 |
274 | if (! $key instanceof AbstractKey) {
275 | throw new Exception\InvalidArgumentException('No key specified for the decryption');
276 | }
277 |
278 | switch ($mode) {
279 | case self::MODE_AUTO:
280 | // check if data is encoded in Base64
281 | $output = base64_decode($data, true);
282 | if ((false !== $output) && ($data === base64_encode($output))) {
283 | $data = $output;
284 | }
285 | break;
286 | case self::MODE_BASE64:
287 | $data = base64_decode($data);
288 | break;
289 | case self::MODE_RAW:
290 | default:
291 | break;
292 | }
293 |
294 | $padding = $this->getOptions()->getOpensslPadding();
295 | if (null === $padding) {
296 | return $key->decrypt($data);
297 | } else {
298 | return $key->decrypt($data, $padding);
299 | }
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/src/FileCipher.php:
--------------------------------------------------------------------------------
1 | cipher = $cipher;
65 | }
66 |
67 | /**
68 | * Set the cipher object
69 | */
70 | public function setCipher(SymmetricInterface $cipher): void
71 | {
72 | $this->cipher = $cipher;
73 | }
74 |
75 | /**
76 | * Get the cipher object
77 | */
78 | public function getCipher(): ?SymmetricInterface
79 | {
80 | return $this->cipher;
81 | }
82 |
83 | /**
84 | * Set the number of iterations for Pbkdf2
85 | */
86 | public function setKeyIteration(int $num): void
87 | {
88 | $this->keyIteration = $num;
89 | }
90 |
91 | /**
92 | * Get the number of iterations for Pbkdf2
93 | */
94 | public function getKeyIteration(): int
95 | {
96 | return $this->keyIteration;
97 | }
98 |
99 | /**
100 | * Set the encryption/decryption key
101 | *
102 | * @throws Exception\InvalidArgumentException
103 | */
104 | public function setKey(string $key): void
105 | {
106 | if ($key === '' || $key === '0') {
107 | throw new Exception\InvalidArgumentException('The key cannot be empty');
108 | }
109 | $this->key = $key;
110 | }
111 |
112 | /**
113 | * Get the key
114 | */
115 | public function getKey(): string|null
116 | {
117 | return $this->key;
118 | }
119 |
120 | /**
121 | * Set algorithm of the symmetric cipher
122 | */
123 | public function setCipherAlgorithm(string $algo): void
124 | {
125 | $this->cipher->setAlgorithm($algo);
126 | }
127 |
128 | /**
129 | * Get the cipher algorithm
130 | */
131 | public function getCipherAlgorithm(): string
132 | {
133 | return $this->cipher->getAlgorithm();
134 | }
135 |
136 | /**
137 | * Get the supported algorithms of the symmetric cipher
138 | */
139 | public function getCipherSupportedAlgorithms(): array
140 | {
141 | return $this->cipher->getSupportedAlgorithms();
142 | }
143 |
144 | /**
145 | * Set the hash algorithm for HMAC authentication
146 | *
147 | * @throws Exception\InvalidArgumentException
148 | */
149 | public function setHashAlgorithm(string $hash): void
150 | {
151 | if (! Hash::isSupported($hash)) {
152 | throw new Exception\InvalidArgumentException(
153 | "The specified hash algorithm '{$hash}' is not supported by Laminas\Crypt\Hash"
154 | );
155 | }
156 | $this->hash = $hash;
157 | }
158 |
159 | /**
160 | * Get the hash algorithm for HMAC authentication
161 | */
162 | public function getHashAlgorithm(): string
163 | {
164 | return $this->hash;
165 | }
166 |
167 | /**
168 | * Set the hash algorithm for the Pbkdf2
169 | *
170 | * @throws Exception\InvalidArgumentException
171 | */
172 | public function setPbkdf2HashAlgorithm(string $hash): void
173 | {
174 | if (! Hash::isSupported($hash)) {
175 | throw new Exception\InvalidArgumentException(
176 | "The specified hash algorithm '{$hash}' is not supported by Laminas\Crypt\Hash"
177 | );
178 | }
179 | $this->pbkdf2Hash = $hash;
180 | }
181 |
182 | /**
183 | * Get the Pbkdf2 hash algorithm
184 | */
185 | public function getPbkdf2HashAlgorithm(): string
186 | {
187 | return $this->pbkdf2Hash;
188 | }
189 |
190 | /**
191 | * Encrypt then authenticate a file using HMAC
192 | *
193 | * @throws Exception\InvalidArgumentException
194 | */
195 | public function encrypt(string $fileIn, string $fileOut): bool
196 | {
197 | $this->checkFileInOut($fileIn, $fileOut);
198 | if (! isset($this->key) || ($this->key === '' || $this->key === '0')) {
199 | throw new Exception\InvalidArgumentException('No key specified for encryption');
200 | }
201 |
202 | $read = fopen($fileIn, "r");
203 | $write = fopen($fileOut, "w");
204 | $iv = Rand::getBytes($this->cipher->getSaltSize());
205 | $keys = Pbkdf2::calc(
206 | $this->getPbkdf2HashAlgorithm(),
207 | $this->getKey(),
208 | $iv,
209 | $this->getKeyIteration(),
210 | $this->cipher->getKeySize() * 2
211 | );
212 | $hmac = '';
213 | $size = 0;
214 | $tot = filesize($fileIn);
215 | $padding = $this->cipher->getPadding();
216 |
217 | $this->cipher->setKey(mb_substr($keys, 0, $this->cipher->getKeySize(), '8bit'));
218 | $this->cipher->setPadding(new Symmetric\Padding\NoPadding());
219 | $this->cipher->setSalt($iv);
220 | $this->cipher->setMode('cbc');
221 |
222 | $hashAlgo = $this->getHashAlgorithm();
223 | $saltSize = $this->cipher->getSaltSize();
224 | $algorithm = $this->cipher->getAlgorithm();
225 | $keyHmac = mb_substr($keys, $this->cipher->getKeySize(), null, '8bit');
226 |
227 | while ($data = fread($read, self::BUFFER_SIZE)) {
228 | $size += mb_strlen($data, '8bit');
229 | // Padding if last block
230 | if ($size === $tot) {
231 | $this->cipher->setPadding($padding);
232 | }
233 | $result = $this->cipher->encrypt($data);
234 | if ($size <= self::BUFFER_SIZE) {
235 | // Write a placeholder for the HMAC and write the IV
236 | fwrite($write, str_repeat('0', Hmac::getOutputSize($hashAlgo)));
237 | } else {
238 | $result = mb_substr($result, $saltSize, null, '8bit');
239 | }
240 | $hmac = Hmac::compute(
241 | $keyHmac,
242 | $hashAlgo,
243 | $algorithm . $hmac . $result
244 | );
245 | $this->cipher->setSalt(mb_substr($result, -1 * $saltSize, null, '8bit'));
246 | if (fwrite($write, $result) !== mb_strlen($result, '8bit')) {
247 | return false;
248 | }
249 | }
250 | $result = true;
251 | // write the HMAC at the beginning of the file
252 | fseek($write, 0);
253 | if (fwrite($write, $hmac) !== mb_strlen($hmac, '8bit')) {
254 | $result = false;
255 | }
256 | fclose($write);
257 | fclose($read);
258 |
259 | return $result;
260 | }
261 |
262 | /**
263 | * Decrypt a file
264 | *
265 | * @throws Exception\InvalidArgumentException
266 | */
267 | public function decrypt(string $fileIn, string $fileOut): bool
268 | {
269 | $this->checkFileInOut($fileIn, $fileOut);
270 | if (! isset($this->key) || ($this->key === '' || $this->key === '0')) {
271 | throw new Exception\InvalidArgumentException('No key specified for decryption');
272 | }
273 |
274 | $read = fopen($fileIn, "r");
275 | $write = fopen($fileOut, "w");
276 | $hmacRead = fread($read, Hmac::getOutputSize($this->getHashAlgorithm()));
277 | $iv = fread($read, $this->cipher->getSaltSize());
278 | $tot = filesize($fileIn);
279 | $hmac = $iv;
280 | $size = mb_strlen($iv, '8bit') + mb_strlen($hmacRead, '8bit');
281 | $keys = Pbkdf2::calc(
282 | $this->getPbkdf2HashAlgorithm(),
283 | $this->getKey(),
284 | $iv,
285 | $this->getKeyIteration(),
286 | $this->cipher->getKeySize() * 2
287 | );
288 | $padding = $this->cipher->getPadding();
289 | $this->cipher->setPadding(new Symmetric\Padding\NoPadding());
290 | $this->cipher->setKey(mb_substr($keys, 0, $this->cipher->getKeySize(), '8bit'));
291 | $this->cipher->setMode('cbc');
292 |
293 | $blockSize = $this->cipher->getBlockSize();
294 | $hashAlgo = $this->getHashAlgorithm();
295 | $algorithm = $this->cipher->getAlgorithm();
296 | $saltSize = $this->cipher->getSaltSize();
297 | $keyHmac = mb_substr($keys, $this->cipher->getKeySize(), null, '8bit');
298 |
299 | while ($data = fread($read, self::BUFFER_SIZE)) {
300 | $size += mb_strlen($data, '8bit');
301 | // Unpadding if last block
302 | if ($size + $blockSize >= $tot) {
303 | $this->cipher->setPadding($padding);
304 | $data .= fread($read, $blockSize);
305 | }
306 | $result = $this->cipher->decrypt($iv . $data);
307 | $hmac = Hmac::compute(
308 | $keyHmac,
309 | $hashAlgo,
310 | $algorithm . $hmac . $data
311 | );
312 | $iv = mb_substr($data, -1 * $saltSize, null, '8bit');
313 | if (fwrite($write, $result) !== mb_strlen($result, '8bit')) {
314 | return false;
315 | }
316 | }
317 | fclose($write);
318 | fclose($read);
319 |
320 | // check for data integrity
321 | if (! Utils::compareStrings($hmac, $hmacRead)) {
322 | unlink($fileOut);
323 | return false;
324 | }
325 |
326 | return true;
327 | }
328 |
329 | /**
330 | * Check that input file exists and output file don't
331 | *
332 | * @throws Exception\InvalidArgumentException
333 | */
334 | protected function checkFileInOut(string $fileIn, string $fileOut): void
335 | {
336 | if (! file_exists($fileIn)) {
337 | throw new Exception\InvalidArgumentException(sprintf(
338 | 'I cannot open the %s file',
339 | $fileIn
340 | ));
341 | }
342 | if (file_exists($fileOut)) {
343 | throw new Exception\InvalidArgumentException(sprintf(
344 | 'The file %s already exists',
345 | $fileOut
346 | ));
347 | }
348 | }
349 | }
350 |
--------------------------------------------------------------------------------
/src/Key/Derivation/Scrypt.php:
--------------------------------------------------------------------------------
1 | 0 and a power of 2");
38 | }
39 | if ($n > PHP_INT_MAX / 128 / $r) {
40 | throw new Exception\InvalidArgumentException("Parameter n is too large");
41 | }
42 | if ($r > PHP_INT_MAX / 128 / $p) {
43 | throw new Exception\InvalidArgumentException("Parameter r is too large");
44 | }
45 |
46 | if (extension_loaded('Scrypt')) {
47 | if ($length < 16) {
48 | throw new Exception\InvalidArgumentException("Key length is too low, must be greater or equal to 16");
49 | }
50 | return hex2bin(scrypt($password, $salt, $n, $r, $p, $length));
51 | }
52 |
53 | $b = Pbkdf2::calc('sha256', $password, $salt, 1, $p * 128 * $r);
54 |
55 | $s = '';
56 | for ($i = 0; $i < $p; $i++) {
57 | $s .= self::scryptROMix(mb_substr($b, $i * 128 * $r, 128 * $r, '8bit'), $n, $r);
58 | }
59 |
60 | return Pbkdf2::calc('sha256', $password, $s, 1, $length);
61 | }
62 |
63 | /**
64 | * scryptROMix
65 | *
66 | * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4
67 | */
68 | protected static function scryptROMix(string $b, int $n, int $r): string
69 | {
70 | $x = $b;
71 | $v = [];
72 | for ($i = 0; $i < $n; $i++) {
73 | $v[$i] = $x;
74 | $x = self::scryptBlockMix($x, $r);
75 | }
76 | for ($i = 0; $i < $n; $i++) {
77 | $j = self::integerify($x) % $n;
78 | $t = $x ^ $v[$j];
79 | $x = self::scryptBlockMix($t, $r);
80 | }
81 | return $x;
82 | }
83 |
84 | /**
85 | * scryptBlockMix
86 | *
87 | * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-3
88 | */
89 | protected static function scryptBlockMix(string $b, int $r): string
90 | {
91 | $x = mb_substr($b, -64, null, '8bit');
92 | $even = '';
93 | $odd = '';
94 | $len = 2 * $r;
95 |
96 | for ($i = 0; $i < $len; $i++) {
97 | if (PHP_INT_SIZE === 4) {
98 | $x = self::salsa208Core32($x ^ mb_substr($b, 64 * $i, 64, '8bit'));
99 | } else {
100 | $x = self::salsa208Core64($x ^ mb_substr($b, 64 * $i, 64, '8bit'));
101 | }
102 | if ($i % 2 === 0) {
103 | $even .= $x;
104 | } else {
105 | $odd .= $x;
106 | }
107 | }
108 | return $even . $odd;
109 | }
110 |
111 | /**
112 | * Salsa 20/8 core (32 bit version)
113 | *
114 | * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2
115 | * @see http://cr.yp.to/salsa20.html
116 | */
117 | protected static function salsa208Core32(string $b): string
118 | {
119 | $b32 = [];
120 | for ($i = 0; $i < 16; $i++) {
121 | [, $b32[$i]] = unpack("V", mb_substr($b, $i * 4, 4, '8bit'));
122 | }
123 |
124 | $x = $b32;
125 | for ($i = 0; $i < 8; $i += 2) {
126 | $a = $x[ 0] + $x[12];
127 | $x[ 4] ^= ($a << 7) | ($a >> 25) & 0x7f;
128 | $a = $x[ 4] + $x[ 0];
129 | $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff;
130 | $a = $x[ 8] + $x[ 4];
131 | $x[12] ^= ($a << 13) | ($a >> 19) & 0x1fff;
132 | $a = $x[12] + $x[ 8];
133 | $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
134 | $a = $x[ 5] + $x[ 1];
135 | $x[ 9] ^= ($a << 7) | ($a >> 25) & 0x7f;
136 | $a = $x[ 9] + $x[ 5];
137 | $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff;
138 | $a = $x[13] + $x[ 9];
139 | $x[ 1] ^= ($a << 13) | ($a >> 19) & 0x1fff;
140 | $a = $x[ 1] + $x[13];
141 | $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
142 | $a = $x[10] + $x[ 6];
143 | $x[14] ^= ($a << 7) | ($a >> 25) & 0x7f;
144 | $a = $x[14] + $x[10];
145 | $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff;
146 | $a = $x[ 2] + $x[14];
147 | $x[ 6] ^= ($a << 13) | ($a >> 19) & 0x1fff;
148 | $a = $x[ 6] + $x[ 2];
149 | $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
150 | $a = $x[15] + $x[11];
151 | $x[ 3] ^= ($a << 7) | ($a >> 25) & 0x7f;
152 | $a = $x[ 3] + $x[15];
153 | $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff;
154 | $a = $x[ 7] + $x[ 3];
155 | $x[11] ^= ($a << 13) | ($a >> 19) & 0x1fff;
156 | $a = $x[11] + $x[ 7];
157 | $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
158 | $a = $x[ 0] + $x[ 3];
159 | $x[ 1] ^= ($a << 7) | ($a >> 25) & 0x7f;
160 | $a = $x[ 1] + $x[ 0];
161 | $x[ 2] ^= ($a << 9) | ($a >> 23) & 0x1ff;
162 | $a = $x[ 2] + $x[ 1];
163 | $x[ 3] ^= ($a << 13) | ($a >> 19) & 0x1fff;
164 | $a = $x[ 3] + $x[ 2];
165 | $x[ 0] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
166 | $a = $x[ 5] + $x[ 4];
167 | $x[ 6] ^= ($a << 7) | ($a >> 25) & 0x7f;
168 | $a = $x[ 6] + $x[ 5];
169 | $x[ 7] ^= ($a << 9) | ($a >> 23) & 0x1ff;
170 | $a = $x[ 7] + $x[ 6];
171 | $x[ 4] ^= ($a << 13) | ($a >> 19) & 0x1fff;
172 | $a = $x[ 4] + $x[ 7];
173 | $x[ 5] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
174 | $a = $x[10] + $x[ 9];
175 | $x[11] ^= ($a << 7) | ($a >> 25) & 0x7f;
176 | $a = $x[11] + $x[10];
177 | $x[ 8] ^= ($a << 9) | ($a >> 23) & 0x1ff;
178 | $a = $x[ 8] + $x[11];
179 | $x[ 9] ^= ($a << 13) | ($a >> 19) & 0x1fff;
180 | $a = $x[ 9] + $x[ 8];
181 | $x[10] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
182 | $a = $x[15] + $x[14];
183 | $x[12] ^= ($a << 7) | ($a >> 25) & 0x7f;
184 | $a = $x[12] + $x[15];
185 | $x[13] ^= ($a << 9) | ($a >> 23) & 0x1ff;
186 | $a = $x[13] + $x[12];
187 | $x[14] ^= ($a << 13) | ($a >> 19) & 0x1fff;
188 | $a = $x[14] + $x[13];
189 | $x[15] ^= ($a << 18) | ($a >> 14) & 0x3ffff;
190 | }
191 | for ($i = 0; $i < 16; $i++) {
192 | $b32[$i] += $x[$i];
193 | }
194 | $result = '';
195 | for ($i = 0; $i < 16; $i++) {
196 | $result .= pack("V", $b32[$i]);
197 | }
198 |
199 | return $result;
200 | }
201 |
202 | /**
203 | * Salsa 20/8 core (64 bit version)
204 | *
205 | * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-2
206 | * @see http://cr.yp.to/salsa20.html
207 | */
208 | protected static function salsa208Core64(string $b): string
209 | {
210 | $b32 = [];
211 | for ($i = 0; $i < 16; $i++) {
212 | [, $b32[$i]] = unpack("V", mb_substr($b, $i * 4, 4, '8bit'));
213 | }
214 |
215 | $x = $b32;
216 | for ($i = 0; $i < 8; $i += 2) {
217 | $a = ($x[ 0] + $x[12]) & 0xffffffff;
218 | $x[ 4] ^= ($a << 7) | ($a >> 25);
219 | $a = ($x[ 4] + $x[ 0]) & 0xffffffff;
220 | $x[ 8] ^= ($a << 9) | ($a >> 23);
221 | $a = ($x[ 8] + $x[ 4]) & 0xffffffff;
222 | $x[12] ^= ($a << 13) | ($a >> 19);
223 | $a = ($x[12] + $x[ 8]) & 0xffffffff;
224 | $x[ 0] ^= ($a << 18) | ($a >> 14);
225 | $a = ($x[ 5] + $x[ 1]) & 0xffffffff;
226 | $x[ 9] ^= ($a << 7) | ($a >> 25);
227 | $a = ($x[ 9] + $x[ 5]) & 0xffffffff;
228 | $x[13] ^= ($a << 9) | ($a >> 23);
229 | $a = ($x[13] + $x[ 9]) & 0xffffffff;
230 | $x[ 1] ^= ($a << 13) | ($a >> 19);
231 | $a = ($x[ 1] + $x[13]) & 0xffffffff;
232 | $x[ 5] ^= ($a << 18) | ($a >> 14);
233 | $a = ($x[10] + $x[ 6]) & 0xffffffff;
234 | $x[14] ^= ($a << 7) | ($a >> 25);
235 | $a = ($x[14] + $x[10]) & 0xffffffff;
236 | $x[ 2] ^= ($a << 9) | ($a >> 23);
237 | $a = ($x[ 2] + $x[14]) & 0xffffffff;
238 | $x[ 6] ^= ($a << 13) | ($a >> 19);
239 | $a = ($x[ 6] + $x[ 2]) & 0xffffffff;
240 | $x[10] ^= ($a << 18) | ($a >> 14);
241 | $a = ($x[15] + $x[11]) & 0xffffffff;
242 | $x[ 3] ^= ($a << 7) | ($a >> 25);
243 | $a = ($x[ 3] + $x[15]) & 0xffffffff;
244 | $x[ 7] ^= ($a << 9) | ($a >> 23);
245 | $a = ($x[ 7] + $x[ 3]) & 0xffffffff;
246 | $x[11] ^= ($a << 13) | ($a >> 19);
247 | $a = ($x[11] + $x[ 7]) & 0xffffffff;
248 | $x[15] ^= ($a << 18) | ($a >> 14);
249 | $a = ($x[ 0] + $x[ 3]) & 0xffffffff;
250 | $x[ 1] ^= ($a << 7) | ($a >> 25);
251 | $a = ($x[ 1] + $x[ 0]) & 0xffffffff;
252 | $x[ 2] ^= ($a << 9) | ($a >> 23);
253 | $a = ($x[ 2] + $x[ 1]) & 0xffffffff;
254 | $x[ 3] ^= ($a << 13) | ($a >> 19);
255 | $a = ($x[ 3] + $x[ 2]) & 0xffffffff;
256 | $x[ 0] ^= ($a << 18) | ($a >> 14);
257 | $a = ($x[ 5] + $x[ 4]) & 0xffffffff;
258 | $x[ 6] ^= ($a << 7) | ($a >> 25);
259 | $a = ($x[ 6] + $x[ 5]) & 0xffffffff;
260 | $x[ 7] ^= ($a << 9) | ($a >> 23);
261 | $a = ($x[ 7] + $x[ 6]) & 0xffffffff;
262 | $x[ 4] ^= ($a << 13) | ($a >> 19);
263 | $a = ($x[ 4] + $x[ 7]) & 0xffffffff;
264 | $x[ 5] ^= ($a << 18) | ($a >> 14);
265 | $a = ($x[10] + $x[ 9]) & 0xffffffff;
266 | $x[11] ^= ($a << 7) | ($a >> 25);
267 | $a = ($x[11] + $x[10]) & 0xffffffff;
268 | $x[ 8] ^= ($a << 9) | ($a >> 23);
269 | $a = ($x[ 8] + $x[11]) & 0xffffffff;
270 | $x[ 9] ^= ($a << 13) | ($a >> 19);
271 | $a = ($x[ 9] + $x[ 8]) & 0xffffffff;
272 | $x[10] ^= ($a << 18) | ($a >> 14);
273 | $a = ($x[15] + $x[14]) & 0xffffffff;
274 | $x[12] ^= ($a << 7) | ($a >> 25);
275 | $a = ($x[12] + $x[15]) & 0xffffffff;
276 | $x[13] ^= ($a << 9) | ($a >> 23);
277 | $a = ($x[13] + $x[12]) & 0xffffffff;
278 | $x[14] ^= ($a << 13) | ($a >> 19);
279 | $a = ($x[14] + $x[13]) & 0xffffffff;
280 | $x[15] ^= ($a << 18) | ($a >> 14);
281 | }
282 | for ($i = 0; $i < 16; $i++) {
283 | $b32[$i] = ($b32[$i] + $x[$i]) & 0xffffffff;
284 | }
285 | $result = '';
286 | for ($i = 0; $i < 16; $i++) {
287 | $result .= pack("V", $b32[$i]);
288 | }
289 |
290 | return $result;
291 | }
292 |
293 | /**
294 | * Integerify
295 | *
296 | * Integerify (B[0] ... B[2 * r - 1]) is defined as the result
297 | * of interpreting B[2 * r - 1] as a little-endian integer.
298 | * Each block B is a string of 64 bytes.
299 | *
300 | * @see https://tools.ietf.org/html/draft-josefsson-scrypt-kdf-01#section-4
301 | */
302 | protected static function integerify(string $b): int
303 | {
304 | $v = 'v';
305 | if (PHP_INT_SIZE === 8) {
306 | $v = 'V';
307 | }
308 | [, $n] = unpack($v, mb_substr($b, -64, null, '8bit'));
309 | return $n;
310 | }
311 | }
312 |
--------------------------------------------------------------------------------
/src/PublicKey/DiffieHellman.php:
--------------------------------------------------------------------------------
1 | math = Math\BigInteger\BigInteger::factory();
97 |
98 | $this->setPrime($prime);
99 | $this->setGenerator($generator);
100 | if ($privateKey !== null) {
101 | $this->setPrivateKey($privateKey, $privateKeyFormat);
102 | }
103 | }
104 |
105 | /**
106 | * Set whether to use openssl extension
107 | *
108 | * @static
109 | */
110 | public static function useOpensslExtension(bool $flag = true): void
111 | {
112 | static::$useOpenssl = $flag;
113 | }
114 |
115 | /**
116 | * Generate own public key. If a private number has not already been set,
117 | * one will be generated at this stage.
118 | *
119 | * @throws RuntimeException
120 | */
121 | public function generateKeys(): static
122 | {
123 | if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) {
124 | $details = [
125 | 'p' => $this->convert($this->getPrime(), self::FORMAT_NUMBER, self::FORMAT_BINARY),
126 | 'g' => $this->convert($this->getGenerator(), self::FORMAT_NUMBER, self::FORMAT_BINARY),
127 | ];
128 | // the priv_key parameter is allowed only for PHP < 7.1
129 | // @see https://bugs.php.net/bug.php?id=73478
130 | if ($this->hasPrivateKey() && PHP_VERSION_ID < 70100) {
131 | $details['priv_key'] = $this->convert(
132 | $this->privateKey,
133 | self::FORMAT_NUMBER,
134 | self::FORMAT_BINARY
135 | );
136 | $opensslKeyResource = openssl_pkey_new(['dh' => $details]);
137 | } else {
138 | $opensslKeyResource = openssl_pkey_new([
139 | 'dh' => $details,
140 | 'private_key_bits' => self::DEFAULT_KEY_SIZE,
141 | 'private_key_type' => OPENSSL_KEYTYPE_DH,
142 | ]);
143 | }
144 |
145 | if (false === $opensslKeyResource) {
146 | throw new Exception\RuntimeException(
147 | 'Can not generate new key; openssl ' . openssl_error_string()
148 | );
149 | }
150 |
151 | $data = openssl_pkey_get_details($opensslKeyResource);
152 |
153 | $this->setPrivateKey($data['dh']['priv_key'], self::FORMAT_BINARY);
154 | $this->setPublicKey($data['dh']['pub_key'], self::FORMAT_BINARY);
155 |
156 | $this->opensslKeyResource = $opensslKeyResource;
157 | } else {
158 | // Private key is lazy generated in the absence of ext/openssl
159 | $publicKey = $this->math->powmod($this->getGenerator(), $this->getPrivateKey(), $this->getPrime());
160 | $this->setPublicKey($publicKey);
161 | }
162 |
163 | return $this;
164 | }
165 |
166 | /**
167 | * Setter for the value of the public number
168 | *
169 | * @throws InvalidArgumentException
170 | */
171 | public function setPublicKey(string $number, string $format = self::FORMAT_NUMBER): static
172 | {
173 | $number = $this->convert($number, $format, self::FORMAT_NUMBER);
174 | if (! preg_match('/^\d+$/', $number)) {
175 | throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number');
176 | }
177 | $this->publicKey = $number;
178 |
179 | return $this;
180 | }
181 |
182 | /**
183 | * Returns own public key for communication to the second party to this transaction
184 | *
185 | * @throws InvalidArgumentException
186 | */
187 | public function getPublicKey(string $format = self::FORMAT_NUMBER): string
188 | {
189 | if ($this->publicKey === null) {
190 | throw new Exception\InvalidArgumentException(
191 | 'A public key has not yet been generated using a prior call to generateKeys()'
192 | );
193 | }
194 |
195 | return $this->convert($this->publicKey, self::FORMAT_NUMBER, $format);
196 | }
197 |
198 | /**
199 | * Compute the shared secret key based on the public key received from the
200 | * the second party to this transaction. This should agree to the secret
201 | * key the second party computes on our own public key.
202 | * Once in agreement, the key is known to only to both parties.
203 | * By default, the function expects the public key to be in binary form
204 | * which is the typical format when being transmitted.
205 | *
206 | * If you need the binary form of the shared secret key, call
207 | * getSharedSecretKey() with the optional parameter for Binary output.
208 | *
209 | * @throws InvalidArgumentException
210 | * @throws RuntimeException
211 | */
212 | public function computeSecretKey(
213 | string $publicKey,
214 | string $publicKeyFormat = self::FORMAT_NUMBER,
215 | string $secretKeyFormat = self::FORMAT_NUMBER
216 | ): string {
217 | if (function_exists('openssl_dh_compute_key') && static::$useOpenssl !== false) {
218 | $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_BINARY);
219 | $secretKey = openssl_dh_compute_key($publicKey, $this->opensslKeyResource);
220 | if (false === $secretKey) {
221 | throw new Exception\RuntimeException(
222 | 'Can not compute key; openssl ' . openssl_error_string()
223 | );
224 | }
225 | $this->secretKey = $this->convert($secretKey, self::FORMAT_BINARY, self::FORMAT_NUMBER);
226 | } else {
227 | $publicKey = $this->convert($publicKey, $publicKeyFormat, self::FORMAT_NUMBER);
228 | if (! preg_match('/^\d+$/', $publicKey)) {
229 | throw new Exception\InvalidArgumentException(
230 | 'Invalid parameter; not a positive natural number'
231 | );
232 | }
233 | $this->secretKey = $this->math->powmod($publicKey, $this->getPrivateKey(), $this->getPrime());
234 | }
235 |
236 | return $this->getSharedSecretKey($secretKeyFormat);
237 | }
238 |
239 | /**
240 | * Return the computed shared secret key from the DiffieHellman transaction
241 | *
242 | * @throws InvalidArgumentException
243 | */
244 | public function getSharedSecretKey(string $format = self::FORMAT_NUMBER): string
245 | {
246 | if ($this->secretKey === null) {
247 | throw new Exception\InvalidArgumentException(
248 | 'A secret key has not yet been computed; call computeSecretKey() first'
249 | );
250 | }
251 |
252 | return $this->convert($this->secretKey, self::FORMAT_NUMBER, $format);
253 | }
254 |
255 | /**
256 | * Setter for the value of the prime number
257 | *
258 | * @throws InvalidArgumentException
259 | */
260 | public function setPrime(string $number): static
261 | {
262 | if (! preg_match('/^\d+$/', $number) || $number < 11) {
263 | throw new Exception\InvalidArgumentException(
264 | 'Invalid parameter; not a positive natural number or too small: '
265 | . 'should be a large natural number prime'
266 | );
267 | }
268 | $this->prime = $number;
269 |
270 | return $this;
271 | }
272 |
273 | /**
274 | * Getter for the value of the prime number
275 | *
276 | * @throws InvalidArgumentException
277 | */
278 | public function getPrime(string $format = self::FORMAT_NUMBER): string
279 | {
280 | if ($this->prime === null) {
281 | throw new Exception\InvalidArgumentException('No prime number has been set');
282 | }
283 |
284 | return $this->convert($this->prime, self::FORMAT_NUMBER, $format);
285 | }
286 |
287 | /**
288 | * Setter for the value of the generator number
289 | *
290 | * @throws InvalidArgumentException
291 | */
292 | public function setGenerator(string $number): static
293 | {
294 | if (! preg_match('/^\d+$/', $number) || $number < 2) {
295 | throw new Exception\InvalidArgumentException(
296 | 'Invalid parameter; not a positive natural number greater than 1'
297 | );
298 | }
299 | $this->generator = $number;
300 |
301 | return $this;
302 | }
303 |
304 | /**
305 | * Getter for the value of the generator number
306 | *
307 | * @throws InvalidArgumentException
308 | */
309 | public function getGenerator(string $format = self::FORMAT_NUMBER): string
310 | {
311 | if ($this->generator === null) {
312 | throw new Exception\InvalidArgumentException('No generator number has been set');
313 | }
314 |
315 | return $this->convert($this->generator, self::FORMAT_NUMBER, $format);
316 | }
317 |
318 | /**
319 | * Setter for the value of the private number
320 | *
321 | * @throws InvalidArgumentException
322 | */
323 | public function setPrivateKey(string $number, string $format = self::FORMAT_NUMBER): static
324 | {
325 | $number = $this->convert($number, $format, self::FORMAT_NUMBER);
326 | if (! preg_match('/^\d+$/', $number)) {
327 | throw new Exception\InvalidArgumentException('Invalid parameter; not a positive natural number');
328 | }
329 | $this->privateKey = $number;
330 |
331 | return $this;
332 | }
333 |
334 | /**
335 | * Getter for the value of the private number
336 | */
337 | public function getPrivateKey(string $format = self::FORMAT_NUMBER): string
338 | {
339 | if (! $this->hasPrivateKey()) {
340 | $this->setPrivateKey($this->generatePrivateKey(), self::FORMAT_BINARY);
341 | }
342 |
343 | return $this->convert($this->privateKey, self::FORMAT_NUMBER, $format);
344 | }
345 |
346 | /**
347 | * Check whether a private key currently exists.
348 | */
349 | public function hasPrivateKey(): bool
350 | {
351 | return $this->privateKey !== null;
352 | }
353 |
354 | /**
355 | * Convert number between formats
356 | */
357 | protected function convert(
358 | string $number,
359 | string $inputFormat = self::FORMAT_NUMBER,
360 | string $outputFormat = self::FORMAT_BINARY
361 | ): string {
362 | if ($inputFormat === $outputFormat) {
363 | return $number;
364 | }
365 |
366 | // convert to number
367 | switch ($inputFormat) {
368 | case self::FORMAT_BINARY:
369 | case self::FORMAT_BTWOC:
370 | $number = $this->math->binToInt($number);
371 | break;
372 | case self::FORMAT_NUMBER:
373 | default:
374 | // do nothing
375 | break;
376 | }
377 |
378 | // convert to output format
379 | return match ($outputFormat) {
380 | self::FORMAT_BINARY => $this->math->intToBin($number),
381 | self::FORMAT_BTWOC => $this->math->intToBin($number, true),
382 | default => $number,
383 | };
384 | }
385 |
386 | /**
387 | * In the event a private number/key has not been set by the user,
388 | * or generated by ext/openssl, a best attempt will be made to
389 | * generate a random key. Having a random number generator installed
390 | * on linux/bsd is highly recommended! The alternative is not recommended
391 | * for production unless without any other option.
392 | */
393 | protected function generatePrivateKey(): string
394 | {
395 | return Math\Rand::getBytes(mb_strlen($this->getPrime(), '8bit'));
396 | }
397 | }
398 |
--------------------------------------------------------------------------------
/src/BlockCipher.php:
--------------------------------------------------------------------------------
1 | get($adapter);
83 | } catch (NotFoundException) {
84 | throw new Exception\RuntimeException(sprintf(
85 | 'The symmetric adapter %s does not exist',
86 | $adapter
87 | ));
88 | }
89 | $cipher->setOptions($options);
90 | return new static($cipher);
91 | }
92 |
93 | /**
94 | * Returns the symmetric cipher plugin manager. If it doesn't exist it's created.
95 | */
96 | public static function getSymmetricPluginManager(): ContainerInterface
97 | {
98 | if (! static::$symmetricPlugins instanceof ContainerInterface) {
99 | static::setSymmetricPluginManager(new SymmetricPluginManager());
100 | }
101 |
102 | return static::$symmetricPlugins;
103 | }
104 |
105 | /**
106 | * Set the symmetric cipher plugin manager
107 | *
108 | * @param string|SymmetricPluginManager $plugins
109 | * @throws Exception\InvalidArgumentException
110 | */
111 | public static function setSymmetricPluginManager(string|ContainerInterface $plugins): void
112 | {
113 | if (is_string($plugins)) {
114 | if (! class_exists($plugins) || ! is_subclass_of($plugins, ContainerInterface::class)) {
115 | throw new Exception\InvalidArgumentException(sprintf(
116 | 'Unable to locate symmetric cipher plugins using class "%s"; '
117 | . 'class does not exist or does not implement ContainerInterface',
118 | $plugins
119 | ));
120 | }
121 | $plugins = new $plugins();
122 | }
123 | static::$symmetricPlugins = $plugins;
124 | }
125 |
126 | /**
127 | * Set the symmetric cipher
128 | */
129 | public function setCipher(SymmetricInterface $cipher): static
130 | {
131 | $this->cipher = $cipher;
132 | return $this;
133 | }
134 |
135 | /**
136 | * Get symmetric cipher
137 | */
138 | public function getCipher(): SymmetricInterface
139 | {
140 | return $this->cipher;
141 | }
142 |
143 | /**
144 | * Set the number of iterations for Pbkdf2
145 | */
146 | public function setKeyIteration(int $num): static
147 | {
148 | $this->keyIteration = $num;
149 |
150 | return $this;
151 | }
152 |
153 | /**
154 | * Get the number of iterations for Pbkdf2
155 | */
156 | public function getKeyIteration(): int
157 | {
158 | return $this->keyIteration;
159 | }
160 |
161 | /**
162 | * Set the salt (IV)
163 | *
164 | * @throws Exception\InvalidArgumentException
165 | */
166 | public function setSalt(string $salt): static
167 | {
168 | try {
169 | $this->cipher->setSalt($salt);
170 | } catch (Symmetric\Exception\InvalidArgumentException $e) {
171 | throw new Exception\InvalidArgumentException("The salt is not valid: " . $e->getMessage());
172 | }
173 | $this->saltSetted = true;
174 |
175 | return $this;
176 | }
177 |
178 | /**
179 | * Get the salt (IV) according to the size requested by the algorithm
180 | */
181 | public function getSalt(): string|null
182 | {
183 | return $this->cipher->getSalt();
184 | }
185 |
186 | /**
187 | * Get the original salt value
188 | */
189 | public function getOriginalSalt(): string
190 | {
191 | return $this->cipher->getOriginalSalt();
192 | }
193 |
194 | /**
195 | * Enable/disable the binary output
196 | */
197 | public function setBinaryOutput(bool $value): static
198 | {
199 | $this->binaryOutput = $value;
200 |
201 | return $this;
202 | }
203 |
204 | /**
205 | * Get the value of binary output
206 | */
207 | public function getBinaryOutput(): bool
208 | {
209 | return $this->binaryOutput;
210 | }
211 |
212 | /**
213 | * Set the encryption/decryption key
214 | *
215 | * @throws Exception\InvalidArgumentException
216 | */
217 | public function setKey(string $key): static
218 | {
219 | if ($key === '' || $key === '0') {
220 | throw new Exception\InvalidArgumentException('The key cannot be empty');
221 | }
222 | $this->key = $key;
223 |
224 | return $this;
225 | }
226 |
227 | /**
228 | * Get the key
229 | */
230 | public function getKey(): string
231 | {
232 | return $this->key;
233 | }
234 |
235 | /**
236 | * Set algorithm of the symmetric cipher
237 | *
238 | * @throws Exception\InvalidArgumentException
239 | */
240 | public function setCipherAlgorithm(string $algo): static
241 | {
242 | try {
243 | $this->cipher->setAlgorithm($algo);
244 | } catch (Symmetric\Exception\InvalidArgumentException $e) {
245 | throw new Exception\InvalidArgumentException($e->getMessage());
246 | }
247 |
248 | return $this;
249 | }
250 |
251 | /**
252 | * Get the cipher algorithm
253 | */
254 | public function getCipherAlgorithm(): string
255 | {
256 | return $this->cipher->getAlgorithm();
257 | }
258 |
259 | /**
260 | * Get the supported algorithms of the symmetric cipher
261 | */
262 | public function getCipherSupportedAlgorithms(): array
263 | {
264 | return $this->cipher->getSupportedAlgorithms();
265 | }
266 |
267 | /**
268 | * Set the hash algorithm for HMAC authentication
269 | *
270 | * @throws Exception\InvalidArgumentException
271 | */
272 | public function setHashAlgorithm(string $hash): static
273 | {
274 | if (! Hash::isSupported($hash)) {
275 | throw new Exception\InvalidArgumentException(
276 | "The specified hash algorithm '{$hash}' is not supported by Laminas\Crypt\Hash"
277 | );
278 | }
279 | $this->hash = $hash;
280 |
281 | return $this;
282 | }
283 |
284 | /**
285 | * Get the hash algorithm for HMAC authentication
286 | */
287 | public function getHashAlgorithm(): string
288 | {
289 | return $this->hash;
290 | }
291 |
292 | /**
293 | * Set the hash algorithm for the Pbkdf2
294 | *
295 | * @throws Exception\InvalidArgumentException
296 | */
297 | public function setPbkdf2HashAlgorithm(string $hash): static
298 | {
299 | if (! Hash::isSupported($hash)) {
300 | throw new Exception\InvalidArgumentException(
301 | "The specified hash algorithm '{$hash}' is not supported by Laminas\Crypt\Hash"
302 | );
303 | }
304 | $this->pbkdf2Hash = $hash;
305 |
306 | return $this;
307 | }
308 |
309 | /**
310 | * Get the Pbkdf2 hash algorithm
311 | */
312 | public function getPbkdf2HashAlgorithm(): string
313 | {
314 | return $this->pbkdf2Hash;
315 | }
316 |
317 | /**
318 | * Encrypt then authenticate using HMAC
319 | *
320 | * @throws Exception\InvalidArgumentException
321 | */
322 | public function encrypt(string $data): string
323 | {
324 | // 0 (as integer), 0.0 (as float) & '0' (as string) will return false, though these should be allowed
325 | // Must be a string, integer, or float in order to encrypt
326 | if (
327 | (is_string($data) && $data === '')
328 | || is_array($data)
329 | || is_object($data)
330 | ) {
331 | throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty');
332 | }
333 |
334 | // Cast to string prior to encrypting
335 | if (! is_string($data)) {
336 | $data = (string) $data;
337 | }
338 |
339 | if (! isset($this->key) || ($this->key === '' || $this->key === '0')) {
340 | throw new Exception\InvalidArgumentException('No key specified for the encryption');
341 | }
342 | $keySize = $this->cipher->getKeySize();
343 | // generate a random salt (IV) if the salt has not been set
344 | if (! $this->saltSetted) {
345 | $this->cipher->setSalt(Rand::getBytes($this->cipher->getSaltSize()));
346 | }
347 |
348 | if (in_array($this->cipher->getMode(), ['ccm', 'gcm'], true)) {
349 | return $this->encryptViaCcmOrGcm($data, $keySize);
350 | }
351 |
352 | // generate the encryption key and the HMAC key for the authentication
353 | $hash = Pbkdf2::calc(
354 | $this->getPbkdf2HashAlgorithm(),
355 | $this->getKey(),
356 | $this->getSalt(),
357 | $this->keyIteration,
358 | $keySize * 2
359 | );
360 | // set the encryption key
361 | $this->cipher->setKey(mb_substr($hash, 0, $keySize, '8bit'));
362 | // set the key for HMAC
363 | $keyHmac = mb_substr($hash, $keySize, null, '8bit');
364 | // encryption
365 | $ciphertext = $this->cipher->encrypt($data);
366 | // HMAC
367 | $hmac = Hmac::compute($keyHmac, $this->hash, $this->cipher->getAlgorithm() . $ciphertext);
368 |
369 | return $this->binaryOutput ? $hmac . $ciphertext : $hmac . base64_encode($ciphertext);
370 | }
371 |
372 | /**
373 | * Decrypt
374 | *
375 | * @throws Exception\InvalidArgumentException
376 | */
377 | public function decrypt(string $data): string|false
378 | {
379 | if (! is_string($data)) {
380 | throw new Exception\InvalidArgumentException('The data to decrypt must be a string');
381 | }
382 | if ('' === $data) {
383 | throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty');
384 | }
385 | if (! isset($this->key) || ($this->key === '' || $this->key === '0')) {
386 | throw new Exception\InvalidArgumentException('No key specified for the decryption');
387 | }
388 |
389 | $keySize = $this->cipher->getKeySize();
390 |
391 | if (in_array($this->cipher->getMode(), ['ccm', 'gcm'], true)) {
392 | return $this->decryptViaCcmOrGcm($data, $keySize);
393 | }
394 |
395 | $hmacSize = Hmac::getOutputSize($this->hash);
396 | $hmac = mb_substr($data, 0, $hmacSize, '8bit');
397 | $ciphertext = mb_substr($data, $hmacSize, null, '8bit') ?: '';
398 | if (! $this->binaryOutput) {
399 | $ciphertext = base64_decode($ciphertext);
400 | }
401 | $iv = mb_substr($ciphertext, 0, $this->cipher->getSaltSize(), '8bit');
402 | // generate the encryption key and the HMAC key for the authentication
403 | $hash = Pbkdf2::calc(
404 | $this->getPbkdf2HashAlgorithm(),
405 | $this->getKey(),
406 | $iv,
407 | $this->keyIteration,
408 | $keySize * 2
409 | );
410 | // set the decryption key
411 | $this->cipher->setKey(mb_substr($hash, 0, $keySize, '8bit'));
412 | // set the key for HMAC
413 | $keyHmac = mb_substr($hash, $keySize, null, '8bit');
414 | $hmacNew = Hmac::compute($keyHmac, $this->hash, $this->cipher->getAlgorithm() . $ciphertext);
415 | if (! Utils::compareStrings($hmacNew, $hmac)) {
416 | return false;
417 | }
418 |
419 | return $this->cipher->decrypt($ciphertext);
420 | }
421 |
422 | /**
423 | * Note: CCM and GCM modes do not need HMAC
424 | *
425 | * @throws Exception\InvalidArgumentException
426 | */
427 | private function encryptViaCcmOrGcm(string $data, int $keySize): string
428 | {
429 | $this->cipher->setKey(Pbkdf2::calc(
430 | $this->getPbkdf2HashAlgorithm(),
431 | $this->getKey(),
432 | $this->getSalt(),
433 | $this->keyIteration,
434 | $keySize
435 | ));
436 |
437 | $cipherText = $this->cipher->encrypt($data);
438 |
439 | return $this->binaryOutput ? $cipherText : base64_encode($cipherText);
440 | }
441 |
442 | /**
443 | * Note: CCM and GCM modes do not need HMAC
444 | *
445 | * @throws Exception\InvalidArgumentException
446 | */
447 | private function decryptViaCcmOrGcm(string $data, int $keySize): string
448 | {
449 | $cipherText = $this->binaryOutput ? $data : base64_decode($data);
450 | $iv = mb_substr($cipherText, $this->cipher->getTagSize(), $this->cipher->getSaltSize(), '8bit');
451 |
452 | $this->cipher->setKey(Pbkdf2::calc(
453 | $this->getPbkdf2HashAlgorithm(),
454 | $this->getKey(),
455 | $iv,
456 | $this->keyIteration,
457 | $keySize
458 | ));
459 |
460 | return $this->cipher->decrypt($cipherText);
461 | }
462 | }
463 |
--------------------------------------------------------------------------------
/src/Symmetric/Openssl.php:
--------------------------------------------------------------------------------
1 | 'aes-256',
81 | 'blowfish' => 'bf',
82 | 'des' => 'des',
83 | 'camellia' => 'camellia-256',
84 | 'cast5' => 'cast5',
85 | 'seed' => 'seed',
86 | ];
87 |
88 | /**
89 | * Encryption modes to support
90 | *
91 | * @var array
92 | */
93 | protected $encryptionModes = [
94 | 'cbc',
95 | 'cfb',
96 | 'ofb',
97 | 'ecb',
98 | 'ctr',
99 | ];
100 |
101 | /**
102 | * Block sizes (in bytes) for each supported algorithm
103 | *
104 | * @var array
105 | */
106 | protected $blockSizes = [
107 | 'aes' => 16,
108 | 'blowfish' => 8,
109 | 'des' => 8,
110 | 'camellia' => 16,
111 | 'cast5' => 8,
112 | 'seed' => 16,
113 | ];
114 |
115 | /**
116 | * Key sizes (in bytes) for each supported algorithm
117 | *
118 | * @var array
119 | */
120 | protected $keySizes = [
121 | 'aes' => 32,
122 | 'blowfish' => 56,
123 | 'des' => 8,
124 | 'camellia' => 32,
125 | 'cast5' => 16,
126 | 'seed' => 16,
127 | ];
128 |
129 | /**
130 | * The OpenSSL supported encryption algorithms
131 | */
132 | protected array $opensslAlgos = [];
133 |
134 | /**
135 | * Additional authentication data (only for PHP 7.1+)
136 | */
137 | protected string $aad = '';
138 |
139 | /**
140 | * Store the tag for authentication (only for PHP 7.1+)
141 | */
142 | protected ?string $tag = null;
143 |
144 | /**
145 | * Tag size for authenticated encryption modes (only for PHP 7.1+)
146 | */
147 | protected int $tagSize = 16;
148 |
149 | /**
150 | * Supported algorithms
151 | *
152 | * @internal This property was declared for compatibility with PHP 8.2,
153 | * and is not supposed to be used directly, other than for BC reasons
154 | *
155 | * @var list
156 | */
157 | public array $supportedAlgos = [];
158 |
159 | /**
160 | * Constructor
161 | *
162 | * @throws Exception\RuntimeException
163 | * @throws Exception\InvalidArgumentException
164 | */
165 | public function __construct(Traversable|array $options = [])
166 | {
167 | if (! extension_loaded('openssl')) {
168 | throw new Exception\RuntimeException(sprintf(
169 | 'You cannot use %s without the OpenSSL extension',
170 | self::class
171 | ));
172 | }
173 | $this->encryptionModes[] = 'gcm';
174 | $this->encryptionModes[] = 'ccm';
175 | $this->setOptions($options);
176 | $this->setDefaultOptions($options);
177 | }
178 |
179 | /**
180 | * Set default options
181 | *
182 | * @throws Exception\RuntimeException
183 | * @throws Exception\InvalidArgumentException
184 | */
185 | public function setOptions(Traversable|array $options): void
186 | {
187 | if (empty($options)) {
188 | return;
189 | }
190 |
191 | if ($options instanceof Traversable) {
192 | $options = ArrayUtils::iteratorToArray($options);
193 | }
194 |
195 | if (! is_array($options)) {
196 | throw new Exception\InvalidArgumentException(
197 | 'The options parameter must be an array or a Traversable'
198 | );
199 | }
200 |
201 | // The algorithm case is not included in the switch because must be
202 | // set before the others
203 | if (isset($options['algo'])) {
204 | $this->setAlgorithm($options['algo']);
205 | } elseif (isset($options['algorithm'])) {
206 | $this->setAlgorithm($options['algorithm']);
207 | }
208 |
209 | foreach ($options as $key => $value) {
210 | switch (strtolower($key)) {
211 | case 'mode':
212 | $this->setMode($value);
213 | break;
214 | case 'key':
215 | $this->setKey($value);
216 | break;
217 | case 'iv':
218 | case 'salt':
219 | $this->setSalt($value);
220 | break;
221 | case 'padding':
222 | $plugins = static::getPaddingPluginManager();
223 | $padding = $plugins->get($value);
224 | $this->padding = $padding;
225 | break;
226 | case 'aad':
227 | $this->setAad($value);
228 | break;
229 | case 'tag_size':
230 | $this->setTagSize($value);
231 | break;
232 | }
233 | }
234 | }
235 |
236 | /**
237 | * Set default options
238 | */
239 | protected function setDefaultOptions(array|ArrayAccess $options = []): void
240 | {
241 | if (isset($options['padding'])) {
242 | return;
243 | }
244 |
245 | $plugins = static::getPaddingPluginManager();
246 | $padding = $plugins->get(self::DEFAULT_PADDING);
247 | $this->padding = $padding;
248 | }
249 |
250 | /**
251 | * Returns the padding plugin manager.
252 | *
253 | * Creates one if none is present.
254 | */
255 | public static function getPaddingPluginManager(): ContainerInterface
256 | {
257 | if (! static::$paddingPlugins instanceof ContainerInterface) {
258 | self::setPaddingPluginManager(new PaddingPluginManager());
259 | }
260 |
261 | return static::$paddingPlugins;
262 | }
263 |
264 | /**
265 | * Set the padding plugin manager
266 | *
267 | * @throws Exception\InvalidArgumentException
268 | */
269 | public static function setPaddingPluginManager(string|ContainerInterface $plugins): void
270 | {
271 | if (is_string($plugins)) {
272 | if (! class_exists($plugins) || ! is_subclass_of($plugins, ContainerInterface::class)) {
273 | throw new Exception\InvalidArgumentException(sprintf(
274 | 'Unable to locate padding plugin manager via class "%s"; '
275 | . 'class does not exist or does not implement ContainerInterface',
276 | $plugins
277 | ));
278 | }
279 |
280 | $plugins = new $plugins();
281 | }
282 |
283 | static::$paddingPlugins = $plugins;
284 | }
285 |
286 | /**
287 | * Get the key size for the selected cipher
288 | */
289 | public function getKeySize(): int
290 | {
291 | return $this->keySizes[$this->algo];
292 | }
293 |
294 | /**
295 | * Set the encryption key
296 | * If the key is longer than maximum supported, it will be truncated by getKey().
297 | *
298 | * @throws Exception\InvalidArgumentException
299 | */
300 | public function setKey(string $key): static
301 | {
302 | $keyLen = mb_strlen($key, '8bit');
303 |
304 | if ($keyLen === 0) {
305 | throw new Exception\InvalidArgumentException('The key cannot be empty');
306 | }
307 |
308 | if ($keyLen < $this->getKeySize()) {
309 | throw new Exception\InvalidArgumentException(sprintf(
310 | 'The size of the key must be at least of %d bytes',
311 | $this->getKeySize()
312 | ));
313 | }
314 |
315 | $this->key = $key;
316 | return $this;
317 | }
318 |
319 | /**
320 | * Get the encryption key
321 | */
322 | public function getKey(): ?string
323 | {
324 | if (! isset($this->key) || ($this->key === '' || $this->key === '0')) {
325 | return null;
326 | }
327 | return mb_substr($this->key, 0, $this->getKeySize(), '8bit');
328 | }
329 |
330 | /**
331 | * Set the encryption algorithm (cipher)
332 | *
333 | * @throws Exception\InvalidArgumentException
334 | */
335 | public function setAlgorithm(string $algo): static
336 | {
337 | if (! in_array($algo, $this->getSupportedAlgorithms())) {
338 | throw new Exception\InvalidArgumentException(sprintf(
339 | 'The algorithm %s is not supported by %s',
340 | $algo,
341 | self::class
342 | ));
343 | }
344 | $this->algo = $algo;
345 | return $this;
346 | }
347 |
348 | /**
349 | * Get the encryption algorithm
350 | */
351 | public function getAlgorithm(): string
352 | {
353 | return $this->algo;
354 | }
355 |
356 | /**
357 | * Set the padding object
358 | */
359 | public function setPadding(PaddingInterface $padding): static
360 | {
361 | $this->padding = $padding;
362 | return $this;
363 | }
364 |
365 | /**
366 | * Get the padding object
367 | */
368 | public function getPadding(): PaddingInterface
369 | {
370 | return $this->padding;
371 | }
372 |
373 | /**
374 | * Set Additional Authentication Data
375 | *
376 | * @throws Exception\InvalidArgumentException
377 | * @throws Exception\RuntimeException
378 | */
379 | public function setAad(string $aad): static
380 | {
381 | if (! $this->isAuthEncAvailable()) {
382 | throw new Exception\RuntimeException(
383 | 'You need PHP 7.1+ and OpenSSL with CCM or GCM mode to use AAD'
384 | );
385 | }
386 |
387 | if (! $this->isCcmOrGcm()) {
388 | throw new Exception\RuntimeException(
389 | 'You can set Additional Authentication Data (AAD) only for CCM or GCM mode'
390 | );
391 | }
392 |
393 | $this->aad = $aad;
394 |
395 | return $this;
396 | }
397 |
398 | /**
399 | * Get the Additional Authentication Data
400 | */
401 | public function getAad(): string
402 | {
403 | return $this->aad;
404 | }
405 |
406 | /**
407 | * Get the authentication tag
408 | */
409 | public function getTag(): string|null
410 | {
411 | return $this->tag;
412 | }
413 |
414 | /**
415 | * Set the tag size for CCM and GCM mode
416 | *
417 | * @throws Exception\InvalidArgumentException
418 | * @throws Exception\RuntimeException
419 | */
420 | public function setTagSize(int $size): static
421 | {
422 | if (! $this->isAuthEncAvailable()) {
423 | throw new Exception\RuntimeException(
424 | 'You need PHP 7.1+ and OpenSSL with CCM or GCM mode to set the Tag Size'
425 | );
426 | }
427 |
428 | if (! $this->isCcmOrGcm()) {
429 | throw new Exception\RuntimeException(
430 | 'You can set the Tag Size only for CCM or GCM mode'
431 | );
432 | }
433 |
434 | if ($this->getMode() === 'gcm' && ($size < 4 || $size > 16)) {
435 | throw new Exception\InvalidArgumentException(
436 | 'The Tag Size must be between 4 to 16 for GCM mode'
437 | );
438 | }
439 |
440 | $this->tagSize = $size;
441 |
442 | return $this;
443 | }
444 |
445 | /**
446 | * Get the tag size for CCM and GCM mode
447 | */
448 | public function getTagSize(): int
449 | {
450 | return $this->tagSize;
451 | }
452 |
453 | /**
454 | * Encrypt
455 | *
456 | * @throws Exception\InvalidArgumentException
457 | */
458 | public function encrypt(string $data): string
459 | {
460 | // Cannot encrypt empty string
461 | if ($data === '') {
462 | throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty');
463 | }
464 |
465 | if (null === $this->getKey()) {
466 | throw new Exception\InvalidArgumentException('No key specified for the encryption');
467 | }
468 |
469 | if (null === $this->getSalt() && $this->getSaltSize() > 0) {
470 | throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty');
471 | }
472 |
473 | if (! $this->getPadding() instanceof PaddingInterface) {
474 | throw new Exception\InvalidArgumentException('You must specify a padding method');
475 | }
476 |
477 | // padding
478 | $data = $this->padding->pad($data, $this->getBlockSize());
479 | $iv = $this->getSalt();
480 |
481 | // encryption with GCM or CCM
482 | if ($this->isCcmOrGcm()) {
483 | $result = openssl_encrypt(
484 | $data,
485 | strtolower($this->encryptionAlgos[$this->algo] . '-' . $this->mode),
486 | $this->getKey(),
487 | OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
488 | $iv,
489 | $tag,
490 | $this->getAad(),
491 | $this->getTagSize()
492 | );
493 | $this->tag = $tag;
494 | } else {
495 | $result = openssl_encrypt(
496 | $data,
497 | strtolower($this->encryptionAlgos[$this->algo] . '-' . $this->mode),
498 | $this->getKey(),
499 | OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
500 | $iv
501 | );
502 | }
503 |
504 | if (false === $result) {
505 | $errMsg = '';
506 | while ($msg = openssl_error_string()) {
507 | $errMsg .= $msg;
508 | }
509 | throw new Exception\RuntimeException(sprintf(
510 | 'OpenSSL error: %s',
511 | $errMsg
512 | ));
513 | }
514 |
515 | if ($this->isCcmOrGcm()) {
516 | return $tag . $iv . $result;
517 | }
518 |
519 | return $iv . $result;
520 | }
521 |
522 | /**
523 | * Decrypt
524 | *
525 | * @throws Exception\InvalidArgumentException
526 | */
527 | public function decrypt(string $data): string
528 | {
529 | if ($data === '' || $data === '0') {
530 | throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty');
531 | }
532 |
533 | if (null === $this->getKey()) {
534 | throw new Exception\InvalidArgumentException('No decryption key specified');
535 | }
536 | if (! $this->getPadding() instanceof PaddingInterface) {
537 | throw new Exception\InvalidArgumentException('You must specify a padding method');
538 | }
539 |
540 | if ($this->isCcmOrGcm()) {
541 | $tag = mb_substr($data, 0, $this->getTagSize(), '8bit');
542 | $data = mb_substr($data, $this->getTagSize(), null, '8bit');
543 | $this->tag = $tag;
544 | }
545 |
546 | $iv = mb_substr($data, 0, $this->getSaltSize(), '8bit');
547 | $ciphertext = mb_substr($data, $this->getSaltSize(), null, '8bit');
548 | $result = $this->attemptOpensslDecrypt($ciphertext, $iv, $this->tag);
549 |
550 | if (false === $result) {
551 | $errMsg = '';
552 |
553 | while ($msg = openssl_error_string()) {
554 | $errMsg .= $msg;
555 | }
556 |
557 | throw new Exception\RuntimeException(sprintf(
558 | 'OpenSSL error: %s',
559 | $errMsg
560 | ));
561 | }
562 |
563 | // unpadding
564 | return $this->padding->strip($result);
565 | }
566 |
567 | /**
568 | * Get the salt (IV) size
569 | */
570 | public function getSaltSize(): int|false
571 | {
572 | return openssl_cipher_iv_length(
573 | $this->encryptionAlgos[$this->algo] . '-' . $this->mode
574 | );
575 | }
576 |
577 | /**
578 | * Get the supported algorithms
579 | */
580 | public function getSupportedAlgorithms(): array
581 | {
582 | if ($this->supportedAlgos === []) {
583 | foreach ($this->encryptionAlgos as $name => $algo) {
584 | // CBC mode is supported by all the algorithms
585 | if (in_array($algo . '-cbc', $this->getOpensslAlgos())) {
586 | $this->supportedAlgos[] = $name;
587 | }
588 | }
589 | }
590 | return $this->supportedAlgos;
591 | }
592 |
593 | /**
594 | * Set the salt (IV)
595 | *
596 | * @throws Exception\InvalidArgumentException
597 | */
598 | public function setSalt(string $salt): static
599 | {
600 | if ($this->getSaltSize() <= 0) {
601 | throw new Exception\InvalidArgumentException(sprintf(
602 | 'You cannot use a salt (IV) for %s in %s mode',
603 | $this->algo,
604 | $this->mode
605 | ));
606 | }
607 |
608 | if ($salt === '' || $salt === '0') {
609 | throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty');
610 | }
611 |
612 | if (mb_strlen($salt, '8bit') < $this->getSaltSize()) {
613 | throw new Exception\InvalidArgumentException(sprintf(
614 | 'The size of the salt (IV) must be at least %d bytes',
615 | $this->getSaltSize()
616 | ));
617 | }
618 |
619 | $this->iv = $salt;
620 | return $this;
621 | }
622 |
623 | /**
624 | * Get the salt (IV) according to the size requested by the algorithm
625 | */
626 | public function getSalt(): ?string
627 | {
628 | if (! isset($this->iv) || ($this->iv === '' || $this->iv === '0')) {
629 | return null;
630 | }
631 |
632 | if (mb_strlen($this->iv, '8bit') < $this->getSaltSize()) {
633 | throw new Exception\RuntimeException(sprintf(
634 | 'The size of the salt (IV) must be at least %d bytes',
635 | $this->getSaltSize()
636 | ));
637 | }
638 |
639 | return mb_substr($this->iv, 0, $this->getSaltSize(), '8bit');
640 | }
641 |
642 | /**
643 | * Get the original salt value
644 | */
645 | public function getOriginalSalt(): string
646 | {
647 | return $this->iv;
648 | }
649 |
650 | /**
651 | * Set the cipher mode
652 | *
653 | * @throws Exception\InvalidArgumentException
654 | */
655 | public function setMode(string $mode): static
656 | {
657 | if ($mode === '' || $mode === '0') {
658 | return $this;
659 | }
660 | if (! in_array($mode, $this->getSupportedModes())) {
661 | throw new Exception\InvalidArgumentException(sprintf(
662 | 'The mode %s is not supported by %s',
663 | $mode,
664 | $this->algo
665 | ));
666 | }
667 | $this->mode = $mode;
668 | return $this;
669 | }
670 |
671 | /**
672 | * Get the cipher mode
673 | */
674 | public function getMode(): string
675 | {
676 | return $this->mode;
677 | }
678 |
679 | /**
680 | * Return the OpenSSL supported encryption algorithms
681 | */
682 | protected function getOpensslAlgos(): array
683 | {
684 | if ($this->opensslAlgos === []) {
685 | $this->opensslAlgos = openssl_get_cipher_methods(true);
686 | }
687 | return $this->opensslAlgos;
688 | }
689 |
690 | /**
691 | * Get all supported encryption modes for the selected algorithm
692 | */
693 | public function getSupportedModes(): array
694 | {
695 | $modes = [];
696 | foreach ($this->encryptionModes as $mode) {
697 | $algo = $this->encryptionAlgos[$this->algo] . '-' . $mode;
698 | if (in_array($algo, $this->getOpensslAlgos())) {
699 | $modes[] = $mode;
700 | }
701 | }
702 | return $modes;
703 | }
704 |
705 | /**
706 | * Get the block size
707 | */
708 | public function getBlockSize(): int
709 | {
710 | return $this->blockSizes[$this->algo];
711 | }
712 |
713 | /**
714 | * Return true if authenticated encryption is available
715 | */
716 | public function isAuthEncAvailable(): bool
717 | {
718 | // Counter with CBC-MAC
719 | $ccm = in_array('aes-256-ccm', $this->getOpensslAlgos());
720 | // Galois/Counter Mode
721 | $gcm = in_array('aes-256-gcm', $this->getOpensslAlgos());
722 |
723 | return PHP_VERSION_ID >= 70100 && ($ccm || $gcm);
724 | }
725 |
726 | private function isCcmOrGcm(): bool
727 | {
728 | return in_array(strtolower($this->mode), ['gcm', 'ccm'], true);
729 | }
730 |
731 | private function attemptOpensslDecrypt(string $cipherText, string $iv, string|null $tag): string|false
732 | {
733 | if ($this->isCcmOrGcm()) {
734 | return openssl_decrypt(
735 | $cipherText,
736 | strtolower($this->encryptionAlgos[$this->algo] . '-' . $this->mode),
737 | $this->getKey(),
738 | OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
739 | $iv,
740 | $tag,
741 | $this->getAad()
742 | );
743 | }
744 |
745 | return openssl_decrypt(
746 | $cipherText,
747 | strtolower($this->encryptionAlgos[$this->algo] . '-' . $this->mode),
748 | $this->getKey(),
749 | OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING,
750 | $iv
751 | );
752 | }
753 | }
754 |
--------------------------------------------------------------------------------