backend barebone done, big chunk of work done
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
// Package auth provides cryptographic core for authentication.
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
// argon2Memory, argon2Time, argon2Threads, argon2KeyLen, and saltLen are parameters
|
||||
// recommended by OWASP for Argon2id (memory=65536 KiB, time=3 iterations, parallelism=2).
|
||||
const (
|
||||
argon2Memory = 65536
|
||||
argon2Time = 3
|
||||
argon2Threads = 2
|
||||
argon2KeyLen = 32
|
||||
saltLen = 16
|
||||
)
|
||||
|
||||
// HashPassword generates an Argon2id hash of the provided password and returns it
|
||||
// in PHC string format: $argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>.
|
||||
func HashPassword(password string) (string, error) {
|
||||
salt := make([]byte, saltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", fmt.Errorf("auth: generate salt: %w", err)
|
||||
}
|
||||
hash := argon2.IDKey([]byte(password), salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
phc := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argon2Memory, argon2Time, argon2Threads, b64Salt, b64Hash)
|
||||
return phc, nil
|
||||
}
|
||||
|
||||
// VerifyPassword checks whether the provided password matches the given Argon2id PHC hash.
|
||||
// It returns (true, nil) on match, (false, nil) on mismatch, or (false, error) on invalid format.
|
||||
func VerifyPassword(phc, password string) (bool, error) {
|
||||
parts := strings.Split(phc, "$")
|
||||
if len(parts) != 6 || parts[1] != "argon2id" {
|
||||
return false, errors.New("auth: invalid hash format")
|
||||
}
|
||||
// Version
|
||||
vPart := parts[2]
|
||||
if !strings.HasPrefix(vPart, "v=") {
|
||||
return false, errors.New("auth: invalid version format")
|
||||
}
|
||||
v, err := strconv.Atoi(strings.TrimPrefix(vPart, "v="))
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: parse version: %w", err)
|
||||
}
|
||||
if v != argon2.Version {
|
||||
return false, errors.New("auth: incompatible version")
|
||||
}
|
||||
// Parameters
|
||||
mem, t, p := uint32(0), uint32(0), uint32(0)
|
||||
for _, kv := range strings.Split(parts[3], ",") {
|
||||
pair := strings.SplitN(kv, "=", 2)
|
||||
if len(pair) != 2 {
|
||||
return false, errors.New("auth: invalid parameter format")
|
||||
}
|
||||
key, val := pair[0], pair[1]
|
||||
switch key {
|
||||
case "m":
|
||||
x, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: parse memory: %w", err)
|
||||
}
|
||||
mem = uint32(x)
|
||||
case "t":
|
||||
x, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: parse time: %w", err)
|
||||
}
|
||||
t = uint32(x)
|
||||
case "p":
|
||||
x, err := strconv.ParseUint(val, 10, 32)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: parse threads: %w", err)
|
||||
}
|
||||
p = uint32(x)
|
||||
default:
|
||||
return false, fmt.Errorf("auth: unknown parameter %q", key)
|
||||
}
|
||||
}
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: decode salt: %w", err)
|
||||
}
|
||||
expected, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("auth: decode hash: %w", err)
|
||||
}
|
||||
computed := argon2.IDKey([]byte(password), salt, t, mem, uint8(p), uint32(len(expected)))
|
||||
if subtle.ConstantTimeCompare(computed, expected) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user