Skip to main content

this_me/kernel/
secret_material.rs

1use sha3::{Digest, Keccak256};
2
3const KECCAK_HMAC_BLOCK_SIZE: usize = 136;
4const BLOB_V2_MAGIC: [u8; 3] = [0xfe, 0x6d, 0x65];
5const BLOB_V3_VERSION: u8 = 0x03;
6const BLOB_V3_NONCE_LENGTH: usize = 16;
7const BLOB_V3_TAG_LENGTH: usize = 16;
8const BLOB_BASE64URL_PREFIX: &str = "b64u:";
9const V3_KDF_LABEL: &str = "this.me/blob/v3/kdf";
10const V3_ENC_INFO_LABEL: &str = "this.me/blob/v3/enc";
11const V3_MAC_INFO_LABEL: &str = "this.me/blob/v3/mac";
12const V3_STREAM_INFO_LABEL: &str = "this.me/blob/v3/stream";
13const V3_TAG_INFO_LABEL: &str = "this.me/blob/v3/tag";
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct BlobV3DerivedKeys {
17    pub enc_key: [u8; 32],
18    pub mac_key: [u8; 32],
19    pub path_context: Vec<u8>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SecretMaterialPurpose {
24    Branch,
25    Value,
26    Enc,
27    Mac,
28}
29
30impl SecretMaterialPurpose {
31    fn as_label(self) -> &'static str {
32        match self {
33            Self::Branch => "this.me/blob/v3/branch",
34            Self::Value => "this.me/blob/v3/value",
35            Self::Enc => "this.me/blob/v3/enc",
36            Self::Mac => "this.me/blob/v3/mac",
37        }
38    }
39}
40
41pub fn derive_secret_material_v3(
42    chain: &[Vec<u8>],
43    purpose: SecretMaterialPurpose,
44) -> Option<[u8; 32]> {
45    if chain.len() < 6 {
46        return None;
47    }
48
49    let mut transcript = Vec::new();
50    transcript.extend_from_slice(V3_KDF_LABEL.as_bytes());
51    transcript.extend_from_slice(&length_prefixed(purpose.as_label().as_bytes()));
52    for segment in chain {
53        transcript.extend_from_slice(&length_prefixed(segment));
54    }
55
56    Some(keccak256(&transcript))
57}
58
59pub fn derive_blob_v3_keys(
60    chain: &[Vec<u8>],
61    mode: SecretMaterialPurpose,
62    path: &[String],
63) -> Option<BlobV3DerivedKeys> {
64    let base_key = derive_secret_material_v3(chain, mode)?;
65    let path_context = path.join(".").into_bytes();
66    let enc_key = hmac_keccak256(
67        &base_key,
68        &[
69            V3_ENC_INFO_LABEL.as_bytes(),
70            &length_prefixed(&path_context),
71        ],
72    );
73    let mac_key = hmac_keccak256(
74        &base_key,
75        &[
76            V3_MAC_INFO_LABEL.as_bytes(),
77            &length_prefixed(&path_context),
78        ],
79    );
80
81    Some(BlobV3DerivedKeys {
82        enc_key,
83        mac_key,
84        path_context,
85    })
86}
87
88pub fn encrypt_blob_v3_cleartext(
89    cleartext: &[u8],
90    keys: &BlobV3DerivedKeys,
91    nonce: [u8; BLOB_V3_NONCE_LENGTH],
92) -> String {
93    let keystream =
94        generate_blob_v3_keystream(&keys.enc_key, &nonce, &keys.path_context, cleartext.len());
95    let ciphertext = cleartext
96        .iter()
97        .zip(keystream.iter())
98        .map(|(clear, key)| clear ^ key)
99        .collect::<Vec<_>>();
100    let header = blob_v3_header();
101    let tag = compute_blob_v3_tag(
102        &keys.mac_key,
103        &header,
104        &nonce,
105        &keys.path_context,
106        &ciphertext,
107    );
108
109    let mut encoded = Vec::new();
110    encoded.extend_from_slice(&header);
111    encoded.extend_from_slice(&nonce);
112    encoded.extend_from_slice(&tag);
113    encoded.extend_from_slice(&ciphertext);
114    format!("{BLOB_BASE64URL_PREFIX}{}", base64_url_encode(&encoded))
115}
116
117pub fn random_blob_v3_nonce() -> Option<[u8; BLOB_V3_NONCE_LENGTH]> {
118    let mut nonce = [0_u8; BLOB_V3_NONCE_LENGTH];
119    getrandom::getrandom(&mut nonce).ok()?;
120    Some(nonce)
121}
122
123pub fn decrypt_blob_v3_cleartext(blob: &str, keys: &BlobV3DerivedKeys) -> Option<Vec<u8>> {
124    let bytes = blob_to_bytes(blob)?;
125    let header_length = BLOB_V2_MAGIC.len() + 1;
126    let min_length = header_length + BLOB_V3_NONCE_LENGTH + BLOB_V3_TAG_LENGTH + 1;
127    if bytes.len() < min_length {
128        return None;
129    }
130    if bytes[..BLOB_V2_MAGIC.len()] != BLOB_V2_MAGIC {
131        return None;
132    }
133    if bytes[BLOB_V2_MAGIC.len()] != BLOB_V3_VERSION {
134        return None;
135    }
136
137    let header = &bytes[..header_length];
138    let nonce_start = header_length;
139    let tag_start = nonce_start + BLOB_V3_NONCE_LENGTH;
140    let ciphertext_start = tag_start + BLOB_V3_TAG_LENGTH;
141    let nonce = &bytes[nonce_start..tag_start];
142    let tag = &bytes[tag_start..ciphertext_start];
143    let ciphertext = &bytes[ciphertext_start..];
144    let expected_tag =
145        compute_blob_v3_tag(&keys.mac_key, header, nonce, &keys.path_context, ciphertext);
146    if !constant_time_equal(tag, &expected_tag) {
147        return None;
148    }
149
150    let keystream =
151        generate_blob_v3_keystream(&keys.enc_key, nonce, &keys.path_context, ciphertext.len());
152    Some(
153        ciphertext
154            .iter()
155            .zip(keystream.iter())
156            .map(|(cipher, key)| cipher ^ key)
157            .collect(),
158    )
159}
160
161pub fn lineage_segment(kind: &str, path_key: &str, value: &str) -> Vec<u8> {
162    let mut out = Vec::new();
163    out.extend_from_slice(kind.as_bytes());
164    out.push(0);
165    out.extend_from_slice(path_key.as_bytes());
166    out.push(0);
167    out.extend_from_slice(value.as_bytes());
168    out
169}
170
171pub fn keccak256(input: &[u8]) -> [u8; 32] {
172    let mut hasher = Keccak256::new();
173    hasher.update(input);
174    hasher.finalize().into()
175}
176
177fn generate_blob_v3_keystream(
178    enc_key: &[u8; 32],
179    nonce: &[u8],
180    path_context: &[u8],
181    length: usize,
182) -> Vec<u8> {
183    let mut out = Vec::with_capacity(length);
184    let prefixed_enc_key = length_prefixed(enc_key);
185    let prefixed_nonce = length_prefixed(nonce);
186    let prefixed_path_context = length_prefixed(path_context);
187    let mut counter = 0_u32;
188
189    while out.len() < length {
190        let block = keccak256_many(&[
191            V3_STREAM_INFO_LABEL.as_bytes(),
192            &prefixed_enc_key,
193            &prefixed_nonce,
194            &counter.to_be_bytes(),
195            &prefixed_path_context,
196        ]);
197        let remaining = length - out.len();
198        out.extend_from_slice(&block[..remaining.min(block.len())]);
199        counter = counter.wrapping_add(1);
200    }
201
202    out
203}
204
205fn compute_blob_v3_tag(
206    mac_key: &[u8; 32],
207    header: &[u8],
208    nonce: &[u8],
209    path_context: &[u8],
210    ciphertext: &[u8],
211) -> [u8; BLOB_V3_TAG_LENGTH] {
212    let full = hmac_keccak256(
213        mac_key,
214        &[
215            V3_TAG_INFO_LABEL.as_bytes(),
216            &length_prefixed(header),
217            &length_prefixed(nonce),
218            &length_prefixed(path_context),
219            &(ciphertext.len() as u64).to_be_bytes(),
220            ciphertext,
221        ],
222    );
223    full[..BLOB_V3_TAG_LENGTH]
224        .try_into()
225        .expect("tag length is fixed")
226}
227
228fn hmac_keccak256(key: &[u8], parts: &[&[u8]]) -> [u8; 32] {
229    let normalized_key = if key.len() > KECCAK_HMAC_BLOCK_SIZE {
230        keccak256(key).to_vec()
231    } else {
232        key.to_vec()
233    };
234    let mut key_block = [0_u8; KECCAK_HMAC_BLOCK_SIZE];
235    key_block[..normalized_key.len()].copy_from_slice(&normalized_key);
236
237    let mut ipad = [0_u8; KECCAK_HMAC_BLOCK_SIZE];
238    let mut opad = [0_u8; KECCAK_HMAC_BLOCK_SIZE];
239    for index in 0..KECCAK_HMAC_BLOCK_SIZE {
240        ipad[index] = key_block[index] ^ 0x36;
241        opad[index] = key_block[index] ^ 0x5c;
242    }
243
244    let mut inner_input = Vec::new();
245    inner_input.extend_from_slice(&ipad);
246    for part in parts {
247        inner_input.extend_from_slice(part);
248    }
249    let inner = keccak256(&inner_input);
250
251    let mut outer_input = Vec::new();
252    outer_input.extend_from_slice(&opad);
253    outer_input.extend_from_slice(&inner);
254    keccak256(&outer_input)
255}
256
257fn keccak256_many(parts: &[&[u8]]) -> [u8; 32] {
258    let mut hasher = Keccak256::new();
259    for part in parts {
260        hasher.update(part);
261    }
262    hasher.finalize().into()
263}
264
265fn length_prefixed(bytes: &[u8]) -> Vec<u8> {
266    let mut out = Vec::with_capacity(4 + bytes.len());
267    out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
268    out.extend_from_slice(bytes);
269    out
270}
271
272fn blob_v3_header() -> [u8; 4] {
273    [
274        BLOB_V2_MAGIC[0],
275        BLOB_V2_MAGIC[1],
276        BLOB_V2_MAGIC[2],
277        BLOB_V3_VERSION,
278    ]
279}
280
281fn constant_time_equal(left: &[u8], right: &[u8]) -> bool {
282    if left.len() != right.len() {
283        return false;
284    }
285    left.iter()
286        .zip(right.iter())
287        .fold(0_u8, |diff, (left, right)| diff | (left ^ right))
288        == 0
289}
290
291fn blob_to_bytes(blob: &str) -> Option<Vec<u8>> {
292    if let Some(value) = blob.strip_prefix(BLOB_BASE64URL_PREFIX) {
293        return base64_url_decode(value);
294    }
295    let clean = blob.strip_prefix("0x").unwrap_or(blob);
296    hex_decode(clean)
297}
298
299pub(crate) fn base64_url_encode(bytes: &[u8]) -> String {
300    const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
301    let mut out = String::new();
302    let mut index = 0;
303    while index < bytes.len() {
304        let first = bytes[index];
305        let second = bytes.get(index + 1).copied();
306        let third = bytes.get(index + 2).copied();
307
308        out.push(ALPHABET[(first >> 2) as usize] as char);
309        out.push(
310            ALPHABET[(((first & 0b0000_0011) << 4) | second.unwrap_or(0) >> 4) as usize] as char,
311        );
312        if let Some(second) = second {
313            out.push(
314                ALPHABET[(((second & 0b0000_1111) << 2) | third.unwrap_or(0) >> 6) as usize]
315                    as char,
316            );
317        }
318        if let Some(third) = third {
319            out.push(ALPHABET[(third & 0b0011_1111) as usize] as char);
320        }
321
322        index += 3;
323    }
324    out
325}
326
327pub(crate) fn base64_url_decode(input: &str) -> Option<Vec<u8>> {
328    let mut values = Vec::new();
329    for byte in input.bytes() {
330        let value = match byte {
331            b'A'..=b'Z' => byte - b'A',
332            b'a'..=b'z' => byte - b'a' + 26,
333            b'0'..=b'9' => byte - b'0' + 52,
334            b'-' => 62,
335            b'_' => 63,
336            b'=' => continue,
337            _ => return None,
338        };
339        values.push(value);
340    }
341
342    let mut out = Vec::new();
343    for chunk in values.chunks(4) {
344        if chunk.len() == 1 {
345            return None;
346        }
347        let first = chunk[0];
348        let second = chunk[1];
349        out.push((first << 2) | (second >> 4));
350        if chunk.len() > 2 {
351            let third = chunk[2];
352            out.push(((second & 0b0000_1111) << 4) | (third >> 2));
353            if chunk.len() > 3 {
354                let fourth = chunk[3];
355                out.push(((third & 0b0000_0011) << 6) | fourth);
356            }
357        }
358    }
359    Some(out)
360}
361
362fn hex_decode(input: &str) -> Option<Vec<u8>> {
363    if !input.len().is_multiple_of(2) {
364        return None;
365    }
366    (0..input.len())
367        .step_by(2)
368        .map(|index| u8::from_str_radix(&input[index..index + 2], 16).ok())
369        .collect()
370}