diff --git a/lib/Cryptomute/Cryptomute.php b/lib/Cryptomute/Cryptomute.php index d3ab879..b047608 100644 --- a/lib/Cryptomute/Cryptomute.php +++ b/lib/Cryptomute/Cryptomute.php @@ -220,7 +220,7 @@ public function setValueRange($minValue, $maxValue) } /** - * Encrypts input data. + * Encrypts input data. Acts as a public alias for _encryptInternal method. * * @param string $input String representation of input number. * @param int $base Input number base. @@ -232,7 +232,24 @@ public function setValueRange($minValue, $maxValue) */ public function encrypt($input, $base = 10, $pad = false, $password = null, $iv = null) { - $this->_validateInput($input, $base); + return $this->_encryptInternal($input, $base, $pad, $password, $iv, true); + } + + /** + * Encrypts input data. + * + * @param string $input String representation of input number. + * @param int $base Input number base. + * @param bool $pad Pad left with zeroes? + * @param string|null $password Encryption password. + * @param string|null $iv Encryption initialization vector. Must be unique! + * @param bool $checkVal Should check if input value is in range? + * + * @return string Outputs encrypted data in the same format as input data. + */ + private function _encryptInternal($input, $base, $pad, $password, $iv, $checkVal = false) + { + $this->_validateInput($input, $base, $checkVal); $this->_validateIv($iv); $hashPassword = $this->_hashPassword($password); $roundKeys = $this->_roundKeys($hashPassword, $iv); @@ -256,7 +273,7 @@ public function encrypt($input, $base = 10, $pad = false, $password = null, $iv $compare = DataConverter::binToDec($binary); return (gmp_cmp($this->minValue, $compare) > 0 || gmp_cmp($compare, $this->maxValue) > 0) - ? $this->encrypt($output, $base, $pad, $password, $iv) + ? $this->_encryptInternal($output, $base, $pad, $password, $iv, false) : $output; } @@ -400,10 +417,11 @@ private function _convertFromBin($binary, $base, $pad) * * @param string $input * @param string $base + * @param bool $checkDomain Should check if input is in domain? * * @throws InvalidArgumentException If provided invalid type. */ - private function _validateInput($input, $base) + private function _validateInput($input, $base, $checkDomain = false) { if (!array_key_exists($base, self::$allowedBases)) { throw new InvalidArgumentException(sprintf( @@ -414,10 +432,24 @@ private function _validateInput($input, $base) if (preg_match(self::$allowedBases[$base], $input) !== 1) { throw new InvalidArgumentException(sprintf( - 'Input data does not match pattern "%s".', + 'Input data "%s" does not match pattern "%s".', + $input, self::$allowedBases[$base] )); } + + if ($checkDomain) { + $compare = gmp_init($input, $base); + + if (gmp_cmp($this->minValue, $compare) > 0 || gmp_cmp($compare, $this->maxValue) > 0) { + throw new InvalidArgumentException(sprintf( + 'Input value "%d" is out of domain range "%d - %d".', + gmp_strval($compare, 10), + $this->minValue, + $this->maxValue + )); + } + } } /** diff --git a/tests/CryptomuteTest.php b/tests/CryptomuteTest.php index c7d23b5..4c9ee7a 100644 --- a/tests/CryptomuteTest.php +++ b/tests/CryptomuteTest.php @@ -1,207 +1,231 @@ - true, - 'aes-128-cbc' => true, - 'aes-128-ecb' => false, - 'aes-192-cbc' => true, - 'aes-192-ecb' => false, - 'camellia-128-cbc' => true, - 'camellia-128-ecb' => false, - 'camellia-192-cbc' => true, - 'camellia-192-ecb' => false, - ]; - - public static $testedRounds = [ - 3 => 'minimum', - 5 => 'normal', - 7 => 'recommended', - 11 => 'insane', - ]; - - public function testReadmeExample() - { - $message = '[%s / %d rounds] Expected README example to work.'; - - foreach (self::$testedCiphers as $cipher => $usesIv) { - foreach (self::$testedRounds as $rounds => $description) { - $cryptomute = $this->getCryptomute($cipher, $rounds); - $password = openssl_random_pseudo_bytes(32); - $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; - - $plainValue = '2048'; - $encoded = $cryptomute->encrypt($plainValue, 10, false, $password, $iv); - $decoded = $cryptomute->decrypt($encoded, 10, false, $password, $iv); - - $this->assertEquals($plainValue, $decoded, sprintf( - $message, - $cipher, - $rounds - )); - } - } - } - - public function testEncodedValuesAreInDomain() - { - $message = '[%s / %d rounds] Base %d value of %s encoded to %s (%s) is still in domain %s - %s.'; - $minVal = 100; - $maxVal = 999; - - foreach (self::$testedCiphers as $cipher => $usesIv) { - foreach (self::$testedRounds as $rounds => $description) { - $cryptomute = $this->getCryptomute($cipher, $rounds, "$minVal", "$maxVal"); - $password = openssl_random_pseudo_bytes(32); - - foreach (Cryptomute::$allowedBases as $base => $pattern) { - for ($i = $minVal; $i <= $maxVal; $i++) { - $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; - - $input = gmp_strval(gmp_init("$i", 10), $base); - $encoded = $cryptomute->encrypt($input, $base, false, $password, $iv); - $intVal = (int) gmp_strval(gmp_init($encoded, $base), 10); - - $this->assertTrue( - $minVal <= $intVal && $intVal <= $maxVal, - sprintf($message, $cipher, $rounds, $base, $i, $encoded, "$intVal", $minVal, $maxVal) - ); - } - } - } - } - } - - public function testDecodesEncodedNumbers() - { - $message = '[%s / %d rounds] Encoded base %d value must decode to initial value [%s].'; - - foreach (self::$testedCiphers as $cipher => $usesIv) { - foreach (self::$testedRounds as $rounds => $description) { - $cryptomute = $this->getCryptomute($cipher, $rounds); - $password = openssl_random_pseudo_bytes(32); - - foreach (Cryptomute::$allowedBases as $base => $pattern) { - for ($i = 0; $i < self::TEST_REPEATS; $i++) { - $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; - - $input = gmp_strval(gmp_random_range( - gmp_init(self::MIN_VALUE, 10), - gmp_init(self::MAX_VALUE, 10) - ), $base); - - $encrypted = $cryptomute->encrypt($input, $base, false, $password, $iv); - $decrypted = $cryptomute->decrypt($encrypted, $base, false, $password, $iv); - - $this->assertEquals($input, $decrypted, sprintf( - $message, - $cipher, - $rounds, - $base, - $input - )); - } - } - } - } - } - - public function testEncodeWithDiffrentPasswordsProducesDiffrentResults() - { - $message = '[%s / %d rounds] Same base %d value encoded with diffrent passwords produce diffrent results [%s].'; - - foreach (self::$testedCiphers as $cipher => $usesIv) { - foreach (self::$testedRounds as $rounds => $description) { - $cryptomute = $this->getCryptomute($cipher, $rounds); - - foreach (Cryptomute::$allowedBases as $base => $pattern) { - for ($i = 0; $i < self::TEST_REPEATS; $i++) { - $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; - - $input = gmp_strval(gmp_random_range( - gmp_init(self::MIN_VALUE, 10), - gmp_init(self::MAX_VALUE, 10) - ), $base); - - $encrypted1 = $cryptomute->encrypt($input, $base, false, 'foo', $iv); - $encrypted2 = $cryptomute->encrypt($input, $base, false, 'bar', $iv); - - $this->assertNotEquals($encrypted1, $encrypted2, sprintf( - $message, - $cipher, - $rounds, - $base, - $input - )); - } - } - } - } - } - - public function testDecodeWithDiffrentPasswordsProducesDiffrentResults() - { - $message = '[%s / %d rounds] Same base %d value decoded with diffrent passwords produce diffrent results [%s].'; - - foreach (self::$testedCiphers as $cipher => $usesIv) { - foreach (self::$testedRounds as $rounds => $description) { - $cryptomute = $this->getCryptomute($cipher, $rounds); - - foreach (Cryptomute::$allowedBases as $base => $pattern) { - for ($i = 0; $i < self::TEST_REPEATS; $i++) { - $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; - - $input = gmp_strval(gmp_random_range( - gmp_init(self::MIN_VALUE, 10), - gmp_init(self::MAX_VALUE, 10) - ), $base); - - $decrypted1 = $cryptomute->decrypt($input, $base, false, 'foo', $iv); - $decrypted2 = $cryptomute->decrypt($input, $base, false, 'bar', $iv); - - $this->assertNotEquals($decrypted1, $decrypted2, sprintf( - $message, - $cipher, - $rounds, - $base, - $input - )); - } - } - } - } - } - - /** - * @param string $cipher - * @param int $rounds - * @param string|null $minVal - * @param string|null $maxVal - * - * @return Cryptomute - */ - private function getCryptomute($cipher, $rounds, $minVal = null, $maxVal = null) - { - $cryptomute = new Cryptomute( - $cipher, - openssl_random_pseudo_bytes(32), - $rounds - ); - - return $cryptomute->setValueRange( - $minVal ?: self::MIN_VALUE, - $maxVal ?: self::MAX_VALUE - ); - } -} + true, + 'aes-128-cbc' => true, + 'aes-128-ecb' => false, + 'aes-192-cbc' => true, + 'aes-192-ecb' => false, + 'camellia-128-cbc' => true, + 'camellia-128-ecb' => false, + 'camellia-192-cbc' => true, + 'camellia-192-ecb' => false, + ]; + + public static $testedRounds = [ + 3 => 'minimum', + 5 => 'normal', + 7 => 'recommended', + 11 => 'insane', + ]; + + public function testReadmeExample() + { + $message = '[%s / %d rounds] Expected README example to work.'; + + foreach (self::$testedCiphers as $cipher => $usesIv) { + foreach (self::$testedRounds as $rounds => $description) { + $cryptomute = $this->getCryptomute($cipher, $rounds); + $password = openssl_random_pseudo_bytes(32); + $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; + + $plainValue = '2048'; + $encoded = $cryptomute->encrypt($plainValue, 10, false, $password, $iv); + $decoded = $cryptomute->decrypt($encoded, 10, false, $password, $iv); + + $this->assertEquals($plainValue, $decoded, sprintf( + $message, + $cipher, + $rounds + )); + } + } + } + + public function testEncodedValuesAreInDomain() + { + $message = '[%s / %d rounds] Base %d value of %s encoded to %s (%s) is still in domain %s - %s.'; + $minVal = 100; + $maxVal = 999; + + foreach (self::$testedCiphers as $cipher => $usesIv) { + foreach (self::$testedRounds as $rounds => $description) { + $cryptomute = $this->getCryptomute($cipher, $rounds, "$minVal", "$maxVal"); + $password = openssl_random_pseudo_bytes(32); + + foreach (Cryptomute::$allowedBases as $base => $pattern) { + for ($i = $minVal; $i <= $maxVal; $i++) { + $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; + + $input = gmp_strval(gmp_init("$i", 10), $base); + $encoded = $cryptomute->encrypt($input, $base, false, $password, $iv); + $intVal = (int) gmp_strval(gmp_init($encoded, $base), 10); + + $this->assertTrue( + $minVal <= $intVal && $intVal <= $maxVal, + sprintf($message, $cipher, $rounds, $base, $i, $encoded, "$intVal", $minVal, $maxVal) + ); + } + } + } + } + } + + public function testDecodesEncodedNumbers() + { + $message = '[%s / %d rounds] Encoded base %d value must decode to initial value [%s].'; + + foreach (self::$testedCiphers as $cipher => $usesIv) { + foreach (self::$testedRounds as $rounds => $description) { + $cryptomute = $this->getCryptomute($cipher, $rounds); + $password = openssl_random_pseudo_bytes(32); + + foreach (Cryptomute::$allowedBases as $base => $pattern) { + for ($i = 0; $i < self::TEST_REPEATS; $i++) { + $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; + + $input = gmp_strval(gmp_random_range( + gmp_init(self::MIN_VALUE, 10), + gmp_init(self::MAX_VALUE, 10) + ), $base); + + $encrypted = $cryptomute->encrypt($input, $base, false, $password, $iv); + $decrypted = $cryptomute->decrypt($encrypted, $base, false, $password, $iv); + + $this->assertEquals($input, $decrypted, sprintf( + $message, + $cipher, + $rounds, + $base, + $input + )); + } + } + } + } + } + + public function testEncodeWithDiffrentPasswordsProducesDiffrentResults() + { + $message = '[%s / %d rounds] Same base %d value encoded with diffrent passwords produce diffrent results [%s].'; + + foreach (self::$testedCiphers as $cipher => $usesIv) { + foreach (self::$testedRounds as $rounds => $description) { + $cryptomute = $this->getCryptomute($cipher, $rounds); + + foreach (Cryptomute::$allowedBases as $base => $pattern) { + for ($i = 0; $i < self::TEST_REPEATS; $i++) { + $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; + + $input = gmp_strval(gmp_random_range( + gmp_init(self::MIN_VALUE, 10), + gmp_init(self::MAX_VALUE, 10) + ), $base); + + $encrypted1 = $cryptomute->encrypt($input, $base, false, 'foo', $iv); + $encrypted2 = $cryptomute->encrypt($input, $base, false, 'bar', $iv); + + $this->assertNotEquals($encrypted1, $encrypted2, sprintf( + $message, + $cipher, + $rounds, + $base, + $input + )); + } + } + } + } + } + + public function testDecodeWithDiffrentPasswordsProducesDiffrentResults() + { + $message = '[%s / %d rounds] Same base %d value decoded with diffrent passwords produce diffrent results [%s].'; + + foreach (self::$testedCiphers as $cipher => $usesIv) { + foreach (self::$testedRounds as $rounds => $description) { + $cryptomute = $this->getCryptomute($cipher, $rounds); + + foreach (Cryptomute::$allowedBases as $base => $pattern) { + for ($i = 0; $i < self::TEST_REPEATS; $i++) { + $iv = $usesIv ? openssl_random_pseudo_bytes(openssl_cipher_iv_length($cipher)) : null; + + $input = gmp_strval(gmp_random_range( + gmp_init(self::MIN_VALUE, 10), + gmp_init(self::MAX_VALUE, 10) + ), $base); + + $decrypted1 = $cryptomute->decrypt($input, $base, false, 'foo', $iv); + $decrypted2 = $cryptomute->decrypt($input, $base, false, 'bar', $iv); + + $this->assertNotEquals($decrypted1, $decrypted2, sprintf( + $message, + $cipher, + $rounds, + $base, + $input + )); + } + } + } + } + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInputLessThanMinValueThrowsException() + { + $cryptomute = $this->getCryptomute('aes-128-cbc', 3); + $cryptomute->setValueRange(5000, 10000); + $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-cbc')); + + $cryptomute->encrypt(4999, 10, true, 'foo', $iv); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInputMoreThanMaxValueThrowsException() + { + $cryptomute = $this->getCryptomute('aes-128-cbc', 3); + $cryptomute->setValueRange(5000, 10000); + $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-128-cbc')); + + $cryptomute->encrypt(10001, 10, true, 'foo', $iv); + } + + /** + * @param string $cipher + * @param int $rounds + * @param string|null $minVal + * @param string|null $maxVal + * + * @return Cryptomute + */ + private function getCryptomute($cipher, $rounds, $minVal = null, $maxVal = null) + { + $cryptomute = new Cryptomute( + $cipher, + openssl_random_pseudo_bytes(32), + $rounds + ); + + return $cryptomute->setValueRange( + $minVal ?: self::MIN_VALUE, + $maxVal ?: self::MAX_VALUE + ); + } +}