Skip to main content

macula_rust/record/
storage_key.rs

1//! A record's 32-byte DHT storage key, as macula_record's storage_key/1
2//! derives it. A node record is stored under its node_id, and every other
3//! record under SHA-256 over MACULA-PQ-STORAGE-KEY-V1, a zero byte, its type
4//! and the fields of its slot, a 32-byte id as it is and any other field
5//! length-prefixed. A tombstone takes the key of the record it withdraws.
6
7use 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
15/// The storage key of `r`. A record stored under its signer must be signed or
16/// verified.
17pub 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
37/// The storage key of a procedure's advertisements.
38pub fn procedure_key(realm_id: &[u8; 32], procedure: &str) -> [u8; 32] {
39    derived(0x06, &[realm_id, &length_prefixed(procedure.as_bytes())])
40}
41
42/// The storage key every announcement of a content id shares.
43pub 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
50/// The storage key of a station's endpoint record.
51pub fn station_endpoint_key(node_id: &[u8; 32]) -> [u8; 32] {
52    derived(0x12, &[node_id])
53}
54
55/// The storage key of an org directory record.
56pub 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
60/// The storage key of a procedure delegation.
61pub 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}