From d5955ca67af7752f954970085b119d87c2048247 Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Tue, 9 Aug 2016 15:29:25 +0200 Subject: [PATCH 1/9] Added the first draft of hybrid encryption --- src/Hybrid.php | 123 ++++++++++++++++++++++++++++++++++++++++++++ test/HybridTest.php | 70 +++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/Hybrid.php create mode 100644 test/HybridTest.php diff --git a/src/Hybrid.php b/src/Hybrid.php new file mode 100644 index 0000000..dea49d3 --- /dev/null +++ b/src/Hybrid.php @@ -0,0 +1,123 @@ +bCipher = (null === $bCipher ) ? BlockCipher::factory('openssl') : $bCipher; + $this->rsa = (null === $rsa ) ? new PublicKey\Rsa() : $rsa; + } + + /** + * Encrypt using a keyrings + * + * @param string $plaintext + * @param array|string $keys + * @return string + * @throws RuntimeException + */ + public function encrypt(string $plaintext, $keys = null) + { + // generate a random session key + $sessionKey = Rand::getBytes($this->bCipher->getCipher()->getKeySize()); + + // encrypt the plaintext with blockcipher algorithm + $this->bCipher->setKey($sessionKey); + $ciphertext = $this->bCipher->encrypt($plaintext); + + if (null === $keys || is_string($keys)) { + $keys = [ '' => $keys ]; + } + + $encKeys = ''; + // encrypt the session key with public keys + foreach ($keys as $id => $pubkey) { + if (is_string($pubkey)) { + $pubkey = new PubKey($pubkey); + } elseif (!($pubkey instanceof PubKey)) { + throw new Exception\RuntimeException(sprintf( + "The public key must be an instance of %s", PubKey::class + )); + } + $encKeys .= sprintf( + "%s:%s:", + base64_encode($id), + base64_encode($this->rsa->encrypt($sessionKey, $pubkey)) + ); + } + return $encKeys . ';' . $ciphertext; + } + + /** + * Decrypt usign a private key + * + * @param string $msg + * @param string $privateKey + * @param string $id + * @return string + * @throws RuntimeException + */ + public function decrypt(string $msg, string $privateKey = null, string $id = '') + { + // get the session key + list($encKeys, $ciphertext) = explode(';', $msg, 2); + + $keys = explode(':', $encKeys); + $pos = array_search(base64_encode($id), $keys); + if (false === $pos) { + throw new Exception\RuntimeException( + "The private key is not valid" + ); + } + + $privKey = new PriKey($privateKey); + // decrypt the session key with privateKey + $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privKey); + + // decrypt the plaintext with the blockcipher algorithm + $this->bCipher->setKey($sessionKey); + return $this->bCipher->decrypt($ciphertext, $sessionKey); + } + + /** + * Get the BlockCipher adapter + * + * @return BlockCipher + */ + public function getBlockCipherInstance() + { + return $this->bCipher; + } + + /** + * Get the Rsa instance + * + * @return Rsa + */ + public function getRsaInstance() + { + return $this->rsa; + } +} diff --git a/test/HybridTest.php b/test/HybridTest.php new file mode 100644 index 0000000..c7e3938 --- /dev/null +++ b/test/HybridTest.php @@ -0,0 +1,70 @@ +markTestSkipped('The OpenSSL extension is required'); + } + $this->hybrid = new Hybrid(); + } + + public function testConstructor() + { + $hybrid = new Hybrid(); + $this->assertInstanceOf(Hybrid::class, $hybrid); + } + + public function testGetDefaultBlockCipherInstance() + { + $bCipher = $this->hybrid->getBlockCipherInstance(); + $this->assertInstanceOf(BlockCipher::class, $bCipher); + } + + public function testGetDefaultRsaInstance() + { + $rsa = $this->hybrid->getRsaInstance(); + $this->assertInstanceOf(Rsa::class, $rsa); + } + + public function testEncryptDecryptWithOneKey() + { + $keys = openssl_pkey_new(array( + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + )); + + // Get the public and private key as string (PEM format) + $details = openssl_pkey_get_details($keys); + $publicKey = $details['key']; + openssl_pkey_export($keys, $privateKey); + + $result = $this->hybrid->encrypt('test', $publicKey); + $this->assertEquals('test', $this->hybrid->decrypt($result, $privateKey)); + } + + public function testEncryptWithMultipleKeys() + { + + } + +} From 69bd9da81f7749afddc48c92254dcb23875f3ac0 Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Tue, 9 Aug 2016 15:57:06 +0200 Subject: [PATCH 2/9] Fixed the type hint for PHP 5 --- .gitignore | 1 + src/Hybrid.php | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 673fe32..f146c86 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ tmp/ zf-mkdoc-theme/ clover.xml +composer.lock coveralls-upload.json phpunit.xml vendor diff --git a/src/Hybrid.php b/src/Hybrid.php index dea49d3..2c55661 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -5,7 +5,14 @@ use Zend\Crypt\PublicKey\Rsa\PublicKey as PubKey; use Zend\Crypt\PublicKey\Rsa\PrivateKey as PriKey; - +/** + * Hybrid encryption (OpenPGP like) + * + * The data are encrypted using a BlockCipher with a random session key + * that is encrypted using RSA with the public key of the receiver. + * The decryption process retrieves the session key using RSA with the private + * key of the receiver and decrypt the data using the BlockCipher. + */ class Hybrid { /** @@ -38,7 +45,7 @@ public function __construct(BlockCipher $bCipher = null, Rsa $rsa = null) * @return string * @throws RuntimeException */ - public function encrypt(string $plaintext, $keys = null) + public function encrypt($plaintext, $keys = null) { // generate a random session key $sessionKey = Rand::getBytes($this->bCipher->getCipher()->getKeySize()); @@ -79,7 +86,7 @@ public function encrypt(string $plaintext, $keys = null) * @return string * @throws RuntimeException */ - public function decrypt(string $msg, string $privateKey = null, string $id = '') + public function decrypt( $msg, $privateKey = null, $id = null) { // get the session key list($encKeys, $ciphertext) = explode(';', $msg, 2); From 3c758e4b62093454c04015a54689dacbf1fafc18 Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Tue, 9 Aug 2016 16:24:57 +0200 Subject: [PATCH 3/9] Fixed CS issues --- src/Hybrid.php | 7 ++++--- test/HybridTest.php | 8 +++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Hybrid.php b/src/Hybrid.php index 2c55661..79a35a6 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -55,7 +55,7 @@ public function encrypt($plaintext, $keys = null) $ciphertext = $this->bCipher->encrypt($plaintext); if (null === $keys || is_string($keys)) { - $keys = [ '' => $keys ]; + $keys = [ '' => $keys ]; } $encKeys = ''; @@ -65,7 +65,8 @@ public function encrypt($plaintext, $keys = null) $pubkey = new PubKey($pubkey); } elseif (!($pubkey instanceof PubKey)) { throw new Exception\RuntimeException(sprintf( - "The public key must be an instance of %s", PubKey::class + "The public key must be an instance of %s", + PubKey::class )); } $encKeys .= sprintf( @@ -86,7 +87,7 @@ public function encrypt($plaintext, $keys = null) * @return string * @throws RuntimeException */ - public function decrypt( $msg, $privateKey = null, $id = null) + public function decrypt($msg, $privateKey = null, $id = null) { // get the session key list($encKeys, $ciphertext) = explode(';', $msg, 2); diff --git a/test/HybridTest.php b/test/HybridTest.php index c7e3938..f8fdb2d 100644 --- a/test/HybridTest.php +++ b/test/HybridTest.php @@ -23,7 +23,7 @@ class HybridTest extends \PHPUnit_Framework_TestCase public function setUp() { if (!extension_loaded('openssl')) { - $this->markTestSkipped('The OpenSSL extension is required'); + $this->markTestSkipped('The OpenSSL extension is required'); } $this->hybrid = new Hybrid(); } @@ -48,10 +48,10 @@ public function testGetDefaultRsaInstance() public function testEncryptDecryptWithOneKey() { - $keys = openssl_pkey_new(array( + $keys = openssl_pkey_new([ "private_key_bits" => 1024, "private_key_type" => OPENSSL_KEYTYPE_RSA, - )); + ]); // Get the public and private key as string (PEM format) $details = openssl_pkey_get_details($keys); @@ -64,7 +64,5 @@ public function testEncryptDecryptWithOneKey() public function testEncryptWithMultipleKeys() { - } - } From 201427445cb548fb3d5033ebe14a47895e4a7d9f Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Tue, 9 Aug 2016 17:24:50 +0200 Subject: [PATCH 4/9] Completed the unit tests for hybrid mode --- src/Hybrid.php | 10 +-- test/HybridTest.php | 145 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 143 insertions(+), 12 deletions(-) diff --git a/src/Hybrid.php b/src/Hybrid.php index 79a35a6..5eeea2a 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -3,7 +3,7 @@ use Zend\Math\Rand; use Zend\Crypt\PublicKey\Rsa\PublicKey as PubKey; -use Zend\Crypt\PublicKey\Rsa\PrivateKey as PriKey; +use Zend\Crypt\PublicKey\Rsa\PrivateKey; /** * Hybrid encryption (OpenPGP like) @@ -54,7 +54,7 @@ public function encrypt($plaintext, $keys = null) $this->bCipher->setKey($sessionKey); $ciphertext = $this->bCipher->encrypt($plaintext); - if (null === $keys || is_string($keys)) { + if (!is_array($keys)) { $keys = [ '' => $keys ]; } @@ -65,7 +65,7 @@ public function encrypt($plaintext, $keys = null) $pubkey = new PubKey($pubkey); } elseif (!($pubkey instanceof PubKey)) { throw new Exception\RuntimeException(sprintf( - "The public key must be an instance of %s", + "The public key must be a string in PEM format or an instance of %s", PubKey::class )); } @@ -96,11 +96,11 @@ public function decrypt($msg, $privateKey = null, $id = null) $pos = array_search(base64_encode($id), $keys); if (false === $pos) { throw new Exception\RuntimeException( - "The private key is not valid" + "This private key cannot be used for decryption" ); } - $privKey = new PriKey($privateKey); + $privKey = new PrivateKey($privateKey); // decrypt the session key with privateKey $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privKey); diff --git a/test/HybridTest.php b/test/HybridTest.php index f8fdb2d..cf801bc 100644 --- a/test/HybridTest.php +++ b/test/HybridTest.php @@ -46,23 +46,154 @@ public function testGetDefaultRsaInstance() $this->assertInstanceOf(Rsa::class, $rsa); } - public function testEncryptDecryptWithOneKey() + public function testEncryptDecryptWithOneStringKey() { - $keys = openssl_pkey_new([ + $opensslKeys = openssl_pkey_new([ "private_key_bits" => 1024, "private_key_type" => OPENSSL_KEYTYPE_RSA, ]); // Get the public and private key as string (PEM format) - $details = openssl_pkey_get_details($keys); + $details = openssl_pkey_get_details($opensslKeys); $publicKey = $details['key']; - openssl_pkey_export($keys, $privateKey); + openssl_pkey_export($opensslKeys, $privateKey); - $result = $this->hybrid->encrypt('test', $publicKey); - $this->assertEquals('test', $this->hybrid->decrypt($result, $privateKey)); + $encrypted = $this->hybrid->encrypt('test', $publicKey); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); + $this->assertEquals('test', $plaintext); } - public function testEncryptWithMultipleKeys() + public function testEncryptWithMultipleStringKeys() { + $publicKeys = []; + $privateKeys = []; + for ($id = 0; $id < 5; $id++) { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + $details = openssl_pkey_get_details($opensslKeys); + $publicKeys[$id] = $details['key']; + openssl_pkey_export($opensslKeys, $privateKeys[$id]); + } + + $encrypted = $this->hybrid->encrypt('test', $publicKeys); + for ($id = 0; $id < 5; $id++) { + $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], $id); + $this->assertEquals('test', $plaintext); + } + } + + public function testEncryptDecryptWithOneObjectKey() + { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + + // Get the public and private key as Zend\Crypt\PublicKey\Rsa objects + $details = openssl_pkey_get_details($opensslKeys); + $publicKey = new Rsa\PublicKey($details['key']); + openssl_pkey_export($opensslKeys, $privateKey); + $privateKey = new Rsa\PrivateKey($privateKey); + + $encrypted = $this->hybrid->encrypt('test', $publicKey); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); + $this->assertEquals('test', $plaintext); + } + + public function testEncryptWithMultipleObjectKeys() + { + $publicKeys = []; + $privateKeys = []; + for ($id = 0; $id < 5; $id++) { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + $details = openssl_pkey_get_details($opensslKeys); + $publicKeys[$id] = new Rsa\PublicKey($details['key']); + openssl_pkey_export($opensslKeys, $privateKey); + $privateKeys[$id] = new Rsa\PrivateKey($privateKey); + } + + $encrypted = $this->hybrid->encrypt('test', $publicKeys); + for ($id = 0; $id < 5; $id++) { + $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], $id); + $this->assertEquals('test', $plaintext); + } + } + + /** + * @expectedException Zend\Crypt\Exception\RuntimeException + */ + public function testFailToDecryptWithOneKey() + { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + + // Get the public and private key as string (PEM format) + $details = openssl_pkey_get_details($opensslKeys); + $publicKey = $details['key']; + + // Generate a new public/private key + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + openssl_pkey_export($opensslKeys, $privateKey); + + // encrypt using a single key + $encrypted = $this->hybrid->encrypt('test', $publicKey); + // try to decrypt using a different private key throws an exception + $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); + } + + /** + * @expectedException Zend\Crypt\Exception\RuntimeException + */ + public function testFailToDecryptWithMultipleKeys() + { + $publicKeys = []; + $privateKeys = []; + for ($id = 0; $id < 5; $id++) { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + $details = openssl_pkey_get_details($opensslKeys); + $publicKeys[$id] = $details['key']; + openssl_pkey_export($opensslKeys, $privateKeys[$id]); + } + + // Generate a new public/private key + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + openssl_pkey_export($opensslKeys, $privateKey); + + // encrypt using a keyrings + $encrypted = $this->hybrid->encrypt('test', $publicKeys); + // try to decrypt using a different private key throws an exception + $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys, $id); + } + + /** + * @expectedException Zend\Crypt\Exception\RuntimeException + */ + public function testFailToEncryptUsingPrivateKey() + { + $opensslKeys = openssl_pkey_new([ + "private_key_bits" => 1024, + "private_key_type" => OPENSSL_KEYTYPE_RSA, + ]); + openssl_pkey_export($opensslKeys, $privateKey); + $privateKey = new Rsa\PrivateKey($privateKey); + + // encrypt using a PrivateKey object throws an exception + $encrypted = $this->hybrid->encrypt('test', $privateKey); } } From 9730ebbb533a4f3f09aee16f1f2ce3342c6fc7f3 Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Wed, 10 Aug 2016 15:25:47 +0200 Subject: [PATCH 5/9] Added the doc + refactor unit tests --- README.md | 3 +- doc/book/hybrid.md | 119 +++++++++++++++++++++++ doc/book/images/zend.crypt.hybrid.png | Bin 0 -> 61734 bytes doc/book/public-key.md | 2 +- mkdocs.yml | 1 + src/Hybrid.php | 32 ++++-- test/HybridTest.php | 134 +++++++++++--------------- 7 files changed, 200 insertions(+), 91 deletions(-) create mode 100644 doc/book/hybrid.md create mode 100644 doc/book/images/zend.crypt.hybrid.png diff --git a/README.md b/README.md index 6dafc3f..af7b9e2 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,12 @@ [![Coverage Status](https://coveralls.io/repos/zendframework/zend-crypt/badge.svg?branch=master)](https://coveralls.io/r/zendframework/zend-crypt?branch=master) `Zend\Crypt` provides support of some cryptographic tools. -The available features are: +Some of the available features are: - encrypt-then-authenticate using symmetric ciphers (the authentication step is provided using HMAC); - encrypt/decrypt using symmetric and public key algorithm (e.g. RSA algorithm); +- encrypt/decrypt using hybrid mode (OpenPHP like); - generate digital sign using public key algorithm (e.g. RSA algorithm); - key exchange using the Diffie-Hellman method; - key derivation function (e.g. using PBKDF2 algorithm); diff --git a/doc/book/hybrid.md b/doc/book/hybrid.md new file mode 100644 index 0000000..56b78e0 --- /dev/null +++ b/doc/book/hybrid.md @@ -0,0 +1,119 @@ +# Encrypt and decrypt using hybrid cryptosystem + +Hybrid is an encryption mode that uses symmetric and public keys ciphers together. +The idea is to take the advantages of the public key cryptography for sharing the +keys and the speed of symmmetric encryption to encrypt the message. + +The hybrid mode is able to encrypt message for one or more receivers and can be +used in multi user scenario, where you can limit the decryption only for some users. + +## How it works + +Suppose we have two users: *Alice* and *Bob*. *Alice* wants to send a message to *Bob* +using an hybrid cryptosystem, she needs to: + +- Obtain *Bob*'s public key; +- Generates a random session key (one-time pad); +- Encrypts the message using a symmetric cipher with the previous session key; +- Encrypts the session key using the *Bob*'s public key; +- Send both of these encryptions to *Bob*. + +A schema of the encryption is reported in the image below: + +![Encryption schema](images/zend.crypt.hybrid.png) + +To decrypt the message *Bob* needs to: + +- Uses his private key to decrypt the session key; +- Uses this session key to decrypt the message. + +## Example of usage + +In order to use the `Zend\Crypt\Hybrid` component you need to have a keyring of +public and private keys. To encrypt a message you can use the following code: + +```php +use Zend\Crypt\Hybrid; +use Zend\Crypt\PublicKey\RsaOptions; + +// Generate public and private key +$rsaOptions = new RsaOptions([ + 'pass_phrase' => 'test' +]); +$rsaOptions->generateKeys([ + 'private_key_bits' => 4096 +]); +$publicKey = $rsaOptions->getPublicKey(); +$privateKey = $rsaOptions->getPrivateKey(); + +$hybrid = new Hybrid(); +$ciphertext = $hybrid->encrypt('message', $publicKey); +$plaintext = $hybrid->decrypt($ciphertext, $privateKey); + +printf($plaintext === 'message' ? "Success\n" : "Error\n"); +``` + +We generated the keys using the [Zend\Crypt\PublicKey\RsaOptions](public-key.md) +component. You can also use a [PEM](https://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail) +strings for the keys. If you use a string for the private key you need to pass +the pass-phrase for decrypt, if present, like in the following example: + +```php +use Zend\Crypt\Hybrid; +use Zend\Crypt\PublicKey\RsaOptions; + +// Generate public and private key +$rsaOptions = new RsaOptions([ + 'pass_phrase' => 'test' +]); +$rsaOptions->generateKeys([ + 'private_key_bits' => 4096 +]); +// Strings in PEM format +$publicKey = $rsaOptions->getPublicKey()->toString(); +$privateKey = $rsaOptions->getPrivateKey()->toString(); + +$hybrid = new Hybrid(); +$ciphertext = $hybrid->encrypt('message', $publicKey); +$plaintext = $hybrid->decrypt($ciphertext, $privateKey, 'test'); // pass-phrase + +printf($plaintext === 'message' ? "Success\n" : "Error\n"); +``` + +The `Hybrid` component uses the `Zend\Crypt\BlockCipher` for the symmetric +cipher and the `Zend\Crypt\Rsa` for the public-key cipher. + +## Encrypt with multiple keys + +The `Zend\Crypt\Hybrid` component can be used to encrypt a message for multiple +users, using a keyring of Ids and public keys. This keyring can be specified using +an array of `[ 'id' => 'publickey' ]`, where `publickey` can be a string (PEM) +or an instance of `Zend\Crypt\PublicKey\Rsa\PublicKey`. The `id` can be any +string, for instance the email address of the users. + +Here is reported an example of encryption using a keyring of 4 keys: + +```php +use Zend\Crypt\Hybrid; +use Zend\Crypt\PublicKey\RsaOptions; + +$publicKeys = []; +$privateKeys = []; +for ($id = 0; $id < 4; $id++) { + $rsaOptions = new RsaOptions([ + 'pass_phrase' => "test-$id" + ]); + $rsaOptions->generateKeys([ + 'private_key_bits' => 4096 + ]); + $publicKeys[$id] = $rsaOptions->getPublicKey(); + $privateKeys[$id] = $rsaOptions->getPrivateKey(); +} + +$hybrid = new Hybrid(); +$encrypted = $hybrid->encrypt('message', $publicKeys); +for ($id = 0; $id < 4; $id++) { + $plaintext = $hybrid->decrypt($encrypted, $privateKeys[$id], null, $id); + printf($plaintext === 'message' ? "Success on %d\n" : "Error on %d\n", $id); +} +``` diff --git a/doc/book/images/zend.crypt.hybrid.png b/doc/book/images/zend.crypt.hybrid.png new file mode 100644 index 0000000000000000000000000000000000000000..5719d16a15220e7e387d3435c8e0d086d3bcc73d GIT binary patch literal 61734 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 Z!29gnP!&~s@eKktCAk-}m2i{b{{c_iY4QL7 literal 0 HcmV?d00001 diff --git a/doc/book/public-key.md b/doc/book/public-key.md index 9e33222..80820ac 100644 --- a/doc/book/public-key.md +++ b/doc/book/public-key.md @@ -146,7 +146,7 @@ use Zend\Crypt\PublicKey\RsaOptions; $rsaOptions = new RsaOptions([ 'pass_phrase' => 'test' -[); +]); $rsaOptions->generateKeys([ 'private_key_bits' => 2048, diff --git a/mkdocs.yml b/mkdocs.yml index 0ed2702..a205d69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,6 +6,7 @@ pages: - Reference: - { 'Block Ciphers': block-cipher.md } - { 'Encrypting Files': files.md } + - { 'Hybrid cryptosystem': hybrid.md } - { 'Key Derivation': key-derivation.md } - { Passwords: password.md } - { 'Public Key Cryptography': public-key.md } diff --git a/src/Hybrid.php b/src/Hybrid.php index 5eeea2a..dce4319 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -61,13 +61,15 @@ public function encrypt($plaintext, $keys = null) $encKeys = ''; // encrypt the session key with public keys foreach ($keys as $id => $pubkey) { - if (is_string($pubkey)) { - $pubkey = new PubKey($pubkey); - } elseif (!($pubkey instanceof PubKey)) { - throw new Exception\RuntimeException(sprintf( - "The public key must be a string in PEM format or an instance of %s", - PubKey::class - )); + if (! $pubkey instanceof PubKey) { + if (is_string($pubkey)) { + $pubkey = new PubKey($pubkey); + } else { + throw new Exception\RuntimeException(sprintf( + "The public key must be a string in PEM format or an instance of %s", + PubKey::class + )); + } } $encKeys .= sprintf( "%s:%s:", @@ -83,11 +85,12 @@ public function encrypt($plaintext, $keys = null) * * @param string $msg * @param string $privateKey + * @param string $passPhrase * @param string $id * @return string * @throws RuntimeException */ - public function decrypt($msg, $privateKey = null, $id = null) + public function decrypt($msg, $privateKey = null, $passPhrase = null, $id = null) { // get the session key list($encKeys, $ciphertext) = explode(';', $msg, 2); @@ -100,9 +103,18 @@ public function decrypt($msg, $privateKey = null, $id = null) ); } - $privKey = new PrivateKey($privateKey); + if (! $privateKey instanceof PrivateKey) { + if (is_string($privateKey)) { + $privateKey = new PrivateKey($privateKey, $passPhrase); + } else { + throw new Exception\RuntimeException(sprintf( + "The private key must be a string in PEM format or an instance of %s", + PrivateKey::class + )); + } + } // decrypt the session key with privateKey - $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privKey); + $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privateKey); // decrypt the plaintext with the blockcipher algorithm $this->bCipher->setKey($sessionKey); diff --git a/test/HybridTest.php b/test/HybridTest.php index cf801bc..4dafdb1 100644 --- a/test/HybridTest.php +++ b/test/HybridTest.php @@ -12,6 +12,7 @@ use Zend\Crypt\Hybrid; use Zend\Crypt\BlockCipher; use Zend\Crypt\PublicKey\Rsa; +use Zend\Crypt\PublicKey\RsaOptions; /** * @group Zend_Crypt @@ -48,54 +49,64 @@ public function testGetDefaultRsaInstance() public function testEncryptDecryptWithOneStringKey() { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - - // Get the public and private key as string (PEM format) - $details = openssl_pkey_get_details($opensslKeys); - $publicKey = $details['key']; - openssl_pkey_export($opensslKeys, $privateKey); + $publicKey = $rsaOptions->getPublicKey()->toString(); + $privateKey = $rsaOptions->getPrivateKey()->toString(); $encrypted = $this->hybrid->encrypt('test', $publicKey); $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); $this->assertEquals('test', $plaintext); } + public function testEncryptDecryptWithOneStringKeyAndPassphrase() + { + $passPhrase = 'test'; + $rsaOptions = new RsaOptions([ + 'pass_phrase' => $passPhrase + ]); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $publicKey = $rsaOptions->getPublicKey()->toString(); + $privateKey = $rsaOptions->getPrivateKey()->toString(); + + $encrypted = $this->hybrid->encrypt('test', $publicKey); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKey, $passPhrase); + $this->assertEquals('test', $plaintext); + } + public function testEncryptWithMultipleStringKeys() { $publicKeys = []; $privateKeys = []; + $rsaOptions = new RsaOptions(); + for ($id = 0; $id < 5; $id++) { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - $details = openssl_pkey_get_details($opensslKeys); - $publicKeys[$id] = $details['key']; - openssl_pkey_export($opensslKeys, $privateKeys[$id]); + $publicKeys[$id] = $rsaOptions->getPublicKey()->toString(); + $privateKeys[$id] = $rsaOptions->getPrivateKey()->toString(); } $encrypted = $this->hybrid->encrypt('test', $publicKeys); for ($id = 0; $id < 5; $id++) { - $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], $id); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], null, $id); $this->assertEquals('test', $plaintext); } } public function testEncryptDecryptWithOneObjectKey() { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - - // Get the public and private key as Zend\Crypt\PublicKey\Rsa objects - $details = openssl_pkey_get_details($opensslKeys); - $publicKey = new Rsa\PublicKey($details['key']); - openssl_pkey_export($opensslKeys, $privateKey); - $privateKey = new Rsa\PrivateKey($privateKey); + $publicKey = $rsaOptions->getPublicKey(); + $privateKey = $rsaOptions->getPrivateKey(); $encrypted = $this->hybrid->encrypt('test', $publicKey); $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); @@ -106,20 +117,19 @@ public function testEncryptWithMultipleObjectKeys() { $publicKeys = []; $privateKeys = []; + $rsaOptions = new RsaOptions(); + for ($id = 0; $id < 5; $id++) { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - $details = openssl_pkey_get_details($opensslKeys); - $publicKeys[$id] = new Rsa\PublicKey($details['key']); - openssl_pkey_export($opensslKeys, $privateKey); - $privateKeys[$id] = new Rsa\PrivateKey($privateKey); + $publicKeys[$id] = $rsaOptions->getPublicKey(); + $privateKeys[$id] = $rsaOptions->getPrivateKey(); } $encrypted = $this->hybrid->encrypt('test', $publicKeys); for ($id = 0; $id < 5; $id++) { - $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], $id); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys[$id], null, $id); $this->assertEquals('test', $plaintext); } } @@ -129,21 +139,16 @@ public function testEncryptWithMultipleObjectKeys() */ public function testFailToDecryptWithOneKey() { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - - // Get the public and private key as string (PEM format) - $details = openssl_pkey_get_details($opensslKeys); - $publicKey = $details['key']; - - // Generate a new public/private key - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $publicKey = $rsaOptions->getPublicKey(); + // Generate a new private key + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - openssl_pkey_export($opensslKeys, $privateKey); + $privateKey = $rsaOptions->getPrivateKey(); // encrypt using a single key $encrypted = $this->hybrid->encrypt('test', $publicKey); @@ -151,47 +156,18 @@ public function testFailToDecryptWithOneKey() $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); } - /** - * @expectedException Zend\Crypt\Exception\RuntimeException - */ - public function testFailToDecryptWithMultipleKeys() - { - $publicKeys = []; - $privateKeys = []; - for ($id = 0; $id < 5; $id++) { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, - ]); - $details = openssl_pkey_get_details($opensslKeys); - $publicKeys[$id] = $details['key']; - openssl_pkey_export($opensslKeys, $privateKeys[$id]); - } - - // Generate a new public/private key - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, - ]); - openssl_pkey_export($opensslKeys, $privateKey); - - // encrypt using a keyrings - $encrypted = $this->hybrid->encrypt('test', $publicKeys); - // try to decrypt using a different private key throws an exception - $plaintext = $this->hybrid->decrypt($encrypted, $privateKeys, $id); - } /** * @expectedException Zend\Crypt\Exception\RuntimeException */ public function testFailToEncryptUsingPrivateKey() { - $opensslKeys = openssl_pkey_new([ - "private_key_bits" => 1024, - "private_key_type" => OPENSSL_KEYTYPE_RSA, + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, ]); - openssl_pkey_export($opensslKeys, $privateKey); - $privateKey = new Rsa\PrivateKey($privateKey); + $publicKey = $rsaOptions->getPublicKey(); + $privateKey = $rsaOptions->getPrivateKey(); // encrypt using a PrivateKey object throws an exception $encrypted = $this->hybrid->encrypt('test', $privateKey); From 4ea4053dab6b1d867ab0d2ed21fa737cf38171ce Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Thu, 11 Aug 2016 12:30:34 +0200 Subject: [PATCH 6/9] Fixed typos in doc + minor refactors --- README.md | 2 +- doc/book/hybrid.md | 45 ++++++++++++++++++++++--------------------- mkdocs.yml | 2 +- src/Hybrid.php | 47 ++++++++++++++++++++++++--------------------- test/HybridTest.php | 5 +---- 5 files changed, 51 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index af7b9e2..ed5fbd6 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Some of the available features are: - encrypt-then-authenticate using symmetric ciphers (the authentication step is provided using HMAC); - encrypt/decrypt using symmetric and public key algorithm (e.g. RSA algorithm); -- encrypt/decrypt using hybrid mode (OpenPHP like); +- encrypt/decrypt using hybrid mode (OpenPGP like); - generate digital sign using public key algorithm (e.g. RSA algorithm); - key exchange using the Diffie-Hellman method; - key derivation function (e.g. using PBKDF2 algorithm); diff --git a/doc/book/hybrid.md b/doc/book/hybrid.md index 56b78e0..160231d 100644 --- a/doc/book/hybrid.md +++ b/doc/book/hybrid.md @@ -1,36 +1,37 @@ -# Encrypt and decrypt using hybrid cryptosystem +# Encrypt and decrypt using hybrid cryptosystem - Since 3.1.0 -Hybrid is an encryption mode that uses symmetric and public keys ciphers together. -The idea is to take the advantages of the public key cryptography for sharing the -keys and the speed of symmmetric encryption to encrypt the message. +Hybrid is an encryption mode that uses symmetric and public key ciphers together. +The approach takes advantage of public key cryptography for sharing keys and +symmetric encryption speed for encrypting messages. -The hybrid mode is able to encrypt message for one or more receivers and can be -used in multi user scenario, where you can limit the decryption only for some users. +Hybrid mode allows you to encrypt a message for one or more receivers, and can +be used in multi-user scenarios where you wish to limit decryption to specific +users. ## How it works Suppose we have two users: *Alice* and *Bob*. *Alice* wants to send a message to *Bob* -using an hybrid cryptosystem, she needs to: +using a hybrid cryptosystem, she needs to: - Obtain *Bob*'s public key; - Generates a random session key (one-time pad); -- Encrypts the message using a symmetric cipher with the previous session key; -- Encrypts the session key using the *Bob*'s public key; -- Send both of these encryptions to *Bob*. +- Encrypts message using a symmetric cipher with the previous session key; +- Encrypts session key using the *Bob*'s public key; +- Sends both the encrypted message and encrypted session key to *Bob*. A schema of the encryption is reported in the image below: ![Encryption schema](images/zend.crypt.hybrid.png) -To decrypt the message *Bob* needs to: +To decrypt the message, *Bob* needs to: - Uses his private key to decrypt the session key; - Uses this session key to decrypt the message. ## Example of usage -In order to use the `Zend\Crypt\Hybrid` component you need to have a keyring of -public and private keys. To encrypt a message you can use the following code: +In order to use the `Zend\Crypt\Hybrid` component, you need to have a keyring of +public and private keys. To encrypt a message, use the following code: ```php use Zend\Crypt\Hybrid; @@ -55,8 +56,8 @@ printf($plaintext === 'message' ? "Success\n" : "Error\n"); We generated the keys using the [Zend\Crypt\PublicKey\RsaOptions](public-key.md) component. You can also use a [PEM](https://en.wikipedia.org/wiki/Privacy-enhanced_Electronic_Mail) -strings for the keys. If you use a string for the private key you need to pass -the pass-phrase for decrypt, if present, like in the following example: +string for the keys. If you use a string for the private key, you need to pass +the pass phrase to use when decrypting, if present, like in the following example: ```php use Zend\Crypt\Hybrid; @@ -80,18 +81,18 @@ $plaintext = $hybrid->decrypt($ciphertext, $privateKey, 'test'); // pass-phrase printf($plaintext === 'message' ? "Success\n" : "Error\n"); ``` -The `Hybrid` component uses the `Zend\Crypt\BlockCipher` for the symmetric -cipher and the `Zend\Crypt\Rsa` for the public-key cipher. +The `Hybrid` component uses `Zend\Crypt\BlockCipher` for the symmetric +cipher and `Zend\Crypt\Rsa` for the public-key cipher. ## Encrypt with multiple keys The `Zend\Crypt\Hybrid` component can be used to encrypt a message for multiple -users, using a keyring of Ids and public keys. This keyring can be specified using -an array of `[ 'id' => 'publickey' ]`, where `publickey` can be a string (PEM) -or an instance of `Zend\Crypt\PublicKey\Rsa\PublicKey`. The `id` can be any -string, for instance the email address of the users. +users, using a keyring of identifiers and public keys. This keyring can be +specified using an array of `[ 'id' => 'publickey' ]`, where `publickey` can be +a string (PEM) or an instance of `Zend\Crypt\PublicKey\Rsa\PublicKey`. The `id` +can be any string, for example, a receipient email address. -Here is reported an example of encryption using a keyring of 4 keys: +The following details encryption using a keyring with 4 keys: ```php use Zend\Crypt\Hybrid; diff --git a/mkdocs.yml b/mkdocs.yml index a205d69..1d7060f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ pages: - Reference: - { 'Block Ciphers': block-cipher.md } - { 'Encrypting Files': files.md } - - { 'Hybrid cryptosystem': hybrid.md } + - { 'Hybrid Cryptosystem': hybrid.md } - { 'Key Derivation': key-derivation.md } - { Passwords: password.md } - { 'Public Key Cryptography': public-key.md } diff --git a/src/Hybrid.php b/src/Hybrid.php index dce4319..7681bfc 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -1,4 +1,12 @@ bCipher->setKey($sessionKey); $ciphertext = $this->bCipher->encrypt($plaintext); - if (!is_array($keys)) { - $keys = [ '' => $keys ]; + if (! is_array($keys)) { + $keys = ['' => $keys]; } $encKeys = ''; // encrypt the session key with public keys foreach ($keys as $id => $pubkey) { - if (! $pubkey instanceof PubKey) { - if (is_string($pubkey)) { - $pubkey = new PubKey($pubkey); - } else { - throw new Exception\RuntimeException(sprintf( - "The public key must be a string in PEM format or an instance of %s", - PubKey::class - )); - } + if (! $pubkey instanceof PubKey && ! is_string($pubkey)) { + throw new Exception\RuntimeException(sprintf( + "The public key must be a string in PEM format or an instance of %s", + PubKey::class + )); } + $pubkey = is_string($pubkey) ? new PubKey($pubkey) : $pubkey; $encKeys .= sprintf( "%s:%s:", base64_encode($id), @@ -81,7 +86,7 @@ public function encrypt($plaintext, $keys = null) } /** - * Decrypt usign a private key + * Decrypt using a private key * * @param string $msg * @param string $privateKey @@ -103,16 +108,14 @@ public function decrypt($msg, $privateKey = null, $passPhrase = null, $id = null ); } - if (! $privateKey instanceof PrivateKey) { - if (is_string($privateKey)) { - $privateKey = new PrivateKey($privateKey, $passPhrase); - } else { - throw new Exception\RuntimeException(sprintf( - "The private key must be a string in PEM format or an instance of %s", - PrivateKey::class - )); - } + if (! $privateKey instanceof PrivateKey && ! is_string($privateKey)) { + throw new Exception\RuntimeException(sprintf( + "The private key must be a string in PEM format or an instance of %s", + PrivateKey::class + )); } + $privateKey = is_string($privateKey) ? new PrivateKey($privateKey, $passPhrase) : $privateKey; + // decrypt the session key with privateKey $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privateKey); diff --git a/test/HybridTest.php b/test/HybridTest.php index 4dafdb1..d4489a9 100644 --- a/test/HybridTest.php +++ b/test/HybridTest.php @@ -14,16 +14,13 @@ use Zend\Crypt\PublicKey\Rsa; use Zend\Crypt\PublicKey\RsaOptions; -/** - * @group Zend_Crypt - */ class HybridTest extends \PHPUnit_Framework_TestCase { protected $hybrid; public function setUp() { - if (!extension_loaded('openssl')) { + if (! extension_loaded('openssl')) { $this->markTestSkipped('The OpenSSL extension is required'); } $this->hybrid = new Hybrid(); From 066f530a3b333d550b735dd78ca42b3a6f4bc130 Mon Sep 17 00:00:00 2001 From: Enrico Zimuel Date: Thu, 11 Aug 2016 12:38:07 +0200 Subject: [PATCH 7/9] Fixed CS issue --- src/Hybrid.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hybrid.php b/src/Hybrid.php index 7681bfc..faf6ac8 100644 --- a/src/Hybrid.php +++ b/src/Hybrid.php @@ -115,7 +115,7 @@ public function decrypt($msg, $privateKey = null, $passPhrase = null, $id = null )); } $privateKey = is_string($privateKey) ? new PrivateKey($privateKey, $passPhrase) : $privateKey; - + // decrypt the session key with privateKey $sessionKey = $this->rsa->decrypt(base64_decode($keys[$pos + 1]), $privateKey); From 73c4074591af830e5252c2f2597a04319c6f2421 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 11 Aug 2016 09:28:00 -0500 Subject: [PATCH 8/9] Re-add composer.lock - Required for the lowest/locked/latest testing strategy. --- .gitignore | 1 - composer.lock | 51 ++++++++++++++++++++++++++------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index f146c86..673fe32 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ tmp/ zf-mkdoc-theme/ clover.xml -composer.lock coveralls-upload.json phpunit.xml vendor diff --git a/composer.lock b/composer.lock index 45ad73b..6b3d967 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "bc0d86bd5b1d85d4b18c0a13f81d61c3", - "content-hash": "d55e6a1880ba1ccc366e10f2ae0a4d73", + "hash": "5a459a1e285594651261aa9c674c605c", + "content-hash": "6c91ee0aaf8627e0545b3c3efb229b8f", "packages": [ { "name": "container-interop/container-interop", @@ -686,16 +686,16 @@ }, { "name": "phpunit/phpunit", - "version": "4.8.26", + "version": "4.8.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74" + "reference": "c062dddcb68e44b563f66ee319ddae2b5a322a90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fc1d8cd5b5de11625979125c5639347896ac2c74", - "reference": "fc1d8cd5b5de11625979125c5639347896ac2c74", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c062dddcb68e44b563f66ee319ddae2b5a322a90", + "reference": "c062dddcb68e44b563f66ee319ddae2b5a322a90", "shasum": "" }, "require": { @@ -754,7 +754,7 @@ "testing", "xunit" ], - "time": "2016-05-17 03:09:28" + "time": "2016-07-21 06:48:14" }, { "name": "phpunit/phpunit-mock-objects", @@ -1186,16 +1186,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.6.1", + "version": "2.6.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "fb72ed32f8418db5e7770be1653e62e0d6f5dd3d" + "reference": "4edb770cb853def6e60c93abb088ad5ac2010c83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/fb72ed32f8418db5e7770be1653e62e0d6f5dd3d", - "reference": "fb72ed32f8418db5e7770be1653e62e0d6f5dd3d", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/4edb770cb853def6e60c93abb088ad5ac2010c83", + "reference": "4edb770cb853def6e60c93abb088ad5ac2010c83", "shasum": "" }, "require": { @@ -1260,20 +1260,20 @@ "phpcs", "standards" ], - "time": "2016-05-30 22:24:32" + "time": "2016-07-13 23:29:13" }, { "name": "symfony/yaml", - "version": "v3.1.1", + "version": "v3.1.3", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "c5a7e7fc273c758b92b85dcb9c46149ccda89623" + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/c5a7e7fc273c758b92b85dcb9c46149ccda89623", - "reference": "c5a7e7fc273c758b92b85dcb9c46149ccda89623", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1819adf2066880c7967df7180f4f662b6f0567ac", + "reference": "1819adf2066880c7967df7180f4f662b6f0567ac", "shasum": "" }, "require": { @@ -1309,32 +1309,33 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2016-06-14 11:18:07" + "time": "2016-07-17 14:02:08" }, { "name": "webmozart/assert", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde" + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", - "reference": "30eed06dd6bc88410a4ff7f77b6d22f3ce13dbde", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bb2d123231c095735130cc8f6d31385a44c7b308", + "reference": "bb2d123231c095735130cc8f6d31385a44c7b308", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3|^7.0" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.2-dev" } }, "autoload": { @@ -1358,7 +1359,7 @@ "check", "validate" ], - "time": "2015-08-24 13:29:44" + "time": "2016-08-09 15:02:57" } ], "aliases": [], From 31c40e1cd6e8ac21631f911ab48521a2adb063d4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 11 Aug 2016 09:29:41 -0500 Subject: [PATCH 9/9] Added CHANGELOG for #32 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52e6bc2..ee59ef0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ All notable changes to this project will be documented in this file, in reverse ### Added -- Nothing. +- [#32](https://github.com/zendframework/zend-crypt/pull/32) adds a new Hybrid + encryption utility, to allow OpenPGP-like encryption/decryption of messages + using OpenSSL. See the documentation for details. ### Deprecated