From 2baa34c37e3613149cc2fd1fc1b40362a65f3082 Mon Sep 17 00:00:00 2001 From: Estelle Poulin Date: Fri, 21 Feb 2025 14:12:29 -0500 Subject: [PATCH 1/2] use pbkdf2-hmac-sha256 for password hash --- client/client.go | 17 +++++------ docs/protocol-specification.md | 52 +++++++++++++++++++++++++++++----- go.mod | 1 + go.sum | 2 ++ server/server.go | 17 +++++------ 5 files changed, 66 insertions(+), 23 deletions(-) diff --git a/client/client.go b/client/client.go index 48034df..ac315b5 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 hex.EncodeToString(hash) } diff --git a/docs/protocol-specification.md b/docs/protocol-specification.md index d40ba5b..8c9b763 100644 --- a/docs/protocol-specification.md +++ b/docs/protocol-specification.md @@ -307,17 +307,55 @@ 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" + "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 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 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..547860f 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,13 @@ 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[:]) - } - } - 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 hex.EncodeToString(hash) } func startConnection(conn net.Conn, s *Server) *Connection { From cad29a185b9a9933e05813b2ee10e7ebbb77c9e0 Mon Sep 17 00:00:00 2001 From: Estelle Poulin Date: Thu, 3 Apr 2025 09:39:05 -0400 Subject: [PATCH 2/2] make the change to the hash algo backwards compatible If the client is updated and knows to use the new KDF it will send sha256: and the server will ues the new algo. If it doesn't it will simply send and the server will fall back to the old algo. --- client/client.go | 2 +- docs/protocol-specification.md | 5 +++-- server/server.go | 24 +++++++++++++++++++----- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/client/client.go b/client/client.go index ac315b5..88a26c4 100644 --- a/client/client.go +++ b/client/client.go @@ -692,5 +692,5 @@ func hash(pwd, salt string, iterations int) string { // 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) + return fmt.Sprintf("sha256:%s", hex.EncodeToString(hash)) } diff --git a/docs/protocol-specification.md b/docs/protocol-specification.md index 8c9b763..2818afb 100644 --- a/docs/protocol-specification.md +++ b/docs/protocol-specification.md @@ -315,6 +315,7 @@ Here's how to implement that in Go. ```go import ( "crypto/sha256" + "fmt" "golang.org/x/crypto/pbkdf2" "encoding/hex" ) @@ -327,7 +328,7 @@ func hash(pwd, salt string, iterations int) string { // specifies the key length in bytes (256 bits for SHA-256). hash := pbkdf2.Key(pwdBytes, saltBytes, iterations, 32, sha256.New) - return hex.EncodeToString(hash) + return fmt.Sprintf("sha256:%s", hex.EncodeToString(hash)) } ``` @@ -350,7 +351,7 @@ def hash(pwd: str, salt: str, iterations: int) -> str: dklen=32, ) - return hash.hex() + return f"sha256:{hash.hex()}" ``` You can gut-check your own implementation by checking that `hash("password", "salt", 50)` diff --git a/server/server.go b/server/server.go index 547860f..fbb674b 100644 --- a/server/server.go +++ b/server/server.go @@ -232,13 +232,27 @@ func cleanupConnection(s *Server, c *Connection) { } func hash(pwd, salt string, iterations int) string { - pwdBytes := []byte(pwd) - saltBytes := []byte(salt) + 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) + // 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) + 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) + } } func startConnection(conn net.Conn, s *Server) *Connection {