Compare commits

...

1 Commits
v0.3.1 ... main

Author SHA1 Message Date
bed67c3b2f Add optional strict libxcrypt standards enforcement 2026-02-17 00:30:37 -06:00
10 changed files with 102 additions and 14 deletions

View File

@ -48,7 +48,3 @@ $ ./test
2024/09/07 18:42:35 Password confirmed, saving new password. 2024/09/07 18:42:35 Password confirmed, saving new password.
2024/09/07 18:42:35 The new password hash to save is: $6$4Eu/l5e.otcRj0rJ$YAlwxJD9pZY9.Z2TjseCbkXiUIrFU2AXh9DPEm5Z1SagxP..xaQCsz7jAgfW4nmUbLh.o23pEZGvvxPCLltf11 2024/09/07 18:42:35 The new password hash to save is: $6$4Eu/l5e.otcRj0rJ$YAlwxJD9pZY9.Z2TjseCbkXiUIrFU2AXh9DPEm5Z1SagxP..xaQCsz7jAgfW4nmUbLh.o23pEZGvvxPCLltf11
``` ```
## Known issues
- It is possible to generate password hashes that are incompatible with libxcrypt by setting a large round count. This may be mitigated in the future by adding an option to disable compatibility and otherwise require compatible parameters to be set.

22
agents.md Normal file
View File

@ -0,0 +1,22 @@
# go-passwd
This project is a Go library for password hashing and verification, designed to be compatible with `libcrypt` standards.
# Code Style Guidelines
- Write pragmatic, systems-oriented Go focused on correctness and clarity.
- Favor explicit control flow and simple data structures over abstraction.
- Use short, consistent receiver names.
- Exported names are clear and descriptive; internal helpers are lowerCamelCase.
- Return errors when callers can act; otherwise log and continue safely.
- Log operational details at debug level, lifecycle events at info, and failures at error level.
- Avoid panics, hidden side effects, and over-engineering.
- Comments are functional and intent-focused, explaining why something exists or what role it plays.
- Exported types and methods have short, direct doc comments that describe responsibility.
- Comments are operational in tone, written for someone maintaining or debugging the system.
- Inline comments are brief and precise, explaining non-obvious logic or marking logical phases of a function.
- Functions with multiple logical steps use short section comments to label each phase (e.g. "Validate input.", "Persist to database.", "Build response.").
- No conversational language, jokes, or speculative notes—comments are concise and purposeful.
- The code is expected to remain readable without comments; comments add clarity where reasoning is not immediately obvious.
- The name of the element (func, struct, var, ect) being commented should be the first word in the comment.
- Comments are expected to be a complete sentence with ending notation such as a period. We are humans with proper english.

View File

@ -88,10 +88,15 @@ func (a *BCrypt) Hash(password []byte, salt []byte) ([]byte, error) {
cStr := a.Params[idx+1:] cStr := a.Params[idx+1:]
if c, cErr := strconv.ParseUint(cStr, 10, 32); cErr == nil { if c, cErr := strconv.ParseUint(cStr, 10, 32); cErr == nil {
cost = uint32(c) cost = uint32(c)
} else {
return nil, cErr
} }
} }
} }
} }
if a.FollowStandards && (cost < 4 || cost > 31) {
return nil, fmt.Errorf("bcrypt cost must be between 4 and 31")
}
// Ensure salt is truncated to 22 bytes. // Ensure salt is truncated to 22 bytes.
if len(salt) > 22 { if len(salt) > 22 {
@ -111,7 +116,7 @@ func (a *BCrypt) Hash(password []byte, salt []byte) ([]byte, error) {
case 'b', 'y': case 'b', 'y':
// No bug, no safety hack needed (standard). // No bug, no safety hack needed (standard).
default: default:
// Unknown minor version defaults to standard (safe). return nil, fmt.Errorf("unsupported bcrypt minor version %q", minor)
} }
c, err := a.setupBlowfishCipher(password, cost, salt, bug, safety) c, err := a.setupBlowfishCipher(password, cost, salt, bug, safety)

4
go.sum
View File

@ -2,16 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4= github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4=
github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA= github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA=
github.com/pedroalbanese/gogost v0.0.0-20240430171730-f95129c7a5af h1:8jbTN9e84FOzAJtCPdy/NEz8983YdD7nqTBMQlTRP4w=
github.com/pedroalbanese/gogost v0.0.0-20240430171730-f95129c7a5af/go.mod h1:A4x4C7B6z2POO1x5CZzKXZVCOFPfjzxxVUbWl2Thhp0=
github.com/pedroalbanese/gogost v0.0.0-20250117160715-44a1f1ec2524 h1:L3ryWlHFZS6yPpv7f//uG15iU+f4IDOc2dzandl72Po= github.com/pedroalbanese/gogost v0.0.0-20250117160715-44a1f1ec2524 h1:L3ryWlHFZS6yPpv7f//uG15iU+f4IDOc2dzandl72Po=
github.com/pedroalbanese/gogost v0.0.0-20250117160715-44a1f1ec2524/go.mod h1:A4x4C7B6z2POO1x5CZzKXZVCOFPfjzxxVUbWl2Thhp0= github.com/pedroalbanese/gogost v0.0.0-20250117160715-44a1f1ec2524/go.mod h1:A4x4C7B6z2POO1x5CZzKXZVCOFPfjzxxVUbWl2Thhp0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -44,6 +44,8 @@ type PasswdInterface interface {
type Passwd struct { type Passwd struct {
Magic string Magic string
Params string Params string
// When true, enforce libxcrypt-compatible parameter limits.
FollowStandards bool
SaltLength int SaltLength int
Salt []byte Salt []byte
i PasswdInterface i PasswdInterface
@ -330,6 +332,11 @@ func (a *Passwd) SetParams(p string) {
a.Params = p a.Params = p
} }
// Enable or disable libxcrypt-compatible parameter checks.
func (a *Passwd) SetFollowStandards(v bool) {
a.FollowStandards = v
}
// Set a salt for hashing, an empty salt will generate a new one. // Set a salt for hashing, an empty salt will generate a new one.
func (a *Passwd) SetSalt(s []byte) { func (a *Passwd) SetSalt(s []byte) {
a.Salt = s a.Salt = s

View File

@ -81,3 +81,42 @@ func TestNewPasswordGeneration(t *testing.T) {
}) })
} }
} }
func TestFollowStandards(t *testing.T) {
t.Run("DefaultIsDisabled", func(t *testing.T) {
p := NewSHA512CryptPasswd().(*SHA512Crypt)
p.SetParams("rounds=999")
hash, err := p.SHashPasswordWithSalt("Test", "salt")
require.NoError(t, err)
assert.Contains(t, hash, "$6$rounds=999$salt$")
assert.False(t, p.FollowStandards)
})
t.Run("SHA512RoundsRejected", func(t *testing.T) {
p := NewSHA512CryptPasswd().(*SHA512Crypt)
p.SetParams("rounds=999")
p.FollowStandards = true
_, err := p.SHashPasswordWithSalt("Test", "salt")
require.Error(t, err)
})
t.Run("BCryptCostRejected", func(t *testing.T) {
p := NewBCryptPasswd().(*BCrypt)
p.SetParams("b$03")
p.FollowStandards = true
_, err := p.SHashPasswordWithSalt("Test", "abcdefghijklmnopqrstuu")
require.Error(t, err)
})
t.Run("SCryptNRejected", func(t *testing.T) {
p := NewSCryptPasswd().(*SCrypt)
require.NoError(t, p.SetSCryptParams(1, 1, 1))
p.FollowStandards = true
_, err := p.SHashPasswordWithSalt("Test", "abcdefghijklmnopqrstuv")
require.Error(t, err)
})
}

View File

@ -46,6 +46,20 @@ func (a *SCrypt) DecodeSCryptParams() (N, r, p int) {
// Hash a password with salt using scrypt standard. // Hash a password with salt using scrypt standard.
func (a *SCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) { func (a *SCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) {
N, r, p := a.DecodeSCryptParams() N, r, p := a.DecodeSCryptParams()
if r < 1 {
return nil, fmt.Errorf("scrypt r must be >= 1")
}
if p < 1 {
return nil, fmt.Errorf("scrypt p must be >= 1")
}
if a.FollowStandards {
if N < 2 || N > 63 {
return nil, fmt.Errorf("scrypt N must be between 2 and 63")
}
if r >= (1<<30) || p >= (1<<30) || uint64(r)*uint64(p) >= (1<<30) {
return nil, fmt.Errorf("scrypt requires r*p < 2^30")
}
}
scryptHash, err := yescrypt.ScryptKey(password, salt, 1<<N, r, p, 32) scryptHash, err := yescrypt.ScryptKey(password, salt, 1<<N, r, p, 32)
b64 := SCryptBase64Encode(scryptHash) b64 := SCryptBase64Encode(scryptHash)

View File

@ -133,6 +133,9 @@ func (a *SHA256Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash [
if err != nil { if err != nil {
return return
} }
if a.FollowStandards && (iterations < 1000 || iterations > 999999999) {
return nil, fmt.Errorf("rounds must be between 1000 and 999999999")
}
} }
// Compute hash. // Compute hash.

View File

@ -133,6 +133,9 @@ func (a *SHA512Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash [
if err != nil { if err != nil {
return return
} }
if a.FollowStandards && (iterations < 1000 || iterations > 999999999) {
return nil, fmt.Errorf("rounds must be between 1000 and 999999999")
}
} }
// Compute hash. // Compute hash.

View File

@ -170,6 +170,9 @@ func (a *SunMD5) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte
if err != nil { if err != nil {
return return
} }
if a.FollowStandards && (iterations < 1 || iterations > 0xFFFFFFFF) {
return nil, fmt.Errorf("rounds must be between 1 and 4294967295")
}
} }
// Compute hash. // Compute hash.