DEV Community

Cover image for Secret Value Manager in Go
Isaac FEI
Isaac FEI

Posted on • Originally published at isaacfei.com

Secret Value Manager in Go

Core Components

The encryption system consists of three main components:

  1. Passphrase management (digesting and verification)
  2. Secret encryption
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

When verifying a passphrase, we:

  1. Split the stored digest to get the original key and salt
  2. Derive a new key using the same salt
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Split the encrypted value into its components
  2. Decode from hex
  3. Derive the same key using the stored salt
  4. 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
}
Enter fullscreen mode Exit fullscreen mode

Security Properties

This implementation provides:

  1. Key Security

    • Unique key for each secret (different salts)
    • High-cost key derivation (100,000 iterations)
    • No plaintext passphrase storage
  2. Encryption Security

    • Authenticated encryption (GCM mode)
    • No IV reuse (random generation)
    • No padding attacks (GCM is streamlike)
  3. Storage Security

    • Safe encoding (hex)
    • Clear component separation
    • Integrity protection

References

Top comments (0)