mirror of
https://github.com/sonr-io/crypto.git
synced 2026-01-11 20:08:57 +00:00
214 lines
5.3 KiB
Go
214 lines
5.3 KiB
Go
// Package argon2 provides secure key derivation using Argon2id
|
|
package argon2
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/argon2"
|
|
)
|
|
|
|
// Config defines Argon2id parameters
|
|
type Config struct {
|
|
Time uint32 // Number of iterations
|
|
Memory uint32 // Memory in KB
|
|
Parallelism uint8 // Number of threads
|
|
SaltLength uint32 // Salt length in bytes
|
|
KeyLength uint32 // Output key length in bytes
|
|
}
|
|
|
|
// DefaultConfig returns secure default parameters
|
|
func DefaultConfig() *Config {
|
|
return &Config{
|
|
Time: 1,
|
|
Memory: 64 * 1024, // 64MB
|
|
Parallelism: 4,
|
|
SaltLength: 32,
|
|
KeyLength: 32,
|
|
}
|
|
}
|
|
|
|
// LightConfig returns lighter parameters for testing
|
|
func LightConfig() *Config {
|
|
return &Config{
|
|
Time: 1,
|
|
Memory: 16 * 1024, // 16MB
|
|
Parallelism: 2,
|
|
SaltLength: 16,
|
|
KeyLength: 32,
|
|
}
|
|
}
|
|
|
|
// HighSecurityConfig returns high-security parameters
|
|
func HighSecurityConfig() *Config {
|
|
return &Config{
|
|
Time: 3,
|
|
Memory: 128 * 1024, // 128MB
|
|
Parallelism: 4,
|
|
SaltLength: 32,
|
|
KeyLength: 32,
|
|
}
|
|
}
|
|
|
|
// KDF implements Argon2id key derivation
|
|
type KDF struct {
|
|
config *Config
|
|
}
|
|
|
|
// New creates a new Argon2id KDF with the given configuration
|
|
func New(config *Config) *KDF {
|
|
if config == nil {
|
|
config = DefaultConfig()
|
|
}
|
|
return &KDF{config: config}
|
|
}
|
|
|
|
// DeriveKey derives a key from password and salt
|
|
func (k *KDF) DeriveKey(password []byte, salt []byte) []byte {
|
|
return argon2.IDKey(
|
|
password,
|
|
salt,
|
|
k.config.Time,
|
|
k.config.Memory,
|
|
k.config.Parallelism,
|
|
k.config.KeyLength,
|
|
)
|
|
}
|
|
|
|
// GenerateSalt generates a cryptographically secure salt
|
|
func (k *KDF) GenerateSalt() ([]byte, error) {
|
|
salt := make([]byte, k.config.SaltLength)
|
|
if _, err := rand.Read(salt); err != nil {
|
|
return nil, fmt.Errorf("failed to generate salt: %w", err)
|
|
}
|
|
return salt, nil
|
|
}
|
|
|
|
// HashPassword generates a hash with embedded salt and parameters
|
|
func (k *KDF) HashPassword(password []byte) (string, error) {
|
|
salt, err := k.GenerateSalt()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
hash := k.DeriveKey(password, salt)
|
|
|
|
// Encode in PHC format: $argon2id$v=19$m=65536,t=1,p=4$salt$hash
|
|
encodedSalt := base64.RawStdEncoding.EncodeToString(salt)
|
|
encodedHash := base64.RawStdEncoding.EncodeToString(hash)
|
|
|
|
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
|
argon2.Version,
|
|
k.config.Memory,
|
|
k.config.Time,
|
|
k.config.Parallelism,
|
|
encodedSalt,
|
|
encodedHash,
|
|
), nil
|
|
}
|
|
|
|
// VerifyPassword verifies a password against a PHC-formatted hash
|
|
func VerifyPassword(password []byte, encodedHash string) (bool, error) {
|
|
params, salt, hash, err := decodeHash(encodedHash)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
kdf := &KDF{config: params}
|
|
derivedHash := kdf.DeriveKey(password, salt)
|
|
|
|
// Constant-time comparison
|
|
return subtle.ConstantTimeCompare(hash, derivedHash) == 1, nil
|
|
}
|
|
|
|
// decodeHash parses PHC-formatted Argon2id hash
|
|
func decodeHash(encodedHash string) (*Config, []byte, []byte, error) {
|
|
parts := strings.Split(encodedHash, "$")
|
|
if len(parts) != 6 {
|
|
return nil, nil, nil, fmt.Errorf("invalid hash format")
|
|
}
|
|
|
|
if parts[1] != "argon2id" {
|
|
return nil, nil, nil, fmt.Errorf("unsupported algorithm: %s", parts[1])
|
|
}
|
|
|
|
var version int
|
|
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("failed to parse version: %w", err)
|
|
}
|
|
|
|
if version != argon2.Version {
|
|
return nil, nil, nil, fmt.Errorf("unsupported Argon2 version: %d", version)
|
|
}
|
|
|
|
var memory, time uint32
|
|
var parallelism uint8
|
|
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &time, ¶llelism)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("failed to parse parameters: %w", err)
|
|
}
|
|
|
|
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("failed to decode salt: %w", err)
|
|
}
|
|
|
|
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("failed to decode hash: %w", err)
|
|
}
|
|
|
|
config := &Config{
|
|
Time: time,
|
|
Memory: memory,
|
|
Parallelism: parallelism,
|
|
SaltLength: uint32(len(salt)),
|
|
KeyLength: uint32(len(hash)),
|
|
}
|
|
|
|
return config, salt, hash, nil
|
|
}
|
|
|
|
// CompareHashes performs constant-time comparison of two hashes
|
|
func CompareHashes(hash1, hash2 []byte) bool {
|
|
return subtle.ConstantTimeCompare(hash1, hash2) == 1
|
|
}
|
|
|
|
// ValidateConfig validates Argon2id parameters
|
|
func ValidateConfig(config *Config) error {
|
|
if config.Time < 1 {
|
|
return fmt.Errorf("time must be at least 1")
|
|
}
|
|
if config.Memory < 8*1024 {
|
|
return fmt.Errorf("memory must be at least 8MB")
|
|
}
|
|
if config.Parallelism < 1 {
|
|
return fmt.Errorf("parallelism must be at least 1")
|
|
}
|
|
if config.SaltLength < 8 {
|
|
return fmt.Errorf("salt length must be at least 8 bytes")
|
|
}
|
|
if config.KeyLength < 16 {
|
|
return fmt.Errorf("key length must be at least 16 bytes")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EstimateTime estimates the time required for key derivation
|
|
func EstimateTime(config *Config, iterations int) string {
|
|
// This is a rough estimate - actual time depends on hardware
|
|
baseTime := float64(config.Time) * float64(config.Memory) / (64 * 1024)
|
|
totalTime := baseTime * float64(iterations)
|
|
|
|
if totalTime < 1 {
|
|
return fmt.Sprintf("%.2f ms", totalTime*1000)
|
|
} else if totalTime < 60 {
|
|
return fmt.Sprintf("%.2f s", totalTime)
|
|
}
|
|
return fmt.Sprintf("%.2f min", totalTime/60)
|
|
}
|