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:

  1. The encryption key is properly derived from the password using a secure key derivation function
  2. The encryption itself uses an authenticated encryption mode
  3. We use proper random number generation for cryptographic operations
  4. 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

  1. Password Strength: The admin password should be cryptographically random and at least 16 characters long.
  2. Memory Security: Consider using techniques to securely clear sensitive data from memory after use.
  3. Error Handling: The implementation returns errors rather than panicking, allowing proper error handling in production systems.
  4. 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.