Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bed67c3b2f | |||
| 8d828adc5e |
10
README.md
10
README.md
@ -5,12 +5,12 @@ This is a libxcrypt compatible password hashing library for the Go language. The
|
|||||||
## Install
|
## Install
|
||||||
|
|
||||||
```
|
```
|
||||||
go get github.com/GRMrGecko/go-passwd
|
go get github.com/grmrgecko/go-passwd
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docs
|
## 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
|
## Example
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ go get github.com/GRMrGecko/go-passwd
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/GRMrGecko/go-passwd"
|
"github.com/grmrgecko/go-passwd"
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -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
22
agents.md
Normal 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.
|
||||||
@ -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
4
go.sum
@ -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=
|
||||||
|
|||||||
17
passwd.go
17
passwd.go
@ -42,11 +42,13 @@ type PasswdInterface interface {
|
|||||||
|
|
||||||
// Base structure.
|
// Base structure.
|
||||||
type Passwd struct {
|
type Passwd struct {
|
||||||
Magic string
|
Magic string
|
||||||
Params string
|
Params string
|
||||||
SaltLength int
|
// When true, enforce libxcrypt-compatible parameter limits.
|
||||||
Salt []byte
|
FollowStandards bool
|
||||||
i PasswdInterface
|
SaltLength int
|
||||||
|
Salt []byte
|
||||||
|
i PasswdInterface
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a password interface based on hash settings string.
|
// Get a password interface based on hash settings string.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
14
s_crypt.go
14
s_crypt.go
@ -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)
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user