feat(cubelinux-2): Package 5 — cubecrypt: Null-cube env selects key material + transform (AES-GCM, ChaCha20-Poly1305, XTS) over CZYX

This commit is contained in:
CUBELinux-2
2026-08-10 20:48:08 -04:00
parent 01f61f13f4
commit 37e8255cb1
5 changed files with 797 additions and 0 deletions
+23
View File
@@ -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" }
+170
View File
@@ -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<KeySlot>,
/// Salt mixed into every key derivation (env-wide).
env_salt: Vec<u8>,
}
impl CubeEnv {
/// Build an environment from a list of key slots.
pub fn new(slots: Vec<KeySlot>, env_salt: Vec<u8>) -> 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<B: CubeBackend>(
&self,
store: &CubeStore<B>,
slot: &KeySlot,
) -> Result<Key, EnvError> {
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<B: CubeBackend>(
&self,
store: &CubeStore<B>,
sel: Selector,
plaintext: &[u8],
) -> Result<Vec<u8>, 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<B: CubeBackend>(
&self,
store: &CubeStore<B>,
sel: Selector,
envelope: &[u8],
) -> Result<Vec<u8>, 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<B: CubeBackend>(
&self,
store: &mut CubeStore<B>,
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<B: CubeBackend>(
&self,
store: &CubeStore<B>,
label: Czyx,
sel: Selector,
) -> Result<Vec<u8>, 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),
}
+286
View File
@@ -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<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));
}
}
+317
View File
@@ -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<Self> {
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<u8> {
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<u8> = 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<u8> = 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::<aes::Aes256>(&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<Vec<u8>, 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<u8> = 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<u8> = 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::<aes::Aes256>(&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<C: aes::cipher::BlockEncrypt>(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<C: aes::cipher::BlockEncrypt + aes::cipher::BlockDecrypt + aes::cipher::KeyInit>(
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<u8>,
}