Compare commits

...

2 Commits
v0.3.0 ... main

Author SHA1 Message Date
bed67c3b2f Add optional strict libxcrypt standards enforcement 2026-02-17 00:30:37 -06:00
8d828adc5e Other case fixes. 2026-02-04 02:23:44 -06:00
10 changed files with 105 additions and 17 deletions

View File

@ -5,12 +5,12 @@ This is a libxcrypt compatible password hashing library for the Go language. The
## Install
```
go get github.com/GRMrGecko/go-passwd
go get github.com/grmrgecko/go-passwd
```
## Docs
[https://pkg.go.dev/github.com/GRMrGecko/go-passwd](https://pkg.go.dev/github.com/GRMrGecko/go-passwd)
[https://pkg.go.dev/github.com/grmrgecko/go-passwd](https://pkg.go.dev/github.com/grmrgecko/go-passwd)
## Example
@ -18,7 +18,7 @@ go get github.com/GRMrGecko/go-passwd
package main
import (
"github.com/GRMrGecko/go-passwd"
"github.com/grmrgecko/go-passwd"
"log"
)
@ -48,7 +48,3 @@ $ ./test
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
```
## 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:]
if c, cErr := strconv.ParseUint(cStr, 10, 32); cErr == nil {
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.
if len(salt) > 22 {
@ -111,7 +116,7 @@ func (a *BCrypt) Hash(password []byte, salt []byte) ([]byte, error) {
case 'b', 'y':
// No bug, no safety hack needed (standard).
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)

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/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4=
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/go.mod h1:A4x4C7B6z2POO1x5CZzKXZVCOFPfjzxxVUbWl2Thhp0=
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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
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/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -44,6 +44,8 @@ type PasswdInterface interface {
type Passwd struct {
Magic string
Params string
// When true, enforce libxcrypt-compatible parameter limits.
FollowStandards bool
SaltLength int
Salt []byte
i PasswdInterface
@ -330,6 +332,11 @@ func (a *Passwd) SetParams(p string) {
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.
func (a *Passwd) SetSalt(s []byte) {
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.
func (a *SCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) {
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)
b64 := SCryptBase64Encode(scryptHash)

View File

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

View File

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

View File

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