-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Auth refactoring and bug fixes #807
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
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
d6fe90f
log missing auth plugin name
julienschmidt 36c70eb
refactor auth handling
julienschmidt cf52770
auth: fix AllowNativePasswords
julienschmidt f3b65ae
auth: remove plugin name print
julienschmidt eb54ab9
packets: attempt to fix writePublicKeyAuthPacket
julienschmidt 86adb9b
packets: do not NUL-terminate auth switch packets
julienschmidt ceb1236
move handleAuthResult to auth
julienschmidt fa931ce
add old_password auth tests
julienschmidt 66f8d59
auth: add empty old_password test
julienschmidt de51fdd
auth: add cleartext auth tests
julienschmidt d0d482d
auth: add native auth tests
julienschmidt 9be743a
auth: add caching_sha2 tests
julienschmidt 7242d1e
rename init and auth packets to documented names
julienschmidt d09ac83
auth: fix plugin name for switched auth methods
julienschmidt fba0ad9
buffer: optimize default branches
julienschmidt f252087
auth: add tests for switch to caching sha2
julienschmidt a27458c
auth: add tests for switch to cleartext password
julienschmidt caae030
auth: add tests for switch to native password
julienschmidt 8985303
auth: sync NUL termination with official connectors
julienschmidt 2846c61
packets: handle missing NUL bytes in AuthSwitchRequests
julienschmidt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,309 @@ | ||
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package | ||
// | ||
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved. | ||
// | ||
// This Source Code Form is subject to the terms of the Mozilla Public | ||
// License, v. 2.0. If a copy of the MPL was not distributed with this file, | ||
// You can obtain one at http://mozilla.org/MPL/2.0/. | ||
|
||
package mysql | ||
|
||
import ( | ||
"crypto/rand" | ||
"crypto/rsa" | ||
"crypto/sha1" | ||
"crypto/sha256" | ||
"crypto/x509" | ||
"encoding/pem" | ||
) | ||
|
||
// Hash password using pre 4.1 (old password) method | ||
// https://github.com/atcurtis/mariadb/blob/master/mysys/my_rnd.c | ||
type myRnd struct { | ||
seed1, seed2 uint32 | ||
} | ||
|
||
const myRndMaxVal = 0x3FFFFFFF | ||
|
||
// Pseudo random number generator | ||
func newMyRnd(seed1, seed2 uint32) *myRnd { | ||
return &myRnd{ | ||
seed1: seed1 % myRndMaxVal, | ||
seed2: seed2 % myRndMaxVal, | ||
} | ||
} | ||
|
||
// Tested to be equivalent to MariaDB's floating point variant | ||
// http://play.golang.org/p/QHvhd4qved | ||
// http://play.golang.org/p/RG0q4ElWDx | ||
func (r *myRnd) NextByte() byte { | ||
r.seed1 = (r.seed1*3 + r.seed2) % myRndMaxVal | ||
r.seed2 = (r.seed1 + r.seed2 + 33) % myRndMaxVal | ||
|
||
return byte(uint64(r.seed1) * 31 / myRndMaxVal) | ||
} | ||
|
||
// Generate binary hash from byte string using insecure pre 4.1 method | ||
func pwHash(password []byte) (result [2]uint32) { | ||
var add uint32 = 7 | ||
var tmp uint32 | ||
|
||
result[0] = 1345345333 | ||
result[1] = 0x12345671 | ||
|
||
for _, c := range password { | ||
// skip spaces and tabs in password | ||
if c == ' ' || c == '\t' { | ||
continue | ||
} | ||
|
||
tmp = uint32(c) | ||
result[0] ^= (((result[0] & 63) + add) * tmp) + (result[0] << 8) | ||
result[1] += (result[1] << 8) ^ result[0] | ||
add += tmp | ||
} | ||
|
||
// Remove sign bit (1<<31)-1) | ||
result[0] &= 0x7FFFFFFF | ||
result[1] &= 0x7FFFFFFF | ||
|
||
return | ||
} | ||
|
||
// Hash password using insecure pre 4.1 method | ||
func scrambleOldPassword(scramble []byte, password string) []byte { | ||
if len(password) == 0 { | ||
return nil | ||
} | ||
|
||
scramble = scramble[:8] | ||
|
||
hashPw := pwHash([]byte(password)) | ||
hashSc := pwHash(scramble) | ||
|
||
r := newMyRnd(hashPw[0]^hashSc[0], hashPw[1]^hashSc[1]) | ||
|
||
var out [8]byte | ||
for i := range out { | ||
out[i] = r.NextByte() + 64 | ||
} | ||
|
||
mask := r.NextByte() | ||
for i := range out { | ||
out[i] ^= mask | ||
} | ||
|
||
return out[:] | ||
} | ||
|
||
// Hash password using 4.1+ method (SHA1) | ||
func scramblePassword(scramble []byte, password string) []byte { | ||
if len(password) == 0 { | ||
return nil | ||
} | ||
|
||
// stage1Hash = SHA1(password) | ||
crypt := sha1.New() | ||
crypt.Write([]byte(password)) | ||
stage1 := crypt.Sum(nil) | ||
|
||
// scrambleHash = SHA1(scramble + SHA1(stage1Hash)) | ||
// inner Hash | ||
crypt.Reset() | ||
crypt.Write(stage1) | ||
hash := crypt.Sum(nil) | ||
|
||
// outer Hash | ||
crypt.Reset() | ||
crypt.Write(scramble) | ||
crypt.Write(hash) | ||
scramble = crypt.Sum(nil) | ||
|
||
// token = scrambleHash XOR stage1Hash | ||
for i := range scramble { | ||
scramble[i] ^= stage1[i] | ||
} | ||
return scramble | ||
} | ||
|
||
// Hash password using MySQL 8+ method (SHA256) | ||
func scrambleSHA256Password(scramble []byte, password string) []byte { | ||
if len(password) == 0 { | ||
return nil | ||
} | ||
|
||
// XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble)) | ||
|
||
crypt := sha256.New() | ||
crypt.Write([]byte(password)) | ||
message1 := crypt.Sum(nil) | ||
|
||
crypt.Reset() | ||
crypt.Write(message1) | ||
message1Hash := crypt.Sum(nil) | ||
|
||
crypt.Reset() | ||
crypt.Write(message1Hash) | ||
crypt.Write(scramble) | ||
message2 := crypt.Sum(nil) | ||
|
||
for i := range message1 { | ||
message1[i] ^= message2[i] | ||
} | ||
|
||
return message1 | ||
} | ||
|
||
func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, bool, error) { | ||
switch plugin { | ||
case "caching_sha2_password": | ||
authResp := scrambleSHA256Password(authData, mc.cfg.Passwd) | ||
return authResp, (authResp == nil), nil | ||
|
||
case "mysql_old_password": | ||
if !mc.cfg.AllowOldPasswords { | ||
return nil, false, ErrOldPassword | ||
} | ||
// Note: there are edge cases where this should work but doesn't; | ||
// this is currently "wontfix": | ||
// https://github.com/go-sql-driver/mysql/issues/184 | ||
authResp := scrambleOldPassword(authData[:8], mc.cfg.Passwd) | ||
return authResp, true, nil | ||
|
||
case "mysql_clear_password": | ||
if !mc.cfg.AllowCleartextPasswords { | ||
return nil, false, ErrCleartextPassword | ||
} | ||
// http://dev.mysql.com/doc/refman/5.7/en/cleartext-authentication-plugin.html | ||
// http://dev.mysql.com/doc/refman/5.7/en/pam-authentication-plugin.html | ||
return []byte(mc.cfg.Passwd), true, nil | ||
|
||
case "mysql_native_password": | ||
if !mc.cfg.AllowNativePasswords { | ||
return nil, false, ErrNativePassword | ||
} | ||
// https://dev.mysql.com/doc/internals/en/secure-password-authentication.html | ||
// Native password authentication only need and will need 20-byte challenge. | ||
authResp := scramblePassword(authData[:20], mc.cfg.Passwd) | ||
return authResp, false, nil | ||
|
||
default: | ||
errLog.Print("unknown auth plugin:", plugin) | ||
return nil, false, ErrUnknownPlugin | ||
} | ||
} | ||
|
||
func (mc *mysqlConn) handleAuthResult(oldAuthData []byte, plugin string) error { | ||
// Read Result Packet | ||
authData, newPlugin, err := mc.readAuthResult() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// handle auth plugin switch, if requested | ||
if newPlugin != "" { | ||
// If CLIENT_PLUGIN_AUTH capability is not supported, no new cipher is | ||
// sent and we have to keep using the cipher sent in the init packet. | ||
if authData == nil { | ||
authData = oldAuthData | ||
} | ||
|
||
plugin = newPlugin | ||
|
||
authResp, addNUL, err := mc.auth(authData, plugin) | ||
if err != nil { | ||
return err | ||
} | ||
if err = mc.writeAuthSwitchPacket(authResp, addNUL); err != nil { | ||
return err | ||
} | ||
|
||
// Read Result Packet | ||
authData, newPlugin, err = mc.readAuthResult() | ||
if err != nil { | ||
return err | ||
} | ||
// Do not allow to change the auth plugin more than once | ||
if newPlugin != "" { | ||
return ErrMalformPkt | ||
} | ||
} | ||
|
||
switch plugin { | ||
|
||
// https://insidemysql.com/preparing-your-community-connector-for-mysql-8-part-2-sha256/ | ||
case "caching_sha2_password": | ||
switch len(authData) { | ||
case 0: | ||
return nil // auth successful | ||
case 1: | ||
switch authData[0] { | ||
case cachingSha2PasswordFastAuthSuccess: | ||
if err = mc.readResultOK(); err == nil { | ||
return nil // auth successful | ||
} | ||
|
||
case cachingSha2PasswordPerformFullAuthentication: | ||
if mc.cfg.tls != nil || mc.cfg.Net == "unix" { | ||
// write cleartext auth packet | ||
err = mc.writeAuthSwitchPacket([]byte(mc.cfg.Passwd), true) | ||
if err != nil { | ||
return err | ||
} | ||
} else { | ||
seed := oldAuthData | ||
|
||
// TODO: allow to specify a local file with the pub key via | ||
// the DSN | ||
|
||
// request public key | ||
data := mc.buf.takeSmallBuffer(4 + 1) | ||
data[4] = cachingSha2PasswordRequestPublicKey | ||
mc.writePacket(data) | ||
|
||
// parse public key | ||
data, err := mc.readPacket() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
block, _ := pem.Decode(data[1:]) | ||
pub, err := x509.ParsePKIXPublicKey(block.Bytes) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// send encrypted password | ||
plain := make([]byte, len(mc.cfg.Passwd)+1) | ||
copy(plain, mc.cfg.Passwd) | ||
for i := range plain { | ||
j := i % len(seed) | ||
plain[i] ^= seed[j] | ||
} | ||
sha1 := sha1.New() | ||
enc, err := rsa.EncryptOAEP(sha1, rand.Reader, pub.(*rsa.PublicKey), plain, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if err = mc.writeAuthSwitchPacket(enc, false); err != nil { | ||
return err | ||
} | ||
} | ||
if err = mc.readResultOK(); err == nil { | ||
return nil // auth successful | ||
} | ||
|
||
default: | ||
return ErrMalformPkt | ||
} | ||
default: | ||
return ErrMalformPkt | ||
} | ||
|
||
default: | ||
return nil // auth successful | ||
} | ||
|
||
return err | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
The actual changes start here. The code above was moved from utils.go