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:
- Encryption at rest - Credentials encrypted with AES-256-GCM
- Key separation - Per-user derived keys via HKDF
- Memory safety - Sensitive data zeroized on drop
- 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:
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¶
- Never roll your own crypto - We use audited, well-maintained crates
- Test tamper detection - GCM catches tampering, but only if you test it
- Key separation matters - HKDF ensures user compromise is isolated
- Memory matters -
zeroizeis cheap insurance against memory scanning
The credential store is a foundational security component. Getting it right before adding features was essential.