Write generated configs 0600, treat new keys as raw AES-256, keep PBKDF2 for unprefixed material, and cache derivation at startup.
This commit is contained in:
@@ -6,9 +6,11 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -24,22 +26,69 @@ const (
|
||||
// Replay protection parameters
|
||||
MaxPacketAge = 5 * time.Minute // Maximum age for UDP packets
|
||||
NonceWindow = 1000 // Number of nonces to track for replay protection
|
||||
|
||||
// RawKeyPrefix marks a hex-encoded 32-byte AES key that must not go through PBKDF2.
|
||||
RawKeyPrefix = "raw:"
|
||||
RawKeyHexLen = 64 // 32 bytes
|
||||
)
|
||||
|
||||
// DeriveKey derives an encryption key from a password using PBKDF2
|
||||
// deriveCache memoizes PBKDF2 so handshake/UDP paths never pay 100k iterations twice.
|
||||
var deriveCache sync.Map // map[string][]byte
|
||||
|
||||
// GenerateRawKey returns a raw: prefixed hex encoding of 32 random bytes.
|
||||
// Paste the entire string into encryption_key; it is used as an AES-256 key (no PBKDF2).
|
||||
func GenerateRawKey() (string, error) {
|
||||
key := make([]byte, PBKDF2KeyLength)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return "", fmt.Errorf("failed to generate random key: %v", err)
|
||||
}
|
||||
return RawKeyPrefix + hex.EncodeToString(key), nil
|
||||
}
|
||||
|
||||
// IsRawKey reports whether material is a raw 256-bit key (raw: + 64 hex chars).
|
||||
func IsRawKey(material string) bool {
|
||||
return strings.HasPrefix(material, RawKeyPrefix)
|
||||
}
|
||||
|
||||
// ResolveKey returns a 32-byte AES key from config material.
|
||||
//
|
||||
// - raw:<64 hex>: hex-decode, no PBKDF2 (new --generate-key / generated configs)
|
||||
// - anything else, including legacy unprefixed 64-hex from older --generate-key:
|
||||
// PBKDF2 with the historical password-derived salt (existing configs keep working)
|
||||
func ResolveKey(material string) ([]byte, error) {
|
||||
if strings.HasPrefix(material, RawKeyPrefix) {
|
||||
hexStr := material[len(RawKeyPrefix):]
|
||||
key, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid raw encryption key: %w", err)
|
||||
}
|
||||
if len(key) != PBKDF2KeyLength {
|
||||
return nil, fmt.Errorf("raw encryption key must be %d bytes, got %d", PBKDF2KeyLength, len(key))
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
return DeriveKey(material), nil
|
||||
}
|
||||
|
||||
// DeriveKey derives an encryption key from a password using PBKDF2.
|
||||
// The first call for a given password runs 100k iterations; later calls return the cached key.
|
||||
func DeriveKey(password string) []byte {
|
||||
// Use a deterministic salt derived from the password hash for consistent key derivation
|
||||
// This ensures the same password always produces the same key while avoiding rainbow tables
|
||||
if v, ok := deriveCache.Load(password); ok {
|
||||
return v.([]byte)
|
||||
}
|
||||
|
||||
// Historical salt is SHA256(password)[:16]. That is not a random salt; keep it only
|
||||
// so existing passphrase and unprefixed hex configs still derive the same key.
|
||||
hasher := sha256.New()
|
||||
hasher.Write([]byte(password))
|
||||
passwordHash := hasher.Sum(nil)
|
||||
|
||||
// Create a deterministic salt from the password hash
|
||||
salt := make([]byte, PBKDF2SaltLength)
|
||||
copy(salt, passwordHash[:PBKDF2SaltLength])
|
||||
|
||||
key := pbkdf2.Key([]byte(password), salt, PBKDF2Iterations, PBKDF2KeyLength, sha256.New)
|
||||
return key
|
||||
actual, _ := deriveCache.LoadOrStore(password, key)
|
||||
return actual.([]byte)
|
||||
}
|
||||
|
||||
// DeriveKeyWithSalt derives an encryption key from a password using PBKDF2 with a custom salt
|
||||
@@ -176,6 +225,21 @@ func ConstantTimeCompare(a, b []byte) bool {
|
||||
|
||||
// ValidateEncryptionKey validates that an encryption key meets security requirements
|
||||
func ValidateEncryptionKey(key string) error {
|
||||
if strings.HasPrefix(key, RawKeyPrefix) {
|
||||
hexStr := key[len(RawKeyPrefix):]
|
||||
if len(hexStr) != RawKeyHexLen {
|
||||
return fmt.Errorf("raw encryption key must be %d hex characters (32 bytes), got %d", RawKeyHexLen, len(hexStr))
|
||||
}
|
||||
decoded, err := hex.DecodeString(hexStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("raw encryption key is not valid hex: %w", err)
|
||||
}
|
||||
if len(decoded) != PBKDF2KeyLength {
|
||||
return fmt.Errorf("raw encryption key must decode to %d bytes", PBKDF2KeyLength)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(key) < 32 {
|
||||
return fmt.Errorf("encryption key must be at least 32 characters long")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user