Files
ntfywui/internal/security/pbkdf2.go
2026-01-12 13:51:52 +01:00

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
}