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 operations
const saltLength int = 32 // Length of salt in bytes for key derivation
const 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
Top comments (0)