use std::fmt;
use std::str::FromStr;
use aes_gcm::aead::generic_array::typenum::U12;
use aes_gcm::aead::rand_core::RngCore;
use aes_gcm::aead::{Aead, AeadCore, KeyInit, OsRng, Payload};
use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm, Nonce};
use zeroize::Zeroizing;
type Aes192Gcm = AesGcm<aes_gcm::aes::Aes192, U12>;
use crate::{Error, ErrorKind, Result};
#[derive(Clone, PartialEq, Eq)]
pub struct SensitiveBytes(Zeroizing<Box<[u8]>>);
impl SensitiveBytes {
pub fn new(bytes: impl Into<Box<[u8]>>) -> Self {
Self(Zeroizing::new(bytes.into()))
}
pub fn as_bytes(&self) -> &[u8] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for SensitiveBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{} bytes REDACTED]", self.0.len())
}
}
impl fmt::Display for SensitiveBytes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{} bytes REDACTED]", self.0.len())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AesKeySize {
#[default]
Bits128 = 128,
Bits192 = 192,
Bits256 = 256,
}
impl AesKeySize {
pub fn key_length(&self) -> usize {
match self {
Self::Bits128 => 16,
Self::Bits192 => 24,
Self::Bits256 => 32,
}
}
pub fn from_key_length(len: usize) -> Result<Self> {
match len {
16 => Ok(Self::Bits128),
24 => Ok(Self::Bits192),
32 => Ok(Self::Bits256),
_ => Err(Error::new(
ErrorKind::FeatureUnsupported,
format!("Unsupported data key length: {len} (must be 16, 24, or 32)"),
)),
}
}
}
impl FromStr for AesKeySize {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
match s {
"128" | "AES_GCM_128" | "AES128_GCM" => Ok(Self::Bits128),
"192" | "AES_GCM_192" | "AES192_GCM" => Ok(Self::Bits192),
"256" | "AES_GCM_256" | "AES256_GCM" => Ok(Self::Bits256),
_ => Err(Error::new(
ErrorKind::FeatureUnsupported,
format!("Unsupported AES key size: {s}"),
)),
}
}
}
pub struct SecureKey {
key: SensitiveBytes,
key_size: AesKeySize,
}
impl SecureKey {
pub fn new(key: &[u8]) -> Result<Self> {
let key_size = AesKeySize::from_key_length(key.len())?;
Ok(Self {
key: SensitiveBytes::new(key),
key_size,
})
}
pub fn generate(key_size: AesKeySize) -> Self {
let mut key = vec![0u8; key_size.key_length()];
OsRng.fill_bytes(&mut key);
Self {
key: SensitiveBytes::new(key),
key_size,
}
}
pub fn key_size(&self) -> AesKeySize {
self.key_size
}
pub fn as_bytes(&self) -> &[u8] {
self.key.as_bytes()
}
}
impl TryFrom<SensitiveBytes> for SecureKey {
type Error = Error;
fn try_from(key: SensitiveBytes) -> Result<Self> {
let key_size = AesKeySize::from_key_length(key.len())?;
Ok(Self { key, key_size })
}
}
pub struct AesGcmCipher {
key: SensitiveBytes,
key_size: AesKeySize,
}
impl AesGcmCipher {
pub const NONCE_LEN: usize = 12;
pub const TAG_LEN: usize = 16;
pub fn new(key: SecureKey) -> Self {
Self {
key: SensitiveBytes::new(key.as_bytes()),
key_size: key.key_size(),
}
}
pub fn encrypt(&self, plaintext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
match self.key_size {
AesKeySize::Bits128 => {
encrypt_aes_gcm::<Aes128Gcm>(self.key.as_bytes(), plaintext, aad)
}
AesKeySize::Bits192 => {
encrypt_aes_gcm::<Aes192Gcm>(self.key.as_bytes(), plaintext, aad)
}
AesKeySize::Bits256 => {
encrypt_aes_gcm::<Aes256Gcm>(self.key.as_bytes(), plaintext, aad)
}
}
}
pub fn decrypt(&self, ciphertext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>> {
if ciphertext.len() < Self::NONCE_LEN + Self::TAG_LEN {
return Err(Error::new(
ErrorKind::DataInvalid,
format!(
"Ciphertext too short: expected at least {} bytes, got {}",
Self::NONCE_LEN + Self::TAG_LEN,
ciphertext.len()
),
));
}
match self.key_size {
AesKeySize::Bits128 => {
decrypt_aes_gcm::<Aes128Gcm>(self.key.as_bytes(), ciphertext, aad)
}
AesKeySize::Bits192 => {
decrypt_aes_gcm::<Aes192Gcm>(self.key.as_bytes(), ciphertext, aad)
}
AesKeySize::Bits256 => {
decrypt_aes_gcm::<Aes256Gcm>(self.key.as_bytes(), ciphertext, aad)
}
}
}
}
fn encrypt_aes_gcm<C>(key_bytes: &[u8], plaintext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>>
where C: Aead + AeadCore + KeyInit {
let cipher = C::new_from_slice(key_bytes).map_err(|e| {
Error::new(ErrorKind::DataInvalid, "Invalid AES key").with_source(anyhow::anyhow!(e))
})?;
let nonce = C::generate_nonce(&mut OsRng);
let ciphertext = if let Some(aad) = aad {
cipher.encrypt(&nonce, Payload {
msg: plaintext,
aad,
})
} else {
cipher.encrypt(&nonce, plaintext.as_ref())
}
.map_err(|e| {
Error::new(ErrorKind::Unexpected, "AES-GCM encryption failed")
.with_source(anyhow::anyhow!(e))
})?;
let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
result.extend_from_slice(&nonce);
result.extend_from_slice(&ciphertext);
Ok(result)
}
fn decrypt_aes_gcm<C>(key_bytes: &[u8], ciphertext: &[u8], aad: Option<&[u8]>) -> Result<Vec<u8>>
where C: Aead + AeadCore + KeyInit {
let cipher = C::new_from_slice(key_bytes).map_err(|e| {
Error::new(ErrorKind::DataInvalid, "Invalid AES key").with_source(anyhow::anyhow!(e))
})?;
let nonce = Nonce::from_slice(&ciphertext[..AesGcmCipher::NONCE_LEN]);
let encrypted_data = &ciphertext[AesGcmCipher::NONCE_LEN..];
let plaintext = if let Some(aad) = aad {
cipher.decrypt(nonce, Payload {
msg: encrypted_data,
aad,
})
} else {
cipher.decrypt(nonce, encrypted_data)
}
.map_err(|e| {
Error::new(ErrorKind::Unexpected, "AES-GCM decryption failed")
.with_source(anyhow::anyhow!(e))
})?;
Ok(plaintext)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_aes_key_size() {
assert_eq!(AesKeySize::Bits128.key_length(), 16);
assert_eq!(AesKeySize::Bits192.key_length(), 24);
assert_eq!(AesKeySize::Bits256.key_length(), 32);
assert_eq!(
AesKeySize::from_key_length(16).unwrap(),
AesKeySize::Bits128
);
assert_eq!(
AesKeySize::from_key_length(24).unwrap(),
AesKeySize::Bits192
);
assert_eq!(
AesKeySize::from_key_length(32).unwrap(),
AesKeySize::Bits256
);
assert!(AesKeySize::from_key_length(8).is_err());
assert_eq!(AesKeySize::from_str("128").unwrap(), AesKeySize::Bits128);
assert_eq!(
AesKeySize::from_str("AES_GCM_128").unwrap(),
AesKeySize::Bits128
);
assert_eq!(
AesKeySize::from_str("AES_GCM_256").unwrap(),
AesKeySize::Bits256
);
assert!(AesKeySize::from_str("INVALID").is_err());
}
#[test]
fn test_secure_key() {
let key1 = SecureKey::generate(AesKeySize::Bits128);
assert_eq!(key1.as_bytes().len(), 16);
assert_eq!(key1.key_size(), AesKeySize::Bits128);
let valid_key = [0u8; 16];
assert!(SecureKey::new(valid_key.as_slice()).is_ok());
let invalid_key = [0u8; 33];
assert!(SecureKey::new(invalid_key.as_slice()).is_err());
}
#[test]
fn test_aes128_gcm_encryption_roundtrip() {
let key = SecureKey::generate(AesKeySize::Bits128);
let cipher = AesGcmCipher::new(key);
let plaintext = b"Hello, Iceberg encryption!";
let aad = b"additional authenticated data";
let ciphertext = cipher.encrypt(plaintext, None).unwrap();
assert!(ciphertext.len() > plaintext.len() + 12); assert_ne!(&ciphertext[12..], plaintext);
let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
assert_eq!(decrypted, plaintext);
let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
assert_eq!(decrypted, plaintext);
assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
}
#[test]
fn test_aes192_gcm_encryption_roundtrip() {
let key = SecureKey::generate(AesKeySize::Bits192);
let cipher = AesGcmCipher::new(key);
let plaintext = b"Hello, Iceberg encryption!";
let aad = b"additional authenticated data";
let ciphertext = cipher.encrypt(plaintext, None).unwrap();
let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
assert_eq!(decrypted, plaintext);
let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
assert_eq!(decrypted, plaintext);
assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
}
#[test]
fn test_aes256_gcm_encryption_roundtrip() {
let key = SecureKey::generate(AesKeySize::Bits256);
let cipher = AesGcmCipher::new(key);
let plaintext = b"Hello, Iceberg encryption!";
let aad = b"additional authenticated data";
let ciphertext = cipher.encrypt(plaintext, None).unwrap();
let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
assert_eq!(decrypted, plaintext);
let ciphertext = cipher.encrypt(plaintext, Some(aad)).unwrap();
let decrypted = cipher.decrypt(&ciphertext, Some(aad)).unwrap();
assert_eq!(decrypted, plaintext);
assert!(cipher.decrypt(&ciphertext, Some(b"wrong aad")).is_err());
}
#[test]
fn test_cross_key_size_incompatibility() {
let plaintext = b"Cross-key test";
let key128 = SecureKey::generate(AesKeySize::Bits128);
let key256 = SecureKey::generate(AesKeySize::Bits256);
let cipher128 = AesGcmCipher::new(key128);
let cipher256 = AesGcmCipher::new(key256);
let ciphertext = cipher128.encrypt(plaintext, None).unwrap();
assert!(cipher256.decrypt(&ciphertext, None).is_err());
}
#[test]
fn test_encryption_with_empty_plaintext() {
let key = SecureKey::generate(AesKeySize::Bits128);
let cipher = AesGcmCipher::new(key);
let plaintext = b"";
let ciphertext = cipher.encrypt(plaintext, None).unwrap();
assert_eq!(ciphertext.len(), 12 + 16);
let decrypted = cipher.decrypt(&ciphertext, None).unwrap();
assert_eq!(decrypted, plaintext);
}
#[test]
fn test_decryption_with_tampered_ciphertext() {
let key = SecureKey::generate(AesKeySize::Bits128);
let cipher = AesGcmCipher::new(key);
let plaintext = b"Sensitive data";
let mut ciphertext = cipher.encrypt(plaintext, None).unwrap();
if ciphertext.len() > 12 {
ciphertext[12] ^= 0xFF;
}
assert!(cipher.decrypt(&ciphertext, None).is_err());
}
#[test]
fn test_different_keys_produce_different_ciphertexts() {
let key1 = SecureKey::generate(AesKeySize::Bits128);
let key2 = SecureKey::generate(AesKeySize::Bits128);
let cipher1 = AesGcmCipher::new(key1);
let cipher2 = AesGcmCipher::new(key2);
let plaintext = b"Same plaintext";
let ciphertext1 = cipher1.encrypt(plaintext, None).unwrap();
let ciphertext2 = cipher2.encrypt(plaintext, None).unwrap();
assert_ne!(&ciphertext1[12..], &ciphertext2[12..]);
}
#[test]
fn test_ciphertext_format_java_compatible() {
let key = SecureKey::generate(AesKeySize::Bits128);
let cipher = AesGcmCipher::new(key);
let plaintext = b"Test data";
let ciphertext = cipher.encrypt(plaintext, None).unwrap();
assert_eq!(
ciphertext.len(),
12 + plaintext.len() + 16,
"Ciphertext should be nonce + plaintext + tag length"
);
let nonce = &ciphertext[..12];
assert_eq!(nonce.len(), 12, "Nonce should be 12 bytes");
let encrypted_with_tag = &ciphertext[12..];
assert_eq!(
encrypted_with_tag.len(),
plaintext.len() + 16,
"Encrypted portion should be plaintext length + 16-byte tag"
);
}
}