-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Add Crypto::Bcrypt::Password#==
#15270
Conversation
Previously, it used `Reference` equality, which compares identity rather than value.
Thanks for this initiative. As always, it's recommended to open an issue first to discuss proposals. This method existed previously but was removed after the discussion in #7753. I think the main issue is that So I'd suggest to consider using a different type for password storage in your model as an alternative solution. We can certainly talk about use cases and reconsider the decision on #7753, but that'll need deep arguments. |
That issue seems to be about using |
All I’m looking for is that two objects of the same type with the same state return
I’m sincerely curious how you think I’m storing it in my DB, because what you’re describing is what I’m doing. I’m storing it in the DB as a string and decoding it into a password when retrieving it. I don’t know of another way to store it, tbh. |
but this PR adds |
That's a valid point then 👍 |
I don't mean for permanent storage. But you must be storing a |
I disagree. That's analogous to holding JSON blobs in their string format and only parsing them temporarily to access their properties, then discarding the parsed data structure. A password passed through bcrypt is not an opaque string like the cleartext password is. It's converted to a string to send over the wire, such as to a database, but it's a data structure containing 4 scalars. This is the concept that |
Issue: it becomes possible to compare bcrypt hashes in non constant time. |
That's not a good analogy. Password hashes are predominantily used for authentication operations, which are typically very infrequent. In most cases when you instantiate a user model, it won't do any password verification.
I disagree. For anything else but the process of verifying and creating a password, the hash should be treated as opaque. Just because the |
I looked into the topic of why we have different versions of bcrypt hash digests, and also why we don't do anything about them during verification. This is because all versions are actually supposed to behave the same, just that various implementations of bcrypt chose to bump the version when they fixed a bug in their implementation.
|
With that out of the way, I can explain why it is invalid to blindly compare
As such, this pull request is invalid. |
Further footgun №1 People might already be using the wrong equality comparison via require "crypto/bcrypt/password"
cost = 10
stored = Crypto::Bcrypt::Password.create("super secret", cost: cost).to_s
cost = 12 # Decided to change the cost in the future
p Crypto::Bcrypt::Password.create("super secret", cost: cost).to_s == stored # => false This tells me that we can probably never change the value of The bcrypt module probably should not have had a "default" cost defined, only suggested via examples. |
Further footgun №2
@jgaskins clearly must have thought that by default it uses all fields, but actually it uses no fields. So this PR makes all Password objects equal to each other! Maybe |
@ysbaddaden Because of this PR? Why would anyone be using this method to compare hashes cryptographically?
@straight-shoota Maybe, if the rest of the core team holds the same position you do about it. Having the algorithm exposed in the
@oprypin This makes sense. The code examples you gave don't, though. They return require "crypto/bcrypt/password"
class Crypto::Bcrypt::Password
def_equals_and_hash version, cost, salt, digest
end
stored = Crypto::Bcrypt::Password.create("super secret", cost: 10).to_s.sub("$2a$", "$2y$")
retrieved = Crypto::Bcrypt::Password.new(stored)
puts Crypto::Bcrypt::Password.create("super secret", cost: 10)
# $2a$10$LHorOxEFXwvVsnvWJSPYTeDInFat9GPJeHg6r5EfgaUB48bo63kHi
puts retrieved
# $2y$10$XVMuriHkS9NYM2EcSBGLD.LV58fY15GNSv/eVwMDesBLeu0PmQsPO They aren't mismatched solely due to the different Unlike SHA, bcrypt isn't deterministic. Every time you call require "crypto/bcrypt/password"
alias Password = Crypto::Bcrypt::Password # This is so clunky to type
2.times { puts Password.create("password") }
# $2a$11$KjbncjfAhTe4xOk5qrem/OpZi7NdbZVq8Sm5.YL2wEe1HlmE.2L4C
# $2a$11$OiAG3SyUL95/NHvguErQfeQN1xv/nOyirpCS4n.3VEuJuci300gs.
Again, this draws a conclusion based on incorrect information. Here is a modified version of your code example where we don't change require "crypto/bcrypt/password"
cost = 10
stored = Crypto::Bcrypt::Password.create("super secret", cost: cost).to_s
# cost = 12 # Decided to change the cost in the future
p Crypto::Bcrypt::Password.create("super secret", cost: cost).to_s == stored # => false The output is exactly the same.
This is precisely what I thought. TIL. |
It was modeled after Ruby's bcrypt gem... I agree we don't need an object, a couple methods would be sufficient:
It would avoid the issues mentioned above, limit the exposed API, simplify the internal implementation, and we could avoid a bunch of intermediary allocations (especially We should definitely consider moving to such a limited API, then deprecate the |
I suppose the only use case for the
They're actually |
Exactly. Maybe a rainbow table or for hacking purposes... in which case there are faster alternatives. |
@jgaskins But the initial big point is still applicable - it can be misleading to directly compare two bcrypt hashes. |
Use case: my
User
records (viaDB::Serializable
) aren't considered==
because theirpassword
properties are not considered==
despite all properties being equal. This leads to confusing spec failures when doingactual_user.should eq expected_user
.Since
Crypto::Bcrypt::Password
doesn't appear to accrue state, as an alternative to adding==
it seems like it could instead be astruct
vs aclass
? This would do the same thing. Both of these ideas pass inspec/std/crypto/bcrypt/password_spec.cr
. I went with thedef_equals
because it's the monkeypatch I use in apps that require passwords (you can't monkeypatch aclass
to astruct
), but I'm also happy to convert it to astruct
.