171 lines
6.0 KiB
Rust
171 lines
6.0 KiB
Rust
//! 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),
|
|
}
|