-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Security] Argon2i Password Encoder #21604
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
With the constant changing of recommended algorithms, would it be an idea to provide a migration layer in Symfony? Currently people will have to build this themselves, but it could be a lot easier if Symfony would provide an extension point, especially now that we can rehash with native php functions. |
That's a great idea since Symfony really should be promoting security best practices (though likely out of the scope of this PR so I'll have a go at starting that separately - if you've got any ideas on what direction that would take, what the config would look like, etc). It's pretty much a given that Argon2i will be the next hashing algorithm supported by |
to me, it is a new feature |
the max password length of 72 for bcrypt is because the bcrypt algorithm does not support longer passwords at all (it truncates the input at 72 chars). |
@@ -14,6 +14,7 @@ | |||
<parameter key="security.encoder.plain.class">Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder</parameter> | |||
<parameter key="security.encoder.pbkdf2.class">Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder</parameter> | |||
<parameter key="security.encoder.bcrypt.class">Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder</parameter> | |||
<parameter key="security.encoder.argon2i.class">Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder</parameter> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't use parameters for class names anymore
if (PHP_VERSION_ID < 70200 && !extension_loaded('libsodium')) { | ||
$this->markTestSkipped('Argon2i algorithm not available.'); | ||
|
||
return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useless. markTestSkipped
is throwing an exception to interrupt the execution of the test
|
||
if (PHP_VERSION_ID >= 70200) { | ||
return $this->encodePasswordNative($raw); | ||
} elseif (extension_loaded('libsodium')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just if
Please rebase this on top of master and change the target branch of the PR |
All done, thanks. I'll update the |
@zanderbaldwin if the algo does not explicitly says it truncates the input, it means it does not do it. you would have to look at the algo description. |
another solution to detect it experimentally: hash a very long password, and then verify it with a string changing a single char in the string. and keep doing it, by increasing the altered index little by little. Once you find a case where the altered string is accepted in verify, it means you passed over the accepted input length. |
Confirmed, I tested up to 8192 characters and there are no false-positives from truncating. More than enough for a |
You can safely use passwords up to 2^32-1 bits long. |
throw new BadCredentialsException('Invalid password.'); | ||
} | ||
|
||
if (PHP_VERSION_ID >= 70200) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer checking for features over versions whenever possible. In this case:
if (function_exists('sodium_crypto_pwhash_str')) {
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, feature checking is more reliable when you think of php on .net or hhvm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. I'll add that in today.
I'm also trying to find a definitive answer on whether PASSWORD_ARGON2I
will be implemented for password_hash()
which would be a better solution than sodium_crypto_passwd_str()
, so that feature test could change.
@@ -94,11 +95,16 @@ protected function execute(InputInterface $input, OutputInterface $output) | |||
|
|||
$encoder = $this->getContainer()->get('security.encoder_factory')->getEncoder($userClass); | |||
$bcryptWithoutEmptySalt = !$emptySalt && $encoder instanceof BCryptPasswordEncoder; | |||
$argon2iWithoutEmptySalt = !$emptySalt && $encoder instanceof Argon2iPasswordEncoder; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest adding a a marker interface on encoders not needing the salt externally, to avoid having to know them one by one (I have the same need for detecting them in FOSUserBundle 2 to avoid generating useless salts).
what do you think @symfony/deciders ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 even though I'm not a decider; I wanted to do something like that but thought it would be adding too much for the scope of this PR.
Is that something I could implement here or separate PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's do it separately indeed. But the introduction of this encoder makes it worth adding such feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea!
|
||
public function setUp() | ||
{ | ||
if (PHP_VERSION_ID < 70200 && !extension_loaded('libsodium')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it possible to run the tests if libsodium is missing, but the sodium_crypto_pwhash_str()
, sodium_memzero()
, and sodium_crypto_pwhash_str_verify()
functions do exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, absolutely. I did change from version-checking to feature-checking in Argon2iPasswordEncoder
but I unfortunately either forgot to update it here, or it snuck in whilst rebasing (either way I missed it and shall update when I get home from work).
The isPasswordValid()
method checks for the function that it uses, but neither encodePassword()
or isPasswordValid()
check for the existence of sodium_memzero()
- is this something that should be done in case one of the check functions has been defined in user-land code? It's a pretty far off edge-case but should at least be aknowledged...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One solution could be to check the result of ReflectionFunctionAbstract::isUserDefined
, though would this be adding too much overhead?
|
I've updated |
EncoderFactoryTest doesn't need to be updated, both bcrypt and pbkdf2 are not listed in this test-class also. |
/** | ||
* @author Elnur Abdurrakhimov <[email protected]> | ||
*/ | ||
class Argon2iPasswordEncoderTest extends \PHPUnit_Framework_TestCase |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to use PHPUnit\Framework\TestCase
.
Confirmed it is: PHP 7.2 introduces @zanderbaldwin What's the status of this PR? Could you finish it? Or do you want someone to take it over? I would like to get this in. |
Sure, I'll get on this in the next few days. Does anyone know a good way to determine if the constant I'm currently testing with the |
@zanderbaldwin Just tried out, using |
@zanbaldwin would you have time to finish this PR for the end of the week? |
I've made most of the changes. Some points I want to clarify are:
I'm thinking of getting rid of the |
return $this->encodePasswordSodiumExtension($raw); | ||
} | ||
|
||
throw new \LogicException('Argon2i algorithm is not supported. Please install libsodium extension or upgrade to PHP 7.2+.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we cannot already (also) do these checks inside SecurityExtension
when the config is parsed? This would exclude WTF moments at runtime.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the extension, it should be an InvalidConfigurationException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I've made the changes in the Security component, I'll get onto putting extra checks in the SecurityBundle config parser 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, "install the libsodium extension" sounds better.
throw new \LogicException('Argon2i algorithm is not supported. Please install libsodium extension or upgrade to PHP 7.2+.'); | ||
} | ||
|
||
private function encodePasswordNative($raw) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
methods should come after public
ones
{ | ||
if (\PHP_VERSION_ID >= 70200 && defined('PASSWORD_ARGON2I')) { | ||
$valid = !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded); | ||
} elseif (function_exists('sodium_crypto_pwhash_str_verify')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use early returns instead of elseif
I've made some changes, but committed them in a separate commit (which can be squashed later).
Feedback welcomed, as always 👍 |
tests are currently failing (see the CI builds) :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with minor text comments
@@ -14,6 +14,7 @@ CHANGELOG | |||
* deprecated HTTP digest authentication | |||
* deprecated command `acl:set` along with `SetAclCommand` class | |||
* deprecated command `init:acl` along with `InitAclCommand` class | |||
* Added support for the new Argon2i password encoder. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
minor: dot at the end should be removed
// Argon2i encoder | ||
if ('argon2i' === $config['algorithm']) { | ||
if (!Argon2iPasswordEncoder::isSupported()) { | ||
throw new InvalidConfigurationException('Argon2i algorithm is not supported. Please install libsodium extension or upgrade to PHP 7.2+.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
install the libsodium extension?
@@ -13,6 +13,7 @@ CHANGELOG | |||
the user will always be logged out when the user has changed between | |||
requests. | |||
* deprecated HTTP digest authentication | |||
* Added a new password encoder for the Argon2i hashing algorithm. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same, no dot
use Symfony\Component\Security\Core\Exception\BadCredentialsException; | ||
|
||
/** | ||
* Argon2iPasswordEncoder uses the Argon2i hashing algorithm provided by libsodium. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would remove the "provided by libsodium" part, which is an implementation detail.
return $valid; | ||
} | ||
|
||
throw new \LogicException('Argon2i algorithm is not supported. Please install libsodium extension or upgrade to PHP 7.2+.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, "install the libsodium extension" sounds better.
return $this->encodePasswordSodiumExtension($raw); | ||
} | ||
|
||
throw new \LogicException('Argon2i algorithm is not supported. Please install libsodium extension or upgrade to PHP 7.2+.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, "install the libsodium extension" sounds better.
That's because Travis installs the |
@zanbaldwin In fact, the code is right (at least on the PHP 5.5 build, see https://travis-ci.org/symfony/symfony/jobs/280855177#L3622), but the Argon2i algorithm is not available. Tests should reflect this possibility. |
{ | ||
return function_exists('sodium_crypto_pwhash_str') | ||
|| extension_loaded('libsodium') | ||
|| (\PHP_VERSION_ID >= 70200 && defined('PASSWORD_ARGON2I')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defined
must be fully-qualified, so that it can be resolved at compile time when the constant is defined in PHP itself (which is the case when it is set on PHP 7.2+).
and the PHP 7.2 case should be there first, so that OPcache can optimize the code. return true || function_exists('sodium_crypto_pwhash_str') || extension_loaded('libsodium')
can be optimized into return true
. But return function_exists('sodium_crypto_pwhash_str') || extension_loaded('libsodium') || true
cannot (as earlier branches of the boolean condition must be evaluated first in PHP)
throw new BadCredentialsException('Invalid password.'); | ||
} | ||
|
||
if (\PHP_VERSION_ID >= 70200 && defined('PASSWORD_ARGON2I')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defined
should be fully qualified here too for optimizations
*/ | ||
public function isPasswordValid($encoded, $raw, $salt) | ||
{ | ||
if (\PHP_VERSION_ID >= 70200 && defined('PASSWORD_ARGON2I')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
defined
should be fully qualified here too for optimizations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
almost good :)
{ | ||
const PASSWORD = 'password'; | ||
|
||
public function setUp() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be protected
'JMS\FooBundle\Entity\User7' => array( | ||
'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', | ||
'arguments' => array(), | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what breaks tests, as it runs on PHP 5.6. I suggest writing a dedicated test in SecurityExtensionTest, skipped if argon2i not supported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite sure how to implement that - will need more details or be pointed in the right direction if I'm to implement that by end of play tomorrow, sorry!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did it for you in 51be4e5 :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, thank you very much @chalasr! Looking at that commit, it would have taken me a long time to figure that out!
if (\PHP_VERSION_ID >= 70200 && \defined('PASSWORD_ARGON2I')) { | ||
return !$this->isPasswordTooLong($raw) && password_verify($raw, $encoded); | ||
} | ||
if (function_exists('sodium_crypto_pwhash_str_verify')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be fully qualified too, considering PHP-CS-Fixer/PHP-CS-Fixer#3048 (comment) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, it should
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, will fully-qualify those two functions too :)
|
||
return $valid; | ||
} | ||
if (extension_loaded('libsodium')) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All good
Should I squash my five commits? |
@zanbaldwin We can do it while merging, as you wish. |
In fact yes, squashing is needed because we can't make it when several authors are in the history. |
Add the Argon2i hashing algorithm provided by libsodium as a core encoder in the Security component, and enable it in the SecurityBundle. Credit to @chalasr for help with unit tests.
Thank you @zanbaldwin. |
This PR was merged into the 3.4 branch. Discussion ---------- [Security] Argon2i Password Encoder | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | WIP Since the [libsodium RFC](https://wiki.php.net/rfc/libsodium) passed with flying colours, I'd like to kick start a discussion about adding Argon2i as a password encoder to the security component. The initial code proposal in this PR supports both the upcoming public API confirmed for PHP 7.2, and the [libsodium PECL extension](https://pecl.php.net/package/libsodium) for those below 7.2 (available for PHP 5.4+). #### Concerns - Should the test cover hash length? At the moment the result of Argon2i is 96 characters, but because the hashing parameters are included in the result (`$argon2i$v=19$m=32768,t=4,p=1$...`) this is not guaranteed. - I've used one password encoder class because the result *should* be the same whether running natively in 7.2 or from the PECL extension, but should the logic be split out into separate private methods (like `Argon2iPasswordEncoder::encodePassword()`) or not (like in `Argon2iPasswordEncoder::isPasswordValid()`)? Since I can't really find anything concrete on Symfony choosing one way over another I'm assuming it's down to personal preference? #### The Future Whilst the libsodium RFC has been approved and the public API confirmed, there has been no confirmation of Argon2i becoming an official algorithm for `passhword_hash()`. If that is confirmed, then the implementation should *absolutely* use the native `password_*` functions since the `sodium_*` functions do not have an equivalent to the `password_needs_rehash()` function. Any feedback would be greatly appreciated 😃 Commits ------- be093dd Argon2i Password Encoder
Add/modify sections for the Argon2i password encoder (symfony/symfony#21604).
Add/modify sections for the Argon2i password encoder (symfony/symfony#21604).
This PR was merged into the 3.4 branch. Discussion ---------- Argon2i Password Encoder | Q | A | | --- | --- | | Doc fix? | no | | New docs? | yes (symfony/symfony#21604) | | Applies to | `3.4` | | Fixed tickets | N/A | Add sections for the Argon2i password encoder. Commits ------- d96fda4 Removed a duplicated reference be4f85c Argon2i Password Encoder
Since the libsodium RFC passed with flying colours, I'd like to kick start a discussion about adding Argon2i as a password encoder to the security component. The initial code proposal in this PR supports both the upcoming public API confirmed for PHP 7.2, and the libsodium PECL extension for those below 7.2 (available for PHP 5.4+).
Concerns
$argon2i$v=19$m=32768,t=4,p=1$...
) this is not guaranteed.Argon2iPasswordEncoder::encodePassword()
) or not (like inArgon2iPasswordEncoder::isPasswordValid()
)? Since I can't really find anything concrete on Symfony choosing one way over another I'm assuming it's down to personal preference?The Future
Whilst the libsodium RFC has been approved and the public API confirmed, there has been no confirmation of Argon2i becoming an official algorithm for
passhword_hash()
. If that is confirmed, then the implementation should absolutely use the nativepassword_*
functions since thesodium_*
functions do not have an equivalent to thepassword_needs_rehash()
function.Any feedback would be greatly appreciated 😃