Skip to content

Defense-in-Depth Credential Security in Rust

How we implemented AES-256-GCM encryption with HKDF key derivation for secure credential storage, including memory safety with zeroize.

The Threat Model

Trading bots hold sensitive credentials: exchange API keys, private keys for signing, and secrets. If an attacker gains read access to the system, they shouldn't be able to extract usable credentials.

Our defense-in-depth strategy:

  1. Encryption at rest - Credentials encrypted with AES-256-GCM
  2. Key separation - Per-user derived keys via HKDF
  3. Memory safety - Sensitive data zeroized on drop
  4. Tamper detection - GCM authentication tag prevents modification

Key Hierarchy

Master Key (from AWS Secrets Manager)
    └── User Key (HKDF derived with user_id as info)
            └── Credential (encrypted with user key)

The master key never encrypts data directly. HKDF derives user-specific keys, so compromising one user's credentials doesn't affect others.

Implementation

Key Derivation

We use HKDF-SHA256 for key derivation:

fn derive_user_key(&self, user_id: &str) -> Result<DerivedKey, CredentialError> {
    let hk = Hkdf::<Sha256>::new(Some(&self.salt), &self.master_key.0);

    let mut okm = [0u8; KEY_SIZE];
    hk.expand(user_id.as_bytes(), &mut okm)?;

    Ok(DerivedKey(okm))
}

The salt is random per store instance. Combined with user_id in the info parameter, this ensures each user gets a unique encryption key.

Encryption

AES-256-GCM provides authenticated encryption:

pub fn encrypt(&self, user_id: &str, plaintext: &[u8]) -> Result<EncryptedCredential, CredentialError> {
    let user_key = self.derive_user_key(user_id)?;
    let cipher = Aes256Gcm::new_from_slice(&user_key.0)?;

    // Random nonce per encryption
    let mut nonce_bytes = [0u8; NONCE_SIZE];
    OsRng.fill_bytes(&mut nonce_bytes);
    let nonce = Nonce::from_slice(&nonce_bytes);

    let ciphertext = cipher.encrypt(nonce, plaintext)?;

    Ok(EncryptedCredential { nonce: nonce_bytes, ciphertext })
}

Each encryption uses a fresh random nonce. Even encrypting the same credential twice produces different ciphertext.

Memory Safety

The zeroize crate ensures sensitive data is wiped when no longer needed:

#[derive(Zeroize, ZeroizeOnDrop)]
struct DerivedKey([u8; KEY_SIZE]);

This prevents secrets from lingering in memory after use, reducing the window for memory-scanning attacks.

Verification

Our test suite validates security properties:

Test Property Verified
test_wrong_user_cannot_decrypt Key separation
test_tampered_ciphertext_fails GCM authentication
test_tampered_nonce_fails Nonce binding
test_different_salt_different_derived_key Salt uniqueness
test_same_plaintext_different_nonce Nonce randomness

Example: verifying that tampering fails authentication:

#[test]
fn test_tampered_ciphertext_fails() {
    let store = CredentialStore::with_salt(&test_master_key(), test_salt()).unwrap();
    let mut encrypted = store.encrypt("user1", b"secret").unwrap();

    // Tamper with ciphertext
    encrypted.ciphertext[0] ^= 0xFF;

    // Decryption should fail due to authentication
    let result = store.decrypt("user1", &encrypted);
    assert!(result.is_err());
}

Production Deployment

In production, the master key comes from AWS Secrets Manager:

resource "aws_secretsmanager_secret" "master_key" {
  name = "arbiter-master-encryption-key"
  recovery_window_in_days = 30
}

The ECS task role has permission to read this secret at startup. The key never touches disk on the application server.

Crate Selection

Crate Version Purpose
aes-gcm 0.10 AEAD encryption
hkdf 0.12 Key derivation
sha2 0.10 Hash for HKDF
zeroize 1.7 Memory clearing
rand 0.8 Nonce generation

All crates are from the RustCrypto project, which follows best practices for cryptographic implementations.

Lessons Learned

  1. Never roll your own crypto - We use audited, well-maintained crates
  2. Test tamper detection - GCM catches tampering, but only if you test it
  3. Key separation matters - HKDF ensures user compromise is isolated
  4. Memory matters - zeroize is cheap insurance against memory scanning

The credential store is a foundational security component. Getting it right before adding features was essential.