Open ScopeyNZ opened 5 years ago
What's the point of the need for a rehash exactly? You mean if they happen to be in MD5, then automatically rehash to bcrypt?
Yes. Or even if they're in bcrypt and you decide to upgrade to argon2i then it would happen on the next user login (because you need to rehash their plain-text password). It's transparent to the user but you're upgrading the password hash.
+1 from me, I hit that shortcoming of our APIs in https://github.com/silverstripe/silverstripe-framework/pull/8061
Ok I've been looking at this, also taking into account @madmatt's comments on #8525 about renaming these classes too. I'm trying to come up with an plan to essentially rewrite this hashing API to be password/member inspecific, and then have some nice drop in code so the existing API still works. Of course we'd still deprecate the whole existing API slated for removal in 5.0
PasswordEncryptor
" API usageHere's my proposal for a new API:
hash($plaintext): string
verify($plaintext, $hash): boolean
needsRehash($hash): boolean
setHashMethod(HashMethodInterface $hashMethod): $this
By default on construct of this service it will use a configured hash method. More on this later. It's pretty commonplace to store the salt on the result of the hash these days, that's what I recommend here. Validating a hash using this API will use a timing safe comparison function (not currently used).
needsRehash
is available for use and will be used internally when verifying to update the hash.
I don't expect the setHashMethod
to be commonly used.
setForMember($plaintext, Member $member): $this
verifyForMember($plaintext, Member $member, $skipRehash = false): boolean
This will wrap a CryptographicHashService
. Should be self explanatory. Requiring a Member
for validation means that you skip things like SELECT WHERE password_hash = ?
which is not timing safe.
When verifying we can have a flow like verify
--TRUE-> needsRehash
--TRUE-> hash
, $member->Hash = $newHash; $member->write()
.
hash($plaintext): string
verify($plaintext, $hash): boolean
identifier(): string
needsRehash($hash): boolean
Hash methods will have to have some sort of identifier. This is so we can identify hash methods from their hash powering the needsRehash
API. Currently PHPs password_hash
does this by prefixing some distinguishable string onto the front of the hash. If we're implementing our own hashers we'll have to do something similar. This could be derived from the FQN of the hash method?
verify
is included because PHP provides an API in the form of password_verify
for hashes generated with password_hash
. In most cases this will just be a case of return hash_equals($hash, $this->hash($plaintext))
so providing an abstract here would be helpful.
needsRehash
exists here again because individual hash methods may opt to indicate a rehash is required if the cost params change (or for password_hash
the algo changes). Some HashMethod
s may just return false
here.
PasswordEncryptor
I've had a look at the existing usage within a cwp-recipe-kitchen-sink
installation:
MemberAuthenticator::checkPassword
PasswordService
Security::encrypt_password
CryptographicHashService
- doesn't necessarily take a Member
Member::encryptPassword
. This method implies there is a point in time during the change password flow where a password is in plain text? This should be removed. Passwords should ONLY enter the database hashed. This is protected - Can we remove in a minor? It's only used by:Member::changePassword
can use PasswordService
to set the password hash directly, without ever setting the plaintext password on the object (which it will do currently)MemberPassword::checkPassword
PasswordService
Member::encryptWithUserSettings
CryptographicHashService
Member::encryptWithUserSettings
and it's usages:
CookieAuthenticationHandler::authenticateRequest
CryptographicHashService
RememberLoginHash::getNewHash
CryptographicHashService
ChangePasswordHandler::setSessionToken
and...Member::generateAutologinTokenAndStoreHash
and...Member::validateAutoLoginToken
CryptographicHashService
Then we can deprecate all existing PasswordEncryptor
APIs too. It'd be nice to keep a Security::(hash / verifyHash)
API too.
I know this is a bit of a leap from "Introduce a needs rehash API". I guess I should rename the issue to "Refactor (password) hashing API"?.
Nice write up @ScopeyNZ! I think the new API you’ve described looks great, I do have one minor concern (that came up here: https://github.com/silverstripe/silverstripe-framework/pull/8061#issuecomment-387675870). Do you think we could deprecate Member::encryptWithUserSettings()
, but leave it using the old “encryption” (sic) APIs to ensure BC?
Nice write up @ScopeyNZ! I think the new API you’ve described looks great, I do have one minor concern (that came up here: #8061 (comment)). Do you think we could deprecate
Member::encryptWithUserSettings()
, but leave it using the old “encryption” (sic) APIs to ensure BC?
I think it'll be possible to create a "HashMethod
" that just wraps the legacy code. I'm going to try and work a little on a POC this Friday which will help identify if this is a problem. I am only a little worried about validating Member::encryptWithUserSettings()
after a users hash method has been updated but the code is comparing an old hash. I think it could be possible though.
I made some really good progress on Friday writing up a POC for this. There's some funky stuff going on that I'm going to have to change that I hope won't break BC. For example I don't know why we do a $member->Password = 'plaintext_password'
and then run a validator on that. Seems sort of dangerous to ever assign the plaintext password on a member object.
I don't know why we do a $member->Password = 'plaintext_password' and then run a validator on that. Seems sort of dangerous to ever assign the plaintext password on a member object.
This is done so that you can load a form's worth of data directly into a Member object, or assign a password directly then write it. It gets hashed on write. What might an alternative look like?
I've raised a WIP PR for POC work here: #8806 . There's still a bunch of work to do but I'm happy to receive any comments if anyone can motivate themselves to have a glance.
This is done so that you can load a form's worth of data directly into a Member object, or assign a password directly then write it. It gets hashed on write. What might an alternative look like?
I have played with "an alternative" in the WIP PR. Personally I think it's pretty dangerous to have passwords temporarily stored in plain text on a Member object. Additionally the existing implementation doesn't really make it possible to store a plain text password on purpose - it's not very deterministic.
Affected Version
4.x
Description
In order to easily facilitate upgrading password hashes as they evolve (and computers get better at generating rainbow tables) we should add an API to check if password hashes need to be rehashed and use this new interface.
This will closely replicate the API provided by PHP for password hashing: http://php.net/manual/en/function.password-needs-rehash.php . Once this is done we should implement a
PasswordEncryptor
that uses thepassword_hash
API from PHP and trust PHP core team to stay up to date - which they seems to be capable of doing: https://wiki.php.net/rfc/argon2_password_hash, https://wiki.php.net/rfc/argon2_password_hash_enhancementsProposal
MemberPassword
calledneedsRehash($password)
or something similar.PasswordEncryptor
interface along the same linesMemberAuthenticator
we use theneedsRehash
and perform the rehash if necessaryPasswordEncryptor
which delegates the providedpassword_hash
API - This could be a follow up I guess.