use std::collections::BTreeMap;
use affinidi_tdk::{
TDK,
common::config::TDKConfig,
dids::{DID, KeyType},
};
use anyhow::{Context, Result, bail};
use chacha20poly1305::{
ChaCha20Poly1305, Key, Nonce,
aead::{Aead, KeyInit, Payload},
};
use chrono::{Duration, Utc};
use dtg_credentials::{DTGCredential, authority::verify_chain, delegation};
use rand::Rng;
trait RoomHost {
fn put(&mut self, room: &str, key: &str, epoch: u32, sealed: Vec<u8>, nonce: [u8; 12]);
fn get(&self, room: &str, key: &str) -> Option<&StoredRecord>;
fn list(&self, room: &str) -> Vec<(&String, &StoredRecord)>;
fn epoch(&self, room: &str) -> u32;
fn set_epoch(&mut self, room: &str, epoch: u32);
}
struct StoredRecord {
sealed: Vec<u8>,
nonce: [u8; 12],
epoch: u32,
}
#[derive(Default)]
struct InMemoryHost {
records: BTreeMap<(String, String), StoredRecord>,
epochs: BTreeMap<String, u32>,
}
impl RoomHost for InMemoryHost {
fn put(&mut self, room: &str, key: &str, epoch: u32, sealed: Vec<u8>, nonce: [u8; 12]) {
self.records.insert(
(room.to_string(), key.to_string()),
StoredRecord {
sealed,
nonce,
epoch,
},
);
}
fn get(&self, room: &str, key: &str) -> Option<&StoredRecord> {
self.records.get(&(room.to_string(), key.to_string()))
}
fn list(&self, room: &str) -> Vec<(&String, &StoredRecord)> {
self.records
.iter()
.filter(|((r, _), _)| r == room)
.map(|((_, k), v)| (k, v))
.collect()
}
fn epoch(&self, room: &str) -> u32 {
*self.epochs.get(room).unwrap_or(&1)
}
fn set_epoch(&mut self, room: &str, epoch: u32) {
self.epochs.insert(room.to_string(), epoch);
}
}
fn aad(room: &str, key: &str, version: u32, epoch: u32) -> Vec<u8> {
format!("{room}|{key}|{version}|{epoch}").into_bytes()
}
fn seal(room_key: &[u8; 32], plaintext: &[u8], aad: &[u8]) -> Result<(Vec<u8>, [u8; 12])> {
let cipher = ChaCha20Poly1305::new(&Key::from(*room_key));
let mut nonce_bytes = [0u8; 12];
rand::rng().fill_bytes(&mut nonce_bytes);
let sealed = cipher
.encrypt(
&Nonce::from(nonce_bytes),
Payload {
msg: plaintext,
aad,
},
)
.map_err(|e| anyhow::anyhow!("seal failed: {e}"))?;
Ok((sealed, nonce_bytes))
}
fn open(room_key: &[u8; 32], sealed: &[u8], nonce: &[u8; 12], aad: &[u8]) -> Result<Vec<u8>> {
let cipher = ChaCha20Poly1305::new(&Key::from(*room_key));
cipher
.decrypt(&Nonce::from(*nonce), Payload { msg: sealed, aad })
.map_err(|e| anyhow::anyhow!("open failed: {e}"))
}
fn new_room_key() -> [u8; 32] {
let mut k = [0u8; 32];
rand::rng().fill_bytes(&mut k);
k
}
fn step(n: u8, title: &str) {
println!("\n\x1b[1m─── {n}. {title}\x1b[0m");
}
#[tokio::main]
async fn main() -> Result<()> {
let tdk = TDK::new(
TDKConfig::builder().with_load_environment(false).build()?,
None,
)
.await?;
let now = Utc::now();
let (room_did, room_secret) = DID::generate_did_key(KeyType::Ed25519)?;
let (alice_did, _alice_secret) = DID::generate_did_key(KeyType::Ed25519)?;
let (bob_did, bob_secret) = DID::generate_did_key(KeyType::Ed25519)?;
let (agent_did, _agent_secret) = DID::generate_did_key(KeyType::Ed25519)?;
let (scheduler_did, scheduler_secret) = DID::generate_did_key(KeyType::Ed25519)?;
let mut host = InMemoryHost::default();
host.set_epoch(&room_did, 1);
let mut room_key = new_room_key();
println!("\x1b[1mA data room, end to end\x1b[0m");
println!("room {room_did}");
println!("alice {alice_did} (owner)");
println!("bob {bob_did}");
println!("agent {agent_did} (Bob's, acts as itself)");
println!("sched {scheduler_did} (acts in Bob's name)");
step(1, "Alice creates the room");
let mut alice_vac = DTGCredential::new_vac(
room_did.clone(),
alice_did.clone(),
room_did.clone(),
vec![
"read".into(),
"write".into(),
"curate".into(),
"admin".into(),
],
now,
now + Duration::days(365),
)?
.with_id("urn:uuid:vac-alice");
alice_vac.sign(&room_secret, None).await?;
println!("room DID minted; owner VAC issued to Alice");
println!(" actions: read, write, curate, admin");
step(2, "Alice invites Bob");
let mut vic = DTGCredential::new_vic(
room_did.clone(),
bob_did.clone(),
now,
Some(now + Duration::days(7)),
)
.with_id("urn:uuid:vic-bob");
vic.sign(&room_secret, None).await?;
println!("VIC issued to Bob, valid 7 days — delivered out of band, never via the host");
step(3, "Bob presents the invitation and joins");
let mut grant = DTGCredential::new_vmc(
room_did.clone(),
bob_did.clone(),
now,
Some(now + Duration::days(30)),
false,
);
grant.sign(&room_secret, None).await?;
let grant_wire = serde_json::to_value(&grant)?;
let mut ack = DTGCredential::new_member_vmc(&grant_wire, now, Some(now + Duration::days(30)))?;
ack.sign(&bob_secret, None).await?;
let mut bob_vac = DTGCredential::new_vac(
room_did.clone(),
bob_did.clone(),
room_did.clone(),
vec!["read".into(), "write".into()],
now,
now + Duration::days(30),
)?
.with_id("urn:uuid:vac-bob");
bob_vac.sign(&room_secret, None).await?;
println!("VMC pair complete — the room granted, Bob acknowledged");
println!(" Bob's VAC actions: read, write (no curate, no admin)");
step(4, "Bob writes a record");
let epoch = host.epoch(&room_did);
verify_chain(
std::slice::from_ref(&bob_vac),
&room_did,
&room_did,
"write",
&bob_did,
Utc::now(),
)
.context("Bob's write must be authorized by a chain reaching the room")?;
let body = b"Decision: ship the correlation-scope proposal to WD02. \
Rationale in the WG minutes for 2026-09-02.";
let a = aad(&room_did, "decision/wd02", 1, epoch);
let (sealed, nonce) = seal(&room_key, body, &a)?;
host.put(&room_did, "decision/wd02", epoch, sealed, nonce);
println!("chain verified for `write` → record sealed under epoch {epoch}");
println!(" AAD binds it to room|key|version|epoch — it cannot be relocated");
step(5, "Bob equips his agent with strictly less than he holds");
let mut agent_vac = bob_vac
.attenuate(
agent_did.clone(),
vec!["read".into()],
now,
now + Duration::hours(4),
Some(agent_did.clone()),
)?
.with_id("urn:uuid:vac-agent");
agent_vac.sign(&bob_secret, None).await?;
println!("Bob issued his agent a VAC: read only · 4 hours · audience-bound to the agent");
let chain = vec![agent_vac.clone(), bob_vac.clone()];
let permitted = verify_chain(&chain, &room_did, &room_did, "read", &agent_did, Utc::now())
.context("the agent's chain must verify for read")?;
let stored = host.get(&room_did, "decision/wd02").expect("record");
let plaintext = open(&room_key, &stored.sealed, &stored.nonce, &a)?;
println!(
"agent chain verified → recalled: \"{}\"",
String::from_utf8_lossy(&plaintext).trim()
);
println!(" permitted actions: {:?}", permitted.actions);
match verify_chain(
&chain,
&room_did,
&room_did,
"write",
&agent_did,
Utc::now(),
) {
Err(e) => println!(" agent `write` correctly refused: {e}"),
Ok(_) => bail!("the agent must not be able to write"),
}
step(6, "Bob appoints a scheduler to act in his name");
let mut appointment = DTGCredential::new_vdc(
bob_did.clone(),
scheduler_did.clone(),
now,
now + Duration::days(30),
vec!["schedule:read".into(), "schedule:propose".into()],
Some(0), )?
.with_id("urn:uuid:vdc-scheduler");
appointment.sign(&bob_secret, None).await?;
println!("Bob issued a VDC: schedule:read + schedule:propose · 30 days · no re-delegation");
let grant_json = serde_json::to_value(appointment.credential())?;
let mut acceptance =
DTGCredential::new_delegate_vdc(&grant_json, now, now + Duration::days(30))?
.with_id("urn:uuid:vdc-scheduler-ack");
acceptance.sign(&scheduler_secret, None).await?;
if !acceptance.accepts(&appointment)? {
bail!("the acceptance must bind to the grant");
}
println!("scheduler countersigned — the delegation edge is complete");
let appointed = delegation::verify_chain(
std::slice::from_ref(&appointment),
&bob_did,
"schedule:propose",
Utc::now(),
)
.context("the scheduler's appointment must verify")?;
println!(
" chain verified → acts are attributed to {}, not to the scheduler",
&appointed.principal[..18]
);
match verify_chain(
std::slice::from_ref(&appointment),
&room_did,
&room_did,
"read",
&scheduler_did,
Utc::now(),
) {
Err(e) => println!(" VDC correctly refused as authority: {e}"),
Ok(_) => bail!("a VDC must never be read as conferring authority"),
}
println!(" reach = what the VDC appoints for ∩ what Bob may do — the second asked live");
step(7, "Alice removes Bob");
verify_chain(
std::slice::from_ref(&alice_vac),
&room_did,
&room_did,
"admin",
&alice_did,
Utc::now(),
)
.context("only a holder of `admin` may rotate the epoch")?;
let old_key = room_key;
room_key = new_room_key();
host.set_epoch(&room_did, 2);
println!("epoch → 2; new key sealed to remaining members only (Alice)");
let epoch = host.epoch(&room_did);
let body2 = b"Follow-up: VAC chain depth capped at 8.";
let a2 = aad(&room_did, "decision/depth", 1, epoch);
let (sealed2, nonce2) = seal(&room_key, body2, &a2)?;
host.put(&room_did, "decision/depth", epoch, sealed2, nonce2);
println!("Alice wrote a record under epoch 2");
let stored2 = host.get(&room_did, "decision/depth").expect("record");
match open(&old_key, &stored2.sealed, &stored2.nonce, &a2) {
Err(_) => println!(" Bob's key cannot open epoch 2 — removal actually removed"),
Ok(_) => bail!("a removed member must not read the next epoch"),
}
let stored1 = host.get(&room_did, "decision/wd02").expect("record");
let still = open(&old_key, &stored1.sealed, &stored1.nonce, &a)?;
println!(
" what he already held, he still holds: \"{}…\"",
String::from_utf8_lossy(&still[..40])
);
step(8, "What the host can see");
println!("The host stores this and nothing else. No plaintext, no member list, no");
println!("credentials — membership was never something it was told.\n");
println!(" room {room_did}");
println!(" epoch {}", host.epoch(&room_did));
for (key, rec) in host.list(&room_did) {
println!(
" record {key} epoch {} {} bytes of ciphertext",
rec.epoch,
rec.sealed.len()
);
println!(" {}", hex_preview(&rec.sealed));
}
println!("\nWhat it cannot see: who is a member, who wrote what, or a single word of it.");
let wrong = aad(&room_did, "decision/depth", 1, 1);
if open(&old_key, &stored1.sealed, &stored1.nonce, &wrong).is_ok() {
bail!("a relocated record must not open");
}
println!("Relocating a record breaks its AAD binding — verified.");
tdk.verify_data(&vic.clone(), None, vic.credential().proof.as_ref().unwrap())
.await
.ok();
println!("\n\x1b[1mDone.\x1b[0m Every credential above is signed; every record is sealed.");
Ok(())
}
fn hex_preview(bytes: &[u8]) -> String {
let n = bytes.len().min(24);
let hex: String = bytes[..n].iter().map(|b| format!("{b:02x}")).collect();
format!("{hex}…")
}