103 lines
2.6 KiB
Go
103 lines
2.6 KiB
Go
package crypto
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
type Argon2Params struct {
|
|
Memory uint32 // KiB
|
|
Time uint32
|
|
Threads uint8
|
|
SaltLen uint32
|
|
KeyLen uint32
|
|
}
|
|
|
|
var DefaultArgon2 = Argon2Params{
|
|
Memory: 7168, // 7 MiB
|
|
Time: 5,
|
|
Threads: 1,
|
|
SaltLen: 16,
|
|
KeyLen: 32,
|
|
}
|
|
|
|
// Hash returns PHC string: $argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>
|
|
func Hash(password string, p Argon2Params) (string, error) {
|
|
salt := make([]byte, p.SaltLen)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return "", err
|
|
}
|
|
sum := argon2.IDKey([]byte(password), salt, p.Time, p.Memory, p.Threads, p.KeyLen)
|
|
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
|
|
p.Memory, p.Time, p.Threads,
|
|
base64.RawStdEncoding.EncodeToString(salt),
|
|
base64.RawStdEncoding.EncodeToString(sum),
|
|
), nil
|
|
}
|
|
|
|
func Verify(password, phc string) (bool, error) {
|
|
parts := strings.Split(phc, "$")
|
|
// Expect: ["", "argon2id", "v=19", "m=...,t=...,p=...", "<saltB64>", "<hashB64>"]
|
|
if len(parts) != 6 || parts[1] != "argon2id" || parts[2] != "v=19" {
|
|
return false, errors.New("invalid PHC header")
|
|
}
|
|
|
|
// parse params
|
|
var mem, iters uint64
|
|
var threads uint64
|
|
for _, kv := range strings.Split(parts[3], ",") {
|
|
if strings.HasPrefix(kv, "m=") {
|
|
v := strings.TrimPrefix(kv, "m=")
|
|
x, err := strconv.ParseUint(v, 10, 32)
|
|
if err != nil {
|
|
return false, fmt.Errorf("mem: %w", err)
|
|
}
|
|
mem = x
|
|
} else if strings.HasPrefix(kv, "t=") {
|
|
v := strings.TrimPrefix(kv, "t=")
|
|
x, err := strconv.ParseUint(v, 10, 32)
|
|
if err != nil {
|
|
return false, fmt.Errorf("time: %w", err)
|
|
}
|
|
iters = x
|
|
} else if strings.HasPrefix(kv, "p=") {
|
|
v := strings.TrimPrefix(kv, "p=")
|
|
x, err := strconv.ParseUint(v, 10, 8)
|
|
if err != nil {
|
|
return false, fmt.Errorf("threads: %w", err)
|
|
}
|
|
threads = x
|
|
}
|
|
}
|
|
if mem == 0 || iters == 0 || threads == 0 {
|
|
return false, errors.New("invalid PHC params")
|
|
}
|
|
|
|
salt, err := b64DecodeFlex(parts[4])
|
|
if err != nil {
|
|
return false, fmt.Errorf("salt decode: %w", err)
|
|
}
|
|
|
|
want, err := b64DecodeFlex(parts[5])
|
|
if err != nil {
|
|
return false, fmt.Errorf("hash decode: %w", err)
|
|
}
|
|
|
|
got := argon2.IDKey([]byte(password), salt, uint32(iters), uint32(mem), uint8(threads), uint32(len(want)))
|
|
return subtle.ConstantTimeCompare(got, want) == 1, nil
|
|
}
|
|
|
|
func b64DecodeFlex(s string) ([]byte, error) {
|
|
if b, err := base64.RawStdEncoding.DecodeString(s); err == nil {
|
|
return b, nil
|
|
}
|
|
return base64.StdEncoding.DecodeString(s) // padded fallback
|
|
}
|