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

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[:])
}