mod cipher;
mod config;
mod tag;
use crate::cipher::{Aes256GcmIv16, Cipher, IvLength};
use crate::tag::{TagDecoder, TagEncoder};
use aes_gcm::aead::{Generate, Payload};
use aes_gcm::{Aes256Gcm, Key};
use thiserror::Error;
use zeroize::Zeroize;
pub use crate::config::VaultConfig;
pub use zeroize::Zeroizing;
pub type Vault = VaultWithConfig<DefaultIvLength>;
pub type VaultIv16 = VaultWithConfig<IvLength16>;
pub type DefaultIvLength = IvLength12;
pub type IvLength12 = Aes256Gcm;
pub type IvLength16 = Aes256GcmIv16;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("Invalid key length")]
InvalidKeyLength,
#[error("Random number generator error")]
Rng,
#[error("AES-GCM encrypt error")]
Encrypt,
#[error("AES-GCM decrypt error")]
Decrypt,
#[error("Unsupported version")]
UnsupportedVersion,
#[error("Unsupported tag")]
UnsupportedTag,
#[error("Decrypted data is not valid UTF-8")]
Utf8,
}
pub fn generate_key() -> Result<Zeroizing<Vec<u8>>, Error> {
let mut key = Key::<Aes256Gcm>::try_generate().map_err(|_| Error::Rng)?;
let bytes = Zeroizing::new(key.to_vec());
key.as_mut_slice().zeroize();
Ok(bytes)
}
#[derive(Clone)]
pub struct VaultWithConfig<I: IvLength = DefaultIvLength> {
cipher: Cipher<I>,
config: VaultConfig,
}
impl<I: IvLength> VaultWithConfig<I> {
pub fn try_new(key: &[u8]) -> Result<Self, Error> {
Ok(Self {
cipher: Cipher::try_new(key).map_err(|_| Error::InvalidKeyLength)?,
config: VaultConfig::default(),
})
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.config.tag = tag.into();
self
}
pub fn with_aad(mut self, aad: impl Into<String>) -> Self {
self.config.aad = aad.into();
self
}
pub fn config(&self) -> &VaultConfig {
&self.config
}
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, Error> {
let iv = self.cipher.init_iv().map_err(|_| Error::Rng)?;
let aad = self.config.aad.as_bytes();
let ciphertext_with_tag = self
.cipher
.encrypt(
Payload {
msg: plaintext,
aad,
},
&iv,
)
.map_err(|_| Error::Encrypt)?;
let (ciphertext, ciphertag) =
ciphertext_with_tag.split_at(ciphertext_with_tag.len() - self.cipher.tag_length());
let encoded_tag = TagEncoder::encode(self.config.tag.as_bytes());
let mut encoded =
Vec::with_capacity(encoded_tag.len() + iv.len() + ciphertext_with_tag.len());
encoded.extend_from_slice(&encoded_tag); encoded.extend_from_slice(&iv);
encoded.extend_from_slice(ciphertag); encoded.extend_from_slice(ciphertext);
Ok(encoded)
}
pub fn decrypt(&self, ciphertext: &[u8]) -> Result<String, Error> {
let (tag, remainder) =
TagDecoder::decode(ciphertext).map_err(|_| Error::UnsupportedVersion)?;
if tag != self.config.tag.as_bytes() {
return Err(Error::UnsupportedTag);
}
let aad = self.config.aad.as_bytes();
let plaintext = self
.cipher
.decrypt(remainder, aad)
.map_err(|_| Error::Decrypt)?;
String::from_utf8(plaintext).map_err(|err| {
err.into_bytes().zeroize();
Error::Utf8
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const PLAINTEXT: &[u8] = b"Hello, world";
#[test]
fn works_with_default_config() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let encrypted = vault.encrypt(PLAINTEXT).expect("encryption failed");
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted.as_bytes(), PLAINTEXT);
}
#[test]
fn works_with_16_byte_iv_length() {
let key = generate_key().expect("key generation failed");
let vault = VaultIv16::try_new(&key).expect("vault creation failed");
let encrypted = vault.encrypt(PLAINTEXT).expect("encryption failed");
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted.as_bytes(), PLAINTEXT);
}
#[test]
fn works_with_custom_tag() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key)
.expect("vault creation failed")
.with_tag("Custom.Tag.V1");
let encrypted = vault.encrypt(PLAINTEXT).expect("encryption failed");
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted.as_bytes(), PLAINTEXT);
}
#[test]
fn works_with_custom_aad() {
let key = generate_key().expect("key generation failed");
let vault: VaultWithConfig<DefaultIvLength> = VaultWithConfig::try_new(&key)
.expect("vault creation failed")
.with_aad("Custom AAD");
let encrypted = vault.encrypt(PLAINTEXT).expect("encryption failed");
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted.as_bytes(), PLAINTEXT);
}
#[test]
fn works_with_empty_plaintext() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let encrypted = vault.encrypt(b"").expect("encryption failed");
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted, "");
}
#[test]
fn works_with_long_form_tlv_tag() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key)
.expect("vault creation failed")
.with_tag("X".repeat(200));
let encrypted = vault.encrypt(PLAINTEXT).expect("encryption failed");
assert_eq!(&encrypted[..3], &[0x01, 0x81, 200]);
let decrypted = vault.decrypt(&encrypted).expect("decryption failed");
assert_eq!(decrypted.as_bytes(), PLAINTEXT);
}
#[test]
fn vault_creation_fails_with_invalid_key_length() {
assert!(matches!(
Vault::try_new(&[0u8; 16]),
Err(Error::InvalidKeyLength)
));
}
#[test]
fn decryption_fails_with_wrong_tag() {
let key = generate_key().expect("key generation failed");
let vault_1 = Vault::try_new(&key)
.expect("vault creation failed")
.with_tag("Tag.V1");
let vault_2 = Vault::try_new(&key)
.expect("vault creation failed")
.with_tag("Tag.V2");
let encrypted = vault_1.encrypt(PLAINTEXT).expect("encryption failed");
assert!(vault_2.decrypt(&encrypted).is_err());
}
#[test]
fn decryption_fails_with_wrong_aad() {
let key = generate_key().expect("key generation failed");
let vault_1 = Vault::try_new(&key)
.expect("vault creation failed")
.with_aad("AAD.V1");
let vault_2 = Vault::try_new(&key)
.expect("vault creation failed")
.with_aad("AAD.V2");
let encrypted = vault_1.encrypt(PLAINTEXT).expect("encryption failed");
assert!(vault_2.decrypt(&encrypted).is_err());
}
#[test]
fn decryption_fails_with_invalid_ciphertext() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let invalid_ciphertext = b"Invalid data";
assert!(vault.decrypt(invalid_ciphertext).is_err());
}
#[test]
fn decryption_fails_with_non_utf8_plaintext() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let encrypted = vault.encrypt(&[0xff, 0xfe]).expect("encryption failed");
assert!(matches!(vault.decrypt(&encrypted), Err(Error::Utf8)));
}
#[test]
fn decryption_fails_with_truncated_ciphertext() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let truncated_ciphertext = hex::decode("010a4145532e47434d2e56310000000000").unwrap();
assert!(vault.decrypt(&truncated_ciphertext).is_err());
}
#[test]
fn decryption_fails_with_oversized_tlv_length() {
let key = generate_key().expect("key generation failed");
let vault = Vault::try_new(&key).expect("vault creation failed");
let mut oversized = vec![0x01, 0x89];
oversized.extend_from_slice(&[0xff; 9]);
oversized.extend_from_slice(&[0x00; 40]);
assert!(vault.decrypt(&oversized).is_err());
}
const FIXTURE_KEY_HEX: &str =
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
fn fixture_key() -> Vec<u8> {
hex::decode(FIXTURE_KEY_HEX).unwrap()
}
#[test]
fn decrypts_v0_2_0_ciphertext_with_default_config() {
let ciphertext = hex::decode(
"010a4145532e47434d2e5631820e66c5bccdbb3ac076aa21c1ab5c86729463ae\
27600fe52aeb913ff95f2f35c6f598171f00542c",
)
.unwrap();
let vault = Vault::try_new(&fixture_key()).expect("vault creation failed");
assert_eq!(vault.decrypt(&ciphertext).unwrap().as_bytes(), PLAINTEXT);
}
#[test]
fn decrypts_v0_2_0_ciphertext_with_16_byte_iv() {
let ciphertext = hex::decode(
"010a4145532e47434d2e5631e886d5a9d84ecebf632f94d6cfd499a4e1d897b9\
1a06912d0355c47ec8a8249682ededba217bd03c88494ca3",
)
.unwrap();
let vault = VaultIv16::try_new(&fixture_key()).expect("vault creation failed");
assert_eq!(vault.decrypt(&ciphertext).unwrap().as_bytes(), PLAINTEXT);
}
#[test]
fn decrypts_v0_2_0_ciphertext_with_custom_tag_and_aad() {
let ciphertext = hex::decode(
"010d437573746f6d2e5461672e563149a59cacd4976401ecae30eff3c9c95f78\
c8d4d5362d4686209703c220a218eb91fd7eece81fdf4d",
)
.unwrap();
let vault = Vault::try_new(&fixture_key())
.expect("vault creation failed")
.with_tag("Custom.Tag.V1")
.with_aad("Custom AAD");
assert_eq!(vault.decrypt(&ciphertext).unwrap().as_bytes(), PLAINTEXT);
}
}