macula_rust/record/
storage_key.rs1use sha2::{Digest, Sha256};
8
9use crate::cbor::Value;
10
11use super::{malformed, payload::is_content_id, Record, RecordError, RecordType};
12
13const STORAGE_KEY_LABEL: &[u8] = b"MACULA-PQ-STORAGE-KEY-V1";
14
15pub fn storage_key(r: &Record) -> Result<[u8; 32], RecordError> {
18 if r.record_type != RecordType::TOMBSTONE {
19 return slot_key(
20 i128::from(r.record_type.0),
21 &r.payload,
22 r.subject.as_deref(),
23 r,
24 );
25 }
26 let Some(Value::Int(withdrawn)) = r.payload.get("withdrawn_type") else {
27 return Err(malformed("a tombstone without an integer withdrawn_type"));
28 };
29 let subject = match r.payload.get("subject") {
30 None => None,
31 Some(Value::Bytes(s)) => Some(s.as_slice()),
32 Some(_) => return Err(malformed("a tombstone's subject that is not bytes")),
33 };
34 slot_key(*withdrawn, &r.payload, subject, r)
35}
36
37pub fn procedure_key(realm_id: &[u8; 32], procedure: &str) -> [u8; 32] {
39 derived(0x06, &[realm_id, &length_prefixed(procedure.as_bytes())])
40}
41
42pub fn content_key(mcid: &[u8]) -> Result<[u8; 32], RecordError> {
44 if !is_content_id(Some(&Value::Bytes(mcid.to_vec()))) {
45 return Err(RecordError::NotAContentId);
46 }
47 Ok(derived(0x11, &[&length_prefixed(mcid)]))
48}
49
50pub fn station_endpoint_key(node_id: &[u8; 32]) -> [u8; 32] {
52 derived(0x12, &[node_id])
53}
54
55pub fn org_directory_key(realm_id: &[u8; 32], org_name: &str) -> [u8; 32] {
57 derived(0x15, &[realm_id, &length_prefixed(org_name.as_bytes())])
58}
59
60pub fn procedure_delegation_key(org_key_id: &[u8; 32], advertiser: &[u8; 32]) -> [u8; 32] {
62 derived(0x16, &[org_key_id, advertiser])
63}
64
65fn slot_key(
66 t: i128,
67 payload: &Value,
68 subject: Option<&[u8]>,
69 r: &Record,
70) -> Result<[u8; 32], RecordError> {
71 let id = |name: &str| -> Result<Vec<u8>, RecordError> {
72 match payload.get(name) {
73 Some(Value::Bytes(b)) if b.len() == 32 => Ok(b.clone()),
74 _ => Err(malformed(format!("a storage key needs a 32-byte {name}"))),
75 }
76 };
77 let text = |name: &str| -> Result<Vec<u8>, RecordError> {
78 match payload.get(name) {
79 Some(Value::Text(t)) => Ok(length_prefixed(t.as_bytes())),
80 _ => Err(malformed(format!("a storage key needs {name} as text"))),
81 }
82 };
83 let signer = || -> Result<Vec<u8>, RecordError> {
84 r.signed
85 .as_ref()
86 .map(|s| s.key_id.to_vec())
87 .ok_or(RecordError::Unsigned)
88 };
89 let key = match t {
90 0x01 => {
91 let signer = signer()?;
92 let mut key = [0u8; 32];
93 key.copy_from_slice(&signer);
94 key
95 }
96 0x03 | 0x04 => derived(t as u8, &[&id("realm_id")?]),
97 0x05 => derived(t as u8, &[&id("realm_id")?, &id("member_node")?]),
98 0x06 => derived(t as u8, &[&id("realm_id")?, &text("procedure")?]),
99 0x0D | 0x0F => derived(t as u8, &[&signer()?]),
100 0x0E => derived(t as u8, &[&signer()?, &text("param_name")?]),
101 0x10 => derived(t as u8, &[&id("station_id")?]),
102 0x11 => match payload.get("mcid") {
103 Some(Value::Bytes(m)) if is_content_id(Some(&Value::Bytes(m.clone()))) => {
104 derived(0x11, &[&length_prefixed(m)])
105 }
106 _ => return Err(RecordError::NotAContentId),
107 },
108 0x12 => derived(t as u8, &[&signer()?]),
109 0x15 => derived(t as u8, &[&id("realm_id")?, &text("org_name")?]),
110 0x16 => derived(t as u8, &[&signer()?, &id("advertiser")?]),
111 0x20..=0xFF => match subject {
112 Some(s) => derived(t as u8, &[&signer()?, &length_prefixed(s)]),
113 None => derived(t as u8, &[&signer()?]),
114 },
115 _ => return Err(malformed(format!("no storage key for type {t:#04x}"))),
116 };
117 Ok(key)
118}
119
120fn derived(t: u8, fields: &[&[u8]]) -> [u8; 32] {
121 let mut h = Sha256::new();
122 h.update(STORAGE_KEY_LABEL);
123 h.update([0, t]);
124 for field in fields {
125 h.update(field);
126 }
127 h.finalize().into()
128}
129
130fn length_prefixed(b: &[u8]) -> Vec<u8> {
131 let mut out = (b.len() as u32).to_be_bytes().to_vec();
132 out.extend_from_slice(b);
133 out
134}