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 diff --git a/README.md b/README.md index 6dafc3f..ed5fbd6 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 (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/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": [], diff --git a/doc/book/hybrid.md b/doc/book/hybrid.md new file mode 100644 index 0000000..160231d --- /dev/null +++ b/doc/book/hybrid.md @@ -0,0 +1,120 @@ +# Encrypt and decrypt using hybrid cryptosystem - Since 3.1.0 + +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. + +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 a hybrid cryptosystem, she needs to: + +- Obtain *Bob*'s public key; +- Generates a random session key (one-time pad); +- 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: + +- 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, 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) +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; +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 `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 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. + +The following details encryption using a keyring with 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 0000000..5719d16 Binary files /dev/null and b/doc/book/images/zend.crypt.hybrid.png differ 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..1d7060f 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 new file mode 100644 index 0000000..faf6ac8 --- /dev/null +++ b/src/Hybrid.php @@ -0,0 +1,146 @@ +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($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 (! is_array($keys)) { + $keys = ['' => $keys]; + } + + $encKeys = ''; + // encrypt the session key with public keys + foreach ($keys as $id => $pubkey) { + 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), + base64_encode($this->rsa->encrypt($sessionKey, $pubkey)) + ); + } + return $encKeys . ';' . $ciphertext; + } + + /** + * Decrypt using a private key + * + * @param string $msg + * @param string $privateKey + * @param string $passPhrase + * @param string $id + * @return string + * @throws RuntimeException + */ + public function decrypt($msg, $privateKey = null, $passPhrase = null, $id = null) + { + // 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( + "This private key cannot be used for decryption" + ); + } + + 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); + + // 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..d4489a9 --- /dev/null +++ b/test/HybridTest.php @@ -0,0 +1,172 @@ +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 testEncryptDecryptWithOneStringKey() + { + $rsaOptions = new RsaOptions(); + $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); + $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++) { + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $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], null, $id); + $this->assertEquals('test', $plaintext); + } + } + + public function testEncryptDecryptWithOneObjectKey() + { + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $publicKey = $rsaOptions->getPublicKey(); + $privateKey = $rsaOptions->getPrivateKey(); + + $encrypted = $this->hybrid->encrypt('test', $publicKey); + $plaintext = $this->hybrid->decrypt($encrypted, $privateKey); + $this->assertEquals('test', $plaintext); + } + + public function testEncryptWithMultipleObjectKeys() + { + $publicKeys = []; + $privateKeys = []; + $rsaOptions = new RsaOptions(); + + for ($id = 0; $id < 5; $id++) { + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $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], null, $id); + $this->assertEquals('test', $plaintext); + } + } + + /** + * @expectedException Zend\Crypt\Exception\RuntimeException + */ + public function testFailToDecryptWithOneKey() + { + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $publicKey = $rsaOptions->getPublicKey(); + // Generate a new private key + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $privateKey = $rsaOptions->getPrivateKey(); + + // 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 testFailToEncryptUsingPrivateKey() + { + $rsaOptions = new RsaOptions(); + $rsaOptions->generateKeys([ + 'private_key_bits' => 1024, + ]); + $publicKey = $rsaOptions->getPublicKey(); + $privateKey = $rsaOptions->getPrivateKey(); + + // encrypt using a PrivateKey object throws an exception + $encrypted = $this->hybrid->encrypt('test', $privateKey); + } +}