Secret Value Manager in Go
Jan 3, 2025
GoCrypto

Core Components
The encryption system consists of three main components:
- Passphrase management (digesting and verification)
- Secret encryption
- Secret decryption
Passphrase Management
The master passphrase is never stored directly. Instead, we store a digest created using PBKDF2:
// Constants for cryptographic operationsconst saltLength int = 32 // Length of salt in bytes for key derivationconst secretKeyLength int = 32 // Length of derived key (256 bits for AES-256)const separator string = "-" // Separator for components in stored values
func DigestPassphrase(passphrase string) string { // Derive a key and get a salt (nil means generate new salt) key, salt := deriveKey(passphrase, nil)
// Store as: <derived_key>-<salt> // Both components are hex-encoded for safe storage digestedPassphrase := strings.Join( []string{hex.EncodeToString(key), hex.EncodeToString(salt)}, separator, ) return digestedPassphrase}
The key derivation function uses PBKDF2 with these specific parameters:
- SHA-256 as the hash function
- 100,000 iterations
- 32-byte output key
- 32-byte random salt
func deriveKey(passphrase string, salt []byte) ([]byte, []byte) { // Generate a random salt if none provided if salt == nil { salt = make([]byte, saltLength) // Use crypto/rand for cryptographically secure random numbers rand.Read(salt) }
// Use PBKDF2 to derive a key from the passphrase // - passphrase: the input secret // - salt: prevents rainbow table attacks // - 100000: iteration count for computational cost // - secretKeyLength: final key length (32 bytes = 256 bits) // - sha256.New: use SHA-256 as the hash function key := pbkdf2.Key( []byte(passphrase), salt, 100000, secretKeyLength, sha256.New, ) return key, salt}
When verifying a passphrase, we:
- Split the stored digest to get the original key and salt
- Derive a new key using the same salt
- Compare the keys
func VerifyPassphrase(passphrase string, digestedPassphrase string) bool { // Split stored value into key and salt components parts := strings.Split(digestedPassphrase, separator) if len(parts) != 2 { return false // Invalid format, verification fails }
// Extract the stored key and salt groundTruthKey := parts[0] // The original derived key salt, _ := hex.DecodeString(parts[1]) // Decode stored salt from hex
// Derive a new key using the same salt and passphrase key, _ := deriveKey(passphrase, salt) // Compare the newly derived key with the stored one return hex.EncodeToString(key) == groundTruthKey}
Secret Encryption
Each secret is encrypted using AES-GCM. The encrypted value format is:
<ciphertext>-<salt>-<iv>
const ivLength int = 12 // 12 bytes is optimal for GCM mode (96 bits)
func Encrypt(passphrase string, value string) (string, error) { // For each encryption: // 1. Generate a new key using a fresh salt // 2. Generate a new IV (nonce) for GCM mode key, salt := deriveKey(passphrase, nil) iv := make([]byte, ivLength) rand.Read(iv) // Cryptographically secure random IV
// Create AES cipher in GCM mode: // 1. Create AES cipher with our derived key // 2. Wrap it in GCM mode for authenticated encryption blockCipher, _ := aes.NewCipher(key) gcmCipher, _ := cipher.NewGCM(blockCipher)
// Encrypt and authenticate in one step // - nil: no additional authenticated data // - iv: nonce for GCM mode // - value: the secret to encrypt ciphertext := gcmCipher.Seal(nil, iv, []byte(value), nil)
// Store as: <ciphertext>-<salt>-<iv> // All components are hex-encoded for safe storage encryptedValue := strings.Join( []string{ hex.EncodeToString(ciphertext), hex.EncodeToString(salt), hex.EncodeToString(iv), }, separator, ) return encryptedValue, nil}
Important aspects of the encryption:
- Each secret gets a unique salt for key derivation
- Each encryption uses a random 12-byte IV
- GCM mode provides authenticated encryption
- All components are hex-encoded for safe storage
Secret Decryption
To decrypt a secret, we:
- Split the encrypted value into its components
- Decode from hex
- Derive the same key using the stored salt
- Decrypt and verify using AES-GCM
func Decrypt(passphrase string, encryptedValue string) (string, error) { // Split stored value into its three components // Format: <ciphertext>-<salt>-<iv> parts := strings.Split(encryptedValue, separator) if len(parts) != 3 { return "", errors.New("invalid encrypted value format") }
// Decode all components from their hex representation ciphertext, _ := hex.DecodeString(parts[0]) // The encrypted secret salt, _ := hex.DecodeString(parts[1]) // Salt used for key derivation iv, _ := hex.DecodeString(parts[2]) // IV used for encryption
// Derive the same key using the stored salt // This will give us the same key used for encryption key, _ := deriveKey(passphrase, salt)
// Set up decryption: // 1. Create AES cipher with the derived key // 2. Wrap in GCM mode for authenticated decryption blockCipher, _ := aes.NewCipher(key) gcmCipher, _ := cipher.NewGCM(blockCipher)
// Decrypt and verify authenticity in one step // If authentication fails, returns an error decryptedValue, err := gcmCipher.Open(nil, iv, ciphertext, nil) if err != nil { return "", err }
return string(decryptedValue), nil}
Security Properties
This implementation provides:
-
Key Security
- Unique key for each secret (different salts)
- High-cost key derivation (100,000 iterations)
- No plaintext passphrase storage
-
Encryption Security
- Authenticated encryption (GCM mode)
- No IV reuse (random generation)
- No padding attacks (GCM is streamlike)
-
Storage Security
- Safe encoding (hex)
- Clear component separation
- Integrity protection
Comments 💬