use crate::WireError;
use crate::aad::Aad;
use crate::leaf::{EncryptedLeaf, LeafType, Plaintext};
use aes_gcm::AesGcm;
use aes_gcm::aead::{Aead, KeyInit, Payload};
use std::collections::HashMap;
use zeroize::Zeroizing;
type SopsGcm32 = AesGcm<aes::Aes256, aes_gcm::aead::consts::U32>;
#[derive(Clone)]
pub struct DataKey(Zeroizing<[u8; 32]>);
impl DataKey {
pub const LEN: usize = 32;
pub fn generate() -> Result<Self, WireError> {
let mut k = [0u8; Self::LEN];
getrandom::getrandom(&mut k).map_err(|e| WireError::Randomness(e.to_string()))?;
Ok(Self(Zeroizing::new(k)))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, WireError> {
if bytes.len() != Self::LEN {
return Err(WireError::DataKeyLength(bytes.len()));
}
let mut k = [0u8; Self::LEN];
k.copy_from_slice(bytes);
Ok(Self(Zeroizing::new(k)))
}
#[must_use]
pub fn expose(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Debug for DataKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("DataKey(*** 32 bytes)")
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Iv([u8; 32]);
impl Iv {
pub const LEN: usize = 32;
pub fn generate() -> Result<Self, WireError> {
let mut iv = [0u8; Self::LEN];
getrandom::getrandom(&mut iv).map_err(|e| WireError::Randomness(e.to_string()))?;
Ok(Self(iv))
}
#[must_use]
pub fn from_wire_exact(bytes: &[u8]) -> Option<Self> {
let arr: [u8; Self::LEN] = bytes.try_into().ok()?;
Some(Self(arr))
}
#[must_use]
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Debug for Iv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Iv({})", hex_lower(&self.0))
}
}
#[derive(Default)]
pub struct IvStash {
seen: HashMap<(LeafType, Vec<u8>, Vec<u8>), Iv>,
}
impl IvStash {
#[must_use]
pub fn new() -> Self {
Self::default()
}
fn key(plaintext: &Plaintext, aad: &Aad) -> (LeafType, Vec<u8>, Vec<u8>) {
(
plaintext.leaf_type(),
plaintext.expose().to_vec(),
aad.as_bytes().to_vec(),
)
}
pub fn remember(&mut self, plaintext: &Plaintext, aad: &Aad, iv: &[u8]) {
if let Some(iv) = Iv::from_wire_exact(iv) {
self.seen.insert(Self::key(plaintext, aad), iv);
}
}
#[must_use]
pub fn recall(&self, plaintext: &Plaintext, aad: &Aad) -> Option<Iv> {
self.seen.get(&Self::key(plaintext, aad)).cloned()
}
#[must_use]
pub fn len(&self) -> usize {
self.seen.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.seen.is_empty()
}
}
impl std::fmt::Debug for IvStash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "IvStash({} pairs)", self.seen.len())
}
}
pub fn encrypt_leaf(
key: &DataKey,
plaintext: &Plaintext,
aad: &Aad,
iv: Option<Iv>,
) -> Result<Option<EncryptedLeaf>, WireError> {
if plaintext.is_empty() {
return Ok(None);
}
let iv = match iv {
Some(iv) => iv,
None => Iv::generate()?,
};
let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
let sealed = gcm
.encrypt(
aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(iv.as_bytes()),
Payload {
msg: plaintext.expose(),
aad: aad.as_bytes(),
},
)
.map_err(|_| WireError::AeadOpen)?;
let split = sealed.len().saturating_sub(TAG_LEN);
let (data, tag) = sealed.split_at(split);
Ok(Some(EncryptedLeaf {
data: data.to_vec(),
iv: iv.as_bytes().to_vec(),
tag: tag.to_vec(),
ty: plaintext.leaf_type(),
}))
}
const TAG_LEN: usize = 16;
pub fn decrypt_leaf(
key: &DataKey,
leaf: &EncryptedLeaf,
aad: &Aad,
stash: Option<&mut IvStash>,
) -> Result<Plaintext, WireError> {
let mut sealed = Vec::with_capacity(leaf.data.len() + leaf.tag.len());
sealed.extend_from_slice(&leaf.data);
sealed.extend_from_slice(&leaf.tag);
let opened = match leaf.iv.len() {
Iv::LEN => {
let gcm = SopsGcm32::new_from_slice(key.expose()).map_err(|_| WireError::AeadOpen)?;
gcm.decrypt(
aes_gcm::Nonce::<aes_gcm::aead::consts::U32>::from_slice(&leaf.iv),
Payload {
msg: &sealed,
aad: aad.as_bytes(),
},
)
}
12 => {
let gcm = aes_gcm::Aes256Gcm::new_from_slice(key.expose())
.map_err(|_| WireError::AeadOpen)?;
gcm.decrypt(
aes_gcm::Nonce::<aes_gcm::aead::consts::U12>::from_slice(&leaf.iv),
Payload {
msg: &sealed,
aad: aad.as_bytes(),
},
)
}
_ => return Err(WireError::AeadOpen),
}
.map_err(|_| WireError::AeadOpen)?;
let plaintext = Plaintext::from_wire(opened, leaf.ty);
if let Some(stash) = stash {
stash.remember(&plaintext, aad, &leaf.iv);
}
Ok(plaintext)
}
pub(crate) fn decrypt_leaf_as_string(
key: &DataKey,
leaf: &EncryptedLeaf,
aad: &Aad,
stash: Option<&mut IvStash>,
) -> Result<Zeroizing<String>, WireError> {
let pt = decrypt_leaf(key, leaf, aad, stash)?;
if pt.leaf_type() != LeafType::Str {
return Err(WireError::DatatypeMismatch { ty: "str" });
}
Ok(Zeroizing::new(
String::from_utf8_lossy(pt.expose()).into_owned(),
))
}
fn hex_lower(bytes: &[u8]) -> String {
use std::fmt::Write as _;
bytes
.iter()
.fold(String::with_capacity(bytes.len() * 2), |mut s, b| {
let _ = write!(s, "{b:02x}");
s
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::aad::AadPath;
fn key() -> DataKey {
DataKey::from_bytes(&[7u8; 32]).expect("32 bytes")
}
fn aad(parts: &[&str]) -> Aad {
let mut p = AadPath::root();
for c in parts {
p.push_key(*c);
}
p.aad()
}
#[test]
fn round_trips_through_the_wire_rendering() {
let a = aad(&["db", "password"]);
let pt = Plaintext::string("s3kr1t");
let leaf = encrypt_leaf(&key(), &pt, &a, None)
.expect("encrypt")
.expect("non-empty");
let rendered = leaf.render();
let reparsed = EncryptedLeaf::parse(&rendered).expect("reparse");
let back = decrypt_leaf(&key(), &reparsed, &a, None).expect("decrypt");
assert_eq!(back.expose(), b"s3kr1t");
assert_eq!(back.leaf_type(), LeafType::Str);
}
#[test]
fn writes_a_thirty_two_byte_nonce() {
let leaf = encrypt_leaf(&key(), &Plaintext::string("x"), &aad(&["k"]), None)
.expect("encrypt")
.expect("non-empty");
assert_eq!(leaf.iv_len(), Iv::LEN);
assert_eq!(leaf.tag.len(), TAG_LEN);
}
#[test]
fn a_leaf_moved_to_another_path_will_not_open() {
let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a", "b"]), None)
.expect("encrypt")
.expect("non-empty");
assert_eq!(
decrypt_leaf(&key(), &leaf, &aad(&["a", "c"]), None),
Err(WireError::AeadOpen)
);
}
#[test]
fn a_wrong_data_key_will_not_open() {
let leaf = encrypt_leaf(&key(), &Plaintext::string("v"), &aad(&["a"]), None)
.expect("encrypt")
.expect("non-empty");
let other = DataKey::from_bytes(&[9u8; 32]).expect("32 bytes");
assert_eq!(
decrypt_leaf(&other, &leaf, &aad(&["a"]), None),
Err(WireError::AeadOpen)
);
}
#[test]
fn a_flipped_ciphertext_bit_will_not_open() {
let mut leaf = encrypt_leaf(&key(), &Plaintext::string("value"), &aad(&["a"]), None)
.expect("encrypt")
.expect("non-empty");
leaf.data[0] ^= 1;
assert_eq!(
decrypt_leaf(&key(), &leaf, &aad(&["a"]), None),
Err(WireError::AeadOpen)
);
}
#[test]
fn empty_is_a_fixed_point_in_both_directions() {
let empty = Plaintext::string("");
assert!(
encrypt_leaf(&key(), &empty, &aad(&["k"]), None)
.expect("encrypt")
.is_none()
);
}
#[test]
fn the_stash_reproduces_previous_bytes_exactly() {
let a = aad(&["k"]);
let pt = Plaintext::string("unchanged");
let first = encrypt_leaf(&key(), &pt, &a, None)
.expect("encrypt")
.expect("non-empty");
let mut stash = IvStash::new();
let recovered = decrypt_leaf(&key(), &first, &a, Some(&mut stash)).expect("decrypt");
assert_eq!(stash.len(), 1);
let second = encrypt_leaf(&key(), &recovered, &a, stash.recall(&recovered, &a))
.expect("re-encrypt")
.expect("non-empty");
assert_eq!(
first.render(),
second.render(),
"an unchanged value must re-encrypt identically"
);
}
#[test]
fn the_stash_key_separates_values_that_share_a_byte_string() {
let a = aad(&["items"]);
let mut stash = IvStash::new();
let iv = [42u8; 32];
stash.remember(&Plaintext::integer(1), &a, &iv);
assert_eq!(stash.len(), 1);
assert!(
stash.recall(&Plaintext::string("1"), &a).is_none(),
"a str must not recall an int's nonce"
);
assert!(
stash.recall(&Plaintext::float(1.0), &a).is_none(),
"a float must not recall an int's nonce"
);
assert!(stash.recall(&Plaintext::integer(1), &a).is_some());
stash.remember(&Plaintext::boolean(true), &a, &iv);
assert!(
stash.recall(&Plaintext::string("True"), &a).is_none(),
"a str must not recall a bool's nonce"
);
assert!(stash.recall(&Plaintext::boolean(true), &a).is_some());
stash.remember(&Plaintext::string("1"), &a, &iv);
stash.remember(&Plaintext::float(1.0), &a, &iv);
stash.remember(&Plaintext::string("True"), &a, &iv);
assert_eq!(stash.len(), 5, "int, bool, str-1, float-1, str-True");
}
#[test]
fn the_stash_key_separates_paths() {
let mut stash = IvStash::new();
stash.remember(&Plaintext::string("v"), &aad(&["a"]), &[1u8; 32]);
assert!(
stash
.recall(&Plaintext::string("v"), &aad(&["b"]))
.is_none()
);
assert!(
stash
.recall(&Plaintext::string("v"), &aad(&["a"]))
.is_some()
);
}
#[test]
fn without_the_stash_the_bytes_change() {
let a = aad(&["k"]);
let pt = Plaintext::string("unchanged");
let first = encrypt_leaf(&key(), &pt, &a, None)
.expect("e")
.expect("non-empty");
let second = encrypt_leaf(&key(), &pt, &a, None)
.expect("e")
.expect("non-empty");
assert_ne!(first.render(), second.render(), "fresh nonces must differ");
}
#[test]
fn a_short_data_key_is_named_not_swallowed() {
let err = DataKey::from_bytes(&[0u8; 16])
.err()
.expect("16 bytes must be refused");
assert_eq!(err, WireError::DataKeyLength(16));
}
#[test]
fn debug_never_shows_key_or_plaintext() {
assert_eq!(format!("{:?}", key()), "DataKey(*** 32 bytes)");
let mut stash = IvStash::new();
stash.remember(&Plaintext::string("hunter2"), &aad(&["k"]), &[0u8; 32]);
let shown = format!("{stash:?}");
assert!(
!shown.contains("hunter2"),
"IvStash Debug leaked a plaintext: {shown}"
);
assert_eq!(shown, "IvStash(1 pairs)");
}
}