diff --git a/.php_cs.cache b/.php_cs.cache new file mode 100644 index 0000000..ae22ad0 --- /dev/null +++ b/.php_cs.cache @@ -0,0 +1 @@ +{"php":"7.2.12","version":"2.13.1:v2.13.1#54814c62d5beef3ba55297b9b3186ed8b8a1b161","rules":{"encoding":true,"full_opening_tag":true,"blank_line_after_namespace":true,"braces":true,"class_definition":true,"elseif":true,"function_declaration":true,"indentation_type":true,"line_ending":true,"lowercase_constants":true,"lowercase_keywords":true,"method_argument_space":{"on_multiline":"ensure_fully_multiline"},"no_break_comment":true,"no_closing_tag":true,"no_spaces_after_function_name":true,"no_spaces_inside_parenthesis":true,"no_trailing_whitespace":true,"no_trailing_whitespace_in_comment":true,"single_blank_line_at_eof":true,"single_class_element_per_statement":{"elements":["property"]},"single_import_per_statement":true,"single_line_after_imports":true,"switch_case_semicolon_to_colon":true,"switch_case_space":true,"visibility_required":true,"psr4":true,"declare_strict_types":true,"strict_param":true,"align_multiline_comment":true,"array_syntax":{"syntax":"short"},"binary_operator_spaces":true,"blank_line_after_opening_tag":true,"blank_line_before_statement":{"statements":["while","declare","do","for","foreach","if","switch","try"]},"cast_spaces":true,"class_attributes_separation":true,"combine_consecutive_issets":true,"combine_consecutive_unsets":true,"compact_nullable_typehint":true,"concat_space":{"spacing":"one"},"declare_equal_normalize":true,"dir_constant":true,"ereg_to_preg":true,"escape_implicit_backslashes":true,"is_null":{"use_yoda_style":true},"linebreak_after_opening_tag":true,"list_syntax":{"syntax":"short"},"lowercase_cast":true,"magic_constant_casing":true,"method_chaining_indentation":true,"method_separation":true,"modernize_types_casting":true,"no_alias_functions":true,"no_blank_lines_after_class_opening":true,"no_blank_lines_after_phpdoc":true,"no_empty_comment":true,"no_empty_phpdoc":true,"no_empty_statement":true,"no_extra_consecutive_blank_lines":{"tokens":["break","continue","extra","return","throw","use","parenthesis_brace_block","square_brace_block","curly_brace_block"]},"no_homoglyph_names":true,"no_leading_import_slash":true,"no_php4_constructor":true,"no_short_bool_cast":true,"no_singleline_whitespace_before_semicolons":true,"no_spaces_around_offset":true,"no_trailing_comma_in_list_call":true,"no_trailing_comma_in_singleline_array":true,"no_unneeded_control_parentheses":true,"no_unneeded_curly_braces":true,"no_unneeded_final_method":true,"no_unreachable_default_argument_value":true,"no_unused_imports":true,"no_useless_else":true,"no_useless_return":true,"no_whitespace_before_comma_in_array":true,"no_whitespace_in_blank_line":true,"normalize_index_brace":true,"return_type_declaration":true,"trim_array_spaces":true,"unary_operator_spaces":true,"whitespace_after_comma_in_array":true,"yoda_style":{"equal":true,"identical":true,"always_move_variable":true},"ordered_imports":true,"ordered_class_elements":true,"no_superfluous_elseif":true,"no_short_echo_tag":true,"no_null_property_initialization":true,"blank_line_before_return":true,"heredoc_to_nowdoc":true,"phpdoc_align":{"tags":["param","return","throws","type","var"]},"phpdoc_to_comment":true},"hashes":{"src\/Exception\/HashingException.php":2572493559,"src\/PasswordLock.php":180371002,"src\/Hasher\/PasswordHasherInterface.php":3091791932,"src\/Hasher\/PasswordHasher.php":3257075415,"tests\/PasswordLockTest.php":3624153454,"tests\/Hasher\/PasswordHasherTest.php":824329663}} \ No newline at end of file diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 0000000..0436875 --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,107 @@ +setRules([ + '@PSR1' => true, + '@PSR2' => true, + 'psr4' => true, + 'declare_strict_types' => true, + 'strict_param' => true, + 'strict_comparison' => false, + 'align_multiline_comment' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => [ + 'statements' => [ + 'while', 'declare', 'do', 'for', 'foreach', 'if', 'switch', 'try' + ] + ], + 'cast_spaces' => true, + 'class_attributes_separation' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => true, + 'dir_constant' => true, + 'ereg_to_preg' => true, + 'escape_implicit_backslashes' => true, + 'is_null' => ['use_yoda_style' => true], + 'linebreak_after_opening_tag' => true, + 'list_syntax' => ['syntax' => 'short'], + 'lowercase_cast' => true, + 'magic_constant_casing' => true, + 'method_chaining_indentation' => true, + 'method_separation' => true, + 'modernize_types_casting' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_comment' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_consecutive_blank_lines' => [ + 'tokens' => [ + 'break', 'continue', 'extra', 'return', 'throw', 'use', + 'parenthesis_brace_block', 'square_brace_block', 'curly_brace_block' + ], + ], + 'no_homoglyph_names' => true, + 'no_leading_import_slash' => true, + 'no_php4_constructor' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unreachable_default_argument_value' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'return_type_declaration' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'whitespace_after_comma_in_array' => true, + 'yoda_style' => [ + 'equal' => true, + 'identical' => true, + 'always_move_variable' => true + ], + 'ordered_imports' => true, + 'ordered_class_elements' => true, + 'no_superfluous_elseif' => true, + 'no_short_echo_tag' => true, + 'no_null_property_initialization' => true, + 'no_closing_tag' => true, + 'blank_line_before_return' => true, + 'class_keyword_remove' => false, + 'self_accessor' => false, + 'encoding' => true, + 'full_opening_tag' => true, + 'heredoc_to_nowdoc' => true, + 'mb_str_functions' => false, + 'phpdoc_align' => [ + 'tags' => [ + 'param', 'return', 'throws', 'type', 'var' + ], + ], + 'phpdoc_to_comment' => true, + 'no_trailing_whitespace' => true, + ]) + ->setRiskyAllowed(true) + ->setUsingCache(true) + ->setHideProgress(false) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__.'/src') + ->name('*.php') + ); \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4dfdbc0..81e8fc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,34 @@ language: php sudo: false -php: - - 7.0 - - 7.1 - - 7.2 - matrix: fast_finish: true + include: + - php: 7.1 + env: + - DEPS=lowest + - php: 7.1 + env: + - CS_CHECK=true + - ANALYZE=true + - php: 7.2 + - php: 7.3 + env: + - NO_CS=true + - php: nightly + env: + - NO_CS=true + allow_failures: + - php: 7.3 + - php: nightly install: - composer self-update + - if [[ $NO_CS == 'true' ]]; then composer remove --dev friendsofphp/php-cs-fixer ; fi - composer update - chmod +x ./run-tests.sh script: - - vendor/bin/phpunit - - vendor/bin/psalm + - composer test + - if [[ $ANALYZE == 'true' ]]; then composer analyze ; fi + - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi \ No newline at end of file diff --git a/README.md b/README.md index 1e79d25..9646933 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **MIT Licensed** - feel free to use to enhance the security of any of your PHP projects -Wraps Bcrypt-SHA384 in Authenticated Encryption. Published by [Paragon Initiative Enteprises](https://paragonie.com). Check out our other [open source projects](https://paragonie.com/projects) too. +Wraps Password Hashing in Authenticated Encryption. Published by [Paragon Initiative Enteprises](https://paragonie.com). Check out our other [open source projects](https://paragonie.com/projects) too. Depends on [defuse/php-encryption](https://github.com/defuse/php-encryption) for authenticated symmetric-key encryption. @@ -16,7 +16,7 @@ A hash then encrypt strategy offers **agility**; if your secret key is compromis * You don't have to worry about the 72 character limit for bcrypt * You don't have to worry about accidentally creating a null-byte truncation vulnerability -* If your database gets hacked, and your database is on a separate machine from your webserver, the attacker has to first decrypt the hashes before attempting to crack any of them. +* If your database gets hacked, and your database is on a separate machine from your web server, the attacker has to first decrypt the hashes before attempting to crack any of them. Here's a [proof-of-concept](http://3v4l.org/61VZq) for the first two points. @@ -27,26 +27,37 @@ But realistically, this library is only about as a secure as bcrypt. ### Hash Password, Encrypt Hash, Authenticate Ciphertext ```php -use \ParagonIE\PasswordLock\PasswordLock; -use \Defuse\Crypto\Key; +lock($_POST['password']); } ``` ### Verify MAC, Decrypt Ciphertext, Verify Password ```php +check($_POST['password'], $storeMe)) { // Success! } } @@ -55,17 +66,75 @@ if (isset($_POST['password'])) { ### Re-encrypt a hash with a different encryption key ```php + 2048 +]); +$lock = new PasswordLock($key,$hasher); +``` + +## Costume Password Hasher + +`ParagonIE\PasswordLock\PasswordLock` accepts any `ParagonIE\PasswordLock\Hasher\PasswordHasherInterface` implementation as the first argument. + +```php +algorithm = $algorithm; + $this->options = $options; + } + + public function hash(string $password): string + { + $hash = password_hash( + Base64::encode( + hash('sha384', $password, true) + ), + $this->getAlgorithm(), + $this->getOptions() + ); + + if (!is_string($hash)) { + throw new HashingException('Unknown hashing error.'); + } + + return $hash; + } + + public function verify(string $password, string $hash): bool + { + return password_verify( + Base64::encode( + hash('sha384', $password, true) + ), + $hash + ); + } + + public function getAlgorithm(): int + { + return $this->algorithm; + } + + public function getOptions(): array + { + return $this->options; + } +} diff --git a/src/Hasher/PasswordHasherInterface.php b/src/Hasher/PasswordHasherInterface.php new file mode 100644 index 0000000..421e7c1 --- /dev/null +++ b/src/Hasher/PasswordHasherInterface.php @@ -0,0 +1,12 @@ +key = $key; + $this->hasher = $hasher ?? new PasswordHasher(); } + /** - * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash - * 2. Verify that the password matches the hash + * 1. Hash password + * 2. Encrypt-then-MAC the hash * - * @param string $password - * @param string $ciphertext - * @param string $aesKey - must be exactly 16 bytes - * @return bool - * @throws \Exception - * @throws \InvalidArgumentException + * @throws EnvironmentIsBrokenException */ - public static function decryptAndVerifyLegacy(string $password, string $ciphertext, string $aesKey): bool + public function lock(string $password): string { - if (Binary::safeStrlen($aesKey) !== 16) { - throw new \Exception("Encryption keys must be 16 bytes long"); - } - $hash = Crypto::legacyDecrypt( - $ciphertext, - $aesKey - ); - if (!\is_string($hash)) { - throw new \Exception("Unknown hashing error."); - } - return \password_verify( - Base64::encode( - \hash('sha256', $password, true) - ), - $hash - ); + $hash = $this->hasher->hash($password); + + return Crypto::encrypt($hash, $this->key); } /** * 1. VerifyHMAC-then-Decrypt the ciphertext to get the hash * 2. Verify that the password matches the hash * - * @param string $password - * @param string $ciphertext - * @param Key $aesKey - * @return bool - * @throws \Exception - * @throws \InvalidArgumentException + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException */ - public static function decryptAndVerify(string $password, string $ciphertext, Key $aesKey): bool + public function check(string $password, string $ciphertext): bool { $hash = Crypto::decrypt( $ciphertext, - $aesKey - ); - if (!\is_string($hash)) { - throw new \Exception("Unknown hashing error."); - } - return \password_verify( - Base64::encode( - \hash('sha384', $password, true) - ), - $hash + $this->key ); + + return $this->hasher->verify($password, $hash); } /** * Key rotation method -- decrypt with your old key then re-encrypt with your new key * - * @param string $ciphertext - * @param Key $oldKey - * @param Key $newKey - * @return string + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException */ public static function rotateKey(string $ciphertext, Key $oldKey, Key $newKey): string { $plaintext = Crypto::decrypt($ciphertext, $oldKey); - return Crypto::encrypt($plaintext, $newKey); - } - /** - * For migrating from an older version of the library - * - * @param string $password - * @param string $ciphertext - * @param string $oldKey - * @param Key $newKey - * @return string - * @throws \Exception - */ - public static function upgradeFromVersion1( - string $password, - string $ciphertext, - string $oldKey, - Key $newKey - ): string { - if (!self::decryptAndVerifyLegacy($password, $ciphertext, $oldKey)) { - throw new \Exception( - 'The correct password is necessary for legacy migration.' - ); - } - $plaintext = Crypto::legacyDecrypt($ciphertext, $oldKey); - return self::hashAndEncrypt($plaintext, $newKey); + return Crypto::encrypt($plaintext, $newKey); } } diff --git a/tests/Hasher/PasswordHasherTest.php b/tests/Hasher/PasswordHasherTest.php new file mode 100644 index 0000000..efdfa65 --- /dev/null +++ b/tests/Hasher/PasswordHasherTest.php @@ -0,0 +1,44 @@ +hasher = new PasswordHasher(); + } + + public function testHash(): void + { + $hash = $this->hasher->hash('RED AIRPLANE'); + + $this->assertTrue( + $this->hasher->verify('RED AIRPLANE', $hash) + ); + + $this->assertFalse( + $this->hasher->verify('RED AiRPLANE', $hash) + ); + } + + public function testDefaultParameters(): void + { + $this->assertEquals( + PASSWORD_DEFAULT, + $this->hasher->getAlgorithm() + ); + + $this->assertSame( + [], + $this->hasher->getOptions() + ); + } +} diff --git a/tests/PasswordLockTest.php b/tests/PasswordLockTest.php index c5e4d59..cb467a3 100644 --- a/tests/PasswordLockTest.php +++ b/tests/PasswordLockTest.php @@ -1,9 +1,15 @@ lock = new PasswordLock( + Key::createNewRandomKey() + ); + } + + /** + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public function testHash(): void { - $key = Key::createNewRandomKey(); + $password = $this->lock->lock('YELLOW SUBMARINE'); - $password = PasswordLock::hashAndEncrypt('YELLOW SUBMARINE', $key); - $this->assertTrue( - PasswordLock::decryptAndVerify('YELLOW SUBMARINE', $password, $key) + $this->lock->check('YELLOW SUBMARINE', $password) ); - + $this->assertFalse( - PasswordLock::decryptAndVerify('YELLOW SUBMARINF', $password, $key) + $this->lock->check('YELLOW SUBMARINF', $password) ); } - + /** * @expectedException \Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException + * + * @throws EnvironmentIsBrokenException */ - public function testBitflip() + public function testBitflip(): void { - $key = Key::createNewRandomKey(); - $password = PasswordLock::hashAndEncrypt('YELLOW SUBMARINE', $key); - $password[0] = (\ord($password[0]) === 0 ? 255 : 0); - - PasswordLock::decryptAndVerify('YELLOW SUBMARINE', $password, $key); + $password = $this->lock->lock('YELLOW SUBMARINE'); + + $password[0] = (0 === ord($password[0]) ? 255 : 0); + + $this->lock->check('YELLOW SUBMARINE', $password); + } + + /** + * @throws EnvironmentIsBrokenException + */ + public function testNullByteTruncation(): void + { + $hash1 = $this->lock->lock("abc\0defg"); + $hash2 = $this->lock->lock("abc"); + + $this->assertNotSame($hash1, $hash2); + } + + /** + * @throws EnvironmentIsBrokenException + * @throws WrongKeyOrModifiedCiphertextException + */ + public function testKeyRotation(): void + { + $key1 = Key::createNewRandomKey(); + $lock1 = new PasswordLock($key1); + + $key2 = Key::createNewRandomKey(); + $lock2 = new PasswordLock($key2); + + $hash1 = $lock1->lock('ParagonIE'); + $hash2 = PasswordLock::rotateKey($hash1, $key1, $key2); + + $this->assertNotSame($hash1, $hash2); + $this->assertTrue($lock2->check('ParagonIE', $hash2)); } }