├── 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 | --------------------------------------------------------------------------------