crates/monetize-server/src/seal.rs
//! **The custody's seal**: AES-256-GCM under a 32-byte key, `magic ‖ nonce ‖
//! ciphertext‖tag`, with associated data that names what the object is.
//!
//! The format is monetize-front's `ledger_backup` seal's, exactly (ring's
//! AES-256-GCM, a random 96-bit nonce, the magic in front): one format for
//! the operator-sealed ledger backup and the custody of a private estate's
//! secrets ([`crate::custody`]). The code is not shared yet: moving
//! ledger_backup's onto this would add an edge to monetize-server that every
//! workspace linking it (gunnar's, the kit's) would have to re-lock, so the
//! ring half here is behind the `custody-ring` feature (and the tests), and a
//! build without it seals nothing and says so.
/// Why an object did not open.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OpenError {
/// It does not start with the expected magic: not this kind of object.
NotSealed,
/// Shorter than a nonce and a tag.
Truncated,
/// The key or the associated data is not the one it was sealed under,
/// or the object was cut or changed.
WrongKey,
/// This build carries no seal (feature `custody-ring`).
NoSeal,
}
#[cfg(any(test, feature = "custody-ring"))]
mod imp {
use super::OpenError;
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM};
use ring::rand::SecureRandom as _;
fn key(kek: &[u8; 32]) -> Result<LessSafeKey, String> {
Ok(LessSafeKey::new(
UnboundKey::new(&AES_256_GCM, kek)
.map_err(|_| "the key is not an AES-256 key".to_string())?,
))
}
/// **32 bytes from the system's random source.**
pub fn random32() -> Result<[u8; 32], String> {
let mut b = [0u8; 32];
ring::rand::SystemRandom::new()
.fill(&mut b)
.map_err(|_| "no randomness".to_string())?;
Ok(b)
}
/// **Seal** `plain`: `magic ‖ nonce ‖ AES-256-GCM(plain, aad)`.
pub fn seal(kek: &[u8; 32], magic: &[u8], aad: &[u8], plain: &[u8]) -> Result<Vec<u8>, String> {
let mut n = [0u8; 12];
ring::rand::SystemRandom::new()
.fill(&mut n)
.map_err(|_| "no randomness".to_string())?;
let mut body = plain.to_vec();
key(kek)?
.seal_in_place_append_tag(Nonce::assume_unique_for_key(n), Aad::from(aad), &mut body)
.map_err(|_| "sealing failed".to_string())?;
let mut out = magic.to_vec();
out.extend_from_slice(&n);
out.extend_from_slice(&body);
Ok(out)
}
/// **Open** what [`seal`] made under the same key, magic and associated data.
pub fn open(
kek: &[u8; 32],
magic: &[u8],
aad: &[u8],
sealed: &[u8],
) -> Result<Vec<u8>, OpenError> {
let rest = sealed.strip_prefix(magic).ok_or(OpenError::NotSealed)?;
if rest.len() < 12 + 16 {
return Err(OpenError::Truncated);
}
let (n, body) = rest.split_at(12);
let mut body = body.to_vec();
let n: [u8; 12] = n.try_into().map_err(|_| OpenError::Truncated)?;
let k = key(kek).map_err(|_| OpenError::WrongKey)?;
let plain = k
.open_in_place(Nonce::assume_unique_for_key(n), Aad::from(aad), &mut body)
.map_err(|_| OpenError::WrongKey)?;
Ok(plain.to_vec())
}
}
/// A build without the seal: every call refuses by name.
#[cfg(not(any(test, feature = "custody-ring")))]
mod imp {
use super::OpenError;
const NONE: &str = "this monetize-server was built without a custody seal (feature `custody-ring`): it keeps no estate's secrets";
pub fn random32() -> Result<[u8; 32], String> {
Err(NONE.into())
}
pub fn seal(
_kek: &[u8; 32],
_magic: &[u8],
_aad: &[u8],
_plain: &[u8],
) -> Result<Vec<u8>, String> {
Err(NONE.into())
}
pub fn open(
_kek: &[u8; 32],
_magic: &[u8],
_aad: &[u8],
_sealed: &[u8],
) -> Result<Vec<u8>, OpenError> {
Err(OpenError::NoSeal)
}
}
pub use imp::{open, random32, seal};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sealed_object_opens_only_under_its_key_magic_and_associated_data() {
let k = random32().unwrap();
let s = seal(&k, b"TEST1\0", b"one", b"the plain bytes").unwrap();
assert_eq!(
open(&k, b"TEST1\0", b"one", &s).unwrap(),
b"the plain bytes"
);
assert_eq!(
open(&k, b"TEST1\0", b"two", &s),
Err(OpenError::WrongKey),
"another name does not open it"
);
assert_eq!(
open(&random32().unwrap(), b"TEST1\0", b"one", &s),
Err(OpenError::WrongKey)
);
assert_eq!(open(&k, b"OTHER\0", b"one", &s), Err(OpenError::NotSealed));
assert_eq!(
open(&k, b"TEST1\0", b"one", &s[..20]),
Err(OpenError::Truncated)
);
let mut cut = s.clone();
let last = cut.len() - 1;
cut[last] ^= 1;
assert_eq!(
open(&k, b"TEST1\0", b"one", &cut),
Err(OpenError::WrongKey),
"a changed object is refused"
);
}
}