99 lines
2.7 KiB
Go
99 lines
2.7 KiB
Go
// FILE: src/internal/scram/credential.go
|
|
package scram
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
// Credential stores SCRAM authentication data
|
|
type Credential struct {
|
|
Username string
|
|
Salt []byte // 16+ bytes
|
|
ArgonTime uint32 // e.g., 3
|
|
ArgonMemory uint32 // e.g., 64*1024 KiB
|
|
ArgonThreads uint8 // e.g., 4
|
|
StoredKey []byte // SHA256(ClientKey)
|
|
ServerKey []byte // For server auth
|
|
PHCHash string
|
|
}
|
|
|
|
// DeriveCredential creates SCRAM credential from password
|
|
func DeriveCredential(username, password string, salt []byte, time, memory uint32, threads uint8) (*Credential, error) {
|
|
if len(salt) < 16 {
|
|
return nil, fmt.Errorf("salt must be at least 16 bytes")
|
|
}
|
|
|
|
// Derive salted password using Argon2id
|
|
saltedPassword := argon2.IDKey([]byte(password), salt, time, memory, threads, 32)
|
|
|
|
// Derive keys
|
|
clientKey := computeHMAC(saltedPassword, []byte("Client Key"))
|
|
serverKey := computeHMAC(saltedPassword, []byte("Server Key"))
|
|
storedKey := sha256.Sum256(clientKey)
|
|
|
|
return &Credential{
|
|
Username: username,
|
|
Salt: salt,
|
|
ArgonTime: time,
|
|
ArgonMemory: memory,
|
|
ArgonThreads: threads,
|
|
StoredKey: storedKey[:],
|
|
ServerKey: serverKey,
|
|
}, nil
|
|
}
|
|
|
|
// MigrateFromPHC converts existing Argon2 PHC hash to SCRAM credential
|
|
func MigrateFromPHC(username, password, phcHash string) (*Credential, error) {
|
|
// Parse PHC: $argon2id$v=19$m=65536,t=3,p=4$salt$hash
|
|
parts := strings.Split(phcHash, "$")
|
|
if len(parts) != 6 || parts[1] != "argon2id" {
|
|
return nil, fmt.Errorf("invalid PHC format")
|
|
}
|
|
|
|
var memory, time uint32
|
|
var threads uint8
|
|
fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, &threads)
|
|
|
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid salt encoding: %w", err)
|
|
}
|
|
|
|
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid hash encoding: %w", err)
|
|
}
|
|
|
|
// Verify password matches
|
|
computedHash := argon2.IDKey([]byte(password), salt, time, memory, threads, uint32(len(expectedHash)))
|
|
if subtle.ConstantTimeCompare(computedHash, expectedHash) != 1 {
|
|
return nil, fmt.Errorf("password verification failed")
|
|
}
|
|
|
|
// Now derive SCRAM credential
|
|
return DeriveCredential(username, password, salt, time, memory, threads)
|
|
}
|
|
|
|
func computeHMAC(key, message []byte) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
mac.Write(message)
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
func xorBytes(a, b []byte) []byte {
|
|
if len(a) != len(b) {
|
|
panic("xor length mismatch")
|
|
}
|
|
result := make([]byte, len(a))
|
|
for i := range a {
|
|
result[i] = a[i] ^ b[i]
|
|
}
|
|
return result
|
|
} |