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)