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}