Skip to content

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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

gdsmith
Copy link

@gdsmith gdsmith commented May 22, 2025

Rather store hashed passwords in the way mysql server does.

See #1037

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

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
@gdsmith
Copy link
Author

gdsmith commented May 22, 2025

Code is fully working as much as I can see but I am seeing failures with the TestCachingSha2Cache tests. I don't fully understand if it's a client problem or a change I've introduced means the server is pulling those credentials multiple times when it didn't previously. I did try debugging and it seemed like the cache was empty even if I confirmed the cache was actually written. Would love some feedback, and some help with the failing tests if at all possible.

@lance6716 lance6716 requested a review from dveeden May 23, 2025 01:41
also update the caching test to allow for the fact that we request the password once on every auth attempt
@gdsmith
Copy link
Author

gdsmith commented May 23, 2025

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)

gdsmith and others added 3 commits May 23, 2025 09:28
encapsulate the creation of credentials to simplify downstream implementation

publicise the password and plugin so downstream implementations are able to access them
@lance6716 lance6716 requested a review from Copilot May 25, 2025 09:53
Copy link
Contributor

@Copilot Copilot AI left a 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 and InMemoryProvider to return and store a Credential 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)

}
if bytes.Equal(plain, dbytes) {
return nil
clientAuthData = mysql.Xor(dbytes, c.salt)
Copy link
Collaborator

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)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems they may have different lengths, at least a malicious attacker can send authData because it just reads from a packet without length check 🤔


// hashCrypt256 salt and hash a password the given number of iterations
func hashCrypt256(source, salt string, iterations uint64) (string, error) {
actualIterations := iterations * ITERATION_MULTIPLIER
Copy link
Collaborator

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

Copy link
Author

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?

Copy link
Collaborator

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?

Copy link
Author

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)
Copy link
Collaborator

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.

Copy link
Author

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
Comment on lines 76 to 79
// 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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// 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.


// hashCrypt256 salt and hash a password the given number of iterations
func hashCrypt256(source, salt string, iterations uint64) (string, error) {
actualIterations := iterations * ITERATION_MULTIPLIER
Copy link
Collaborator

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?

@dveeden
Copy link
Collaborator

dveeden commented Aug 20, 2025

@gdsmith could you merge master into your branch to update it and have a look at the conversations that aren't resolved yet?

@gdsmith
Copy link
Author

gdsmith commented Aug 20, 2025

I've merged and added the suggestions. Is it my role to resolve the conversations here?

@dveeden
Copy link
Collaborator

dveeden commented Aug 20, 2025

I've merged and added the suggestions. Is it my role to resolve the conversations here?

For the conversations/comments: have a look at them and feel free to click "Resolve conversation" if you think they are handled.

@dveeden
Copy link
Collaborator

dveeden commented Aug 20, 2025

golangci/errcheck is failing Adduser() is called in a few places without checking the error that it returns.

Best to handle the error and if that isn't possible to explicitly ignore it.

@lance6716
Copy link
Collaborator

Hi @gdsmith,

I've resolved a few comments, and I'd like to follow up on the remaining ones. This seems like a really valuable and helpful PR. If you're busy with other tasks, I'd be happy to help move it forward. What do you think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants