Files
cubelinux-2/cubecrypt/src/lib.rs
T

287 lines
9.9 KiB
Rust

//! 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<HashBackend>,
head: Czyx,
next: u8,
}
impl AccessLog {
/// Bind a log to a Null-cube head coordinate (entries append along X).
pub fn new(store: CubeStore<HashBackend>, 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));
}
}