diff --git a/.circleci/config.yml b/.circleci/config.yml index 2abe67fa..756b460e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,7 @@ jobs: - run: mkdir -p coverage - run: php vendor/phpunit/phpunit/phpunit --coverage-html coverage --coverage-clover=coverage.clover - run: php vendor/infection/infection/bin/infection + - run: php vendor/squizlabs/php_codesniffer/bin/phpcs src | tee codesniff.log - run: php examples/support.php - run: wget https://scrutinizer-ci.com/ocular.phar - run: php ocular.phar code-coverage:upload --format=php-clover coverage.clover @@ -25,6 +26,8 @@ jobs: path: coverage - store_artifacts: path: infection.log + - store_artifacts: + path: codesniff.log build-72: docker: diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3445f1be..c47446f2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,10 @@ # Changes in Dcrypt +## 12.0.2 +- More clarity and unity in internal API +- Add codesniffer to circle ci testing +- Lots of cs fixes + ## 12.0.1 - Much more efficient testing config - Fix spelling mistakes diff --git a/docs/KEYS.md b/docs/KEYS.md index 8279db70..6b929c95 100644 --- a/docs/KEYS.md +++ b/docs/KEYS.md @@ -1,30 +1,29 @@ # Guide to dcrypt keys Dcrypt likes __BIG__ keys. -This document explains some of the reasoning behind these design decisions and some tips on handling keys. +This document explains the reasoning behind this design decision and some tips on key management. ## Why 2048 bytes though? +The large size of key size of 2048 bytes enables dcrypt to forgo any computationally wasteful (at best) and potentially dangerous (at worst) password derivation while still providing very strong security and brute force resistance. + At 2048 bytes the probability of every byte in the 0x00 to 0xFF range being used at least once in a pseudo-random string approaches 1. -This statistical truth is exploited to prevent, with a high degree of confidence, entire classes of implementation errors like double encoding of keys. +This statistical curiosity can be leveraged to differentiate between strong and weak entropy sources. +Particularly, implementation mistakes like double encoding of the key can be rejected by ensuring that the byte stream uses most of the 2^8 keyspace. -A basic test is performed at run time to indicate whether the key is likely to be pseudo-random. -An exception is raised if the key does not pass this test. -This test is not perfect but it is simple and fast. -It may become conditional in the future. +Before encryption a basic key entropy test is performed which rejects keys that are not likely to be pseudo-random. +This test is not perfect but it is simple and fast, and it may become conditional in the future. -A generic of the randomness test is this as follows: +A generic of the randomness test is as follows: ```php ~/secret.key +head -c 2048 /dev/urandom | base64 -w 0 > /path/to/secret.key ``` PHP static function: diff --git a/docs/PHILOSOPHY.md b/docs/PHILOSOPHY.md index a7e41d85..16b0f121 100644 --- a/docs/PHILOSOPHY.md +++ b/docs/PHILOSOPHY.md @@ -1,7 +1,9 @@ # Philosophy + - Use PHP's native functions instead of polyfills - Require no external dependencies -- Use strong typing and strict type enforcement +- Use strong typing and strict type enforcement over type-checking conditional code - Make source as simple as possible to audit - Prefer clarity over backwards compatibility +- Lowest possible SLOC to achieve goals - Iterate often to obtain perfection \ No newline at end of file diff --git a/src/Aes256Ecb.php b/src/Aes256Ecb.php index 0317ce38..e2ed8feb 100644 --- a/src/Aes256Ecb.php +++ b/src/Aes256Ecb.php @@ -1,34 +1,34 @@ - - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ - -namespace Dcrypt; - -/** - * Symmetric AES-256-ECB encryption functions powered by OpenSSL. - * - * @category Dcrypt - * @package Dcrypt - * @author Michael Meyer (mmeyer2k) - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ -class Aes256Ecb extends Aes256Gcm -{ - /** - * AES-256 cipher identifier that will be passed to openssl - * - * @var string - */ - const CIPHER = 'aes-256-ecb'; + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Dcrypt; + +/** + * Symmetric AES-256-ECB encryption functions powered by OpenSSL. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ +class Aes256Ecb extends Aes256Gcm +{ + /** + * AES-256 cipher identifier that will be passed to openssl + * + * @var string + */ + const CIPHER = 'aes-256-ecb'; } \ No newline at end of file diff --git a/src/Aes256Ofb.php b/src/Aes256Ofb.php index b24c9a98..48a92f78 100644 --- a/src/Aes256Ofb.php +++ b/src/Aes256Ofb.php @@ -1,34 +1,34 @@ - - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ - -namespace Dcrypt; - -/** - * Symmetric AES-256-GCM encryption functions powered by OpenSSL. - * - * @category Dcrypt - * @package Dcrypt - * @author Michael Meyer (mmeyer2k) - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ -class Aes256Ofb extends Aes256Gcm -{ - /** - * AES-256 cipher identifier that will be passed to openssl - * - * @var string - */ - const CIPHER = 'aes-256-ofb'; -} + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Dcrypt; + +/** + * Symmetric AES-256-GCM encryption functions powered by OpenSSL. + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ +class Aes256Ofb extends Aes256Gcm +{ + /** + * AES-256 cipher identifier that will be passed to openssl + * + * @var string + */ + const CIPHER = 'aes-256-ofb'; +} diff --git a/src/Exceptions/InvalidChecksumException.php b/src/Exceptions/InvalidChecksumException.php index baa3adc3..946cc172 100644 --- a/src/Exceptions/InvalidChecksumException.php +++ b/src/Exceptions/InvalidChecksumException.php @@ -1,20 +1,29 @@ - - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ - -namespace Dcrypt\Exceptions; - -class InvalidChecksumException extends \Exception -{ - + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Dcrypt\Exceptions; + +/** + * A handler for checksum exceptions + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ +class InvalidChecksumException extends \Exception +{ + const MESSAGE = 'Invalid ciphertext checksum'; } \ No newline at end of file diff --git a/src/Exceptions/InvalidKeyException.php b/src/Exceptions/InvalidKeyException.php index f2cac3a5..eeb52dc2 100644 --- a/src/Exceptions/InvalidKeyException.php +++ b/src/Exceptions/InvalidKeyException.php @@ -1,20 +1,30 @@ - - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ - -namespace Dcrypt\Exceptions; - -class InvalidKeyException extends \Exception -{ - + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Dcrypt\Exceptions; + +/** + * A handler for key exceptions + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ +class InvalidKeyException extends \Exception +{ + const KEYLENGTH = 'Key must be at least 2048 bytes and base64 encoded'; + const KEYRANDOM = 'Key does not contain the minimum amount of entropy'; } \ No newline at end of file diff --git a/src/OneTimePad.php b/src/OneTimePad.php index 7467dd1a..ff06426d 100644 --- a/src/OneTimePad.php +++ b/src/OneTimePad.php @@ -29,17 +29,24 @@ class OneTimePad /** * Encrypt or decrypt a binary input string. * - * @param string $input Input data to encrypt - * @param string $key Encryption/decryption key to use on input - * @param string $algo Hashing algo to generate keystream + * @param string $input Input data to encrypt + * @param string $key Encryption/decryption key to use on input + * @param string $algo Hashing algo to generate keystream + * * @return string */ - public static function crypt(string $input, string $key, string $algo = 'sha3-512'): string - { + public static function crypt( + string $input, + string $key, + string $algo = 'sha3-512' + ): string { + // Split the input into chunks sized the same as the hash size $chunks = \str_split($input, Str::hashSize($algo)); + // Determine total input length $length = Str::strlen($input); + // Create a new key object $key = new OpensslKey($algo, $key, ''); foreach ($chunks as $i => &$chunk) { diff --git a/src/OpensslBridge.php b/src/OpensslBridge.php index 1a041f02..9e0a8b87 100644 --- a/src/OpensslBridge.php +++ b/src/OpensslBridge.php @@ -15,7 +15,8 @@ namespace Dcrypt; /** - * Provides functionality common to the dcrypt AES block ciphers. Extend this class to customize your cipher suite. + * Provides functionality common to the dcrypt AES block ciphers. + * Extend this class to customize your cipher suite. * * @category Dcrypt * @package Dcrypt diff --git a/src/OpensslKey.php b/src/OpensslKey.php index 09f2f5bf..fa8aa98f 100644 --- a/src/OpensslKey.php +++ b/src/OpensslKey.php @@ -1,123 +1,134 @@ - - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ - -namespace Dcrypt; - -use Dcrypt\Exceptions\InvalidKeyException; - -/** - * Provides key derivation functions - * - * @category Dcrypt - * @package Dcrypt - * @author Michael Meyer (mmeyer2k) - * @license http://opensource.org/licenses/MIT The MIT License (MIT) - * @link https://github.com/mmeyer2k/dcrypt - */ -final class OpensslKey -{ - /** - * @var string - */ - private $key; - - /** - * @var string - */ - private $algo; - - /** - * @var string - */ - private $ivr; - - /** - * OpensslKey constructor. - * - * @param string $algo Algo to use for HKDF - * @param string $key Key - * @param string $ivr Initialization vector - * @throws InvalidKeyException - */ - public function __construct(string $algo, string $key, string $ivr) - { - // Store the key as what was supplied - $this->key = \base64_decode($key); - - // Make sure key was properly decoded and meets minimum required length - if (!is_string($this->key) || Str::strlen($this->key) < 2048) { - throw new InvalidKeyException("Key must be at least 2048 bytes and base64 encoded."); - } - - // Make sure key meets minimum entropy requirement - if (\count(\array_unique(\str_split($this->key))) < 250) { - throw new InvalidKeyException("Key does not contain the minimum amount of entropy."); - } - - // Store algo in object - $this->algo = $algo; - - // Store init vector in object - $this->ivr = $ivr; - } - - /** - * Generate the authentication key - * - * @param string $info - * @return string - */ - public function authenticationKey(string $info): string - { - return $this->deriveKey(__FUNCTION__ . '|' . $info); - } - - /** - * Generate the encryption key - * - * @param string $info - * @return string - */ - public function encryptionKey(string $info): string - { - return $this->deriveKey(__FUNCTION__ . '|' . $info); - } - - /** - * Derive a key with differing info string parameters - * - * @param string $info Info parameter to provide to hash_hkdf - * @return string - */ - public function deriveKey(string $info): string - { - return \hash_hkdf($this->algo, $this->key, 0, $info, $this->ivr); - } - - /** - * Generate a new key that meets requirements for dcrypt - * - * @param int $size Size of key in bytes - * @return string - * @throws InvalidKeyException - */ - public static function create(int $bytes = 2048): string - { - if ($bytes < 2048) { - throw new InvalidKeyException('Keys must be at least 2048 bytes long.'); - } - - return \base64_encode(\random_bytes($bytes)); - } + + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ + +namespace Dcrypt; + +use Dcrypt\Exceptions\InvalidKeyException; + +/** + * Provides key derivation functions + * + * @category Dcrypt + * @package Dcrypt + * @author Michael Meyer (mmeyer2k) + * @license http://opensource.org/licenses/MIT The MIT License (MIT) + * @link https://github.com/mmeyer2k/dcrypt + */ +final class OpensslKey +{ + /** + * High entropy key + * + * @var string + */ + private $_key; + + /** + * Algo string + * + * @var string + */ + private $_algo; + + /** + * High entropy salt + * + * @var string + */ + private $_ivr; + + /** + * OpensslKey constructor. + * + * @param string $algo Algo to use for HKDF + * @param string $key Key + * @param string $ivr Initialization vector + * + * @throws InvalidKeyException + */ + public function __construct(string $algo, string $key, string $ivr) + { + // Store the key as what was supplied + $this->_key = \base64_decode($key); + + // Make sure key was properly decoded and meets minimum required length + if (!is_string($this->_key) || Str::strlen($this->_key) < 2048) { + throw new InvalidKeyException(InvalidKeyException::KEYLENGTH); + } + + // Make sure key meets minimum entropy requirement + if (\count(\array_unique(\str_split($this->_key))) < 250) { + throw new InvalidKeyException(InvalidKeyException::KEYRANDOM); + } + + // Store algo in object + $this->_algo = $algo; + + // Store init vector in object + $this->_ivr = $ivr; + } + + /** + * Generate the authentication key + * + * @param string $info The extra info parameter for hash_hkdf + * + * @return string + */ + public function authenticationKey(string $info): string + { + return $this->deriveKey(__FUNCTION__ . '|' . $info); + } + + /** + * Generate the encryption key + * + * @param string $info The extra info parameter for hash_hkdf + * + * @return string + */ + public function encryptionKey(string $info): string + { + return $this->deriveKey(__FUNCTION__ . '|' . $info); + } + + /** + * Derive a key with differing info string parameters + * + * @param string $info Info parameter to provide to hash_hkdf + * + * @return string + */ + public function deriveKey(string $info): string + { + return \hash_hkdf($this->_algo, $this->_key, 0, $info, $this->_ivr); + } + + /** + * Generate a new key that meets requirements for dcrypt + * + * @param int $bytes Size of key in bytes + * + * @return string + * @throws InvalidKeyException + */ + public static function create(int $bytes = 2048): string + { + if ($bytes < 2048) { + throw new InvalidKeyException('Keys must be at least 2048 bytes long.'); + } + + return \base64_encode(\random_bytes($bytes)); + } } \ No newline at end of file diff --git a/src/OpensslStack.php b/src/OpensslStack.php index 81106c35..10df85ab 100644 --- a/src/OpensslStack.php +++ b/src/OpensslStack.php @@ -30,14 +30,14 @@ class OpensslStack * * @var array */ - private $stack = []; + private $_stack = []; /** * High entropy key * * @var string */ - private $key; + private $_key; /** * OpensslStack constructor. @@ -46,7 +46,7 @@ class OpensslStack */ public function __construct(string $key) { - $this->key = $key; + $this->_key = $key; } /** @@ -59,7 +59,7 @@ public function __construct(string $key) */ public function add(string $cipher, string $algo): self { - $this->stack[] = [$cipher, $algo]; + $this->_stack[] = [$cipher, $algo]; return $this; } @@ -73,8 +73,8 @@ public function add(string $cipher, string $algo): self */ public function encrypt(string $data): string { - foreach ($this->stack as $s) { - $data = OpensslStatic::encrypt($data, $this->key, $s[0], $s[1]); + foreach ($this->_stack as $s) { + $data = OpensslStatic::encrypt($data, $this->_key, $s[0], $s[1]); } return $data; @@ -89,8 +89,8 @@ public function encrypt(string $data): string */ public function decrypt(string $data): string { - foreach (\array_reverse($this->stack) as $s) { - $data = OpensslStatic::decrypt($data, $this->key, $s[0], $s[1]); + foreach (\array_reverse($this->_stack) as $s) { + $data = OpensslStatic::decrypt($data, $this->_key, $s[0], $s[1]); } return $data; diff --git a/src/OpensslStatic.php b/src/OpensslStatic.php index 151bcd5f..bc61cf8e 100644 --- a/src/OpensslStatic.php +++ b/src/OpensslStatic.php @@ -38,12 +38,16 @@ final class OpensslStatic extends OpensslWrapper * @return string * @throws \Exception */ - public static function decrypt(string $data, string $key, string $cipher, string $algo): string - { + public static function decrypt( + string $data, + string $key, + string $cipher, + string $algo + ): string { // Calculate the hash checksum size in bytes for the specified algo $hsz = Str::hashSize($algo); - // Find the tag size for this cipher mode. Unless using GCM/CCM this will be zero. + // Get the tag size in bytes for this cipher mode $tsz = parent::tagRequired($cipher) ? 4 : 0; // Ask openssl for the IV size needed for specified cipher @@ -67,13 +71,16 @@ public static function decrypt(string $data, string $key, string $cipher, string // Calculate checksum of message payload for verification $chk = \hash_hmac($algo, $msg, $key->authenticationKey($cipher), true); - // Compare given checksum against computed checksum using a time-safe function + // Compare given checksum against computed checksum if (!Str::equal($chk, $sum)) { - throw new InvalidChecksumException('Decryption can not proceed due to invalid ciphertext checksum.'); + throw new InvalidChecksumException(InvalidChecksumException::MESSAGE); } + // Derive the encryption key + $enc = $key->encryptionKey($cipher); + // Decrypt message and return - return parent::openssl_decrypt($msg, $cipher, $key->encryptionKey($cipher), $ivr, $tag); + return parent::opensslDecrypt($msg, $cipher, $enc, $ivr, $tag); } /** @@ -87,8 +94,12 @@ public static function decrypt(string $data, string $key, string $cipher, string * @return string * @throws \Exception */ - public static function encrypt(string $data, string $key, string $cipher, string $algo): string - { + public static function encrypt( + string $data, + string $key, + string $cipher, + string $algo + ): string { // Generate IV of appropriate size $ivr = parent::ivGenerate($cipher); @@ -98,8 +109,11 @@ public static function encrypt(string $data, string $key, string $cipher, string // Create a placeholder for the authentication tag to be passed by reference $tag = ''; + // Derive the encryption key + $enc = $key->encryptionKey($cipher); + // Encrypt the plaintext - $msg = parent::openssl_encrypt($data, $cipher, $key->encryptionKey($cipher), $ivr, $tag); + $msg = parent::opensslEncrypt($data, $cipher, $enc, $ivr, $tag); // Generate the ciphertext checksum $chk = \hash_hmac($algo, $msg, $key->authenticationKey($cipher), true); diff --git a/src/OpensslWrapper.php b/src/OpensslWrapper.php index 3f499fc1..5817f73a 100644 --- a/src/OpensslWrapper.php +++ b/src/OpensslWrapper.php @@ -28,20 +28,25 @@ class OpensslWrapper /** * OpenSSL encrypt wrapper function * - * @param string $data Data to decrypt - * @param string $method Cipher method to use - * @param string $key Key string - * @param string $iv Initialization vector - * @param string $tag AAD tag + * @param string $data Data to decrypt + * @param string $cipher Cipher method to use + * @param string $key Key string + * @param string $iv Initialization vector + * @param string $tag AAD tag * * @return string */ - protected static function openssl_encrypt(string $data, string $method, string $key, string $iv, string &$tag): string - { - if (OpensslStatic::tagRequired($method)) { - return \openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv, $tag, '', 4); + protected static function opensslEncrypt( + string $data, + string $cipher, + string $key, + string $iv, + string &$tag + ): string { + if (OpensslStatic::tagRequired($cipher)) { + return \openssl_encrypt($data, $cipher, $key, 1, $iv, $tag, '', 4); } else { - return \openssl_encrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); + return \openssl_encrypt($data, $cipher, $key, 1, $iv); } } @@ -49,25 +54,31 @@ protected static function openssl_encrypt(string $data, string $method, string $ * OpenSSL decrypt wrapper function * * @param string $input Data to decrypt - * @param string $method Cipher method to use + * @param string $cipher Cipher method to use * @param string $key Key string * @param string $iv Initialization vector + * @param string $tag AAD authentication tag * * @return string */ - protected static function openssl_decrypt(string $input, string $method, string $key, string $iv, string $tag): string - { - if (OpensslStatic::tagRequired($method)) { - return \openssl_decrypt($input, $method, $key, OPENSSL_RAW_DATA, $iv, $tag, ''); + protected static function opensslDecrypt( + string $input, + string $cipher, + string $key, + string $iv, + string $tag + ): string { + if (OpensslStatic::tagRequired($cipher)) { + return \openssl_decrypt($input, $cipher, $key, 1, $iv, $tag, ''); } else { - return \openssl_decrypt($input, $method, $key, OPENSSL_RAW_DATA, $iv); + return \openssl_decrypt($input, $cipher, $key, 1, $iv); } } /** * Get IV size for specified CIPHER. * - * @param string $cipher + * @param string $cipher Openssl cipher * * @return int */ @@ -81,7 +92,7 @@ protected static function ivSize(string $cipher): int /** * Get a correctly sized IV for the specified cipher * - * @param string $cipher + * @param string $cipher Openssl cipher * * @return string * @throws \Exception @@ -100,7 +111,7 @@ protected static function ivGenerate(string $cipher): string /** * Determines if the provided cipher requires a tag * - * @param string $cipher + * @param string $cipher Openssl cipher * * @return bool */ diff --git a/src/Str.php b/src/Str.php index 0eca10f2..d6d0f634 100644 --- a/src/Str.php +++ b/src/Str.php @@ -85,8 +85,11 @@ public static function strlen(string $string): int * * @return string */ - public static function substr(string $string, int $start, int $length = null): string - { + public static function substr( + string $string, + int $start, + int $length = null + ): string { return \mb_substr($string, $start, $length, '8bit'); } }