When building secure systems, we often need to protect sensitive cryptographic keys. A common pattern is encrypting a root key using an admin password. This post will walk through implementing this pattern securely in Go using industry-standard cryptographic practices.
The Problem
Imagine you have a system with a root key that needs to be stored securely. You want to encrypt this key using an admin password (like a secure 16-character random string) so that only someone with the admin password can access the root key. This is a common requirement in key management systems, HSMs, and other security-critical applications.
Security Requirements
For this implementation, we need to ensure:
- The encryption key is properly derived from the password using a secure key derivation function
- The encryption itself uses an authenticated encryption mode
- We use proper random number generation for cryptographic operations
- Protection against rainbow table attacks through salting
Implementation
Here’s a secure implementation using Go’s crypto packages:
package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"golang.org/x/crypto/pbkdf2"
"io"
)
const (
saltSize = 16
keySize = 32 // for AES-256
iterations = 480000
)
// EncryptRootKey encrypts a root key using an admin password
func EncryptRootKey(adminPassword string, rootKey []byte) (encryptedData []byte, err error) {
// Generate a random salt
salt := make([]byte, saltSize)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return nil, err
}
// Derive key from password using PBKDF2
key := pbkdf2.Key([]byte(adminPassword), salt, iterations, keySize, sha256.New)
// Create AES cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Create GCM mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Generate nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// Encrypt the root key
// Final format: salt + nonce + encrypted data
encryptedKey := gcm.Seal(nil, nonce, rootKey, nil)
encryptedData = make([]byte, len(salt)+len(nonce)+len(encryptedKey))
// Combine salt + nonce + encrypted data into single byte slice
copy(encryptedData[:saltSize], salt)
copy(encryptedData[saltSize:saltSize+len(nonce)], nonce)
copy(encryptedData[saltSize+len(nonce):], encryptedKey)
return encryptedData, nil
}
// DecryptRootKey decrypts a root key using an admin password
func DecryptRootKey(adminPassword string, encryptedData []byte) ([]byte, error) {
// Extract salt
salt := encryptedData[:saltSize]
// Derive key from password using PBKDF2
key := pbkdf2.Key([]byte(adminPassword), salt, iterations, keySize, sha256.New)
// Create AES cipher
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// Create GCM mode
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// Extract nonce and ciphertext
nonceSize := gcm.NonceSize()
nonce := encryptedData[saltSize : saltSize+nonceSize]
ciphertext := encryptedData[saltSize+nonceSize:]
// Decrypt the root key
return gcm.Open(nil, nonce, ciphertext, nil)
}
Understanding the Implementation
Let’s break down the key components:
1. Key Derivation
We use PBKDF2 (Password-Based Key Derivation Function 2) to derive an encryption key from the admin password. PBKDF2 applies a pseudorandom function (in this case, HMAC-SHA256) to the password along with a salt value over many iterations. This serves two purposes:
- Makes the key derivation computationally expensive, slowing down brute-force attacks
- With the salt, prevents rainbow table attacks
2. Encryption
For the actual encryption, we use AES-GCM (Galois/Counter Mode), which provides:
- Confidentiality through AES encryption
- Authentication through the GCM mode
- Protection against tampering
- Built-in nonce handling
3. Data Format
The encrypted output combines three pieces of data:
- Salt (16 bytes)
- Nonce (12 bytes for GCM)
- Encrypted data (original length + 16 bytes for GCM tag)
This format allows us to store everything needed for decryption in a single byte slice.
Usage Example
Here’s how to use these functions in your code:
func main() {
adminPassword := "your-16-char-secure-password"
rootKey := make([]byte, 32) // Your actual root key
rand.Read(rootKey) // Fill with random data for this example
// Encrypt the root key
encryptedData, err := EncryptRootKey(adminPassword, rootKey)
if err != nil {
log.Fatal(err)
}
// Later, decrypt the root key
decryptedKey, err := DecryptRootKey(adminPassword, encryptedData)
if err != nil {
log.Fatal(err)
}
}
Security Considerations
- Password Strength: The admin password should be cryptographically random and at least 16 characters long.
- Memory Security: Consider using techniques to securely clear sensitive data from memory after use.
- Error Handling: The implementation returns errors rather than panicking, allowing proper error handling in production systems.
- Key Size: We use AES-256 (32-byte key) for future-proofing, though AES-128 would also be secure for most current applications.
Conclusion
This implementation provides a secure way to encrypt root keys using admin passwords in Go. It uses well-tested cryptographic primitives and follows security best practices. Remember that cryptographic implementations should be regularly reviewed and updated as security standards evolve.
The code is designed to be straightforward to use while maintaining strong security properties. However, always have your cryptographic implementations reviewed by security experts before using them in production systems.