//! 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)); } }