97 lines
2.3 KiB
Go
97 lines
2.3 KiB
Go
package security
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func pbkdf2SHA256(password, salt []byte, iter, keyLen int) []byte {
|
|
// PBKDF2 per RFC2898
|
|
hLen := 32 // sha256
|
|
numBlocks := (keyLen + hLen - 1) / hLen
|
|
var out []byte
|
|
for block := 1; block <= numBlocks; block++ {
|
|
t := pbkdf2F(password, salt, iter, block)
|
|
out = append(out, t...)
|
|
}
|
|
return out[:keyLen]
|
|
}
|
|
|
|
func pbkdf2F(password, salt []byte, iter, blockNum int) []byte {
|
|
// U1 = PRF(P, S || INT(blockNum))
|
|
// Uc = PRF(P, Uc-1)
|
|
// T = U1 XOR U2 XOR ... XOR Uiter
|
|
b := make([]byte, len(salt)+4)
|
|
copy(b, salt)
|
|
b[len(salt)+0] = byte(blockNum >> 24)
|
|
b[len(salt)+1] = byte(blockNum >> 16)
|
|
b[len(salt)+2] = byte(blockNum >> 8)
|
|
b[len(salt)+3] = byte(blockNum)
|
|
|
|
u := hmacSHA256(password, b)
|
|
t := make([]byte, len(u))
|
|
copy(t, u)
|
|
for i := 2; i <= iter; i++ {
|
|
u = hmacSHA256(password, u)
|
|
for j := range t {
|
|
t[j] ^= u[j]
|
|
}
|
|
}
|
|
return t
|
|
}
|
|
|
|
func hmacSHA256(key, msg []byte) []byte {
|
|
m := hmac.New(sha256.New, key)
|
|
m.Write(msg)
|
|
return m.Sum(nil)
|
|
}
|
|
|
|
func HashPasswordPBKDF2(password string, salt []byte, iter int) string {
|
|
key := pbkdf2SHA256([]byte(password), salt, iter, 32)
|
|
return fmt.Sprintf("pbkdf2_sha256$%d$%s$%s",
|
|
iter,
|
|
base64.RawURLEncoding.EncodeToString(salt),
|
|
base64.RawURLEncoding.EncodeToString(key),
|
|
)
|
|
}
|
|
|
|
func VerifyPasswordPBKDF2(password, encoded string) (bool, error) {
|
|
// Go's fmt scanning does not support "scanset" verbs like %[^$]. Parse explicitly.
|
|
parts := strings.Split(encoded, "$")
|
|
if len(parts) != 4 {
|
|
return false, fmt.Errorf("parse hash: expected 4 parts, got %d", len(parts))
|
|
}
|
|
algo := parts[0]
|
|
iter, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return false, fmt.Errorf("parse hash iter: %w", err)
|
|
}
|
|
saltB64 := parts[2]
|
|
keyB64 := parts[3]
|
|
if algo != "pbkdf2_sha256" {
|
|
return false, fmt.Errorf("unsupported algo %q", algo)
|
|
}
|
|
salt, err := base64.RawURLEncoding.DecodeString(saltB64)
|
|
if err != nil {
|
|
return false, fmt.Errorf("salt decode: %w", err)
|
|
}
|
|
want, err := base64.RawURLEncoding.DecodeString(keyB64)
|
|
if err != nil {
|
|
return false, fmt.Errorf("key decode: %w", err)
|
|
}
|
|
got := pbkdf2SHA256([]byte(password), salt, iter, len(want))
|
|
// constant-time compare
|
|
if len(got) != len(want) {
|
|
return false, nil
|
|
}
|
|
var diff byte
|
|
for i := range got {
|
|
diff |= got[i] ^ want[i]
|
|
}
|
|
return diff == 0, nil
|
|
}
|