initial commit

This commit is contained in:
Jon Lundy 2022-12-07 14:19:04 -07:00
commit 0c36a25333
Signed by untrusted user who does not match committer: xuu
GPG Key ID: C63E6D61F3035024
10 changed files with 808 additions and 0 deletions

12
go.mod Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
})
}
}