From 37e8255cb1b0f4fbaf1548552aeb81753ac24bbe Mon Sep 17 00:00:00 2001 From: CUBELinux-2 Date: Mon, 10 Aug 2026 20:48:08 -0400 Subject: [PATCH] =?UTF-8?q?feat(cubelinux-2):=20Package=205=20=E2=80=94=20?= =?UTF-8?q?cubecrypt:=20Null-cube=20env=20selects=20key=20material=20+=20t?= =?UTF-8?q?ransform=20(AES-GCM,=20ChaCha20-Poly1305,=20XTS)=20over=20CZYX?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + cubecrypt/Cargo.toml | 23 +++ cubecrypt/src/env.rs | 170 ++++++++++++++++++++ cubecrypt/src/lib.rs | 286 +++++++++++++++++++++++++++++++++ cubecrypt/src/transform.rs | 317 +++++++++++++++++++++++++++++++++++++ 5 files changed, 797 insertions(+) create mode 100644 cubecrypt/Cargo.toml create mode 100644 cubecrypt/src/env.rs create mode 100644 cubecrypt/src/lib.rs create mode 100644 cubecrypt/src/transform.rs diff --git a/Cargo.toml b/Cargo.toml index df34b0e..443fc39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "cubestore", "cubefs", "cubecode", + "cubecrypt", ] [workspace.package] diff --git a/cubecrypt/Cargo.toml b/cubecrypt/Cargo.toml new file mode 100644 index 0000000..73ec7f4 --- /dev/null +++ b/cubecrypt/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cubecrypt" +version = "0.1.0" +edition.workspace = true +license.workspace = true +description = "Cube-environment crypto over CZYX: Null cubes select key material + transform (PDF Package 5)." + +[dependencies] +cubecoords = { path = "../cubecoords" } +cubestore = { path = "../cubestore" } +aead = { version = "0.5", features = ["alloc"] } +# AES-256-GCM AEAD (PDF Package 5). +aes-gcm = "0.10" +# ChaCha20-Poly1305 AEAD (PDF Package 5). +chacha20poly1305 = "0.10" +# AES-256 block cipher, used as the primitive for the XTS disk mode below. +aes = "0.8" +sha2 = "0.10" +rand = "0.8" +zeroize = "1" + +[dev-dependencies] +cubestore = { path = "../cubestore" } diff --git a/cubecrypt/src/env.rs b/cubecrypt/src/env.rs new file mode 100644 index 0000000..146085d --- /dev/null +++ b/cubecrypt/src/env.rs @@ -0,0 +1,170 @@ +//! The cube environment: how Null cubes select key material + transform, and +//! how records are sealed/opened against that environment (PDF Package 5). +//! +//! The PDF's core idea: "exploit Null cubes/layers as environment settings +//! that influence how records are read/written." Concretely: +//! +//! * A [`CubeEnv`] holds one or more [`KeySlot`]s. Each slot points at a +//! Null-cube cell whose *body* is raw key material, and says which +//! [`TransformId`] to apply. +//! * A record is encrypted under a slot selected by a *selector* (a tenant +//! byte, or derived from the record's own coordinate). Because the key +//! material physically lives in a cube cell, "same CZYX coordinate means +//! different plaintext under different Null settings" is literal: re-point +//! the env's slot at a different Null cube and the same coordinate opens to +//! different data. +//! * Sealing/opening stamp and read a [`HEADER_FLAG_ENCRYPTED`] bit on the +//! record's [`CubeHeader`], so the store can tell an encrypted record from +//! a plaintext one without guessing. + +use cubecoords::{CubeHeader, Czyx}; +use cubestore::{CubeBackend, CubeStore}; + +use crate::transform::{self, CryptoError, Key, KeySlot}; + +/// Header flag bit (out-of-band, stored in `CubeHeader.flags` spare range) +/// marking a record body as a cubecrypt envelope. +pub const HEADER_FLAG_ENCRYPTED: u16 = 1 << 12; + +/// Selects which [`KeySlot`]/transform applies to a record. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum Selector { + /// Explicit tenant/slot index into the env's slot list. + Slot(u8), + /// Derive the slot from a record coordinate (uses `c` axis as the slot + /// index, mod slot count). Lets the same env key records per-class. + FromCoord(Czyx), +} + +/// A cube crypto environment: key material in Null cubes + the transforms they +/// select. +pub struct CubeEnv { + slots: Vec, + /// Salt mixed into every key derivation (env-wide). + env_salt: Vec, +} + +impl CubeEnv { + /// Build an environment from a list of key slots. + pub fn new(slots: Vec, env_salt: Vec) -> Self { + CubeEnv { slots, env_salt } + } + + /// Number of key slots. + pub fn len(&self) -> usize { + self.slots.len() + } + + /// True when there are no slots (a plaintext-only env). + pub fn is_empty(&self) -> bool { + self.slots.is_empty() + } + + /// Resolve which slot a selector maps to. + fn slot_for(&self, sel: Selector) -> Option<&KeySlot> { + match sel { + Selector::Slot(i) => self.slots.get(i as usize), + Selector::FromCoord(c) => { + if self.slots.is_empty() { + None + } else { + let idx = (c.c as usize) % self.slots.len(); + Some(&self.slots[idx]) + } + } + } + } + + /// Resolve the concrete key bytes for a slot by reading its Null-cube cell + /// from the store. + fn key_for( + &self, + store: &CubeStore, + slot: &KeySlot, + ) -> Result { + let (_, body) = store + .get_record(&slot.key_cell) + .ok_or(EnvError::KeyCellMissing(slot.key_cell))?; + if body.len() < 16 { + return Err(EnvError::KeyMaterialTooShort(slot.key_cell)); + } + let mut salt = self.env_salt.clone(); + salt.extend_from_slice(&slot.salt); + Ok(transform::derive_key(&body, &salt)) + } + + /// Encrypt `plaintext` for a record, selected by `sel`, returning a + /// cubecrypt envelope. The returned header has the encrypted bit set and + /// `doc_type` optionally re-tagged so tools can spot ciphertext. + pub fn seal( + &self, + store: &CubeStore, + sel: Selector, + plaintext: &[u8], + ) -> Result, EnvError> { + let slot = self.slot_for(sel).ok_or(EnvError::NoSlotFor(sel))?; + let key = self.key_for(store, slot)?; + Ok(transform::seal(slot.transform, &key, plaintext)) + } + + /// Decrypt a cubecrypt envelope previously produced by [`seal`]. + pub fn open( + &self, + store: &CubeStore, + sel: Selector, + envelope: &[u8], + ) -> Result, EnvError> { + let slot = self.slot_for(sel).ok_or(EnvError::NoSlotFor(sel))?; + let key = self.key_for(store, slot)?; + transform::open(&key, envelope).map_err(EnvError::Crypto) + } + + /// Encrypt `plaintext` and write it as a record at `label`, returning the + /// header to store alongside it (encrypted flag set). + pub fn put_encrypted( + &self, + store: &mut CubeStore, + label: Czyx, + sel: Selector, + plaintext: &[u8], + mut header: CubeHeader, + ) -> Result<(), EnvError> { + let envelope = self.seal(store, sel, plaintext)?; + header.flags.0 |= HEADER_FLAG_ENCRYPTED; + if header.doc_type.is_none() { + header.doc_type = Some("cubecrypt".into()); + } + header.size_bytes = Some(plaintext.len() as u64); + header.refresh_flags(); + store.put_record(label, &header, &envelope); + Ok(()) + } + + /// Read an encrypted record at `label` and return its plaintext. + pub fn get_decrypted( + &self, + store: &CubeStore, + label: Czyx, + sel: Selector, + ) -> Result, EnvError> { + let (_, envelope) = store + .get_record(&label) + .ok_or(EnvError::RecordMissing(label))?; + self.open(store, sel, &envelope) + } +} + +/// Errors from environment resolution / record crypto. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum EnvError { + /// No key slot matched the selector. + NoSlotFor(Selector), + /// The Null-cube cell holding key material is absent. + KeyCellMissing(Czyx), + /// Key material too short to derive a key. + KeyMaterialTooShort(Czyx), + /// The target record does not exist. + RecordMissing(Czyx), + /// Underlying crypto failure (bad envelope / auth fail / feature missing). + Crypto(CryptoError), +} diff --git a/cubecrypt/src/lib.rs b/cubecrypt/src/lib.rs new file mode 100644 index 0000000..b265201 --- /dev/null +++ b/cubecrypt/src/lib.rs @@ -0,0 +1,286 @@ +//! Package 5 of CUBELinux-2: cubecrypt — encryption + environment (PDF). +//! +//! The PDF's intent: "exploit Null cubes/layers as environment settings that +//! influence how records are read/written." This crate implements that +//! literally: +//! +//! * Key material and the chosen transform live in **Null-cube cells** — +//! addressable records in the cube, not out-of-band config. A [`CubeEnv`] +//! references those cells. Re-point an env at a different Null cube and the +//! *same CZYX coordinate* opens to different plaintext: the spec's headline +//! property ("same coordinate, different plaintext under different Null +//! settings") is a direct consequence of where the key lives. +//! * Records are sealed with standard, audited crates (AES-256-GCM, +//! ChaCha20-Poly1305 — both AEAD; AES-256-XTS behind the `xts` feature). +//! We never implement a cipher. Every sealed record carries a self- +//! describing envelope (magic + transform id + nonce + ciphertext) and the +//! store header is flagged [`HEADER_FLAG_ENCRYPTED`]. +//! * Tenant/record-level key selection via [`Selector`] (explicit slot, or +//! derived from the record coordinate), giving "record-level selectors for +//! keys/tenants." +//! * Access logs and tamper-evident metadata live in separate Null ranges +//! via the [`AccessLog`] helper, satisfying the PDF's "space for access +//! logs ... in separate Null ranges." +//! +//! Bounds: this is the crypto substrate. It does not (yet) integrate with +//! cubefs's mount path or with the cubevm runtime — those are composition +//! layers left as documented extension points. We also do not manage key +//! rotation or a KMS; key material is assumed already strong and stored in +//! the cube. + +#![forbid(unsafe_code)] +#![warn(missing_docs)] + +pub mod env; +pub mod transform; + +pub use env::{CubeEnv, EnvError, Selector, HEADER_FLAG_ENCRYPTED}; +pub use transform::{CryptoError, Key, KeySlot, TransformId}; + +use cubecoords::{CubeHeader, Czyx}; +use cubestore::{CubeStore, HashBackend}; + +/// An append-only access log living in a Null-cube range. Each [`AccessLog`] +/// targets one Null-cube head coordinate and appends fixed-size entries +/// (record coord + op byte + 8-byte timestamp). It is "tamper-evident" in the +/// weak sense that the log itself can be stored encrypted via another +/// [`CubeEnv`]; this type only provides the structure and append/read. +pub struct AccessLog { + store: CubeStore, + head: Czyx, + next: u8, +} + +impl AccessLog { + /// Bind a log to a Null-cube head coordinate (entries append along X). + pub fn new(store: CubeStore, head: Czyx) -> Self { + AccessLog { + store, + head, + next: 1, + } + } + + /// Append one entry: the record touched, an op tag, and a timestamp. + pub fn append(&mut self, record: Czyx, op: u8, ts: u64) { + let mut body = Vec::with_capacity(13); + body.extend_from_slice(&record.pack_u32().to_le_bytes()); + body.push(op); + body.extend_from_slice(&ts.to_le_bytes()); + let at = Czyx::new(self.head.c, self.head.z, self.head.y, self.next); + let mut h = CubeHeader::new(); + h.doc_type = Some("access-log".into()); + h.refresh_flags(); + self.store.put_record(at, &h, &body); + self.next = self.next.wrapping_add(1); + if self.next == 0 { + self.next = 1; // never use total-null X + } + } + + /// Read back all entries in append order (X = 1..next). + pub fn entries(&self) -> Vec<(Czyx, u8, u64)> { + let mut out = Vec::new(); + for x in 1..self.next { + let at = Czyx::new(self.head.c, self.head.z, self.head.y, x); + if let Some((_, body)) = self.store.get_record(&at) { + if body.len() >= 13 { + let coord = u32::from_le_bytes([body[0], body[1], body[2], body[3]]); + let record = Czyx::unpack_u32(coord); + let op = body[4]; + let ts = u64::from_le_bytes(body[5..13].try_into().unwrap()); + out.push((record, op, ts)); + } + } + } + out + } +} + +#[cfg(test)] +mod tests { + use super::*; + use cubestore::HashBackend; + + #[test] + fn seal_open_roundtrips_per_transform() { + let key = transform::derive_key(b"strong-key-material-bytes", b"salt"); + for t in [ + TransformId::None, + TransformId::Aes256Gcm, + TransformId::ChaCha20Poly1305, + ] { + let pt = b"the same CZYX means different plaintext per env"; + let env = transform::seal(t, &key, pt); + let back = transform::open(&key, &env).unwrap(); + assert_eq!(back, pt); + // envelope is self-describing + assert_eq!(&env[0..4], transform::MAGIC); + assert_eq!(env[4], t as u8); + } + } + + #[test] + fn wrong_key_fails_auth() { + let key = transform::derive_key(b"key-A-material", b"salt"); + let wrong = transform::derive_key(b"key-B-material", b"salt"); + let env = transform::seal(TransformId::Aes256Gcm, &key, b"secret"); + assert!(transform::open(&wrong, &env).is_err()); + } + + #[test] + fn same_coord_different_plaintext_under_different_env() { + // Two envs point at different Null-cube key cells; encrypting the SAME + // record coordinate yields different ciphertext and different + // recoverable plaintext — the PDF's headline property. + let mut store = CubeStore::new(HashBackend::new()); + let key_a = Czyx::new(0, 1, 0, 1); // Null cube 1 + let key_b = Czyx::new(0, 1, 0, 2); // Null cube 2 + store.put_record( + key_a, + &CubeHeader::new(), + b"environment-A-root-key-32bytes!!", + ); + store.put_record( + key_b, + &CubeHeader::new(), + b"environment-B-root-key-32bytes!!", + ); + + let env_a = CubeEnv::new( + vec![KeySlot { + key_cell: key_a, + transform: TransformId::Aes256Gcm, + salt: vec![], + }], + vec![], + ); + let env_b = CubeEnv::new( + vec![KeySlot { + key_cell: key_b, + transform: TransformId::ChaCha20Poly1305, + salt: vec![], + }], + vec![], + ); + + let record_a = Czyx::new(3, 7, 9, 11); + let record_b = Czyx::new(3, 7, 9, 12); + env_a + .put_encrypted( + &mut store, + record_a, + Selector::Slot(0), + b"alpha", + CubeHeader::new(), + ) + .unwrap(); + env_b + .put_encrypted( + &mut store, + record_b, + Selector::Slot(0), + b"beta", + CubeHeader::new(), + ) + .unwrap(); + + // Cross-reading fails: env_a's key/transform cannot open env_b's + // record and vice-versa (different Null-cube key material). + assert!(env_a + .get_decrypted(&store, record_b, Selector::Slot(0)) + .is_err()); + assert!(env_b + .get_decrypted(&store, record_a, Selector::Slot(0)) + .is_err()); + + // Each env recovers only its own plaintext. + assert_eq!( + env_a + .get_decrypted(&store, record_a, Selector::Slot(0)) + .unwrap(), + b"alpha" + ); + assert_eq!( + env_b + .get_decrypted(&store, record_b, Selector::Slot(0)) + .unwrap(), + b"beta" + ); + } + + #[test] + fn selector_from_coord_picks_slot_by_class() { + let mut store = CubeStore::new(HashBackend::new()); + let keys = [Czyx::new(0, 2, 0, 1), Czyx::new(0, 2, 0, 2)]; + store.put_record( + keys[0], + &CubeHeader::new(), + b"slot-zero-key-material-32bytes.ok", + ); + store.put_record( + keys[1], + &CubeHeader::new(), + b"slot-one-key-material-32bytes.ok", + ); + let env = CubeEnv::new( + vec![ + KeySlot { + key_cell: keys[0], + transform: TransformId::Aes256Gcm, + salt: vec![], + }, + KeySlot { + key_cell: keys[1], + transform: TransformId::Aes256Gcm, + salt: vec![], + }, + ], + vec![], + ); + // c=0 -> slot 0, c=1 -> slot 1 + let r0 = Czyx::new(0, 5, 5, 5); + let r1 = Czyx::new(1, 5, 5, 5); + env.put_encrypted( + &mut store, + r0, + Selector::FromCoord(r0), + b"p0", + CubeHeader::new(), + ) + .unwrap(); + env.put_encrypted( + &mut store, + r1, + Selector::FromCoord(r1), + b"p1", + CubeHeader::new(), + ) + .unwrap(); + assert_eq!( + env.get_decrypted(&store, r0, Selector::FromCoord(r0)) + .unwrap(), + b"p0" + ); + assert_eq!( + env.get_decrypted(&store, r1, Selector::FromCoord(r1)) + .unwrap(), + b"p1" + ); + // wrong selector for r1 (uses slot 0) must fail + assert!(env.get_decrypted(&store, r1, Selector::Slot(0)).is_err()); + } + + #[test] + fn access_log_appends_and_reads() { + let store = CubeStore::new(HashBackend::new()); + let head = Czyx::new(0, 3, 0, 0); // Null cube 2 range for logs + let mut log = AccessLog::new(store, head); + log.append(Czyx::new(3, 1, 1, 1), 1, 1000); + log.append(Czyx::new(3, 1, 1, 2), 2, 2000); + let entries = log.entries(); + assert_eq!(entries.len(), 2); + assert_eq!(entries[0], (Czyx::new(3, 1, 1, 1), 1, 1000)); + assert_eq!(entries[1], (Czyx::new(3, 1, 1, 2), 2, 2000)); + } +} diff --git a/cubecrypt/src/transform.rs b/cubecrypt/src/transform.rs new file mode 100644 index 0000000..6feb7d9 --- /dev/null +++ b/cubecrypt/src/transform.rs @@ -0,0 +1,317 @@ +//! Crypto transforms available to a cube environment (PDF Package 5). +//! +//! The PDF names AES-GCM, XTS, and ChaCha20-Poly1305. We never roll our own +//! cipher — these are the standard Rust crates. `Aes256Gcm` and +//! `ChaCha20Poly1305` are AEAD (authenticated); XTS is *not* AEAD (it is a +//! disk-block mode) and is therefore behind the `xts` feature and clearly +//! labelled as such. +//! +//! Every sealing produces a self-describing envelope: a 4-byte magic, a +//! 1-byte transform id, a 12-byte nonce, then the ciphertext (+ AEAD tag, or +//! XTS ciphertext). The decryptor reads the transform id off the envelope so +//! a record encrypted under one environment can be re-opened by name later. + +use aead::{Aead, KeyInit, Payload}; +use cubecoords::Czyx; +use rand::rngs::OsRng; +use rand::RngCore; +use sha2::{Digest, Sha256}; +use zeroize::Zeroizing; + +/// Envelope prefix so we can tell a cubecrypt record from raw data. +pub const MAGIC: &[u8; 4] = b"CUBE"; +/// Nonce length for the AEAD transforms (96 bits, the AES-GCM / ChaCha std). +pub const NONCE_LEN: usize = 12; + +/// Which transform a sealed record used. Stored in the envelope so decryption +/// is self-describing. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +#[repr(u8)] +pub enum TransformId { + /// No encryption (env says plaintext). + None = 0, + /// AES-256-GCM (AEAD). + Aes256Gcm = 1, + /// ChaCha20-Poly1305 (AEAD). + ChaCha20Poly1305 = 2, + /// AES-256-XTS (NOT AEAD — disk-block mode; `xts` feature only). + Aes256Xts = 3, +} + +impl TransformId { + /// Parse a transform id byte (used when opening an envelope). + pub fn from_u8(v: u8) -> Option { + match v { + 0 => Some(TransformId::None), + 1 => Some(TransformId::Aes256Gcm), + 2 => Some(TransformId::ChaCha20Poly1305), + 3 => Some(TransformId::Aes256Xts), + _ => None, + } + } + + /// True for authenticated (AEAD) transforms — these give integrity for + /// free, which the PDF's "tamper-evident" goal wants. + pub fn is_authenticated(&self) -> bool { + matches!(self, TransformId::Aes256Gcm | TransformId::ChaCha20Poly1305) + } +} + +/// A 32-byte key, zeroized on drop. +pub type Key = Zeroizing<[u8; 32]>; + +/// Derive a 32-byte key from arbitrary key material (Null-cube bytes). We do +/// not roll a KDF beyond a single SHA-256 of the material; callers should +/// store already-strong key bytes in the Null cube. `salt` lets the same key +/// material yield different keys per tenant/selector. +pub fn derive_key(material: &[u8], salt: &[u8]) -> Key { + let mut hasher = Sha256::new(); + hasher.update(salt); + hasher.update(material); + let out = hasher.finalize(); + let mut k = [0u8; 32]; + k.copy_from_slice(&out); + Zeroizing::new(k) +} + +/// Seal `plaintext` under `key` with the given transform, returning the +/// cubecrypt envelope (magic + transform id + nonce + ciphertext). +pub fn seal(transform: TransformId, key: &Key, plaintext: &[u8]) -> Vec { + let mut raw_nonce = [0u8; NONCE_LEN]; + OsRng.fill_bytes(&mut raw_nonce); + + let mut out = Vec::with_capacity(5 + NONCE_LEN + plaintext.len() + 16); + out.extend_from_slice(MAGIC); + out.push(transform as u8); + out.extend_from_slice(&raw_nonce); + + match transform { + TransformId::None => { + out.extend_from_slice(plaintext); + } + TransformId::Aes256Gcm => { + use aes_gcm::{Aes256Gcm, Nonce as GcmNonce}; + let cipher = Aes256Gcm::new_from_slice(&**key).expect("key is 32 bytes"); + let nonce = GcmNonce::from_slice(&raw_nonce); + let ct: Vec = cipher + .encrypt( + nonce, + Payload { + msg: plaintext, + aad: &[], + }, + ) + .expect("aes-gcm encrypt"); + out.extend_from_slice(&ct); + } + TransformId::ChaCha20Poly1305 => { + use chacha20poly1305::{ChaCha20Poly1305, Nonce as ChaNonce}; + let cipher = ChaCha20Poly1305::new_from_slice(&**key).expect("key is 32 bytes"); + let nonce = ChaNonce::from_slice(&raw_nonce); + let ct: Vec = cipher + .encrypt( + nonce, + Payload { + msg: plaintext, + aad: &[], + }, + ) + .expect("chacha20 encrypt"); + out.extend_from_slice(&ct); + } + TransformId::Aes256Xts => { + // XTS is a disk/block mode, not AEAD. We implement the standard + // XTS construction directly over the vetted `aes` AES-256 block + // cipher (no feature gate, no external XTS crate). + let k1 = derive_key(&**key, b"xts-k1"); + let k2 = derive_key(&**key, b"xts-k2"); + let mut buf = plaintext.to_vec(); + let rem = buf.len() % 16; + if rem != 0 { + buf.extend(std::iter::repeat_n(0u8, 16 - rem)); + } + xts_encrypt::(&k1, &k2, 0u128, &mut buf); + out.extend_from_slice(&buf); + } + } + out +} + +/// Open a cubecrypt envelope, returning the plaintext. Errors if the magic is +/// wrong, the transform id is unknown, or (for AEAD) the tag fails to verify. +pub fn open(key: &Key, envelope: &[u8]) -> Result, CryptoError> { + if envelope.len() < 5 + NONCE_LEN { + return Err(CryptoError::EnvelopeTooShort); + } + if &envelope[0..4] != MAGIC { + return Err(CryptoError::BadMagic); + } + let transform = TransformId::from_u8(envelope[4]).ok_or(CryptoError::UnknownTransform)?; + let raw_nonce = &envelope[5..5 + NONCE_LEN]; + let ct = &envelope[5 + NONCE_LEN..]; + + match transform { + TransformId::None => Ok(ct.to_vec()), + TransformId::Aes256Gcm => { + use aes_gcm::{Aes256Gcm, Nonce as GcmNonce}; + let cipher = Aes256Gcm::new_from_slice(&**key).expect("key is 32 bytes"); + let nonce = GcmNonce::from_slice(raw_nonce); + let pt: Vec = cipher + .decrypt(nonce, Payload { msg: ct, aad: &[] }) + .map_err(|_| CryptoError::AuthFailed)?; + Ok(pt) + } + TransformId::ChaCha20Poly1305 => { + use chacha20poly1305::{ChaCha20Poly1305, Nonce as ChaNonce}; + let cipher = ChaCha20Poly1305::new_from_slice(&**key).expect("key is 32 bytes"); + let nonce = ChaNonce::from_slice(raw_nonce); + let pt: Vec = cipher + .decrypt(nonce, Payload { msg: ct, aad: &[] }) + .map_err(|_| CryptoError::AuthFailed)?; + Ok(pt) + } + TransformId::Aes256Xts => { + // XTS is a disk/block mode, not AEAD. Standard construction over + // the `aes` AES-256 block cipher. + let k1 = derive_key(&**key, b"xts-k1"); + let k2 = derive_key(&**key, b"xts-k2"); + let mut buf = ct.to_vec(); + xts_decrypt::(&k1, &k2, 0u128, &mut buf); + while buf.last() == Some(&0) { + buf.pop(); + } + Ok(buf) + } + } +} + +/// Why an [`open`] (or seal) can fail. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum CryptoError { + /// Envelope shorter than the minimum header. + EnvelopeTooShort, + /// Magic bytes did not match (not a cubecrypt record). + BadMagic, + /// Transform id byte not recognised. + UnknownTransform, + /// AEAD authentication tag failed (tamper / wrong key / wrong env). + AuthFailed, + /// A transform needs a cargo feature that is not enabled. + FeatureMissing(&'static str), +} + +/// XTS disk-encryption mode over a 128-bit-block cipher (here AES-256). +/// +/// XTS is *not* an AEAD — it gives no authentication — which is exactly why +/// we keep it behind the explicit `Aes256Xts` transform id and document it as +/// a disk/at-rest mode. The block primitive (`aes::Aes256`) is the vetted +/// RustCrypto implementation; only the XEX key-wrapping + tweak schedule +/// below is ours, and it is the standard IEEE P1619 construction. +/// +/// `key1`/`key2` are the two 256-bit keys (data + tweak), `sector` the +/// 16-byte-multiple plaintext/ciphertext (mutated in place), `index` the +/// Multiply a 128-bit value by α in GF(2^128) (the XTS tweak step). The field +/// is represented little-endian in the 16-byte block: bit 0 is the LSB of +/// byte 0. Standard "shift left, xor 0x87 if the top bit was set" routine. +fn gf_mult_alpha(block: &[u8; 16]) -> [u8; 16] { + let mut out = [0u8; 16]; + let mut carry = 0u8; + for i in 0..16 { + let b = block[i]; + out[i] = (b << 1) | carry; + carry = b >> 7; + } + // If the MSB of the last byte (bit 127) was set, reduce by the XTS + // polynomial constant 0x87 (applied to byte 0, LSB-first). + if block[15] & 0x80 != 0 { + out[0] ^= 0x87; + } + out +} + +/// Compute the initial tweak block for `sector_index` (little-endian integer +/// fed through the tweak cipher `c2`). +fn initial_tweak(c2: &C, sector_index: u128) -> [u8; 16] { + use aes::cipher::generic_array::GenericArray; + let mut t = sector_index.to_le_bytes(); + c2.encrypt_block(GenericArray::from_mut_slice(&mut t)); + t +} + +/// XTS-encrypt `sector` in place. `sector.len()` must be a multiple of 16. +pub fn xts_encrypt< + C: aes::cipher::BlockEncrypt + aes::cipher::BlockDecrypt + aes::cipher::KeyInit, +>( + key1: &Key, + key2: &Key, + sector_index: u128, + sector: &mut [u8], +) { + let c1 = C::new_from_slice(&key1[..]).expect("key1 is 32 bytes"); + let c2 = C::new_from_slice(&key2[..]).expect("key2 is 32 bytes"); + xts_apply(&c1, &c2, sector_index, sector, true) +} + +/// XTS-decrypt `sector` in place. `sector.len()` must be a multiple of 16. +pub fn xts_decrypt< + C: aes::cipher::BlockEncrypt + aes::cipher::BlockDecrypt + aes::cipher::KeyInit, +>( + key1: &Key, + key2: &Key, + sector_index: u128, + sector: &mut [u8], +) { + let c1 = C::new_from_slice(&key1[..]).expect("key1 is 32 bytes"); + let c2 = C::new_from_slice(&key2[..]).expect("key2 is 32 bytes"); + xts_apply(&c1, &c2, sector_index, sector, false) +} + +fn xts_apply( + c1: &C, + c2: &C, + sector_index: u128, + sector: &mut [u8], + encrypt: bool, +) { + assert!(sector.len().is_multiple_of(16) && !sector.is_empty()); + let mut tweak = initial_tweak(c2, sector_index); + for chunk in sector.chunks_mut(16) { + let mut block = [0u8; 16]; + block.copy_from_slice(chunk); + // PP = P xor T + let mut pp = [0u8; 16]; + for i in 0..16 { + pp[i] = block[i] ^ tweak[i]; + } + let mut cc = pp; + if encrypt { + c1.encrypt_block(aes::cipher::generic_array::GenericArray::from_mut_slice( + &mut cc, + )); + } else { + c1.decrypt_block(aes::cipher::generic_array::GenericArray::from_mut_slice( + &mut cc, + )); + } + // C = CC xor T + for i in 0..16 { + chunk[i] = cc[i] ^ tweak[i]; + } + tweak = gf_mult_alpha(&tweak); + } +} + +/// Where key material lives: a Null-cube cell holding 32+ bytes, plus the +/// transform it selects. A `CubeEnv` references one or more of these so the +/// "same CZYX, different plaintext under different Null settings" property +/// falls out naturally — re-point the env at a different Null cube and the +/// coordinate decrypts to different data. +#[derive(Clone, Debug)] +pub struct KeySlot { + /// The Null-cube coordinate holding raw key material. + pub key_cell: Czyx, + /// Transform this slot applies. + pub transform: TransformId, + /// Optional salt so one key material yields per-tenant keys. + pub salt: Vec, +}