You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
323 lines
7.9 KiB
323 lines
7.9 KiB
|
2 months ago
|
package password
|
||
|
|
|
||
|
|
import (
|
||
|
|
"crypto/rand"
|
||
|
|
"crypto/sha256"
|
||
|
|
"crypto/subtle"
|
||
|
|
"encoding/base64"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"golang.org/x/crypto/argon2"
|
||
|
|
"golang.org/x/crypto/pbkdf2"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
AlgorithmArgon2id = "argon2id"
|
||
|
|
AlgorithmPBKDF2SHA256 = "pbkdf2-hmac-sha256"
|
||
|
|
AlgorithmPrototypeSHA256 = "prototype-sha256"
|
||
|
|
)
|
||
|
|
|
||
|
|
var ErrUnsupportedKDF = errors.New("unsupported password KDF")
|
||
|
|
|
||
|
|
type Record struct {
|
||
|
|
Algorithm string `json:"algorithm"`
|
||
|
|
Version int `json:"version"`
|
||
|
|
Salt string `json:"salt"`
|
||
|
|
Hash string `json:"hash"`
|
||
|
|
Params Params `json:"params"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type Params struct {
|
||
|
|
MemoryKiB uint32 `json:"memory_kib,omitempty"`
|
||
|
|
Iterations uint32 `json:"iterations,omitempty"`
|
||
|
|
Parallelism uint8 `json:"parallelism,omitempty"`
|
||
|
|
KeyLength uint32 `json:"key_length,omitempty"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type KDF interface {
|
||
|
|
Algorithm() string
|
||
|
|
Hash(password, salt string) (Record, error)
|
||
|
|
Verify(record Record, password string) (bool, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
type Registry struct {
|
||
|
|
kdfs map[string]KDF
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewRegistry(kdfs ...KDF) Registry {
|
||
|
|
registry := Registry{kdfs: make(map[string]KDF, len(kdfs))}
|
||
|
|
for _, kdf := range kdfs {
|
||
|
|
if kdf != nil {
|
||
|
|
registry.kdfs[kdf.Algorithm()] = kdf
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return registry
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewPrototypeRegistry() Registry {
|
||
|
|
return NewRegistry(PrototypeSHA256{})
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewProductionRegistry() Registry {
|
||
|
|
return NewRegistry(NewArgon2id(Argon2idDefaultParams()), PrototypeSHA256{})
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r Registry) Verify(record Record, password string) (bool, error) {
|
||
|
|
kdf, ok := r.kdfs[record.Algorithm]
|
||
|
|
if !ok {
|
||
|
|
return false, fmt.Errorf("%w: %s", ErrUnsupportedKDF, record.Algorithm)
|
||
|
|
}
|
||
|
|
return kdf.Verify(record, password)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r Registry) Supports(algorithm string) bool {
|
||
|
|
_, ok := r.kdfs[algorithm]
|
||
|
|
return ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r Registry) Algorithms() []string {
|
||
|
|
algorithms := make([]string, 0, len(r.kdfs))
|
||
|
|
for algorithm := range r.kdfs {
|
||
|
|
algorithms = append(algorithms, algorithm)
|
||
|
|
}
|
||
|
|
sort.Strings(algorithms)
|
||
|
|
return algorithms
|
||
|
|
}
|
||
|
|
|
||
|
|
type PrototypeSHA256 struct{}
|
||
|
|
|
||
|
|
func (PrototypeSHA256) Algorithm() string {
|
||
|
|
return AlgorithmPrototypeSHA256
|
||
|
|
}
|
||
|
|
|
||
|
|
func (PrototypeSHA256) DefaultAuthSeedCacheKey() string {
|
||
|
|
return AlgorithmPrototypeSHA256 + ":v1"
|
||
|
|
}
|
||
|
|
|
||
|
|
func (PrototypeSHA256) Hash(passwordValue, salt string) (Record, error) {
|
||
|
|
if strings.TrimSpace(salt) == "" {
|
||
|
|
return Record{}, errors.New("password salt must not be empty")
|
||
|
|
}
|
||
|
|
return Record{
|
||
|
|
Algorithm: AlgorithmPrototypeSHA256,
|
||
|
|
Version: 1,
|
||
|
|
Salt: salt,
|
||
|
|
Hash: prototypeHash(salt, passwordValue),
|
||
|
|
Params: Params{
|
||
|
|
KeyLength: 32,
|
||
|
|
},
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf PrototypeSHA256) Verify(record Record, passwordValue string) (bool, error) {
|
||
|
|
if record.Algorithm != kdf.Algorithm() {
|
||
|
|
return false, fmt.Errorf("%w: %s", ErrUnsupportedKDF, record.Algorithm)
|
||
|
|
}
|
||
|
|
if record.Version != 1 || strings.TrimSpace(record.Salt) == "" || record.Hash == "" {
|
||
|
|
return false, errors.New("invalid prototype password hash record")
|
||
|
|
}
|
||
|
|
next, err := kdf.Hash(passwordValue, record.Salt)
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
return next.Hash == record.Hash, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func Argon2idPolicyRecord() Record {
|
||
|
|
return Record{
|
||
|
|
Algorithm: AlgorithmArgon2id,
|
||
|
|
Version: 1,
|
||
|
|
Params: Argon2idDefaultParams(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type Argon2id struct {
|
||
|
|
params Params
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewArgon2id(params Params) Argon2id {
|
||
|
|
return Argon2id{params: normalizeArgon2idParams(params)}
|
||
|
|
}
|
||
|
|
|
||
|
|
func Argon2idDefaultParams() Params {
|
||
|
|
return Params{
|
||
|
|
MemoryKiB: 64 * 1024,
|
||
|
|
Iterations: 3,
|
||
|
|
Parallelism: 1,
|
||
|
|
KeyLength: 32,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (Argon2id) Algorithm() string {
|
||
|
|
return AlgorithmArgon2id
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf Argon2id) DefaultAuthSeedCacheKey() string {
|
||
|
|
params := normalizeArgon2idParams(kdf.params)
|
||
|
|
return fmt.Sprintf(
|
||
|
|
"%s:v1:m=%d:t=%d:p=%d:l=%d",
|
||
|
|
AlgorithmArgon2id,
|
||
|
|
params.MemoryKiB,
|
||
|
|
params.Iterations,
|
||
|
|
params.Parallelism,
|
||
|
|
params.KeyLength,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf Argon2id) Hash(passwordValue, salt string) (Record, error) {
|
||
|
|
if strings.TrimSpace(salt) == "" {
|
||
|
|
return Record{}, errors.New("password salt must not be empty")
|
||
|
|
}
|
||
|
|
params := normalizeArgon2idParams(kdf.params)
|
||
|
|
hash := argon2.IDKey(
|
||
|
|
[]byte(passwordValue),
|
||
|
|
[]byte(salt),
|
||
|
|
params.Iterations,
|
||
|
|
params.MemoryKiB,
|
||
|
|
params.Parallelism,
|
||
|
|
params.KeyLength,
|
||
|
|
)
|
||
|
|
return Record{
|
||
|
|
Algorithm: AlgorithmArgon2id,
|
||
|
|
Version: 1,
|
||
|
|
Salt: salt,
|
||
|
|
Hash: base64.StdEncoding.EncodeToString(hash),
|
||
|
|
Params: params,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf Argon2id) Verify(record Record, passwordValue string) (bool, error) {
|
||
|
|
if record.Algorithm != kdf.Algorithm() {
|
||
|
|
return false, fmt.Errorf("%w: %s", ErrUnsupportedKDF, record.Algorithm)
|
||
|
|
}
|
||
|
|
if record.Version != 1 || strings.TrimSpace(record.Salt) == "" || record.Hash == "" {
|
||
|
|
return false, errors.New("invalid argon2id password hash record")
|
||
|
|
}
|
||
|
|
expected, err := argon2idKDFFromRecord(record).Hash(passwordValue, record.Salt)
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
return subtle.ConstantTimeCompare([]byte(expected.Hash), []byte(record.Hash)) == 1, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func argon2idKDFFromRecord(record Record) Argon2id {
|
||
|
|
return NewArgon2id(record.Params)
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizeArgon2idParams(params Params) Params {
|
||
|
|
defaults := Argon2idDefaultParams()
|
||
|
|
if params.MemoryKiB == 0 {
|
||
|
|
params.MemoryKiB = defaults.MemoryKiB
|
||
|
|
}
|
||
|
|
if params.Iterations == 0 {
|
||
|
|
params.Iterations = defaults.Iterations
|
||
|
|
}
|
||
|
|
if params.Parallelism == 0 {
|
||
|
|
params.Parallelism = defaults.Parallelism
|
||
|
|
}
|
||
|
|
if params.KeyLength == 0 {
|
||
|
|
params.KeyLength = defaults.KeyLength
|
||
|
|
}
|
||
|
|
return params
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewSalt() (string, error) {
|
||
|
|
var raw [16]byte
|
||
|
|
if _, err := rand.Read(raw[:]); err != nil {
|
||
|
|
return "", fmt.Errorf("generate password salt: %w", err)
|
||
|
|
}
|
||
|
|
return base64.RawURLEncoding.EncodeToString(raw[:]), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func PBKDF2SHA256PolicyRecord() Record {
|
||
|
|
return Record{
|
||
|
|
Algorithm: AlgorithmPBKDF2SHA256,
|
||
|
|
Version: 1,
|
||
|
|
Params: PBKDF2SHA256DefaultParams(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type PBKDF2SHA256 struct {
|
||
|
|
params Params
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewPBKDF2SHA256(params Params) PBKDF2SHA256 {
|
||
|
|
return PBKDF2SHA256{params: normalizePBKDF2SHA256Params(params)}
|
||
|
|
}
|
||
|
|
|
||
|
|
func PBKDF2SHA256DefaultParams() Params {
|
||
|
|
return Params{
|
||
|
|
Iterations: 310000,
|
||
|
|
KeyLength: 32,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (PBKDF2SHA256) Algorithm() string {
|
||
|
|
return AlgorithmPBKDF2SHA256
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf PBKDF2SHA256) DefaultAuthSeedCacheKey() string {
|
||
|
|
params := normalizePBKDF2SHA256Params(kdf.params)
|
||
|
|
return fmt.Sprintf(
|
||
|
|
"%s:v1:i=%d:l=%d",
|
||
|
|
AlgorithmPBKDF2SHA256,
|
||
|
|
params.Iterations,
|
||
|
|
params.KeyLength,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf PBKDF2SHA256) Hash(passwordValue, salt string) (Record, error) {
|
||
|
|
if strings.TrimSpace(salt) == "" {
|
||
|
|
return Record{}, errors.New("password salt must not be empty")
|
||
|
|
}
|
||
|
|
params := normalizePBKDF2SHA256Params(kdf.params)
|
||
|
|
hash := pbkdf2.Key([]byte(passwordValue), []byte(salt), int(params.Iterations), int(params.KeyLength), sha256.New)
|
||
|
|
return Record{
|
||
|
|
Algorithm: AlgorithmPBKDF2SHA256,
|
||
|
|
Version: 1,
|
||
|
|
Salt: salt,
|
||
|
|
Hash: base64.StdEncoding.EncodeToString(hash),
|
||
|
|
Params: params,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (kdf PBKDF2SHA256) Verify(record Record, passwordValue string) (bool, error) {
|
||
|
|
if record.Algorithm != kdf.Algorithm() {
|
||
|
|
return false, fmt.Errorf("%w: %s", ErrUnsupportedKDF, record.Algorithm)
|
||
|
|
}
|
||
|
|
if record.Version != 1 || strings.TrimSpace(record.Salt) == "" || record.Hash == "" {
|
||
|
|
return false, errors.New("invalid pbkdf2 password hash record")
|
||
|
|
}
|
||
|
|
expected, err := pbkdf2SHA256KDFFromRecord(record).Hash(passwordValue, record.Salt)
|
||
|
|
if err != nil {
|
||
|
|
return false, err
|
||
|
|
}
|
||
|
|
return subtle.ConstantTimeCompare([]byte(expected.Hash), []byte(record.Hash)) == 1, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func pbkdf2SHA256KDFFromRecord(record Record) PBKDF2SHA256 {
|
||
|
|
return NewPBKDF2SHA256(record.Params)
|
||
|
|
}
|
||
|
|
|
||
|
|
func normalizePBKDF2SHA256Params(params Params) Params {
|
||
|
|
defaults := PBKDF2SHA256DefaultParams()
|
||
|
|
if params.Iterations == 0 {
|
||
|
|
params.Iterations = defaults.Iterations
|
||
|
|
}
|
||
|
|
if params.KeyLength == 0 {
|
||
|
|
params.KeyLength = defaults.KeyLength
|
||
|
|
}
|
||
|
|
return Params{
|
||
|
|
Iterations: params.Iterations,
|
||
|
|
KeyLength: params.KeyLength,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func prototypeHash(salt, passwordValue string) string {
|
||
|
|
sum := sha256.Sum256([]byte(salt + ":" + passwordValue))
|
||
|
|
return base64.StdEncoding.EncodeToString(sum[:])
|
||
|
|
}
|