From c36648a187afc397e428329f71eb3a9e99965009 Mon Sep 17 00:00:00 2001 From: Paragon Initiative Enterprises Date: Wed, 18 May 2016 16:51:29 -0400 Subject: [PATCH] EasyRSA redesign --- .travis.yml | 2 -- README.md | 11 +++++-- composer.json | 6 ++-- phpunit.sh | 2 +- src/EasyRSA.php | 68 ++++++++++++++++------------------------ src/EasyRSAInterface.php | 9 +++--- src/KeyPair.php | 58 ++++++++++++++++++++++++++++++++++ src/PrivateKey.php | 47 +++++++++++++++++++++++++++ src/PublicKey.php | 32 +++++++++++++++++++ test/EncryptionTest.php | 37 +++++++++++++--------- test/KeyPairTest.php | 32 +++++++++++++++++++ test/SignatureTest.php | 7 +++-- 12 files changed, 241 insertions(+), 70 deletions(-) create mode 100644 src/KeyPair.php create mode 100644 src/PrivateKey.php create mode 100644 src/PublicKey.php create mode 100644 test/KeyPairTest.php diff --git a/.travis.yml b/.travis.yml index 77a60dd..582812e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ php: - "7.0" - "5.6" -- "5.5" -- "5.4" - "hhvm" sudo: false diff --git a/README.md b/README.md index 22fe5bd..0337227 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,19 @@ EasyRSA is MIT licensed and brought to you by the secure PHP development team at You can generate 2048-bit keys (or larger) using EasyRSA. The default size is 2048. ```php -use \ParagonIE\EasyRSA\EasyRSA; +use \ParagonIE\EasyRSA\KeyPair; + +KeyPair::generateKeyPair(4096); + $secretKey = $keyPair->getPrivateKey(); + $publicKey = $keyPair->getPublicKey(); -list($secretKey, $publicKey) = EasyRSA::generateKeyPair(4096); ``` ### Encrypting/Decrypting a Message ```php +use \ParagonIE\EasyRSA\EasyRSA; + $ciphertext = EasyRSA::encrypt($message, $publicKey); $plaintext = EasyRSA::decrypt($ciphertext, $secretKey); @@ -47,6 +52,8 @@ $plaintext = EasyRSA::decrypt($ciphertext, $secretKey); ### Signing/Verifying a Message ```php +use \ParagonIE\EasyRSA\EasyRSA; + $signature = EasyRSA::sign($message, $secretKey); if (EasyRSA::verify($message, $signature, $publicKey)) { diff --git a/composer.json b/composer.json index fb88c0d..de51283 100644 --- a/composer.json +++ b/composer.json @@ -16,8 +16,10 @@ }, "require": { "phpseclib/phpseclib": "^2.0", - "defuse/php-encryption": "^1.2", - "sarciszewski/php-future": "^0.4" + "defuse/php-encryption": "^2.0", + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1|^2", + "sarciszewski/php-future": "^0" }, "require-dev": { "phpunit/phpunit": "4.*|5.*" diff --git a/phpunit.sh b/phpunit.sh index 8e3c4e7..82a3dfb 100644 --- a/phpunit.sh +++ b/phpunit.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -php vendor/bin/phpunit test +vendor/bin/phpunit test if [ $? -ne 0 ]; then # Test failure exit 1 diff --git a/src/EasyRSA.php b/src/EasyRSA.php index eed5933..30d37c8 100644 --- a/src/EasyRSA.php +++ b/src/EasyRSA.php @@ -4,35 +4,18 @@ // PHPSecLib: use \phpseclib\Crypt\RSA; // defuse/php-encryption: -use \Crypto; +use \ParagonIE\ConstantTime\Base64; +use \Defuse\Crypto\Key; +use \Defuse\Crypto\Crypto; // Typed Exceptions: use \ParagonIE\EasyRSA\Exception\InvalidChecksumException; use \ParagonIE\EasyRSA\Exception\InvalidCiphertextException; -use \ParagonIE\EasyRSA\Exception\InvalidKeyException; class EasyRSA implements EasyRSAInterface { const SEPARATOR = '$'; const VERSION_TAG = "EzR1"; - - /** - * Generate a private/public RSA key pair - * - * @return array [private, public] - */ - public static function generateKeyPair($size = 2048) - { - if ($size < 2048) { - throw new InvalidKeyException('Key size must be at least 2048 bits.'); - } - $rsa = new RSA(); - $keypair = $rsa->createKey($size); - return array( - $keypair['privatekey'], - $keypair['publickey'] - ); - } - + /** * Encrypt a message with defuse/php-encryption, using an ephemeral key, * then encrypt the key with RSA. @@ -42,19 +25,19 @@ public static function generateKeyPair($size = 2048) * * @return string */ - public static function encrypt($plaintext, $rsaPublicKey) + public static function encrypt($plaintext, PublicKey $rsaPublicKey) { // Random encryption key - $ephemeral = Crypto::createNewRandomKey(); + $ephemeral = Key::createNewRandomKey(); // Encrypt the actual message - $symmetric = \base64_encode( - Crypto::encrypt($plaintext, $ephemeral) + $symmetric = Base64::encode( + Crypto::encrypt($plaintext, $ephemeral, true) ); // Use RSA to encrypt the encryption key $storeKey = \base64_encode( - self::rsaEncrypt($ephemeral, $rsaPublicKey) + self::rsaEncrypt($ephemeral->saveToAsciiSafeString(), $rsaPublicKey) ); $packaged = \implode(self::SEPARATOR, @@ -85,7 +68,7 @@ public static function encrypt($plaintext, $rsaPublicKey) * * @return string */ - public static function decrypt($ciphertext, $rsaPrivateKey) + public static function decrypt($ciphertext, PrivateKey $rsaPrivateKey) { $split = explode(self::SEPARATOR, $ciphertext); if (\count($split) !== 4) { @@ -103,13 +86,16 @@ public static function decrypt($ciphertext, $rsaPrivateKey) throw new InvalidChecksumException('Invalid checksum'); } - $key = self::rsaDecrypt( - \base64_decode($split[1]), - $rsaPrivateKey + $key = Key::loadFromAsciiSafeString( + self::rsaDecrypt( + Base64::decode($split[1]), + $rsaPrivateKey + ) ); return Crypto::Decrypt( - \base64_decode($split[2]), - $key + Base64::decode($split[2]), + $key, + true ); } @@ -120,13 +106,13 @@ public static function decrypt($ciphertext, $rsaPrivateKey) * @param string $rsaPrivateKey * @return string */ - public static function sign($message, $rsaPrivateKey) + public static function sign($message, PrivateKey $rsaPrivateKey) { $rsa = new RSA(); $rsa->setSignatureMode(RSA::SIGNATURE_PSS); $rsa->setMGFHash('sha256'); - $rsa->loadKey($rsaPrivateKey); + $rsa->loadKey($rsaPrivateKey->getKey()); return $rsa->sign($message); } @@ -135,16 +121,16 @@ public static function sign($message, $rsaPrivateKey) * * @param string $message * @param string $signature - * @param string $rsaPublicKey + * @param PublicKey $rsaPublicKey * @return bool */ - public static function verify($message, $signature, $rsaPublicKey) + public static function verify($message, $signature, PublicKey $rsaPublicKey) { $rsa = new RSA(); $rsa->setSignatureMode(RSA::SIGNATURE_PSS); $rsa->setMGFHash('sha256'); - $rsa->loadKey($rsaPublicKey); + $rsa->loadKey($rsaPublicKey->getKey()); return $rsa->verify($message, $signature); } @@ -155,13 +141,13 @@ public static function verify($message, $signature, $rsaPublicKey) * @param string $rsaPublicKey * @return string */ - protected static function rsaEncrypt($plaintext, $rsaPublicKey) + protected static function rsaEncrypt($plaintext, PublicKey $rsaPublicKey) { $rsa = new RSA(); $rsa->setEncryptionMode(RSA::ENCRYPTION_OAEP); $rsa->setMGFHash('sha256'); - $rsa->loadKey($rsaPublicKey); + $rsa->loadKey($rsaPublicKey->getKey()); return $rsa->encrypt($plaintext); } @@ -172,13 +158,13 @@ protected static function rsaEncrypt($plaintext, $rsaPublicKey) * @param string $rsaPrivateKey * @return string */ - protected static function rsaDecrypt($ciphertext, $rsaPrivateKey) + protected static function rsaDecrypt($ciphertext, PrivateKey $rsaPrivateKey) { $rsa = new RSA(); $rsa->setEncryptionMode(RSA::ENCRYPTION_OAEP); $rsa->setMGFHash('sha256'); - $rsa->loadKey($rsaPrivateKey); + $rsa->loadKey($rsaPrivateKey->getKey()); $return = @$rsa->decrypt($ciphertext); if ($return === false) { diff --git a/src/EasyRSAInterface.php b/src/EasyRSAInterface.php index 319e5a0..b4e1188 100644 --- a/src/EasyRSAInterface.php +++ b/src/EasyRSAInterface.php @@ -3,9 +3,8 @@ interface EasyRSAInterface { - public static function generateKeyPair(); - public static function encrypt($plaintext, $rsaPublicKey); - public static function decrypt($ciphertext, $rsaPrivateKey); - public static function sign($plaintext, $rsaPrivateKey); - public static function verify($ciphertext, $signature, $rsaPublicKey); + public static function encrypt($plaintext, PublicKey $rsaPublicKey); + public static function decrypt($ciphertext, PrivateKey $rsaPrivateKey); + public static function sign($plaintext, PrivateKey $rsaPrivateKey); + public static function verify($ciphertext, $signature, PublicKey $rsaPublicKey); } diff --git a/src/KeyPair.php b/src/KeyPair.php new file mode 100644 index 0000000..688cf2a --- /dev/null +++ b/src/KeyPair.php @@ -0,0 +1,58 @@ +privateKey = $privateKey; + if (!$publicKey) { + $publicKey = $this->privateKey->getPublicKey(); + } + $this->publicKey = $publicKey; + } + + /** + * Generate a private/public RSA key pair + * + * @param int $size Key size + * @param string $passphrase Optional - password-protected private key + * + * @return self + * @throws InvalidKeyException + */ + public static function generateKeyPair($size = 2048) + { + if ($size < 2048) { + throw new InvalidKeyException('Key size must be at least 2048 bits.'); + } + $rsa = new RSA(); + $keypair = $rsa->createKey($size); + return new KeyPair( + new PrivateKey($keypair['privatekey']), + new PublicKey($keypair['publickey']) + ); + } + + /** + * @return PublicKey + */ + public function getPublicKey() + { + return $this->publicKey; + } + + /** + * @return PrivateKey + */ + public function getPrivateKey() + { + return $this->privateKey; + } +} \ No newline at end of file diff --git a/src/PrivateKey.php b/src/PrivateKey.php new file mode 100644 index 0000000..e72424c --- /dev/null +++ b/src/PrivateKey.php @@ -0,0 +1,47 @@ +keyMaterial = $string; + } + + /** + * @return array + */ + public function __debugInfo() + { + return []; + } + + /** + * return PublicKey + */ + public function getPublicKey() + { + $res = \openssl_pkey_get_private($this->keyMaterial); + $pubkey = \openssl_pkey_get_details($res); + $public = \rtrim( + \str_replace("\n", "\r\n", $pubkey['key']), + "\r\n" + ); + return new PublicKey($public); + } + + /** + * @return string + */ + public function getKey() + { + return $this->keyMaterial; + } +} \ No newline at end of file diff --git a/src/PublicKey.php b/src/PublicKey.php new file mode 100644 index 0000000..e1c770b --- /dev/null +++ b/src/PublicKey.php @@ -0,0 +1,32 @@ +keyMaterial = $string; + } + + /** + * @return array + */ + public function __debugInfo() + { + return []; + } + + /** + * @return string + */ + public function getKey() + { + return $this->keyMaterial; + } +} diff --git a/test/EncryptionTest.php b/test/EncryptionTest.php index 4c359d2..5ee860b 100644 --- a/test/EncryptionTest.php +++ b/test/EncryptionTest.php @@ -1,11 +1,15 @@ getPrivateKey(); + $publicKey = $keyPair->getPublicKey(); $plain = str_repeat( 'This is a relatively long plaintext message, far longer than RSA could safely encrypt directly.' . "\n", @@ -16,13 +20,15 @@ public function testEncrypt() $dissect = explode('$', $encrypt); $this->assertEquals(EasyRSA::VERSION_TAG, $dissect[0]); - $this->assertTrue($decrypt === $plain); + $this->assertEquals($decrypt, $plain); - $size = strlen($plain) + (16 - (strlen($plain) % 16)); + $size = strlen($plain); + $size += 4; // Header $size += 16; // IV + $size += 32; // HHKF Salt $size += 32; // HMAC $this->assertEquals( - strlen(base64_decode($dissect[2])), + strlen(Base64::decode($dissect[2])), $size ); } @@ -30,12 +36,14 @@ public function testEncrypt() public function testFailure() { try { - list($secretKey, $publicKey) = EasyRSA::generateKeyPair(1024); + KeyPair::generateKeyPair(1024); $this->fail('Accepts too small of a key size!'); return; - } catch (\Exception $e) { - list($secretKey, $publicKey) = EasyRSA::generateKeyPair(2048); + } catch (\Exception $ex) { + $keyPair = KeyPair::generateKeyPair(2048); } + $secretKey = $keyPair->getPrivateKey(); + $publicKey = $keyPair->getPublicKey(); $plain = 'Short Message'; $encrypt = EasyRSA::encrypt($plain, $publicKey); @@ -50,9 +58,8 @@ public function testFailure() ); $dissect[1] = base64_encode($dissect[1]); try { - $dummy = EasyRSA::decrypt(implode('$', $dissect), $secretKey); + EasyRSA::decrypt(implode('$', $dissect), $secretKey); $this->fail('Checksum collision or logic error.'); - unset($dummy); return; } catch (\Exception $ex) { $this->assertInstanceOf('\ParagonIE\EasyRSA\Exception\InvalidChecksumException', $ex); @@ -64,7 +71,7 @@ public function testFailure() ); try { - $dummy = EasyRSA::decrypt(implode('$', $dissect), $secretKey); + EasyRSA::decrypt(implode('$', $dissect), $secretKey); $this->fail('This should not have passed.'); } catch (\Exception $ex) { $this->assertInstanceOf('\ParagonIE\EasyRSA\Exception\InvalidCiphertextException', $ex); @@ -80,7 +87,7 @@ public function testFailure() $dissect[2][$l] = \chr( \ord($dissect[2][$l]) ^ (1 << mt_rand(0, 7)) ); - $dissect[2] = base64_encode($dissect[2]); + $dissect[2] = Base64::encode($dissect[2]); try { $dummy = EasyRSA::decrypt(implode('$', $dissect), $secretKey); $this->fail('Checksum collision or logic error.'); @@ -94,12 +101,12 @@ public function testFailure() 0, 16 ); - + try { - $decrypt = EasyRSA::decrypt(implode('$', $dissect), $secretKey); + EasyRSA::decrypt(implode('$', $dissect), $secretKey); $this->fail('This should not have passed.'); } catch (\Exception $ex) { - $this->assertInstanceOf('\\InvalidCiphertextException', $ex); + $this->assertInstanceOf('\Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException', $ex); } } -} \ No newline at end of file +} diff --git a/test/KeyPairTest.php b/test/KeyPairTest.php new file mode 100644 index 0000000..643aecc --- /dev/null +++ b/test/KeyPairTest.php @@ -0,0 +1,32 @@ +getPrivateKey(); + $public = $kp->getPublicKey(); + $this->assertEquals( + $kp->getPublicKey()->getKey(), + $public->getKey() + ); + + $this->assertEquals( + $private->getPublicKey()->getKey(), + $public->getKey() + ); + + $kp2 = new KeyPair($private); + $this->assertEquals( + $kp->getPublicKey()->getKey(), + $kp2->getPublicKey()->getKey() + ); + + $this->assertEquals( + $kp2->getPublicKey()->getKey(), + $public->getKey() + ); + } +} \ No newline at end of file diff --git a/test/SignatureTest.php b/test/SignatureTest.php index 3c553dc..2169036 100644 --- a/test/SignatureTest.php +++ b/test/SignatureTest.php @@ -1,11 +1,14 @@ getPrivateKey(); + $publicKey = $keyPair->getPublicKey(); $plain = 'This is a message.'; $signature = EasyRSA::sign($plain, $secretKey);