feat: BREAKING: change from string to []byte

This commit is contained in:
Jon Lundy 2022-12-10 08:58:08 -07:00
parent a4bb55f56a
commit fc8d628cc5
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG Key ID: C63E6D61F3035024
10 changed files with 188 additions and 169 deletions

View File

@ -25,4 +25,4 @@ jobs:
run: go build -v ./... run: go build -v ./...
- name: Test - name: Test
run: go test -v ./... run: go test -v -cover ./...

View File

@ -7,9 +7,12 @@
Here is an example of usage: Here is an example of usage:
```go ```go
// Example of upgrading password hash to a greater complexity.
//
// Note: This example uses very unsecure hash functions to allow for predictable output. Use of argon2.Argon2id or scrypt.Scrypt2 for greater hash security is recommended.
func Example() { func Example() {
pass := "my_pass" pass := []byte("my_pass")
hash := "$1$81ed91e1131a3a5a50d8a68e8ef85fa0" hash := []byte("$1$81ed91e1131a3a5a50d8a68e8ef85fa0")
pwd := passwd.New( pwd := passwd.New(
argon2.Argon2id, // first is preferred type. argon2.Argon2id, // first is preferred type.
@ -19,23 +22,25 @@ func Example() {
_, err := pwd.Passwd(pass, hash) _, err := pwd.Passwd(pass, hash)
if err != nil { if err != nil {
fmt.Println("fail: ", err) fmt.Println("fail: ", err)
return
} }
// Check if we want to update. // Check if we want to update.
if !pwd.IsPreferred(hash) { if !pwd.IsPreferred(hash) {
newHash, err := pwd.Passwd(pass, "") newHash, err := pwd.Passwd(pass, nil)
if err != nil { if err != nil {
fmt.Println("fail: ", err) fmt.Println("fail: ", err)
return
} }
fmt.Println("new hash:", newHash) fmt.Println("new hash:", string(newHash)[:31], "...")
} }
// Output: // Output:
// new hash: $argon2id$... // new hash: $argon2id$v=19,m=65536,t=1,p=4$ ...
} }
``` ```
https://github.com/sour-is/go-passwd/blob/main/passwd_test.go#L33-L59 https://github.com/sour-is/go-passwd/blob/main/passwd_test.go#L40-L68
This shows how one would set a preferred hashing type and if the current version of ones password is not the preferred type updates it to enhance the security of the hashed password when someone logs in. This shows how one would set a preferred hashing type and if the current version of ones password is not the preferred type updates it to enhance the security of the hashed password when someone logs in.
@ -61,12 +66,12 @@ https://github.com/sour-is/go-passwd/blob/main/passwd_test.go#L28-L31
Circling back to the `IsPreferred` method. A hasher can define its own `IsPreferred` method that will be called to check if the current hash meets the complexity requirements. This is good for updating the password hashes to be more secure over time. Circling back to the `IsPreferred` method. A hasher can define its own `IsPreferred` method that will be called to check if the current hash meets the complexity requirements. This is good for updating the password hashes to be more secure over time.
```go ```go
func (p *Passwd) IsPreferred(hash string) bool { func (p *Passwd) IsPreferred(hash []byte) bool {
_, algo := p.getAlgo(hash) _, algo := p.getAlgo(hash)
if algo != nil && algo == p.d { if algo != nil && algo == p.d {
// if the algorithm defines its own check for preference. // if the algorithm defines its own check for preference.
if ck, ok := algo.(interface{ IsPreferred(string) bool }); ok { if ck, ok := algo.(interface{ IsPreferred([]byte) bool }); ok {
return ck.IsPreferred(hash) return ck.IsPreferred(hash)
} }

View File

@ -1,13 +1,13 @@
package passwd package passwd
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"strings"
) )
type Passwder interface { type Passwder interface {
Passwd(string, string) (string, error) Passwd(pass, hash []byte) ([]byte, error)
ApplyPasswd(*Passwd) ApplyPasswd(*Passwd)
} }
@ -45,8 +45,8 @@ func (p *Passwd) SetFallthrough(pass Passwder) {
p.f = pass p.f = pass
} }
func (p *Passwd) Passwd(pass, hash string) (string, error) { func (p *Passwd) Passwd(pass, hash []byte) ([]byte, error) {
if hash == "" { if hash == nil {
return p.d.Passwd(pass, hash) return p.d.Passwd(pass, hash)
} }
name, algo := p.getAlgo(hash) name, algo := p.getAlgo(hash)
@ -54,17 +54,17 @@ func (p *Passwd) Passwd(pass, hash string) (string, error) {
algo = p.f algo = p.f
} }
if algo == nil { if algo == nil {
return "", fmt.Errorf("%w: %s", ErrNoHandler, name) return nil, fmt.Errorf("%w: %s", ErrNoHandler, name)
} }
return algo.Passwd(pass, hash) return algo.Passwd(pass, hash)
} }
func (p *Passwd) IsPreferred(hash string) bool { func (p *Passwd) IsPreferred(hash []byte) bool {
_, algo := p.getAlgo(hash) _, algo := p.getAlgo(hash)
if algo != nil && algo == p.d { if algo != nil && algo == p.d {
// if the algorithm defines its own check for preference. // if the algorithm defines its own check for preference.
if ck, ok := algo.(interface{ IsPreferred(string) bool }); ok { if ck, ok := algo.(interface{ IsPreferred([]byte) bool }); ok {
return ck.IsPreferred(hash) return ck.IsPreferred(hash)
} }
@ -73,17 +73,18 @@ func (p *Passwd) IsPreferred(hash string) bool {
return false return false
} }
func (p *Passwd) getAlgo(hash string) (string, Passwder) { func (p *Passwd) getAlgo(hash []byte) (string, Passwder) {
var algo string var algo string
if !strings.HasPrefix(hash, "$") { if !bytes.HasPrefix(hash, []byte("$")) {
return p.getName(p.f), p.f return p.getName(p.f), p.f
} }
if _, h, ok := strings.Cut(hash, "$"); ok { if _, h, ok := bytes.Cut(hash, []byte("$")); ok {
algo, _, ok = strings.Cut(h, "$") a, _, ok := bytes.Cut(h, []byte("$"))
if !ok { if !ok {
return "", nil return "", nil
} }
algo = string(a)
if passwd, ok := p.m[algo]; ok { if passwd, ok := p.m[algo]; ok {
return algo, passwd return algo, passwd

View File

@ -1,24 +1,28 @@
package passwd_test package passwd_test
import ( import (
"bytes"
"crypto/subtle" "crypto/subtle"
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/matryer/is" "github.com/matryer/is"
"github.com/sour-is/go-passwd" "github.com/sour-is/go-passwd"
"github.com/sour-is/go-passwd/pkg/argon2"
"github.com/sour-is/go-passwd/pkg/unix" "github.com/sour-is/go-passwd/pkg/unix"
) )
type plainPasswd struct{} type plainPasswd struct{}
func (p *plainPasswd) Passwd(pass string, check string) (string, error) { func (p *plainPasswd) Passwd(pass, check []byte) ([]byte, error) {
if check == "" { if check == nil {
return fmt.Sprint("$plain$", pass), nil var b bytes.Buffer
b.WriteString("$plain$")
b.Write(pass)
return b.Bytes(), nil
} }
if subtle.ConstantTimeCompare([]byte(pass), []byte(strings.TrimPrefix(check, "$plain$"))) == 1 { if subtle.ConstantTimeCompare([]byte(pass), []byte(bytes.TrimPrefix(check, []byte("$plain$")))) == 1 {
return check, nil return check, nil
} }
@ -34,50 +38,52 @@ func (p *plainPasswd) ApplyPasswd(passwd *passwd.Passwd) {
// //
// Note: This example uses very unsecure hash functions to allow for predictable output. Use of argon2.Argon2id or scrypt.Scrypt2 for greater hash security is recommended. // Note: This example uses very unsecure hash functions to allow for predictable output. Use of argon2.Argon2id or scrypt.Scrypt2 for greater hash security is recommended.
func Example() { func Example() {
pass := "my_pass" pass := []byte("my_pass")
hash := "my_pass" hash := []byte("$1$81ed91e1131a3a5a50d8a68e8ef85fa0")
pwd := passwd.New( pwd := passwd.New(
&unix.MD5{}, // first is preferred type. argon2.Argon2id, // first is preferred type.
&plainPasswd{}, &unix.MD5{},
) )
_, err := pwd.Passwd(pass, hash) _, err := pwd.Passwd(pass, hash)
if err != nil { if err != nil {
fmt.Println("fail: ", err) fmt.Println("fail: ", err)
return
} }
// Check if we want to update. // Check if we want to update.
if !pwd.IsPreferred(hash) { if !pwd.IsPreferred(hash) {
newHash, err := pwd.Passwd(pass, "") newHash, err := pwd.Passwd(pass, nil)
if err != nil { if err != nil {
fmt.Println("fail: ", err) fmt.Println("fail: ", err)
return
} }
fmt.Println("new hash:", newHash) fmt.Println("new hash:", string(newHash)[:31], "...")
} }
// Output: // Output:
// new hash: $1$81ed91e1131a3a5a50d8a68e8ef85fa0 // new hash: $argon2id$v=19,m=65536,t=1,p=4$ ...
} }
func TestPasswdHash(t *testing.T) { func TestPasswdHash(t *testing.T) {
type testCase struct { type testCase struct {
pass, hash string pass, hash []byte
} }
tests := []testCase{ tests := []testCase{
{"passwd", "passwd"}, {[]byte("passwd"), []byte("passwd")},
{"passwd", "$plain$passwd"}, {[]byte("passwd"), []byte("$plain$passwd")},
} }
algos := []passwd.Passwder{&plainPasswd{}} algos := []passwd.Passwder{&plainPasswd{}}
is := is.New(t) is := is.New(t)
// Generate additional test cases for each algo. // Generate additional test cases for each algo.
for _, algo := range algos { for _, algo := range algos {
hash, err := algo.Passwd("passwd", "") hash, err := algo.Passwd([]byte("passwd"), nil)
is.NoErr(err) is.NoErr(err)
tests = append(tests, testCase{"passwd", hash}) tests = append(tests, testCase{[]byte("passwd"), hash})
} }
pass := passwd.New(algos...) pass := passwd.New(algos...)
@ -98,9 +104,9 @@ func TestPasswdIsPreferred(t *testing.T) {
pass := passwd.New(&plainPasswd{}) pass := passwd.New(&plainPasswd{})
ok := pass.IsPreferred("$plain$passwd") ok := pass.IsPreferred([]byte("$plain$passwd"))
is.True(ok) is.True(ok)
ok = pass.IsPreferred("$foo$passwd") ok = pass.IsPreferred([]byte("$foo$passwd"))
is.True(!ok) is.True(!ok)
} }

View File

@ -1,12 +1,12 @@
package argon2 package argon2
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"golang.org/x/crypto/argon2" "golang.org/x/crypto/argon2"
@ -73,35 +73,35 @@ func NewArgon2id(
} }
} }
func (p *argon) Passwd(pass string, check string) (string, error) { func (p *argon) Passwd(pass, check []byte) ([]byte, error) {
var args *pwArgs var args *pwArgs
var err error var err error
if check == "" { if check == nil {
args = p.defaultArgs() args = p.defaultArgs()
_, err := rand.Read(args.salt) _, err := rand.Read(args.salt)
if err != nil { if err != nil {
return "", err return nil, err
} }
args.hash = p.keyFn([]byte(pass), args.salt, args.time, args.memory, args.threads, args.keyLen) args.hash = p.keyFn(pass, args.salt, args.time, args.memory, args.threads, args.keyLen)
} else { } else {
args, err = p.parseArgs(check) args, err = p.parseArgs(check)
if err != nil { if err != nil {
return "", err return nil, err
} }
hash := p.keyFn([]byte(pass), args.salt, args.time, args.memory, args.threads, args.keyLen) hash := p.keyFn(pass, args.salt, args.time, args.memory, args.threads, args.keyLen)
if subtle.ConstantTimeCompare(hash, args.hash) == 0 { if subtle.ConstantTimeCompare(hash, args.hash) == 0 {
return "", passwd.ErrNoMatch return nil, passwd.ErrNoMatch
} }
} }
return args.String(), nil return args.Bytes(), nil
} }
func (p *argon) ApplyPasswd(passwd *passwd.Passwd) { func (p *argon) ApplyPasswd(passwd *passwd.Passwd) {
passwd.Register(p.name, p) passwd.Register(p.name, p)
} }
func (s *argon) IsPreferred(hash string) bool { func (s *argon) IsPreferred(hash []byte) bool {
args, err := s.parseArgs(hash) args, err := s.parseArgs(hash)
if err != nil { if err != nil {
return false return false
@ -142,29 +142,33 @@ func (p *argon) defaultArgs() *pwArgs {
salt: make([]byte, p.saltLen), salt: make([]byte, p.saltLen),
} }
} }
func (p *argon) parseArgs(hash string) (*pwArgs, error) { func (p *argon) parseArgs(hash []byte) (*pwArgs, error) {
pfx := "$" + p.name + "$" pfx := []byte("$" + p.name + "$")
if !strings.HasPrefix(hash, pfx) { if !bytes.HasPrefix(hash, pfx) {
return nil, fmt.Errorf("%w: missing prefix", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing prefix", passwd.ErrBadHash)
} }
hash = strings.TrimPrefix(hash, pfx) hash = bytes.TrimPrefix(hash, pfx)
args, hash, ok := strings.Cut(hash, "$") args, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing args", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing args", passwd.ErrBadHash)
} }
salt, hash, ok := strings.Cut(hash, "$") salt, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing salt", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing salt", passwd.ErrBadHash)
} }
var err error var err error
pass := p.defaultArgs() pass := p.defaultArgs()
pass.salt, err = base64.RawStdEncoding.DecodeString(salt)
pass.salt = make([]byte, base64.RawStdEncoding.DecodedLen(len(salt)))
_, err = base64.RawStdEncoding.Decode(pass.salt, salt)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash) return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash)
} }
pass.hash, err = base64.RawStdEncoding.DecodeString(hash)
pass.hash = make([]byte, base64.RawStdEncoding.DecodedLen(len(hash)))
_, err = base64.RawStdEncoding.Decode(pass.hash, hash)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash) return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash)
} }
@ -172,23 +176,23 @@ func (p *argon) parseArgs(hash string) (*pwArgs, error) {
pass.name = p.name pass.name = p.name
pass.keyLen = uint32(len(pass.hash)) pass.keyLen = uint32(len(pass.hash))
for _, part := range strings.Split(args, ",") { for _, part := range bytes.Split(args, []byte(",")) {
if k, v, ok := strings.Cut(part, "="); ok { if k, v, ok := bytes.Cut(part, []byte("=")); ok {
switch k { switch string(k) {
case "v": case "v":
if i, err := strconv.ParseUint(v, 10, 8); err == nil { if i, err := strconv.ParseUint(string(v), 10, 8); err == nil {
pass.version = uint8(i) pass.version = uint8(i)
} }
case "m": case "m":
if i, err := strconv.ParseUint(v, 10, 32); err == nil { if i, err := strconv.ParseUint(string(v), 10, 32); err == nil {
pass.memory = uint32(i) pass.memory = uint32(i)
} }
case "t": case "t":
if i, err := strconv.ParseUint(v, 10, 32); err == nil { if i, err := strconv.ParseUint(string(v), 10, 32); err == nil {
pass.time = uint32(i) pass.time = uint32(i)
} }
case "p": case "p":
if i, err := strconv.ParseUint(v, 10, 8); err == nil { if i, err := strconv.ParseUint(string(v), 10, 8); err == nil {
pass.threads = uint8(i) pass.threads = uint8(i)
} }
} }
@ -209,9 +213,19 @@ type pwArgs struct {
hash []byte hash []byte
} }
func (p *pwArgs) String() string { func (p *pwArgs) Bytes() []byte {
salt := base64.RawStdEncoding.EncodeToString(p.salt) var b bytes.Buffer
hash := base64.RawStdEncoding.EncodeToString(p.hash)
return fmt.Sprintf("$%s$v=%d,m=%d,t=%d,p=%d$%s$%s", p.name, p.version, p.memory, p.time, p.threads, salt, hash) fmt.Fprintf(&b, "$%s$v=%d,m=%d,t=%d,p=%d$", p.name, p.version, p.memory, p.time, p.threads)
salt := make([]byte, base64.RawURLEncoding.EncodedLen(len(p.salt)))
base64.RawStdEncoding.Encode(salt, p.salt)
b.Write(salt)
hash := make([]byte, base64.RawURLEncoding.EncodedLen(len(p.hash)))
base64.RawStdEncoding.Encode(hash, p.hash)
b.WriteRune('$')
b.Write(hash)
return b.Bytes()
} }

View File

@ -13,7 +13,7 @@ import (
func TestPasswdHash(t *testing.T) { func TestPasswdHash(t *testing.T) {
type testCase struct { type testCase struct {
pass, hash string pass, hash []byte
} }
tests := []testCase{} tests := []testCase{}
@ -22,9 +22,9 @@ func TestPasswdHash(t *testing.T) {
is := is.New(t) is := is.New(t)
// Generate additional test cases for each algo. // Generate additional test cases for each algo.
for _, algo := range algos { for _, algo := range algos {
hash, err := algo.Passwd("passwd", "") hash, err := algo.Passwd([]byte("passwd"), nil)
is.NoErr(err) is.NoErr(err)
tests = append(tests, testCase{"passwd", hash}) tests = append(tests, testCase{[]byte("passwd"), hash})
} }
pass := passwd.New(algos...) pass := passwd.New(algos...)
@ -45,12 +45,12 @@ func TestPasswdIsPreferred(t *testing.T) {
pass := passwd.New(argon2.Argon2i, &unix.MD5{}) pass := passwd.New(argon2.Argon2i, &unix.MD5{})
ok := pass.IsPreferred("$argon2i$v=19,m=32768,t=3,p=4$LdaB2Z4EI4lwpxTc78QUFw$VhlPSK0tdF226QCLC24IIrmQcMBmg47Ik9h/Yq6htFI") ok := pass.IsPreferred([]byte("$argon2i$v=19,m=32768,t=3,p=4$LdaB2Z4EI4lwpxTc78QUFw$VhlPSK0tdF226QCLC24IIrmQcMBmg47Ik9h/Yq6htFI"))
is.True(ok) is.True(ok)
ok = pass.IsPreferred("$argon2i$v=19,m=1024,t=2,p=4$LdaB2Z4EI4lwpxTc78QUFw$VhlPSK0tdF226QCLC24IIrmQcMBmg47Ik9h/Yq6htFI") ok = pass.IsPreferred([]byte("$argon2i$v=19,m=1024,t=2,p=4$LdaB2Z4EI4lwpxTc78QUFw$VhlPSK0tdF226QCLC24IIrmQcMBmg47Ik9h/Yq6htFI"))
is.True(!ok) is.True(!ok)
ok = pass.IsPreferred("$1$76a2173be6393254e72ffa4d6df1030a") ok = pass.IsPreferred([]byte("$1$76a2173be6393254e72ffa4d6df1030a"))
is.True(!ok) is.True(!ok)
} }

View File

@ -1,13 +1,13 @@
package scrypt package scrypt
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strconv" "strconv"
"strings"
"github.com/sour-is/go-passwd" "github.com/sour-is/go-passwd"
"golang.org/x/crypto/scrypt" "golang.org/x/crypto/scrypt"
@ -22,8 +22,10 @@ type scryptpw struct {
name string name string
encoder interface { encoder interface {
EncodeToString(src []byte) string EncodedLen(n int) int
DecodeString(s string) ([]byte, error) Encode(dst, src []byte)
DecodedLen(x int) int
Decode(dst, src []byte) (n int, err error)
} }
} }
type scryptArgs struct { type scryptArgs struct {
@ -38,8 +40,10 @@ type scryptArgs struct {
hash []byte hash []byte
encoder interface { encoder interface {
EncodeToString(src []byte) string EncodedLen(n int) int
DecodeString(s string) ([]byte, error) Encode(dst, src []byte)
DecodedLen(x int) int
Decode(dst, src []byte) (n int, err error)
} }
} }
@ -55,37 +59,37 @@ var Scrypt2 = &scryptpw{
name: "s2", encoder: base64.RawStdEncoding, name: "s2", encoder: base64.RawStdEncoding,
} }
func (s *scryptpw) Passwd(pass string, check string) (string, error) { func (s *scryptpw) Passwd(pass, check []byte) ([]byte, error) {
var args *scryptArgs var args *scryptArgs
var err error var err error
if check == "" { if check == nil {
args = s.defaultArgs() args = s.defaultArgs()
_, err := rand.Read(args.salt) _, err := rand.Read(args.salt)
if err != nil { if err != nil {
return "", err return nil, err
} }
args.hash, err = scrypt.Key([]byte(pass), args.salt, args.N, args.R, args.P, args.DKLen) args.hash, err = scrypt.Key(pass, args.salt, args.N, args.R, args.P, args.DKLen)
if err != nil { if err != nil {
return "", err return nil, err
} }
} else { } else {
args, err = s.parseArgs(check) args, err = s.parseArgs(check)
if err != nil { if err != nil {
return "", err return nil, err
} }
hash, err := scrypt.Key([]byte(pass), args.salt, args.N, args.R, args.P, args.DKLen) hash, err := scrypt.Key([]byte(pass), args.salt, args.N, args.R, args.P, args.DKLen)
if err != nil { if err != nil {
return "", err return nil, err
} }
if subtle.ConstantTimeCompare(hash, args.hash) == 0 { if subtle.ConstantTimeCompare(hash, args.hash) == 0 {
return "", passwd.ErrNoMatch return nil, passwd.ErrNoMatch
} }
} }
return args.String(), nil return args.Bytes(), nil
} }
func (s *scryptpw) ApplyPasswd(p *passwd.Passwd) { func (s *scryptpw) ApplyPasswd(p *passwd.Passwd) {
p.Register(s.name, s) p.Register(s.name, s)
@ -93,7 +97,7 @@ func (s *scryptpw) ApplyPasswd(p *passwd.Passwd) {
p.SetFallthrough(s) p.SetFallthrough(s)
} }
} }
func (s *scryptpw) IsPreferred(hash string) bool { func (s *scryptpw) IsPreferred(hash []byte) bool {
args, err := s.parseArgs(hash) args, err := s.parseArgs(hash)
if err != nil { if err != nil {
return false return false
@ -129,49 +133,52 @@ func (s *scryptpw) defaultArgs() *scryptArgs {
encoder: s.encoder, encoder: s.encoder,
} }
} }
func (s *scryptpw) parseArgs(hash string) (*scryptArgs, error) {
func (s *scryptpw) parseArgs(hash []byte) (*scryptArgs, error) {
args := s.defaultArgs() args := s.defaultArgs()
name := "$" + s.name + "$" name := []byte("$" + s.name + "$")
hash = strings.TrimPrefix(hash, name) hash = bytes.TrimPrefix(hash, name)
N, hash, ok := strings.Cut(hash, "$") N, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing args: N", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing args: N", passwd.ErrBadHash)
} }
if n, err := strconv.Atoi(N); err == nil { if n, err := strconv.Atoi(string(N)); err == nil {
args.N = n args.N = n
} }
R, hash, ok := strings.Cut(hash, "$") R, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing args: R", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing args: R", passwd.ErrBadHash)
} }
if r, err := strconv.Atoi(R); err == nil { if r, err := strconv.Atoi(string(R)); err == nil {
args.R = r args.R = r
} }
P, hash, ok := strings.Cut(hash, "$") P, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing args: P", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing args: P", passwd.ErrBadHash)
} }
if p, err := strconv.Atoi(P); err == nil { if p, err := strconv.Atoi(string(P)); err == nil {
args.P = p args.P = p
} }
salt, hash, ok := strings.Cut(hash, "$") salt, hash, ok := bytes.Cut(hash, []byte("$"))
if !ok { if !ok {
return nil, fmt.Errorf("%w: missing args: salt", passwd.ErrBadHash) return nil, fmt.Errorf("%w: missing args: salt", passwd.ErrBadHash)
} }
var err error var err error
args.salt, err = s.encoder.DecodeString(salt) args.salt = make([]byte, s.encoder.DecodedLen(len(salt)))
_, err = s.encoder.Decode(args.salt, salt)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash) return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash)
} }
args.SaltLen = len(args.salt) args.SaltLen = len(args.salt)
args.hash, err = s.encoder.DecodeString(hash) args.hash = make([]byte, s.encoder.DecodedLen(len(hash)))
_, err = s.encoder.Decode(args.hash, hash)
if err != nil { if err != nil {
return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash) return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash)
} }
@ -179,22 +186,38 @@ func (s *scryptpw) parseArgs(hash string) (*scryptArgs, error) {
return args, nil return args, nil
} }
func (s *scryptArgs) String() string {
var name string
if s.name != "s1" {
name = "$" + s.name + "$"
}
salt := s.encoder.EncodeToString(s.salt)
hash := s.encoder.EncodeToString(s.hash)
return fmt.Sprintf("%s%d$%d$%d$%s$%s", name, s.N, s.R, s.P, salt, hash) func (s *scryptArgs) Bytes() []byte {
var b bytes.Buffer
if s.name != "s1" {
b.WriteRune('$')
b.WriteString(s.name)
b.WriteRune('$')
}
fmt.Fprintf(&b, "%d$%d$%d", s.N, s.R, s.P)
salt := make([]byte, s.encoder.EncodedLen(len(s.salt)))
s.encoder.Encode(salt, s.salt)
b.WriteRune('$')
b.Write(salt)
hash := make([]byte, s.encoder.EncodedLen(len(s.hash)))
s.encoder.Encode(hash, s.hash)
b.WriteRune('$')
b.Write(hash)
return b.Bytes()
} }
type hexenc struct{} type hexenc struct{}
func (hexenc) EncodeToString(src []byte) string { func (hexenc) Encode(dst, src []byte) {
return hex.EncodeToString(src) hex.Encode(dst, src)
} }
func (hexenc) DecodeString(s string) ([]byte, error) { func (hexenc) EncodedLen(n int) int { return hex.EncodedLen(n) }
return hex.DecodeString(s) func (hexenc) Decode(dst, src []byte) (n int, err error) {
return hex.Decode(dst, src)
} }
func (hexenc) DecodedLen(x int) int { return hex.DecodedLen(x) }

View File

@ -13,7 +13,7 @@ import (
func TestPasswdHash(t *testing.T) { func TestPasswdHash(t *testing.T) {
type testCase struct { type testCase struct {
pass, hash string pass, hash []byte
} }
tests := []testCase{} tests := []testCase{}
@ -22,9 +22,9 @@ func TestPasswdHash(t *testing.T) {
is := is.New(t) is := is.New(t)
// Generate additional test cases for each algo. // Generate additional test cases for each algo.
for _, algo := range algos { for _, algo := range algos {
hash, err := algo.Passwd("passwd", "") hash, err := algo.Passwd([]byte("passwd"), nil)
is.NoErr(err) is.NoErr(err)
tests = append(tests, testCase{"passwd", hash}) tests = append(tests, testCase{[]byte("passwd"), hash})
} }
pass := passwd.New(algos...) pass := passwd.New(algos...)
@ -45,15 +45,15 @@ func TestPasswdIsPreferred(t *testing.T) {
pass := passwd.New(scrypt.Scrypt2, &unix.MD5{}) pass := passwd.New(scrypt.Scrypt2, &unix.MD5{})
ok := pass.IsPreferred("16384$8$1$b97ed09792dd74b71dcb7fc8caf04a89$0b5cda82b17298ec4bf6d2139f7ea8587d8478fcc68c09e2506a7cf08b2817c0") ok := pass.IsPreferred([]byte("16384$8$1$b97ed09792dd74b71dcb7fc8caf04a89$0b5cda82b17298ec4bf6d2139f7ea8587d8478fcc68c09e2506a7cf08b2817c0"))
is.True(!ok) is.True(!ok)
ok = pass.IsPreferred("$s2$16384$8$1$iEdwbgXyKa5GNGNW/0NsOA$9YN/hzbskVVDZ887ppqv5su0n8SxVXwDB/rhVhAc9xQ") ok = pass.IsPreferred([]byte("$s2$16384$8$1$iEdwbgXyKa5GNGNW/0NsOA$9YN/hzbskVVDZ887ppqv5su0n8SxVXwDB/rhVhAc9xQ"))
is.True(ok) is.True(ok)
ok = pass.IsPreferred("$s2$16384$7$1$iEdwbgXyKa5GNGNW/0NsOA$9YN/hzbskVVDZ887ppqv5su0n8SxVXwDB/rhVhAc9xQ") ok = pass.IsPreferred([]byte("$s2$16384$7$1$iEdwbgXyKa5GNGNW/0NsOA$9YN/hzbskVVDZ887ppqv5su0n8SxVXwDB/rhVhAc9xQ"))
is.True(!ok) is.True(!ok)
ok = pass.IsPreferred("$1$76a2173be6393254e72ffa4d6df1030a") ok = pass.IsPreferred([]byte("$1$76a2173be6393254e72ffa4d6df1030a"))
is.True(!ok) is.True(!ok)
} }

View File

@ -16,11 +16,11 @@ var All = []passwd.Passwder{
type MD5 struct{} type MD5 struct{}
func (p *MD5) Passwd(pass string, check string) (string, error) { func (p *MD5) Passwd(pass, check []byte) ([]byte, error) {
h := md5.New() h := md5.New()
fmt.Fprint(h, pass) h.Write(pass)
hash := fmt.Sprintf("$1$%x", h.Sum(nil)) hash := []byte(fmt.Sprintf("$1$%x", h.Sum(nil)))
return hashCheck(hash, check) return hashCheck(hash, check)
} }
@ -31,18 +31,18 @@ func (p *MD5) ApplyPasswd(passwd *passwd.Passwd) {
type Blowfish struct{} type Blowfish struct{}
func (p *Blowfish) Passwd(pass string, check string) (string, error) { func (p *Blowfish) Passwd(pass, check []byte) ([]byte, error) {
if check == "" { if check == nil {
b, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) b, err := bcrypt.GenerateFromPassword(pass, bcrypt.DefaultCost)
if err != nil { if err != nil {
return "", err return nil, err
} }
return string(b), nil return b, nil
} }
err := bcrypt.CompareHashAndPassword([]byte(check), []byte(pass)) err := bcrypt.CompareHashAndPassword(check, pass)
if err != nil { if err != nil {
return "", err return nil, err
} }
return check, nil return check, nil
} }
@ -51,42 +51,12 @@ func (p *Blowfish) ApplyPasswd(passwd *passwd.Passwd) {
passwd.Register("2a", p) passwd.Register("2a", p)
} }
// type SHA256 struct{} func hashCheck(hash, check []byte) ([]byte, error) {
if check == nil {
// func (p *SHA256) Passwd(pass string, check string) (string, error) {
// h := sha256.New()
// fmt.Fprint(h, pass)
// hash := fmt.Sprintf("$5$%x", h.Sum(nil))
// return hashCheck(hash, check)
// }
// func (p *SHA256) ApplyPasswd(passwd *passwd.Passwd) {
// passwd.Register("5", p)
// }
// type SHA512 struct{}
// func (p *SHA512) Passwd(pass string, check string) (string, error) {
// h := sha512.New()
// fmt.Fprint(h, pass)
// hash := fmt.Sprintf("$6$%x", h.Sum(nil))
// return hashCheck(hash, check)
// }
// func (p *SHA512) ApplyPasswd(passwd *passwd.Passwd) {
// passwd.Register("6", p)
// }
func hashCheck(hash, check string) (string, error) {
if check == "" {
return hash, nil return hash, nil
} }
if subtle.ConstantTimeCompare([]byte(hash), []byte(check)) == 1 { if subtle.ConstantTimeCompare(hash, check) == 1 {
return hash, nil return hash, nil
} }

View File

@ -12,20 +12,20 @@ import (
func TestPasswdHash(t *testing.T) { func TestPasswdHash(t *testing.T) {
type testCase struct { type testCase struct {
pass, hash string pass, hash []byte
} }
tests := []testCase{ tests := []testCase{
{"passwd", "$1$76a2173be6393254e72ffa4d6df1030a"}, {[]byte("passwd"), []byte("$1$76a2173be6393254e72ffa4d6df1030a")},
{"passwd", "$2a$10$GkJwB.nOaaeAvRGgyl2TI.kruM8e.iIo.OozgdslegpNlC/vIFKRq"}, {[]byte("passwd"), []byte("$2a$10$GkJwB.nOaaeAvRGgyl2TI.kruM8e.iIo.OozgdslegpNlC/vIFKRq")},
} }
is := is.New(t) is := is.New(t)
// Generate additional test cases for each algo. // Generate additional test cases for each algo.
for _, algo := range unix.All { for _, algo := range unix.All {
hash, err := algo.Passwd("passwd", "") hash, err := algo.Passwd([]byte("passwd"), nil)
is.NoErr(err) is.NoErr(err)
tests = append(tests, testCase{"passwd", hash}) tests = append(tests, testCase{[]byte("passwd"), hash})
} }
pass := passwd.New(unix.All...) pass := passwd.New(unix.All...)
@ -35,7 +35,7 @@ func TestPasswdHash(t *testing.T) {
is := is.New(t) is := is.New(t)
hash, err := pass.Passwd(tt.pass, tt.hash) hash, err := pass.Passwd(tt.pass, tt.hash)
is.Equal(hash, tt.hash) is.Equal(string(hash), string(tt.hash))
is.NoErr(err) is.NoErr(err)
}) })
} }