initial commit
This commit is contained in:
commit
0c36a25333
12
go.mod
Normal file
12
go.mod
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
module github.com/sour-is/go-passwd
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/matryer/is v1.4.0
|
||||||
|
golang.org/x/crypto v0.3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/sys v0.2.0 // indirect
|
||||||
|
)
|
14
go.sum
Normal file
14
go.sum
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
github.com/elithrar/simple-scrypt v1.3.0 h1:KIlOlxdoQf9JWKl5lMAJ28SY2URB0XTRDn2TckyzAZg=
|
||||||
|
github.com/elithrar/simple-scrypt v1.3.0/go.mod h1:U2XQRI95XHY0St410VE3UjT7vuKb1qPwrl/EJwEqnZo=
|
||||||
|
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||||
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
|
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
|
||||||
|
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||||
|
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
||||||
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/hlandau/easymetric.v1 v1.0.0 h1:ZbfbH7W3giuVDjWUoFhDOjjv20hiPr5HZ2yMV5f9IeE=
|
||||||
|
gopkg.in/hlandau/easymetric.v1 v1.0.0/go.mod h1:yh75hypuFzAxmvECh3ZKGCvFnIfapYJh2wv7ASaX2RE=
|
||||||
|
gopkg.in/hlandau/measurable.v1 v1.0.1 h1:wH5UZKCRUnRr1iD+xIZfwhtxhmr+bprRJttqA1Rklf4=
|
||||||
|
gopkg.in/hlandau/measurable.v1 v1.0.1/go.mod h1:6N+SYJGMTmetsx7wskULP+juuO+++tsHJkAgzvzsbuM=
|
||||||
|
gopkg.in/hlandau/passlib.v1 v1.0.11 h1:vKeHwGRdWBD9mm4bJ56GAAdBXpFUYvg/BYYkmphjnmA=
|
||||||
|
gopkg.in/hlandau/passlib.v1 v1.0.11/go.mod h1:wxGAv2CtQHlzWY8NJp+p045yl4WHyX7v2T6XbOcmqjM=
|
102
passwd.go
Normal file
102
passwd.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package passwd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Passwder interface {
|
||||||
|
Passwd(string, string) (string, error)
|
||||||
|
ApplyPasswd(*Passwd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Passwd struct {
|
||||||
|
m map[string]Passwder
|
||||||
|
d Passwder
|
||||||
|
f Passwder
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(opts ...Passwder) *Passwd {
|
||||||
|
p := &Passwd{m: make(map[string]Passwder)}
|
||||||
|
p.Options(opts...)
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) Options(opts ...Passwder) {
|
||||||
|
for _, o := range opts {
|
||||||
|
o.ApplyPasswd(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) Register(name string, pass Passwder) {
|
||||||
|
p.m[name] = pass
|
||||||
|
if p.d == nil {
|
||||||
|
p.SetDefault(pass)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) SetDefault(pass Passwder) {
|
||||||
|
p.d = pass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) SetFallthrough(pass Passwder) {
|
||||||
|
p.f = pass
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) Passwd(pass, hash string) (string, error) {
|
||||||
|
if hash == "" {
|
||||||
|
return p.d.Passwd(pass, hash)
|
||||||
|
}
|
||||||
|
name, algo := p.getAlgo(hash)
|
||||||
|
if algo == nil {
|
||||||
|
algo = p.f
|
||||||
|
}
|
||||||
|
if algo == nil {
|
||||||
|
return "", fmt.Errorf("%w: %s", ErrNoHandler, name)
|
||||||
|
}
|
||||||
|
return algo.Passwd(pass, hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) IsPreferred(hash string) bool {
|
||||||
|
_, algo := p.getAlgo(hash)
|
||||||
|
if algo != nil && algo == p.d {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) getAlgo(hash string) (string, Passwder) {
|
||||||
|
var algo string
|
||||||
|
if _, h, ok := strings.Cut(hash, "$"); ok {
|
||||||
|
algo, _, ok = strings.Cut(h, "$")
|
||||||
|
if !ok {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if passwd, ok := p.m[algo]; ok {
|
||||||
|
return algo, passwd
|
||||||
|
}
|
||||||
|
|
||||||
|
return algo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.getName(p.f), p.f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Passwd) getName(n Passwder) string {
|
||||||
|
if n == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for k, v := range p.m {
|
||||||
|
if v == n {
|
||||||
|
return k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNoMatch = errors.New("password does not match")
|
||||||
|
var ErrBadHash = errors.New("password hash is malformed")
|
||||||
|
var ErrNoHandler = errors.New("password handler not registered")
|
103
passwd_test.go
Normal file
103
passwd_test.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package passwd_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"github.com/sour-is/go-passwd/pkg/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type plainPasswd struct{}
|
||||||
|
|
||||||
|
func (p *plainPasswd) Passwd(pass string, check string) (string, error) {
|
||||||
|
if check == "" {
|
||||||
|
return fmt.Sprint("$plain$", pass), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(pass), []byte(strings.TrimPrefix(check, "$plain$"))) == 1 {
|
||||||
|
return check, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return check, passwd.ErrNoMatch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *plainPasswd) ApplyPasswd(passwd *passwd.Passwd) {
|
||||||
|
passwd.Register("plain", p)
|
||||||
|
passwd.SetFallthrough(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
pass := "my_pass"
|
||||||
|
hash := "my_pass"
|
||||||
|
|
||||||
|
pwd := passwd.New(
|
||||||
|
&unix.MD5{}, // first is preferred type.
|
||||||
|
&plainPasswd{},
|
||||||
|
)
|
||||||
|
|
||||||
|
_, err := pwd.Passwd(pass, hash)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("fail: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we want to update.
|
||||||
|
if !pwd.IsPreferred(hash) {
|
||||||
|
newHash, err := pwd.Passwd(pass, "")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("fail: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("new hash:", newHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
// new hash: $1$81ed91e1131a3a5a50d8a68e8ef85fa0
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswdHash(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
pass, hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{"passwd", "passwd"},
|
||||||
|
{"passwd", "$plain$passwd"},
|
||||||
|
}
|
||||||
|
algos := []passwd.Passwder{&plainPasswd{}}
|
||||||
|
|
||||||
|
is := is.New(t)
|
||||||
|
// Generate additional test cases for each algo.
|
||||||
|
for _, algo := range algos {
|
||||||
|
hash, err := algo.Passwd("passwd", "")
|
||||||
|
is.NoErr(err)
|
||||||
|
tests = append(tests, testCase{"passwd", hash})
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := passwd.New(algos...)
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprint("Test-", i), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
hash, err := pass.Passwd(tt.pass, tt.hash)
|
||||||
|
is.Equal(hash, tt.hash)
|
||||||
|
is.NoErr(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPasswdIsPreferred(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
pass := passwd.New(&plainPasswd{})
|
||||||
|
|
||||||
|
ok := pass.IsPreferred("$plain$passwd")
|
||||||
|
is.True(ok)
|
||||||
|
|
||||||
|
ok = pass.IsPreferred("$foo$passwd")
|
||||||
|
is.True(!ok)
|
||||||
|
}
|
187
pkg/argon2/argon2.go
Normal file
187
pkg/argon2/argon2.go
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
package argon2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
)
|
||||||
|
|
||||||
|
type argon struct {
|
||||||
|
version uint8
|
||||||
|
time uint32
|
||||||
|
memory uint32
|
||||||
|
threads uint8
|
||||||
|
keyLen uint32
|
||||||
|
saltLen uint32
|
||||||
|
|
||||||
|
name string
|
||||||
|
keyFn func(password, salt []byte, time, memory uint32, threads uint8, keyLen uint32) []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var All = []passwd.Passwder{Argon2i, Argon2id}
|
||||||
|
|
||||||
|
// Argon2i sets default recommended values.
|
||||||
|
var Argon2i = NewArgon2i(3, 32*1024, 4, 32, 16)
|
||||||
|
|
||||||
|
// Argon2id sets default recommended values.
|
||||||
|
var Argon2id = NewArgon2id(1, 64*1024, 4, 32, 16)
|
||||||
|
|
||||||
|
// NewArgon2i creates a new
|
||||||
|
func NewArgon2i(
|
||||||
|
time uint32,
|
||||||
|
memory uint32,
|
||||||
|
threads uint8,
|
||||||
|
keyLen uint32,
|
||||||
|
saltLen uint32,
|
||||||
|
) *argon {
|
||||||
|
return &argon{
|
||||||
|
version: argon2.Version,
|
||||||
|
time: time,
|
||||||
|
memory: memory,
|
||||||
|
threads: threads,
|
||||||
|
keyLen: keyLen,
|
||||||
|
saltLen: saltLen,
|
||||||
|
name: "argon2i",
|
||||||
|
keyFn: argon2.Key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewArgon2i creates a new
|
||||||
|
func NewArgon2id(
|
||||||
|
time uint32,
|
||||||
|
memory uint32,
|
||||||
|
threads uint8,
|
||||||
|
keyLen uint32,
|
||||||
|
saltLen uint32,
|
||||||
|
) *argon {
|
||||||
|
return &argon{
|
||||||
|
version: argon2.Version,
|
||||||
|
time: time,
|
||||||
|
memory: memory,
|
||||||
|
threads: threads,
|
||||||
|
keyLen: keyLen,
|
||||||
|
saltLen: saltLen,
|
||||||
|
name: "argon2id",
|
||||||
|
keyFn: argon2.IDKey,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *argon) Passwd(pass string, check string) (string, error) {
|
||||||
|
var args *pwArgs
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if check == "" {
|
||||||
|
args = p.defaultArgs()
|
||||||
|
_, err := rand.Read(args.salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
args.hash = p.keyFn([]byte(pass), args.salt, args.time, args.memory, args.threads, args.keyLen)
|
||||||
|
} else {
|
||||||
|
args, err = p.parseArgs(check)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hash := p.keyFn([]byte(pass), args.salt, args.time, args.memory, args.threads, args.keyLen)
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(hash, args.hash) == 0 {
|
||||||
|
return "", passwd.ErrNoMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.String(), nil
|
||||||
|
}
|
||||||
|
func (p *argon) ApplyPasswd(passwd *passwd.Passwd) {
|
||||||
|
passwd.Register(p.name, p)
|
||||||
|
}
|
||||||
|
func (p *argon) defaultArgs() *pwArgs {
|
||||||
|
return &pwArgs{
|
||||||
|
name: p.name,
|
||||||
|
version: p.version,
|
||||||
|
time: p.time,
|
||||||
|
memory: p.memory,
|
||||||
|
threads: p.threads,
|
||||||
|
keyLen: p.keyLen,
|
||||||
|
salt: make([]byte, p.saltLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (p *argon) parseArgs(hash string) (*pwArgs, error) {
|
||||||
|
pfx := "$" + p.name + "$"
|
||||||
|
|
||||||
|
if !strings.HasPrefix(hash, pfx) {
|
||||||
|
return nil, fmt.Errorf("%w: missing prefix", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
hash = strings.TrimPrefix(hash, pfx)
|
||||||
|
args, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing args", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
salt, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing salt", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
pass := p.defaultArgs()
|
||||||
|
pass.salt, err = base64.RawStdEncoding.DecodeString(salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
pass.hash, err = base64.RawStdEncoding.DecodeString(hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
pass.name = p.name
|
||||||
|
pass.keyLen = uint32(len(pass.hash))
|
||||||
|
|
||||||
|
for _, part := range strings.Split(args, ",") {
|
||||||
|
if k, v, ok := strings.Cut(part, "="); ok {
|
||||||
|
switch k {
|
||||||
|
case "v":
|
||||||
|
if i, err := strconv.ParseUint(v, 10, 8); err == nil {
|
||||||
|
pass.version = uint8(i)
|
||||||
|
}
|
||||||
|
case "m":
|
||||||
|
if i, err := strconv.ParseUint(v, 10, 32); err == nil {
|
||||||
|
pass.memory = uint32(i)
|
||||||
|
}
|
||||||
|
case "t":
|
||||||
|
if i, err := strconv.ParseUint(v, 10, 32); err == nil {
|
||||||
|
pass.time = uint32(i)
|
||||||
|
}
|
||||||
|
case "p":
|
||||||
|
if i, err := strconv.ParseUint(v, 10, 8); err == nil {
|
||||||
|
pass.threads = uint8(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pass, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type pwArgs struct {
|
||||||
|
name string
|
||||||
|
version uint8
|
||||||
|
time uint32
|
||||||
|
memory uint32
|
||||||
|
threads uint8
|
||||||
|
keyLen uint32
|
||||||
|
salt []byte
|
||||||
|
hash []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pwArgs) String() string {
|
||||||
|
salt := base64.RawStdEncoding.EncodeToString(p.salt)
|
||||||
|
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)
|
||||||
|
}
|
40
pkg/argon2/argon2_test.go
Normal file
40
pkg/argon2/argon2_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package argon2_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"github.com/sour-is/go-passwd/pkg/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPasswdHash(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
pass, hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{}
|
||||||
|
algos := argon2.All
|
||||||
|
|
||||||
|
is := is.New(t)
|
||||||
|
// Generate additional test cases for each algo.
|
||||||
|
for _, algo := range algos {
|
||||||
|
hash, err := algo.Passwd("passwd", "")
|
||||||
|
is.NoErr(err)
|
||||||
|
tests = append(tests, testCase{"passwd", hash})
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := passwd.New(algos...)
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprint("Test-", i), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
hash, err := pass.Passwd(tt.pass, tt.hash)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(hash, tt.hash)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
174
pkg/scrypt/scrypt.go
Normal file
174
pkg/scrypt/scrypt.go
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
package scrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"golang.org/x/crypto/scrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type scryptpw struct {
|
||||||
|
N int // CPU/memory cost parameter (logN)
|
||||||
|
R int // block size parameter (octets)
|
||||||
|
P int // parallelisation parameter (positive int)
|
||||||
|
SaltLen int // bytes to use as salt (octets)
|
||||||
|
DKLen int // length of the derived key (octets)
|
||||||
|
|
||||||
|
name string
|
||||||
|
encoder interface {
|
||||||
|
EncodeToString(src []byte) string
|
||||||
|
DecodeString(s string) ([]byte, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type scryptArgs struct {
|
||||||
|
N int // CPU/memory cost parameter (logN)
|
||||||
|
R int // block size parameter (octets)
|
||||||
|
P int // parallelisation parameter (positive int)
|
||||||
|
SaltLen int // bytes to use as salt (octets)
|
||||||
|
DKLen int // length of the derived key (octets)
|
||||||
|
|
||||||
|
name string
|
||||||
|
salt []byte
|
||||||
|
hash []byte
|
||||||
|
|
||||||
|
encoder interface {
|
||||||
|
EncodeToString(src []byte) string
|
||||||
|
DecodeString(s string) ([]byte, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var All = []passwd.Passwder{Simple, Scrypt2}
|
||||||
|
|
||||||
|
var Simple = &scryptpw{
|
||||||
|
N: 16384, R: 8, P: 1, SaltLen: 16, DKLen: 32,
|
||||||
|
name: "s1", encoder: hexenc{},
|
||||||
|
}
|
||||||
|
|
||||||
|
var Scrypt2 = &scryptpw{
|
||||||
|
N: 16384, R: 8, P: 1, SaltLen: 16, DKLen: 32,
|
||||||
|
name: "s2", encoder: base64.RawStdEncoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *scryptpw) Passwd(pass string, check string) (string, error) {
|
||||||
|
var args *scryptArgs
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if check == "" {
|
||||||
|
args = s.defaultArgs()
|
||||||
|
_, err := rand.Read(args.salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
args.hash, err = scrypt.Key([]byte(pass), args.salt, args.N, args.R, args.P, args.DKLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
args, err = s.parseArgs(check)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hash, err := scrypt.Key([]byte(pass), args.salt, args.N, args.R, args.P, args.DKLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare(hash, args.hash) == 0 {
|
||||||
|
return "", passwd.ErrNoMatch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args.String(), nil
|
||||||
|
}
|
||||||
|
func (s *scryptpw) ApplyPasswd(p *passwd.Passwd) {
|
||||||
|
p.Register(s.name, s)
|
||||||
|
if s.name == "s1" {
|
||||||
|
p.SetFallthrough(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *scryptpw) defaultArgs() *scryptArgs {
|
||||||
|
return &scryptArgs{
|
||||||
|
name: s.name,
|
||||||
|
N: s.N,
|
||||||
|
R: s.R,
|
||||||
|
P: s.P,
|
||||||
|
DKLen: s.DKLen,
|
||||||
|
SaltLen: s.SaltLen,
|
||||||
|
salt: make([]byte, s.SaltLen),
|
||||||
|
encoder: s.encoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func (s *scryptpw) parseArgs(hash string) (*scryptArgs, error) {
|
||||||
|
args := s.defaultArgs()
|
||||||
|
|
||||||
|
name := "$" + s.name + "$"
|
||||||
|
hash = strings.TrimPrefix(hash, name)
|
||||||
|
|
||||||
|
N, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing args: N", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
if n, err := strconv.Atoi(N); err == nil {
|
||||||
|
args.N = n
|
||||||
|
}
|
||||||
|
|
||||||
|
R, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing args: R", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
if r, err := strconv.Atoi(R); err == nil {
|
||||||
|
args.R = r
|
||||||
|
}
|
||||||
|
|
||||||
|
P, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing args: P", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
if p, err := strconv.Atoi(P); err == nil {
|
||||||
|
args.P = p
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, hash, ok := strings.Cut(hash, "$")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: missing args: salt", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
args.salt, err = s.encoder.DecodeString(salt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: corrupt salt part", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
args.hash, err = s.encoder.DecodeString(hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: corrupt hash part", passwd.ErrBadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
type hexenc struct{}
|
||||||
|
|
||||||
|
func (hexenc) EncodeToString(src []byte) string {
|
||||||
|
return hex.EncodeToString(src)
|
||||||
|
}
|
||||||
|
func (hexenc) DecodeString(s string) ([]byte, error) {
|
||||||
|
return hex.DecodeString(s)
|
||||||
|
}
|
40
pkg/scrypt/scrypt_test.go
Normal file
40
pkg/scrypt/scrypt_test.go
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
package scrypt_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"github.com/sour-is/go-passwd/pkg/scrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPasswdHash(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
pass, hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{}
|
||||||
|
algos := scrypt.All
|
||||||
|
|
||||||
|
is := is.New(t)
|
||||||
|
// Generate additional test cases for each algo.
|
||||||
|
for _, algo := range algos {
|
||||||
|
hash, err := algo.Passwd("passwd", "")
|
||||||
|
is.NoErr(err)
|
||||||
|
tests = append(tests, testCase{"passwd", hash})
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := passwd.New(algos...)
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprint("Test-", i), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
hash, err := pass.Passwd(tt.pass, tt.hash)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(hash, tt.hash)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
94
pkg/unix/unix.go
Normal file
94
pkg/unix/unix.go
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package unix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var All = []passwd.Passwder{
|
||||||
|
&Blowfish{},
|
||||||
|
&MD5{},
|
||||||
|
}
|
||||||
|
|
||||||
|
type MD5 struct{}
|
||||||
|
|
||||||
|
func (p *MD5) Passwd(pass string, check string) (string, error) {
|
||||||
|
h := md5.New()
|
||||||
|
fmt.Fprint(h, pass)
|
||||||
|
|
||||||
|
hash := fmt.Sprintf("$1$%x", h.Sum(nil))
|
||||||
|
|
||||||
|
return hashCheck(hash, check)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *MD5) ApplyPasswd(passwd *passwd.Passwd) {
|
||||||
|
passwd.Register("1", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Blowfish struct{}
|
||||||
|
|
||||||
|
func (p *Blowfish) Passwd(pass string, check string) (string, error) {
|
||||||
|
if check == "" {
|
||||||
|
b, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(check), []byte(pass))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return check, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Blowfish) ApplyPasswd(passwd *passwd.Passwd) {
|
||||||
|
passwd.Register("2a", p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type SHA256 struct{}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(hash), []byte(check)) == 1 {
|
||||||
|
return hash, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash, passwd.ErrNoMatch
|
||||||
|
}
|
42
pkg/unix/unix_test.go
Normal file
42
pkg/unix/unix_test.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package unix_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
"github.com/sour-is/go-passwd"
|
||||||
|
"github.com/sour-is/go-passwd/pkg/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPasswdHash(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
pass, hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
{"passwd", "$1$76a2173be6393254e72ffa4d6df1030a"},
|
||||||
|
{"passwd", "$2a$10$GkJwB.nOaaeAvRGgyl2TI.kruM8e.iIo.OozgdslegpNlC/vIFKRq"},
|
||||||
|
}
|
||||||
|
|
||||||
|
is := is.New(t)
|
||||||
|
// Generate additional test cases for each algo.
|
||||||
|
for _, algo := range unix.All {
|
||||||
|
hash, err := algo.Passwd("passwd", "")
|
||||||
|
is.NoErr(err)
|
||||||
|
tests = append(tests, testCase{"passwd", hash})
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := passwd.New(unix.All...)
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprint("Test-", i), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
hash, err := pass.Passwd(tt.pass, tt.hash)
|
||||||
|
is.Equal(hash, tt.hash)
|
||||||
|
is.NoErr(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user