use aes_gcm::{
aead::{Aead, KeyInit},
Aes256Gcm, Nonce,
};
use base64::prelude::*;
use rand::rngs::SysRng;
use rand::TryRng;
use serde::{Deserialize, Serialize};
use std::fmt;
use zeroize::Zeroize;
pub const CURRENT_KEY_VERSION: u32 = 2;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct EncryptedData {
pub key_version: u32,
pub salt: String,
pub iv: String,
pub data: String,
}
#[derive(Zeroize)]
#[zeroize(drop)]
pub struct EncryptionKey {
key_bytes: [u8; 32],
key_version: u32,
}
impl EncryptionKey {
#[cfg(test)]
fn new(key_bytes: [u8; 32], key_version: u32) -> Self {
Self {
key_bytes,
key_version,
}
}
pub fn from_derived_bytes(bytes: &[u8], key_version: u32) -> Self {
let mut key = [0u8; 32];
key.copy_from_slice(&bytes[..32]);
Self {
key_bytes: key,
key_version,
}
}
pub fn version(&self) -> u32 {
self.key_version
}
pub(crate) fn key_bytes(&self) -> &[u8; 32] {
&self.key_bytes
}
}
impl fmt::Debug for EncryptionKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EncryptionKey")
.field("key_version", &self.key_version)
.field("key_bytes", &"[REDACTED]")
.finish()
}
}
pub(crate) fn encrypt(
plaintext: &str,
key: &EncryptionKey,
) -> Result<EncryptedData, EncryptionError> {
let cipher = Aes256Gcm::new(key.key_bytes().into());
let mut iv_bytes = [0u8; 12];
SysRng
.try_fill_bytes(&mut iv_bytes)
.map_err(|e| EncryptionError::Encryption(format!("rng failure: {e}")))?;
let mut salt_bytes = [0u8; 32];
SysRng
.try_fill_bytes(&mut salt_bytes)
.map_err(|e| EncryptionError::Encryption(format!("rng failure: {e}")))?;
let ciphertext = cipher
.encrypt(&Nonce::from(iv_bytes), plaintext.as_bytes())
.map_err(|e| EncryptionError::Encryption(e.to_string()))?;
Ok(EncryptedData {
key_version: key.key_version,
salt: BASE64_STANDARD.encode(salt_bytes),
iv: BASE64_STANDARD.encode(iv_bytes),
data: BASE64_STANDARD.encode(&ciphertext),
})
}
pub(crate) fn decrypt(
encrypted: &EncryptedData,
key: &EncryptionKey,
) -> Result<String, EncryptionError> {
let cipher = Aes256Gcm::new(key.key_bytes().into());
let iv_bytes = BASE64_STANDARD
.decode(&encrypted.iv)
.map_err(|e| EncryptionError::Decoding(e.to_string()))?;
let nonce: Nonce<_> = iv_bytes
.as_slice()
.try_into()
.map_err(|_| EncryptionError::Decoding("invalid IV length".to_string()))?;
let ciphertext = BASE64_STANDARD
.decode(&encrypted.data)
.map_err(|e| EncryptionError::Decoding(e.to_string()))?;
let plaintext = cipher
.decrypt(&nonce, ciphertext.as_ref())
.map_err(|e| EncryptionError::Decryption(e.to_string()))?;
String::from_utf8(plaintext).map_err(|e| EncryptionError::Decryption(e.to_string()))
}
#[derive(Debug, thiserror::Error)]
pub enum EncryptionError {
#[error("encryption error: {0}")]
Encryption(String),
#[error("decryption error: {0}")]
Decryption(String),
#[error("base64 decoding error: {0}")]
Decoding(String),
#[error("key version mismatch: expected {expected}, got {actual}")]
KeyVersionMismatch { expected: u32, actual: u32 },
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_key() -> EncryptionKey {
let key_bytes = [42u8; 32];
EncryptionKey::new(key_bytes, CURRENT_KEY_VERSION)
}
#[test]
fn test_encrypt_decrypt_round_trip() {
let key = make_test_key();
let plaintext = "hello, world! this is a secret API key";
let encrypted = encrypt(plaintext, &key).unwrap();
let decrypted = decrypt(&encrypted, &key).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_encrypted_data_has_different_iv_each_time() {
let key = make_test_key();
let plaintext = "same input";
let encrypted1 = encrypt(plaintext, &key).unwrap();
let encrypted2 = encrypt(plaintext, &key).unwrap();
assert_ne!(encrypted1.iv, encrypted2.iv);
assert_ne!(encrypted1.data, encrypted2.data);
}
#[test]
fn test_encrypt_decrypt_with_key_version() {
let key = EncryptionKey::new([7u8; 32], 2);
let plaintext = "versioned encryption test";
let encrypted = encrypt(plaintext, &key).unwrap();
assert_eq!(encrypted.key_version, 2);
let decrypted = decrypt(&encrypted, &key).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_decrypt_with_wrong_key_fails() {
let key1 = EncryptionKey::new([1u8; 32], 1);
let key2 = EncryptionKey::new([2u8; 32], 1);
let encrypted = encrypt("secret stuff", &key1).unwrap();
let result = decrypt(&encrypted, &key2);
assert!(result.is_err());
}
#[test]
fn test_encryption_key_debug_redacts_key_bytes() {
let key = EncryptionKey::new([0xABu8; 32], 2);
let debug_output = format!("{:?}", key);
assert!(
debug_output.contains("[REDACTED]"),
"Debug must redact key_bytes, got: {debug_output}"
);
assert!(
!debug_output.contains("AB"),
"Debug must not leak key bytes, got: {debug_output}"
);
assert!(
debug_output.contains("key_version"),
"Debug must show key_version, got: {debug_output}"
);
}
#[test]
fn test_encryption_key_version_accessor() {
let key = EncryptionKey::new([0u8; 32], 7);
assert_eq!(key.version(), 7);
}
#[test]
fn test_encryption_key_key_bytes_accessor() {
let key = EncryptionKey::new([0x42u8; 32], 2);
assert_eq!(key.key_bytes(), &[0x42u8; 32]);
}
#[test]
fn test_encryption_key_from_derived_bytes_takes_first_32() {
let derived = [0xAAu8; 64];
let key = EncryptionKey::from_derived_bytes(&derived, 3);
assert_eq!(key.key_bytes(), &[0xAAu8; 32]);
assert_eq!(key.version(), 3);
}
}