Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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"
        );
    }
}