Compare commits

..

No commits in common. "main" and "v0.3.0" have entirely different histories.
main ... v0.3.0

10 changed files with 17 additions and 105 deletions

View File

@ -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,3 +48,7 @@ $ ./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.

View File

@ -1,22 +0,0 @@
# 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,15 +88,10 @@ 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 {
@ -116,7 +111,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:
return nil, fmt.Errorf("unsupported bcrypt minor version %q", minor) // Unknown minor version defaults to standard (safe).
} }
c, err := a.setupBlowfishCipher(password, cost, salt, bug, safety) c, err := a.setupBlowfishCipher(password, cost, salt, bug, safety)

4
go.sum
View File

@ -2,12 +2,16 @@ 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,8 +44,6 @@ 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
@ -332,11 +330,6 @@ 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,42 +81,3 @@ 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,20 +46,6 @@ 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,9 +133,6 @@ 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,9 +133,6 @@ 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,9 +170,6 @@ 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.