diff --git a/client/client.go b/client/client.go index 48034df..88a26c4 100644 --- a/client/client.go +++ b/client/client.go @@ -4,6 +4,7 @@ import ( "bufio" "crypto/sha256" "crypto/tls" + "encoding/hex" "encoding/json" "fmt" "io" @@ -16,6 +17,7 @@ import ( "github.com/contribsys/faktory/internal/pool" "github.com/contribsys/faktory/util" + "golang.org/x/crypto/pbkdf2" ) const ( @@ -684,12 +686,11 @@ func readResponse(rdr *bufio.Reader) ([]byte, error) { } func hash(pwd, salt string, iterations int) string { - data := []byte(pwd + salt) - hash := sha256.Sum256(data) - if iterations > 1 { - for i := 1; i < iterations; i++ { - hash = sha256.Sum256(hash[:]) - } - } - return fmt.Sprintf("%x", hash) + pwdBytes := []byte(pwd) + saltBytes := []byte(salt) + + // The '32' parameter specifies the key length in bytes (256 bits for SHA-256) + hash := pbkdf2.Key(pwdBytes, saltBytes, iterations, 32, sha256.New) + + return fmt.Sprintf("sha256:%s", hex.EncodeToString(hash)) } diff --git a/docs/protocol-specification.md b/docs/protocol-specification.md index d40ba5b..2818afb 100644 --- a/docs/protocol-specification.md +++ b/docs/protocol-specification.md @@ -307,17 +307,56 @@ commands. When the server `HI` includes an iteration count `i` and a salt `s`, a client MUST include a `pwdhash` String-typed field in their `HELLO`. -This field should be the hexadecimal representation of the `i`th SHA256 -hash of the client password concatenated with the value in `s`. +This field should be the PBKDF2-HMAC-SHA256 digest represented +as a hex string. -```example -hash = password + s -for 0..i { - hash = sha256(hash) +Here's how to implement that in Go. + +```go +import ( + "crypto/sha256" + "fmt" + "golang.org/x/crypto/pbkdf2" + "encoding/hex" +) + +func hash(pwd, salt string, iterations int) string { + pwdBytes := []byte(pwd) + saltBytes := []byte(salt) + + // Generate the hash using PBKDF2-HMAC-SHA256. The '32' parameter + // specifies the key length in bytes (256 bits for SHA-256). + hash := pbkdf2.Key(pwdBytes, saltBytes, iterations, 32, sha256.New) + + return fmt.Sprintf("sha256:%s", hex.EncodeToString(hash)) } -hex(hash) ``` +And in Python. + +```python +import hashlib + +def hash(pwd: str, salt: str, iterations: int) -> str: + pwd_bytes = pwd.encode('utf-8') + salt_bytes = salt.encode('utf-8') + + # Generate the hash using PBKDF2-HMAC-SHA256. The dklen parameter + # specifies the key length in bytest (256 bits for SHA-256). + hash = hashlib.pbkdf2_hmac( + 'sha256', + pwd_bytes, + salt_bytes, + iterations, + dklen=32, + ) + + return f"sha256:{hash.hex()}" +``` + +You can gut-check your own implementation by checking that `hash("password", "salt", 50)` +returns `926891811ee18e4539b150e9c3888a1afb0eb6fb827ad0c01ab6b4b918a513ac`. + #### Required Fields for Consumers A client that wishes to act as a consumer MUST include the following diff --git a/go.mod b/go.mod index 16fd7df..c5270c2 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require github.com/redis/go-redis/v9 v9.6.1 require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + golang.org/x/crypto v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1d72f54..7c1874f 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0 github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/server/server.go b/server/server.go index 2cd8e67..fbb674b 100644 --- a/server/server.go +++ b/server/server.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "crypto/subtle" "crypto/tls" + "encoding/hex" "errors" "fmt" "io" @@ -24,6 +25,7 @@ import ( "github.com/contribsys/faktory/storage" "github.com/contribsys/faktory/util" "github.com/redis/go-redis/v9" + "golang.org/x/crypto/pbkdf2" ) type RuntimeStats struct { @@ -230,14 +232,27 @@ func cleanupConnection(s *Server, c *Connection) { } func hash(pwd, salt string, iterations int) string { - bytes := []byte(pwd + salt) - hash := sha256.Sum256(bytes) - if iterations > 1 { - for i := 1; i < iterations; i++ { - hash = sha256.Sum256(hash[:]) + if strings.HasPrefix(pwd, "sha256:") { + pwd := strings.TrimPrefix(pwd, "sha256:") + pwdBytes := []byte(pwd) + saltBytes := []byte(salt) + + // The '32' parameter specifies the key length in bytes (256 bits for SHA-256) + hash := pbkdf2.Key(pwdBytes, saltBytes, iterations, 32, sha256.New) + + return hex.EncodeToString(hash) + } else { + bytes := []byte(pwd + salt) + hash := sha256.Sum256(bytes) + + if iterations > 1 { + for i := 1; i < iterations; i++ { + hash = sha256.Sum256(hash[:]) + } } + + return fmt.Sprintf("%x", hash) } - return fmt.Sprintf("%x", hash) } func startConnection(conn net.Conn, s *Server) *Connection {