-
Notifications
You must be signed in to change notification settings - Fork 1k
Don't store plaintext passwords #1040
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
base: master
Are you sure you want to change the base?
Conversation
Rather store hashed passwords in the way mysql server does. here we: - update CredentialProvider to return new Credential struct - Credential includes the plugin that the user was created with - update InMemoryProvider to handle hashing of passwords and add default auth method to make usage backwards compatible - update server authentication to use mysql server methods of comparing hashes rather than relying on having the plaintext password available - rework the password negotiation to switch plugin type to match the stored credentials - add hashing and comparison functions for the above where missing from existing libraries
Code is fully working as much as I can see but I am seeing failures with the |
also update the caching test to allow for the fact that we request the password once on every auth attempt
The changes to the code is what has broken the test, we are now getting the password to confirm the auth method. I think the code changes break the way you are checking if we are using the cache response, but I'm not sure how to update the test to ensure we are using the cache (though through debugging I can confirm that it does) |
encapsulate the creation of credentials to simplify downstream implementation publicise the password and plugin so downstream implementations are able to access them
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.
Pull Request Overview
Avoid storing plaintext passwords by hashing credentials and updating authentication flow to match MySQL server behavior.
- Change
CredentialProvider
andInMemoryProvider
to return and store aCredential
struct containing a hashed password and plugin name. - Refactor server handshake and auth logic to use hashed credentials, plugin negotiation, and unified
compareAuthData
. - Add utility functions in
mysql/util.go
for password hashing, encoding/decoding, and plugin-specific comparisons.
Reviewed Changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
server/server_test.go | Adapt tests for new AddUser signature and NewCustomizedConn |
server/resp.go | Track authPluginName on switch requests |
server/handshake_resp.go | Use credential.authPluginName for auth switch logic |
server/credential_provider.go | Return Credential (hash+plugin), hash on add, preserve defaults |
server/conn.go | Replace plaintext password field with Credential |
server/caching_sha2_cache_test.go | Update call count assertion and NewCustomizedConn usage |
server/auth_switch_response.go | Simplify to compareAuthData |
server/auth.go | Refactor all comparisons to use Credential |
mysql/util.go | Add native, SHA2, SHA256 hashing + encode/decode helpers |
client/auth.go | Update client to use new hash util signatures |
Comments suppressed due to low confidence (4)
server/auth_switch_response.go:11
- The import 'github.com/pingcap/tidb/pkg/parser/auth' is unused in this file and should be removed to avoid compile errors.
"github.com/pingcap/tidb/pkg/parser/auth"
mysql/util.go:216
- rand.Read is used here but package 'crypto/rand' is not imported; either import 'crypto/rand' or qualify this call.
_, err := rand.Read(salt)
mysql/util.go:90
- strings.ToUpper is used in EncodePasswordHex but the 'strings' package is not imported; add
import "strings"
.
hexstr := strings.ToUpper(hex.EncodeToString(passwordHash))
server/auth.go:106
- compareNativePasswordAuthData accepts a Credential parameter but decodes c.credential.password instead of the passed-in credential; change to use
credential.password
.
password, err := mysql.DecodePasswordHex(c.credential.password)
crypt.Write(hash) | ||
scramble = crypt.Sum(nil) | ||
crypt.Write(stage2) | ||
scrambleHash := crypt.Sum(nil) |
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.
nit: seems stage2
is not used anymore, so we can pass-in stage2[:0]
to reuse the array under the slice.
} | ||
if bytes.Equal(plain, dbytes) { | ||
return nil | ||
clientAuthData = mysql.Xor(dbytes, c.salt) |
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 need more time to check if the lengths of dbytes
and c.salt
are equal)
@@ -135,6 +202,91 @@ func EncryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, | |||
return rsa.EncryptOAEP(sha1v, rand.Reader, pub, plain, nil) | |||
} | |||
|
|||
const ( | |||
SALT_LENGTH = 16 |
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.
nit: My IDE says "Use camel case instead of snake case". However this project has both style so you can ignore it.
|
||
// hashCrypt256 salt and hash a password the given number of iterations | ||
func hashCrypt256(source, salt string, iterations uint64) (string, error) { | ||
actualIterations := iterations * ITERATION_MULTIPLIER |
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.
for this function, the const ITERATION_MULTIPLIER
is directly used. But for generateUserSalt
SALT_LENGTH
is pass-in as an argument. I think generateUserSalt
can also directly use SALT_LENGTH
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 tried to match mysql server salt generation, but I'm wondering if it's used there to generate both the salt for the stored password hashes and for scramble generation. I see the scramble generation here is using something similar but not quite the same, perhaps the two can/should be combined?
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 didn't get your point clearly.
perhaps the two can/should be combined?
Do you mean the "scramble generation" related functions are similar to "salt generation" functions, so you want to merge them? Can you give a simple explanation about which functions are affected?
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, I was looking at RandomBuf
in mysql/util.go:319
which I believe may be better suited to use crypto/rand
. I think that mysql is avoiding the multi-byte UTF-8 to simplify client/server salt exchanges so it either makes sense to remove that in the salt generation or combine the two functions here and generate salts that suit both purposes.
} | ||
|
||
hashHex := hex.EncodeToString(hash[:]) | ||
digest := fmt.Sprintf("$%d$%s$%s", iterations, salt, hashHex) |
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 generateUserSalt
I see some restrictions are added to the random bytes. Is this line the reason of it? I think it's OK to remove the restrictions to make the whole logic more simple. We can hex-encode the salt
without the restrictions.
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.
You might be right, I was trying to recreate what was being done in mysql-server, but it may not be necessary here
mysql/util.go
Outdated
// FROM vitess.io/vitess/go/mysql/auth_server.go | ||
// DecodePasswordHex decodes the standard format used by MySQL | ||
// for 4.1 style password hashes. It drops the optionally leading * before | ||
// decoding the rest as a hex encoded string. |
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.
// FROM vitess.io/vitess/go/mysql/auth_server.go | |
// DecodePasswordHex decodes the standard format used by MySQL | |
// for 4.1 style password hashes. It drops the optionally leading * before | |
// decoding the rest as a hex encoded string. | |
// DecodePasswordHex decodes the standard format used by MySQL | |
// for 4.1 style password hashes. It drops the optionally leading * before | |
// decoding the rest as a hex encoded string. | |
// the implementation is also learned from vitess.io/vitess/go/mysql/auth_server.go |
Golang's coding style suggests the function name DecodePasswordHex
being put at the beginning.
// stage2Hash = SHA1(stage1Hash) | ||
crypt.Reset() | ||
crypt.Write(stage1) | ||
return crypt.Sum(nil) |
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.
return crypt.Sum(nil) | |
return crypt.Sum(stage1[:0]) |
The argument nil
will let crypt.Sum
allocate a new array of the returned slice. I think using stage1[:0]
will reuse the underlying array of stage1
to avoid that allocation.
crypt := sha1.New() | ||
crypt.Write(stage1) | ||
stage2 := crypt.Sum(nil) | ||
|
||
// check(candidate_hash2 == hash_stage2) | ||
// use ConstantTimeCompare to mitigate timing based attacks | ||
return subtle.ConstantTimeCompare(stage2, stored) == 1 |
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.
crypt := sha1.New() | |
crypt.Write(stage1) | |
stage2 := crypt.Sum(nil) | |
// check(candidate_hash2 == hash_stage2) | |
// use ConstantTimeCompare to mitigate timing based attacks | |
return subtle.ConstantTimeCompare(stage2, stored) == 1 | |
stage2 := sha1.Sum(stage1) | |
// check(candidate_hash2 == hash_stage2) | |
// use ConstantTimeCompare to mitigate timing based attacks | |
return subtle.ConstantTimeCompare(stage2[:], stored) == 1 |
we can reuse the builtin functions.
if len(password) == 0 { | ||
return nil | ||
} | ||
|
||
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) | ||
|
||
crypt := sha256.New() | ||
crypt.Write([]byte(password)) | ||
crypt.Write(password) | ||
message1 := crypt.Sum(nil) |
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.
Oh, I see that you just mimic the old code, so my comments like crypt.Sum(stage1[:0])
or using sha1.Sum
should be applied to the old codes as well. This is a bit irrelevant to your PR, so you can ignore these comments and I'll try to fix them in another PR.
|
||
// hashCrypt256 salt and hash a password the given number of iterations | ||
func hashCrypt256(source, salt string, iterations uint64) (string, error) { | ||
actualIterations := iterations * ITERATION_MULTIPLIER |
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 didn't get your point clearly.
perhaps the two can/should be combined?
Do you mean the "scramble generation" related functions are similar to "salt generation" functions, so you want to merge them? Can you give a simple explanation about which functions are affected?
Rather store hashed passwords in the way mysql server does.
See #1037
here we: