1use anyhow::{bail, Context, Result};
13use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
14use chacha20poly1305::aead::{Aead, KeyInit, Payload as AeadPayload};
15use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use std::convert::TryInto;
19
20pub const RECORD_DESCRIPTOR_MAGIC: &[u8; 4] = b"BRD1";
21pub const RECORD_DESCRIPTOR_VERSION: u8 = 2;
22pub const RECORD_DESCRIPTOR_PREFIX_LENGTH: usize = 19;
23
24pub const METADATA_GRAYSCALE_NIBBLE_BASE: u8 = 120;
25
26pub const SIGNED_RELEASE_REFERENCE_VERSION: u8 = 2;
29pub const SIGNED_RELEASE_REFERENCE_HASH_LENGTH: usize = 32;
30pub const SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH: usize = 64;
31pub const SIGNED_RELEASE_REFERENCE_MAX_KEY_ID_LENGTH: usize = u16::MAX as usize;
32
33pub const CACHE_ENCRYPTION_DESCRIPTOR_VERSION: u8 = 1;
34pub const CACHE_ENCRYPTION_ALGORITHM_XCHACHA20POLY1305: &str = "xchacha20-poly1305";
35pub const CACHE_KEY_DERIVATION_HKDF_SHA256: &str = "hkdf-sha256";
36pub const CACHE_ENCRYPTION_SECRET_LENGTH: usize = 32;
37pub const CACHE_ENCRYPTION_RECORD_BINDING_HASH_LENGTH: usize = 32;
38pub const CACHE_ENCRYPTION_NONCE_LENGTH: usize = 24;
39pub const CACHE_ENCRYPTION_TAG_LENGTH: usize = 16;
40pub const CACHE_ENCRYPTION_ENVELOPE_MAGIC: &[u8; 4] = b"BCE1";
41pub const CACHE_ENCRYPTION_ENVELOPE_VERSION: u8 = 1;
42pub const CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305: u8 = 1;
43pub const CACHE_ENCRYPTION_INFO: &[u8] = b"bitneedle-cache-encryption-v1";
44pub const CACHE_ENCRYPTION_NONCE_INFO: &[u8] = b"bitneedle-cache-encryption-nonce-v1";
45pub const CACHE_ENCRYPTION_AAD_DOMAIN: &[u8] = b"bitneedle-cache-encryption-aad-v1";
46pub const CACHE_ENCRYPTION_NONCE_DOMAIN: &[u8] = b"bitneedle-cache-nonce-v1";
47
48pub const RECORD_PROFILE_SINGLE45_CODE: u8 = 0;
49pub const RECORD_PROFILE_LP_CODE: u8 = 1;
50pub const RECORD_PROFILE_SINGLE45: &str = "single45";
51pub const RECORD_PROFILE_LP: &str = "lp";
52
53pub const RELEASE_ID_LENGTH: usize = 16;
54
55pub const SEGMENT_DESCRIPTOR_CRC32: u8 = 1;
56pub const SEGMENT_STREAM_BYTE_LENGTH: u8 = 2;
57pub const SEGMENT_RECORD_PROFILE: u8 = 4;
58pub const SEGMENT_TITLE: u8 = 5;
59pub const SEGMENT_ARTIST: u8 = 6;
60pub const SEGMENT_PAYLOAD_ENCODING: u8 = 7;
61pub const SEGMENT_RELEASE_ID: u8 = 8;
62pub const SEGMENT_CATALOG_NUMBER: u8 = 9;
63pub const SEGMENT_LABEL: u8 = 10;
64pub const SEGMENT_ARTWORK_CREDIT: u8 = 11;
65pub const SEGMENT_CANONICAL_URL: u8 = 13;
66pub const SEGMENT_CREATED_AT: u8 = 14;
67pub const SEGMENT_SIGNED_RELEASE_REFERENCE: u8 = 16;
68pub const SEGMENT_BSC_POINTER: u8 = 21;
69pub const SEGMENT_TONED_CARRIER_MAP: u8 = 22;
70pub const SEGMENT_CACHE_ENCRYPTION: u8 = 23;
71pub const SEGMENT_COPYRIGHT_YEAR: u8 = 24;
72pub const SEGMENT_COPYRIGHT_HOLDER: u8 = 25;
73
74pub const PAYLOAD_ENCODING_RGB: &str = "rgb";
75pub const PAYLOAD_ENCODING_TONED_V1: &str = "toned-v1";
76pub const PAYLOAD_ENCODING_RGB_CODE: u8 = 0;
77pub const PAYLOAD_ENCODING_TONED_V1_CODE: u8 = 1;
78
79pub const TONED_CARRIER_MAP_VERSION: u8 = 1;
80pub const TONED_ORDERING_BASE_PROXIMITY: u8 = 0;
81pub const TONED_ORDERING_CHROMA_PROXIMITY: u8 = 1;
82pub const TONED_MIN_BITS_PER_PIXEL: u8 = 1;
83pub const TONED_MAX_BITS_PER_PIXEL: u8 = 24;
84pub const TONED_MAX_SPAN_COUNT: usize = u16::MAX as usize;
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87pub enum CacheEncryptionAlgorithm {
88 #[serde(rename = "xchacha20-poly1305")]
89 XChaCha20Poly1305,
90}
91
92impl CacheEncryptionAlgorithm {
93 pub fn wire_code(self) -> u8 {
94 match self {
95 Self::XChaCha20Poly1305 => CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305,
96 }
97 }
98
99 pub fn from_wire_code(code: u8) -> Result<Self> {
100 match code {
101 CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305 => Ok(Self::XChaCha20Poly1305),
102 _ => bail!("unsupported cache encryption algorithm code {code}"),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108pub enum CacheKeyDerivation {
109 #[serde(rename = "hkdf-sha256")]
110 HkdfSha256,
111}
112
113impl CacheKeyDerivation {
114 pub fn wire_code(self) -> u8 {
115 match self {
116 Self::HkdfSha256 => 1,
117 }
118 }
119
120 pub fn from_wire_code(code: u8) -> Result<Self> {
121 match code {
122 1 => Ok(Self::HkdfSha256),
123 _ => bail!("unsupported cache key derivation code {code}"),
124 }
125 }
126}
127
128fn serialize_secret_base64url<S>(secret: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
129where
130 S: serde::Serializer,
131{
132 serializer.serialize_str(&URL_SAFE_NO_PAD.encode(secret))
133}
134
135fn deserialize_secret_base64url<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
136where
137 D: serde::Deserializer<'de>,
138{
139 let text = String::deserialize(deserializer)?;
140 URL_SAFE_NO_PAD
141 .decode(text.as_bytes())
142 .map_err(serde::de::Error::custom)
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(rename_all = "camelCase")]
147pub struct CacheEncryptionDescriptor {
148 pub version: u8,
149 pub algorithm: CacheEncryptionAlgorithm,
150 pub key_derivation: CacheKeyDerivation,
151 #[serde(
152 serialize_with = "serialize_secret_base64url",
153 deserialize_with = "deserialize_secret_base64url"
154 )]
155 pub secret: Vec<u8>,
156}
157
158impl CacheEncryptionDescriptor {
159 pub fn validate(&self) -> Result<()> {
160 if self.version != CACHE_ENCRYPTION_DESCRIPTOR_VERSION {
161 bail!(
162 "unsupported cache encryption descriptor version: {}",
163 self.version
164 );
165 }
166 match self.algorithm {
167 CacheEncryptionAlgorithm::XChaCha20Poly1305 => {}
168 }
169 match self.key_derivation {
170 CacheKeyDerivation::HkdfSha256 => {}
171 }
172 if self.secret.len() != CACHE_ENCRYPTION_SECRET_LENGTH {
173 bail!(
174 "cache encryption secret must be exactly {} bytes",
175 CACHE_ENCRYPTION_SECRET_LENGTH
176 );
177 }
178 Ok(())
179 }
180
181 pub fn secret(&self) -> &[u8] {
182 self.secret.as_slice()
183 }
184
185 pub fn from_secret_base64url(secret: &str) -> Result<Self> {
186 let secret = URL_SAFE_NO_PAD
187 .decode(secret.as_bytes())
188 .context("cache encryption secret is not valid base64url")?;
189 let descriptor = Self {
190 version: CACHE_ENCRYPTION_DESCRIPTOR_VERSION,
191 algorithm: CacheEncryptionAlgorithm::XChaCha20Poly1305,
192 key_derivation: CacheKeyDerivation::HkdfSha256,
193 secret,
194 };
195 descriptor.validate()?;
196 Ok(descriptor)
197 }
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
201#[serde(rename_all = "camelCase")]
202pub struct CacheEncryptionContext {
203 pub protocol_version: u8,
204 pub cache_format_version: u8,
205 pub cache_store_name: String,
206 pub cache_key: String,
207 pub chunk_index: u64,
208 pub packet_offset: u64,
209 pub plaintext_length: usize,
210 pub codec_identifier: String,
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct CacheEncryptionEnvelope {
215 pub version: u8,
216 pub algorithm: u8,
217 pub flags: u16,
218 pub record_binding_hash: [u8; CACHE_ENCRYPTION_RECORD_BINDING_HASH_LENGTH],
219 pub chunk_index: u64,
220 pub packet_offset: u64,
221 pub plaintext_length: u32,
222 pub nonce: [u8; CACHE_ENCRYPTION_NONCE_LENGTH],
223 pub ciphertext: Vec<u8>,
224}
225
226impl CacheEncryptionEnvelope {
227 pub const HEADER_LENGTH: usize = 4
228 + 1
229 + 1
230 + 2
231 + CACHE_ENCRYPTION_RECORD_BINDING_HASH_LENGTH
232 + 8
233 + 8
234 + 4
235 + CACHE_ENCRYPTION_NONCE_LENGTH;
236
237 pub fn parse(bytes: &[u8]) -> Result<Self> {
238 if bytes.len() < Self::HEADER_LENGTH + CACHE_ENCRYPTION_TAG_LENGTH {
239 bail!("invalid BCE1 envelope: truncated header or ciphertext");
240 }
241 if bytes.get(0..4) != Some(CACHE_ENCRYPTION_ENVELOPE_MAGIC.as_slice()) {
242 bail!("invalid BCE1 envelope: magic mismatch");
243 }
244
245 let version = bytes[4];
246 if version != CACHE_ENCRYPTION_ENVELOPE_VERSION {
247 bail!("unsupported BCE1 envelope version {version}");
248 }
249
250 let algorithm = bytes[5];
251 if algorithm != CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305 {
252 bail!("unsupported BCE1 envelope algorithm {algorithm}");
253 }
254
255 let flags = u16::from_be_bytes(bytes[6..8].try_into().expect("slice length"));
256 let record_binding_hash = bytes[8..40].try_into().expect("slice length");
257 let chunk_index = u64::from_be_bytes(bytes[40..48].try_into().expect("slice length"));
258 let packet_offset = u64::from_be_bytes(bytes[48..56].try_into().expect("slice length"));
259 let plaintext_length = u32::from_be_bytes(bytes[56..60].try_into().expect("slice length"));
260 let nonce = bytes[60..84].try_into().expect("slice length");
261 let ciphertext = bytes[84..].to_vec();
262
263 if plaintext_length == 0 {
264 bail!("invalid BCE1 envelope: empty plaintext length");
265 }
266 if ciphertext.len() != plaintext_length as usize + CACHE_ENCRYPTION_TAG_LENGTH {
267 bail!("invalid BCE1 envelope: ciphertext length mismatch");
268 }
269
270 Ok(Self {
271 version,
272 algorithm,
273 flags,
274 record_binding_hash,
275 chunk_index,
276 packet_offset,
277 plaintext_length,
278 nonce,
279 ciphertext,
280 })
281 }
282
283 pub fn encode(&self) -> Result<Vec<u8>> {
284 if self.version != CACHE_ENCRYPTION_ENVELOPE_VERSION {
285 bail!("unsupported BCE1 envelope version {}", self.version);
286 }
287 if self.algorithm != CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305 {
288 bail!("unsupported BCE1 envelope algorithm {}", self.algorithm);
289 }
290 if self.plaintext_length == 0 {
291 bail!("invalid BCE1 envelope: empty plaintext length");
292 }
293 if self.ciphertext.len() != self.plaintext_length as usize + CACHE_ENCRYPTION_TAG_LENGTH {
294 bail!("invalid BCE1 envelope: ciphertext length mismatch");
295 }
296
297 let mut out = Vec::with_capacity(Self::HEADER_LENGTH + self.ciphertext.len());
298 out.extend_from_slice(CACHE_ENCRYPTION_ENVELOPE_MAGIC);
299 out.push(self.version);
300 out.push(self.algorithm);
301 out.extend_from_slice(&self.flags.to_be_bytes());
302 out.extend_from_slice(&self.record_binding_hash);
303 out.extend_from_slice(&self.chunk_index.to_be_bytes());
304 out.extend_from_slice(&self.packet_offset.to_be_bytes());
305 out.extend_from_slice(&self.plaintext_length.to_be_bytes());
306 out.extend_from_slice(&self.nonce);
307 out.extend_from_slice(&self.ciphertext);
308 Ok(out)
309 }
310}
311
312fn push_u8(out: &mut Vec<u8>, value: u8) {
313 out.push(value);
314}
315
316fn push_u16(out: &mut Vec<u8>, value: u16) {
317 out.extend_from_slice(&value.to_be_bytes());
318}
319
320fn push_u32(out: &mut Vec<u8>, value: u32) {
321 out.extend_from_slice(&value.to_be_bytes());
322}
323
324fn push_u64(out: &mut Vec<u8>, value: u64) {
325 out.extend_from_slice(&value.to_be_bytes());
326}
327
328#[allow(dead_code)]
329fn push_len_prefixed_bytes(out: &mut Vec<u8>, tag: u8, bytes: &[u8]) {
330 out.push(tag);
331 push_u32(out, u32::try_from(bytes.len()).unwrap_or(u32::MAX));
332 out.extend_from_slice(bytes);
333}
334
335fn push_len_prefixed_string(out: &mut Vec<u8>, tag: u8, value: Option<&str>) {
336 out.push(tag);
337 match value {
338 Some(value) => {
339 let bytes = value.as_bytes();
340 push_u32(out, u32::try_from(bytes.len()).unwrap_or(u32::MAX));
341 out.extend_from_slice(bytes);
342 }
343 None => push_u32(out, 0),
344 }
345}
346
347fn push_len_prefixed_u8_slice<const N: usize>(out: &mut Vec<u8>, tag: u8, value: Option<&[u8; N]>) {
348 out.push(tag);
349 match value {
350 Some(value) => {
351 push_u32(out, N as u32);
352 out.extend_from_slice(value);
353 }
354 None => push_u32(out, 0),
355 }
356}
357
358fn cache_encryption_identity_bytes(descriptor: &RecordDescriptor) -> Result<Vec<u8>> {
359 let mut out = Vec::new();
360 out.extend_from_slice(b"bitneedle.record-descriptor.cache-identity.v1");
361 push_u8(&mut out, descriptor.version);
362 push_u8(&mut out, u8::from(descriptor.checksum_protected));
363 push_u64(&mut out, descriptor.b_value_bits);
364 push_len_prefixed_string(&mut out, 1, Some(&descriptor.record_profile));
365 push_u64(&mut out, descriptor.stream_byte_length as u64);
366 push_len_prefixed_string(&mut out, 2, Some(&descriptor.payload_encoding));
367 push_len_prefixed_string(&mut out, 3, descriptor.title.as_deref());
368 push_len_prefixed_string(&mut out, 4, descriptor.artist.as_deref());
369 push_len_prefixed_u8_slice(&mut out, 5, descriptor.release_id.as_ref());
370 push_len_prefixed_string(&mut out, 6, descriptor.catalog_number.as_deref());
371 push_len_prefixed_string(&mut out, 7, descriptor.label.as_deref());
372 push_len_prefixed_string(&mut out, 8, descriptor.artwork_credit.as_deref());
373 push_len_prefixed_string(&mut out, 9, descriptor.canonical_url.as_deref());
374 out.push(10);
375 match descriptor.created_at {
376 Some(value) => {
377 push_u32(&mut out, 8);
378 push_u64(&mut out, value);
379 }
380 None => push_u32(&mut out, 0),
381 }
382 out.push(12);
383 match descriptor.bsc_pointer.as_ref() {
384 Some(pointer) => {
385 push_u32(
386 &mut out,
387 u32::try_from(pointer.len()).context("BSC pointer exceeds u32")?,
388 );
389 out.extend_from_slice(pointer);
390 }
391 None => push_u32(&mut out, 0),
392 }
393 out.push(13);
394 push_u32(
395 &mut out,
396 u32::try_from(descriptor.tone_spans.len()).context("tone span count exceeds u32")?,
397 );
398 for span in &descriptor.tone_spans {
399 push_u32(
400 &mut out,
401 u32::try_from(span.byte_length).context("tone span byte length exceeds u32")?,
402 );
403 out.extend_from_slice(&span.base);
404 push_u8(&mut out, span.luma_tolerance);
405 push_u8(&mut out, span.bits_per_pixel);
406 push_u8(&mut out, span.ordering.wire_code());
407 }
408 out.push(14);
409 match descriptor.copyright_year {
410 Some(value) => {
411 push_u32(&mut out, 2);
412 push_u16(&mut out, value);
413 }
414 None => push_u32(&mut out, 0),
415 }
416 push_len_prefixed_string(&mut out, 15, descriptor.copyright_holder.as_deref());
417 Ok(out)
418}
419
420pub fn cache_encryption_record_binding_hash(
421 descriptor: &RecordDescriptor,
422) -> Result<[u8; CACHE_ENCRYPTION_RECORD_BINDING_HASH_LENGTH]> {
423 let identity = cache_encryption_identity_bytes(descriptor)?;
424 Ok(Sha256::digest(identity).into())
425}
426
427pub fn derive_cache_encryption_key(descriptor: &RecordDescriptor) -> Result<[u8; 32]> {
428 let cache_encryption = descriptor
429 .cache_encryption
430 .as_ref()
431 .context("record descriptor is missing cache encryption descriptor")?;
432 cache_encryption.validate()?;
433
434 let salt = cache_encryption_record_binding_hash(descriptor)?;
435 Ok(hkdf_sha256_32(
436 &salt,
437 cache_encryption.secret(),
438 CACHE_ENCRYPTION_INFO,
439 ))
440}
441
442fn derive_cache_nonce_key(descriptor: &RecordDescriptor) -> Result<[u8; 32]> {
445 let cache_encryption = descriptor
446 .cache_encryption
447 .as_ref()
448 .context("record descriptor is missing cache encryption descriptor")?;
449 cache_encryption.validate()?;
450
451 let salt = cache_encryption_record_binding_hash(descriptor)?;
452 Ok(hkdf_sha256_32(
453 &salt,
454 cache_encryption.secret(),
455 CACHE_ENCRYPTION_NONCE_INFO,
456 ))
457}
458
459fn derive_cache_nonce(
471 nonce_key: &[u8; 32],
472 context: &CacheEncryptionContext,
473 plaintext: &[u8],
474) -> [u8; CACHE_ENCRYPTION_NONCE_LENGTH] {
475 let plaintext_hash: [u8; 32] = Sha256::digest(plaintext).into();
476 let mut input =
477 Vec::with_capacity(CACHE_ENCRYPTION_NONCE_DOMAIN.len() + 32 + context.cache_key.len() + 20);
478 input.extend_from_slice(CACHE_ENCRYPTION_NONCE_DOMAIN);
479 input.extend_from_slice(&plaintext_hash);
480 push_len_prefixed_string(&mut input, 1, Some(&context.cache_key));
481 push_u64(&mut input, context.chunk_index);
482 push_u64(&mut input, context.packet_offset);
483 let mac = hmac_sha256(nonce_key, &input);
484 let mut nonce = [0u8; CACHE_ENCRYPTION_NONCE_LENGTH];
485 nonce.copy_from_slice(&mac[..CACHE_ENCRYPTION_NONCE_LENGTH]);
486 nonce
487}
488
489fn hex_encode(bytes: &[u8]) -> String {
490 const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
491 let mut out = String::with_capacity(bytes.len() * 2);
492 for byte in bytes {
493 out.push(HEX_DIGITS[(byte >> 4) as usize] as char);
494 out.push(HEX_DIGITS[(byte & 0x0f) as usize] as char);
495 }
496 out
497}
498
499pub fn cache_encryption_record_binding_hash_hex(descriptor: &RecordDescriptor) -> Result<String> {
504 Ok(hex_encode(&cache_encryption_record_binding_hash(
505 descriptor,
506 )?))
507}
508
509fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
510 const BLOCK_SIZE: usize = 64;
511 let mut key_block = [0u8; BLOCK_SIZE];
512 if key.len() > BLOCK_SIZE {
513 let hashed: [u8; 32] = Sha256::digest(key).into();
514 key_block[..hashed.len()].copy_from_slice(&hashed);
515 } else {
516 key_block[..key.len()].copy_from_slice(key);
517 }
518
519 let mut inner_pad = [0u8; BLOCK_SIZE];
520 let mut outer_pad = [0u8; BLOCK_SIZE];
521 for index in 0..BLOCK_SIZE {
522 inner_pad[index] = key_block[index] ^ 0x36;
523 outer_pad[index] = key_block[index] ^ 0x5c;
524 }
525
526 let mut inner = Sha256::new();
527 inner.update(inner_pad);
528 inner.update(data);
529 let inner_digest = inner.finalize();
530
531 let mut outer = Sha256::new();
532 outer.update(outer_pad);
533 outer.update(inner_digest);
534 outer.finalize().into()
535}
536
537fn hkdf_sha256_32(salt: &[u8], ikm: &[u8], info: &[u8]) -> [u8; 32] {
538 let prk = hmac_sha256(salt, ikm);
539 let mut okm_input = Vec::with_capacity(info.len() + 1);
540 okm_input.extend_from_slice(info);
541 okm_input.push(1);
542 hmac_sha256(&prk, &okm_input)
543}
544
545pub fn cache_encryption_aad(
546 descriptor: &RecordDescriptor,
547 context: &CacheEncryptionContext,
548) -> Result<Vec<u8>> {
549 let binding_hash = cache_encryption_record_binding_hash(descriptor)?;
550 let mut out = Vec::new();
551 out.extend_from_slice(CACHE_ENCRYPTION_AAD_DOMAIN);
552 push_u8(&mut out, context.protocol_version);
553 push_u8(&mut out, context.cache_format_version);
554 out.extend_from_slice(&binding_hash);
555 push_len_prefixed_string(&mut out, 1, Some(&context.cache_store_name));
556 push_len_prefixed_string(&mut out, 2, Some(&context.cache_key));
557 push_u64(&mut out, context.chunk_index);
558 push_u64(&mut out, context.packet_offset);
559 push_u64(
560 &mut out,
561 u64::try_from(context.plaintext_length).context("plaintext length exceeds u64")?,
562 );
563 push_len_prefixed_string(&mut out, 3, Some(&context.codec_identifier));
564 Ok(out)
565}
566
567pub fn encrypt_cache_envelope(
568 descriptor: &RecordDescriptor,
569 context: &CacheEncryptionContext,
570 plaintext: &[u8],
571) -> Result<Vec<u8>> {
572 if plaintext.is_empty() {
573 bail!("cache plaintext must not be empty");
574 }
575 if plaintext.len() != context.plaintext_length {
576 bail!("cache plaintext length mismatch");
577 }
578 if !descriptor
579 .cache_encryption
580 .as_ref()
581 .is_some_and(|value| value.validate().is_ok())
582 {
583 return Err(anyhow::anyhow!(
584 "record descriptor is missing a valid cache encryption descriptor"
585 ));
586 }
587
588 let key = derive_cache_encryption_key(descriptor)?;
589 let nonce_key = derive_cache_nonce_key(descriptor)?;
590 let nonce = derive_cache_nonce(&nonce_key, context, plaintext);
591 let aad = cache_encryption_aad(descriptor, context)?;
592 let ciphertext = XChaCha20Poly1305::new(Key::from_slice(&key))
593 .encrypt(
594 XNonce::from_slice(&nonce),
595 AeadPayload {
596 msg: plaintext,
597 aad: &aad,
598 },
599 )
600 .map_err(|_| anyhow::anyhow!("failed to encrypt cache payload"))?;
601
602 let envelope = CacheEncryptionEnvelope {
603 version: CACHE_ENCRYPTION_ENVELOPE_VERSION,
604 algorithm: CACHE_ENCRYPTION_ENVELOPE_ALGORITHM_XCHACHA20POLY1305,
605 flags: 0,
606 record_binding_hash: cache_encryption_record_binding_hash(descriptor)?,
607 chunk_index: context.chunk_index,
608 packet_offset: context.packet_offset,
609 plaintext_length: u32::try_from(plaintext.len()).context("plaintext length exceeds u32")?,
610 nonce,
611 ciphertext,
612 };
613 envelope.encode()
614}
615
616pub fn decrypt_cache_envelope(
617 descriptor: &RecordDescriptor,
618 context: &CacheEncryptionContext,
619 envelope_bytes: &[u8],
620) -> Result<Vec<u8>> {
621 let envelope = CacheEncryptionEnvelope::parse(envelope_bytes)?;
622 let expected_binding_hash = cache_encryption_record_binding_hash(descriptor)?;
623 if envelope.record_binding_hash != expected_binding_hash {
624 bail!("record binding hash mismatch");
625 }
626 let mut resolved_context = context.clone();
627 resolved_context.chunk_index = envelope.chunk_index;
628 resolved_context.packet_offset = envelope.packet_offset;
629 resolved_context.plaintext_length = envelope.plaintext_length as usize;
630 let key = derive_cache_encryption_key(descriptor)?;
631 let aad = cache_encryption_aad(descriptor, &resolved_context)?;
632 XChaCha20Poly1305::new(Key::from_slice(&key))
633 .decrypt(
634 XNonce::from_slice(&envelope.nonce),
635 AeadPayload {
636 msg: &envelope.ciphertext,
637 aad: &aad,
638 },
639 )
640 .map_err(|_| anyhow::anyhow!("cache authentication failed"))
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
644#[serde(rename_all = "camelCase")]
645pub enum ToneOrdering {
646 BaseProximity,
647 ChromaProximity,
648}
649
650impl ToneOrdering {
651 pub fn wire_code(self) -> u8 {
652 match self {
653 Self::BaseProximity => TONED_ORDERING_BASE_PROXIMITY,
654 Self::ChromaProximity => TONED_ORDERING_CHROMA_PROXIMITY,
655 }
656 }
657
658 pub fn from_wire_code(code: u8) -> Result<Self> {
659 match code {
660 TONED_ORDERING_BASE_PROXIMITY => Ok(Self::BaseProximity),
661 TONED_ORDERING_CHROMA_PROXIMITY => Ok(Self::ChromaProximity),
662 _ => bail!("unknown toned carrier ordering code {code}"),
663 }
664 }
665}
666
667#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
668#[serde(rename_all = "camelCase")]
669pub struct ToneSpanDescriptor {
670 pub byte_length: usize,
671 pub base: [u8; 3],
672 pub luma_tolerance: u8,
673 pub bits_per_pixel: u8,
674 pub ordering: ToneOrdering,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(rename_all = "camelCase")]
679pub struct ResolvedToneSpan {
680 pub index: usize,
681 pub byte_offset: usize,
682 pub byte_length: usize,
683 pub pixel_offset: usize,
684 pub pixel_count: usize,
685 pub base: [u8; 3],
686 pub luma_tolerance: u8,
687 pub bits_per_pixel: u8,
688 pub ordering: ToneOrdering,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(rename_all = "camelCase")]
697pub struct SignedReleaseReference {
698 pub version: u8,
699 pub release_commitment_sha256: [u8; SIGNED_RELEASE_REFERENCE_HASH_LENGTH],
700 pub key_id: Vec<u8>,
701 pub signature: Vec<u8>,
702}
703
704impl SignedReleaseReference {
705 pub fn validate(&self) -> Result<()> {
706 if self.version != SIGNED_RELEASE_REFERENCE_VERSION {
707 bail!(
708 "unsupported signed release reference version: {}",
709 self.version
710 );
711 }
712 if self.key_id.is_empty() {
713 bail!("signature key ID must not be empty");
714 }
715 if self.key_id.len() > SIGNED_RELEASE_REFERENCE_MAX_KEY_ID_LENGTH {
716 bail!("signature key ID exceeds u16 length limit");
717 }
718 if self.signature.len() != SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH {
719 bail!("signature must be exactly {SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH} bytes");
720 }
721 Ok(())
722 }
723}
724
725pub fn encode_cache_encryption_descriptor(
726 cache_encryption: &CacheEncryptionDescriptor,
727) -> Result<Vec<u8>> {
728 cache_encryption.validate()?;
729 let mut out = Vec::with_capacity(4 + CACHE_ENCRYPTION_SECRET_LENGTH);
730 out.push(cache_encryption.version);
731 out.push(cache_encryption.algorithm.wire_code());
732 out.push(cache_encryption.key_derivation.wire_code());
733 out.push(
734 u8::try_from(cache_encryption.secret.len())
735 .context("cache encryption secret exceeds u8")?,
736 );
737 out.extend_from_slice(cache_encryption.secret());
738 Ok(out)
739}
740
741pub fn decode_cache_encryption_descriptor(bytes: &[u8]) -> Result<CacheEncryptionDescriptor> {
742 if bytes.len() < 4 {
743 bail!("cache encryption descriptor is truncated");
744 }
745 let version = bytes[0];
746 let algorithm = CacheEncryptionAlgorithm::from_wire_code(bytes[1])?;
747 let key_derivation = CacheKeyDerivation::from_wire_code(bytes[2])?;
748 let secret_len = usize::from(bytes[3]);
749 let secret = bytes[4..].to_vec();
750 if secret_len != secret.len() {
751 bail!("cache encryption secret length mismatch");
752 }
753 let descriptor = CacheEncryptionDescriptor {
754 version,
755 algorithm,
756 key_derivation,
757 secret,
758 };
759 descriptor.validate()?;
760 Ok(descriptor)
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
770#[serde(rename_all = "camelCase")]
771pub struct RecordDescriptor {
772 pub version: u8,
773 pub checksum_protected: bool,
774 pub b_value_bits: u64,
775 pub record_profile: String,
776 pub stream_byte_length: usize,
777 pub payload_encoding: String,
778 pub title: Option<String>,
779 pub artist: Option<String>,
780 pub release_id: Option<[u8; RELEASE_ID_LENGTH]>,
781 pub catalog_number: Option<String>,
782 pub label: Option<String>,
783 pub artwork_credit: Option<String>,
784 pub canonical_url: Option<String>,
785 pub created_at: Option<u64>,
786 pub copyright_year: Option<u16>,
788 pub copyright_holder: Option<String>,
791 pub signed_release_reference: Option<SignedReleaseReference>,
792 pub bsc_pointer: Option<Vec<u8>>,
793 pub tone_spans: Vec<ToneSpanDescriptor>,
794 pub cache_encryption: Option<CacheEncryptionDescriptor>,
795}
796
797impl RecordDescriptor {
798 pub fn b_value(&self) -> f64 {
799 f64::from_bits(self.b_value_bits)
800 }
801
802 pub fn cache_encryption(&self) -> Option<&CacheEncryptionDescriptor> {
803 self.cache_encryption.as_ref()
804 }
805
806 pub fn validate_cache_encryption(&self) -> Result<()> {
807 if let Some(cache_encryption) = self.cache_encryption.as_ref() {
808 cache_encryption.validate()?;
809 }
810 Ok(())
811 }
812}
813
814#[derive(Debug, Clone, PartialEq, Eq)]
815pub struct DescriptorPrefix {
816 pub version: u8,
817 pub payload_len: usize,
818 pub segment_count: usize,
819 pub segment_stream_len: usize,
820 pub b_value_bits: u64,
821}
822
823pub fn metadata_pixel_count_for_byte_length(byte_length: usize) -> usize {
824 byte_length.saturating_mul(2)
825}
826
827pub fn metadata_byte_capacity_for_pixel_count(pixel_count: usize) -> usize {
828 pixel_count / 2
829}
830
831pub fn metadata_bytes_from_grayscale_rgba(
832 rgba: &[u8],
833 indices: &[usize],
834 byte_length: usize,
835 label: &str,
836) -> Result<Vec<u8>> {
837 let pixel_count = metadata_pixel_count_for_byte_length(byte_length);
838 if indices.len() < pixel_count {
839 bail!("{label} spiral capacity is too small");
840 }
841
842 let mut bytes = Vec::with_capacity(byte_length);
843 for byte_number in 0..byte_length {
844 let mut nibbles = [0u8; 2];
845 for nibble_index in 0..2 {
846 let pixel_index = indices[byte_number * 2 + nibble_index];
847 let rgba_index = pixel_index
848 .checked_mul(4)
849 .context("metadata RGBA index overflow")?;
850 if rgba_index + 3 >= rgba.len() {
851 bail!("{label} spiral pixel index is outside RGBA buffer");
852 }
853
854 let red = rgba[rgba_index];
855 let green = rgba[rgba_index + 1];
856 let blue = rgba[rgba_index + 2];
857 let alpha = rgba[rgba_index + 3];
858
859 if alpha == 0 {
860 bail!("{label} spiral pixel is empty");
861 }
862 if red != green || green != blue {
863 bail!("{label} metadata pixel is not grayscale");
864 }
865
866 let nibble = red
867 .checked_sub(METADATA_GRAYSCALE_NIBBLE_BASE)
868 .context("metadata pixel is below grayscale nibble range")?;
869 if nibble > 0x0f {
870 bail!("{label} metadata pixel is outside grayscale nibble range");
871 }
872 nibbles[nibble_index] = nibble;
873 }
874 bytes.push((nibbles[0] << 4) | nibbles[1]);
875 }
876 Ok(bytes)
877}
878
879pub fn record_profile_code(record_profile: &str) -> Result<u8> {
880 match record_profile {
881 RECORD_PROFILE_SINGLE45 => Ok(RECORD_PROFILE_SINGLE45_CODE),
882 RECORD_PROFILE_LP => Ok(RECORD_PROFILE_LP_CODE),
883 other => bail!("unsupported canonical record profile {other}"),
884 }
885}
886
887pub fn record_profile_from_code(code: u8) -> Result<String> {
888 match code {
889 RECORD_PROFILE_SINGLE45_CODE => Ok(RECORD_PROFILE_SINGLE45.to_string()),
890 RECORD_PROFILE_LP_CODE => Ok(RECORD_PROFILE_LP.to_string()),
891 other => bail!("unknown record profile code {other}"),
892 }
893}
894
895pub fn payload_encoding_code(payload_encoding: &str) -> Result<u8> {
896 match payload_encoding {
897 PAYLOAD_ENCODING_RGB => Ok(PAYLOAD_ENCODING_RGB_CODE),
898 PAYLOAD_ENCODING_TONED_V1 => Ok(PAYLOAD_ENCODING_TONED_V1_CODE),
899 other => bail!("unsupported canonical payload encoding {other}"),
900 }
901}
902
903pub fn payload_encoding_from_code(code: u8) -> Result<String> {
904 match code {
905 PAYLOAD_ENCODING_RGB_CODE => Ok(PAYLOAD_ENCODING_RGB.to_string()),
906 PAYLOAD_ENCODING_TONED_V1_CODE => Ok(PAYLOAD_ENCODING_TONED_V1.to_string()),
907 other => bail!("unknown payload encoding code {other}"),
908 }
909}
910
911const RELEASE_ID_TAGGED_PREFIX: &str = "rel_";
912const RELEASE_ID_ULID_TEXT_LENGTH: usize = 26;
913const CROCKFORD_BASE32: &[u8; 32] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
914
915pub fn release_id_to_bytes(text: &str) -> Result<[u8; RELEASE_ID_LENGTH]> {
917 let ulid_text = text
918 .strip_prefix(RELEASE_ID_TAGGED_PREFIX)
919 .context("release ID is missing the rel_ prefix")?;
920 if ulid_text.len() != RELEASE_ID_ULID_TEXT_LENGTH {
921 bail!("release ID must be 26 Crockford Base32 characters");
922 }
923
924 let mut bits: u128 = 0;
925 for (index, byte) in ulid_text.bytes().enumerate() {
926 let upper = byte.to_ascii_uppercase();
927 let digit = CROCKFORD_BASE32
928 .iter()
929 .position(|&candidate| candidate == upper)
930 .context("release ID contains a non-canonical Crockford Base32 character")?;
931
932 if index == 0 && digit > 7 {
937 bail!("release ID exceeds the 128-bit ULID range");
938 }
939
940 bits = (bits << 5) | digit as u128;
941 }
942 Ok(bits.to_be_bytes())
943}
944
945pub fn release_id_to_text(bytes: [u8; RELEASE_ID_LENGTH]) -> String {
948 let mut value = u128::from_be_bytes(bytes);
949 let mut chars = [b'0'; RELEASE_ID_ULID_TEXT_LENGTH];
950 for index in (0..RELEASE_ID_ULID_TEXT_LENGTH).rev() {
951 chars[index] = CROCKFORD_BASE32[(value & 0x1f) as usize];
952 value >>= 5;
953 }
954 let mut text =
955 String::with_capacity(RELEASE_ID_TAGGED_PREFIX.len() + RELEASE_ID_ULID_TEXT_LENGTH);
956 text.push_str(RELEASE_ID_TAGGED_PREFIX);
957 text.push_str(std::str::from_utf8(&chars).expect("Crockford Base32 alphabet is ASCII"));
958 text
959}
960
961pub fn decode_descriptor_prefix(bytes: &[u8]) -> Result<DescriptorPrefix> {
962 if bytes.len() < RECORD_DESCRIPTOR_PREFIX_LENGTH {
963 bail!("record descriptor payload too short");
964 }
965 if &bytes[..4] != RECORD_DESCRIPTOR_MAGIC {
966 bail!("record descriptor magic mismatch");
967 }
968
969 let version = bytes[4];
970 let payload_len = u16::from_be_bytes(bytes[5..7].try_into().expect("slice length")) as usize;
971 let segment_count = u16::from_be_bytes(bytes[7..9].try_into().expect("slice length")) as usize;
972 let segment_stream_len =
973 u16::from_be_bytes(bytes[9..11].try_into().expect("slice length")) as usize;
974 let b_value_bits = u64::from_be_bytes(bytes[11..19].try_into().expect("slice length"));
975
976 if payload_len < RECORD_DESCRIPTOR_PREFIX_LENGTH || payload_len > bytes.len() {
977 bail!("record descriptor payload length is invalid");
978 }
979
980 Ok(DescriptorPrefix {
981 version,
982 payload_len,
983 segment_count,
984 segment_stream_len,
985 b_value_bits,
986 })
987}
988
989pub fn validate_tone_span(span: &ToneSpanDescriptor, index: usize) -> Result<()> {
990 if span.byte_length == 0 {
991 bail!("tone span {index} byte length must be greater than zero");
992 }
993 if !(TONED_MIN_BITS_PER_PIXEL..=TONED_MAX_BITS_PER_PIXEL).contains(&span.bits_per_pixel) {
994 bail!(
995 "tone span {index} bits per pixel must be between {} and {}",
996 TONED_MIN_BITS_PER_PIXEL,
997 TONED_MAX_BITS_PER_PIXEL
998 );
999 }
1000 Ok(())
1001}
1002
1003pub fn resolve_tone_spans(
1004 spans: &[ToneSpanDescriptor],
1005 expected_byte_length: Option<usize>,
1006) -> Result<Vec<ResolvedToneSpan>> {
1007 if spans.is_empty() {
1008 bail!("toned-v1 carrier map must contain at least one span");
1009 }
1010 if spans.len() > TONED_MAX_SPAN_COUNT {
1011 bail!("tone span count exceeds u16 range");
1012 }
1013
1014 let mut byte_offset = 0usize;
1015 let mut pixel_offset = 0usize;
1016 let mut resolved = Vec::with_capacity(spans.len());
1017
1018 for (index, span) in spans.iter().enumerate() {
1019 validate_tone_span(span, index)?;
1020 let bit_length = span
1021 .byte_length
1022 .checked_mul(8)
1023 .context("tone span bit length overflow")?;
1024 let pixel_count = bit_length.div_ceil(usize::from(span.bits_per_pixel));
1025
1026 resolved.push(ResolvedToneSpan {
1027 index,
1028 byte_offset,
1029 byte_length: span.byte_length,
1030 pixel_offset,
1031 pixel_count,
1032 base: span.base,
1033 luma_tolerance: span.luma_tolerance,
1034 bits_per_pixel: span.bits_per_pixel,
1035 ordering: span.ordering,
1036 });
1037
1038 byte_offset = byte_offset
1039 .checked_add(span.byte_length)
1040 .context("tone span total byte length overflow")?;
1041 pixel_offset = pixel_offset
1042 .checked_add(pixel_count)
1043 .context("tone span total pixel count overflow")?;
1044 }
1045
1046 if let Some(expected) = expected_byte_length {
1047 if byte_offset != expected {
1048 bail!("tone spans cover {byte_offset} bytes, expected {expected}");
1049 }
1050 }
1051
1052 Ok(resolved)
1053}
1054
1055pub fn toned_pixel_count(
1056 spans: &[ToneSpanDescriptor],
1057 expected_byte_length: Option<usize>,
1058) -> Result<usize> {
1059 Ok(resolve_tone_spans(spans, expected_byte_length)?
1060 .last()
1061 .map(|span| span.pixel_offset + span.pixel_count)
1062 .unwrap_or(0))
1063}
1064
1065pub fn encode_toned_carrier_map(
1066 spans: &[ToneSpanDescriptor],
1067 expected_byte_length: Option<usize>,
1068) -> Result<Vec<u8>> {
1069 resolve_tone_spans(spans, expected_byte_length)?;
1070
1071 let mut out = Vec::new();
1072 out.push(TONED_CARRIER_MAP_VERSION);
1073 out.extend_from_slice(
1074 &u16::try_from(spans.len())
1075 .context("tone span count exceeds u16")?
1076 .to_be_bytes(),
1077 );
1078
1079 for span in spans {
1080 push_varuint(
1081 &mut out,
1082 u64::try_from(span.byte_length).context("tone span byte length exceeds u64")?,
1083 );
1084 out.extend_from_slice(&span.base);
1085 out.push(span.luma_tolerance);
1086 out.push(span.bits_per_pixel);
1087 out.push(span.ordering.wire_code());
1088 }
1089
1090 Ok(out)
1091}
1092
1093pub fn decode_toned_carrier_map(
1094 bytes: &[u8],
1095 expected_byte_length: Option<usize>,
1096) -> Result<Vec<ToneSpanDescriptor>> {
1097 let mut cursor = ByteCursor::new(bytes);
1098 let version = cursor.read_u8("toned carrier map version")?;
1099 if version != TONED_CARRIER_MAP_VERSION {
1100 bail!("unsupported toned carrier map version {version}");
1101 }
1102
1103 let count = usize::from(cursor.read_u16be("tone span count")?);
1104 if count == 0 {
1105 bail!("toned-v1 carrier map must contain at least one span");
1106 }
1107
1108 let mut spans = Vec::with_capacity(count);
1109 for index in 0..count {
1110 let byte_length = usize::try_from(cursor.read_varuint("tone span byte length")?)
1111 .context("tone span byte length exceeds usize")?;
1112 let base = [
1113 cursor.read_u8("tone span base red")?,
1114 cursor.read_u8("tone span base green")?,
1115 cursor.read_u8("tone span base blue")?,
1116 ];
1117 let luma_tolerance = cursor.read_u8("tone span luma tolerance")?;
1118 let bits_per_pixel = cursor.read_u8("tone span bits per pixel")?;
1119 let ordering = ToneOrdering::from_wire_code(cursor.read_u8("tone span ordering")?)?;
1120
1121 let span = ToneSpanDescriptor {
1122 byte_length,
1123 base,
1124 luma_tolerance,
1125 bits_per_pixel,
1126 ordering,
1127 };
1128 validate_tone_span(&span, index)?;
1129 spans.push(span);
1130 }
1131
1132 if cursor.remaining() != 0 {
1133 bail!(
1134 "toned carrier map contains {} trailing bytes",
1135 cursor.remaining()
1136 );
1137 }
1138
1139 resolve_tone_spans(&spans, expected_byte_length)?;
1140 Ok(spans)
1141}
1142
1143fn push_varuint(out: &mut Vec<u8>, mut value: u64) {
1144 loop {
1145 let mut byte = (value & 0x7f) as u8;
1146 value >>= 7;
1147 if value != 0 {
1148 byte |= 0x80;
1149 }
1150 out.push(byte);
1151 if value == 0 {
1152 break;
1153 }
1154 }
1155}
1156
1157pub fn decode_signed_release_reference(bytes: &[u8]) -> Result<SignedReleaseReference> {
1158 let mut cursor = ByteCursor::new(bytes);
1159
1160 let version = cursor.read_u8("signed release reference version")?;
1161 let release_commitment_sha256 = cursor
1162 .read_bytes(
1163 SIGNED_RELEASE_REFERENCE_HASH_LENGTH,
1164 "release commitment SHA-256",
1165 )?
1166 .try_into()
1167 .expect("length checked");
1168 let key_id_len = cursor.read_u16be("signature key ID length")? as usize;
1169 let key_id = cursor.read_bytes(key_id_len, "signature key ID")?.to_vec();
1170 let signature = cursor
1171 .read_bytes(SIGNED_RELEASE_REFERENCE_SIGNATURE_LENGTH, "signature")?
1172 .to_vec();
1173
1174 if cursor.remaining() != 0 {
1175 bail!(
1176 "signed release reference contains {} trailing bytes",
1177 cursor.remaining()
1178 );
1179 }
1180
1181 let reference = SignedReleaseReference {
1182 version,
1183 release_commitment_sha256,
1184 key_id,
1185 signature,
1186 };
1187 reference.validate()?;
1188 Ok(reference)
1189}
1190
1191pub fn decode_record_descriptor_bytes(bytes: &[u8]) -> Result<RecordDescriptor> {
1192 let prefix = decode_descriptor_prefix(bytes)?;
1193
1194 if prefix.version != RECORD_DESCRIPTOR_VERSION {
1195 bail!("record descriptor version mismatch");
1196 }
1197 if prefix.payload_len != RECORD_DESCRIPTOR_PREFIX_LENGTH + prefix.segment_stream_len {
1198 bail!("record descriptor segment stream length mismatch");
1199 }
1200
1201 let body = &bytes[RECORD_DESCRIPTOR_PREFIX_LENGTH..prefix.payload_len];
1202 let mut offset = 0usize;
1203 let mut parsed_segments = 0usize;
1204
1205 let mut crc32_range = None;
1206 let mut crc32 = None;
1207 let mut stream_byte_length = None;
1208 let mut record_profile = None;
1209 let mut payload_encoding = None;
1210 let mut title = None;
1211 let mut artist = None;
1212 let mut release_id = None;
1213 let mut catalog_number = None;
1214 let mut label = None;
1215 let mut artwork_credit = None;
1216 let mut canonical_url = None;
1217 let mut created_at = None;
1218 let mut copyright_year = None;
1219 let mut copyright_holder = None;
1220 let mut signed_release_reference = None;
1221 let mut bsc_pointer = None;
1222 let mut tone_spans = None;
1223 let mut cache_encryption = None;
1224
1225 while offset < body.len() {
1226 if parsed_segments >= prefix.segment_count {
1227 bail!("record descriptor contains more segments than declared");
1228 }
1229 if offset + 3 > body.len() {
1230 bail!("record descriptor segment is truncated");
1231 }
1232
1233 let kind = body[offset];
1234 let len = u16::from_be_bytes(
1235 body[offset + 1..offset + 3]
1236 .try_into()
1237 .expect("slice length"),
1238 ) as usize;
1239 let payload_start = offset + 3;
1240 let payload_end = payload_start
1241 .checked_add(len)
1242 .context("record descriptor segment length overflow")?;
1243 if payload_end > body.len() {
1244 bail!("record descriptor segment payload is truncated");
1245 }
1246
1247 let payload = &body[payload_start..payload_end];
1248 match kind {
1249 SEGMENT_DESCRIPTOR_CRC32 => {
1250 if crc32.is_some() {
1251 bail!("duplicate record descriptor CRC32 segment");
1252 }
1253 if payload.len() != 4 {
1254 bail!("record descriptor CRC32 segment has invalid length");
1255 }
1256 crc32 = Some(u32::from_be_bytes(
1257 payload.try_into().expect("slice length"),
1258 ));
1259 let absolute_start = RECORD_DESCRIPTOR_PREFIX_LENGTH + payload_start;
1260 crc32_range = Some(absolute_start..absolute_start + payload.len());
1261 }
1262 SEGMENT_STREAM_BYTE_LENGTH => {
1263 if stream_byte_length.is_some() {
1264 bail!("duplicate stream byte length segment");
1265 }
1266 if payload.len() != 4 {
1267 bail!("stream byte length segment has invalid length");
1268 }
1269 let raw_len = u32::from_be_bytes(payload.try_into().expect("slice length"));
1270 if raw_len == 0 {
1271 bail!("stream byte length must not be zero");
1272 }
1273 stream_byte_length = Some(raw_len as usize);
1274 }
1275 SEGMENT_RECORD_PROFILE => {
1276 if payload.len() != 1 {
1277 bail!("record profile segment has invalid length");
1278 }
1279 assign_once(
1280 &mut record_profile,
1281 record_profile_from_code(payload[0])?,
1282 "record profile",
1283 )?
1284 }
1285 SEGMENT_PAYLOAD_ENCODING => {
1286 if payload.len() != 1 {
1287 bail!("payload encoding segment has invalid length");
1288 }
1289 assign_once(
1290 &mut payload_encoding,
1291 payload_encoding_from_code(payload[0])?,
1292 "payload encoding",
1293 )?
1294 }
1295 SEGMENT_TITLE => {
1296 assign_once(&mut title, decode_optional_text(payload, "title")?, "title")?
1297 }
1298 SEGMENT_ARTIST => assign_once(
1299 &mut artist,
1300 decode_optional_text(payload, "artist")?,
1301 "artist",
1302 )?,
1303 SEGMENT_RELEASE_ID => {
1304 if payload.len() != RELEASE_ID_LENGTH {
1305 bail!("release ID segment has invalid length");
1306 }
1307 assign_once(
1308 &mut release_id,
1309 <[u8; RELEASE_ID_LENGTH]>::try_from(payload).expect("length checked"),
1310 "release ID",
1311 )?
1312 }
1313 SEGMENT_CATALOG_NUMBER => assign_once(
1314 &mut catalog_number,
1315 decode_optional_text(payload, "catalog number")?,
1316 "catalog number",
1317 )?,
1318 SEGMENT_LABEL => {
1319 assign_once(&mut label, decode_optional_text(payload, "label")?, "label")?
1320 }
1321 SEGMENT_ARTWORK_CREDIT => assign_once(
1322 &mut artwork_credit,
1323 decode_optional_text(payload, "artwork credit")?,
1324 "artwork credit",
1325 )?,
1326 SEGMENT_CANONICAL_URL => assign_once(
1327 &mut canonical_url,
1328 decode_optional_text(payload, "canonical URL")?,
1329 "canonical URL",
1330 )?,
1331 SEGMENT_CREATED_AT => {
1332 if payload.len() != 8 {
1333 bail!("created-at segment has invalid length");
1334 }
1335 assign_once(
1336 &mut created_at,
1337 u64::from_be_bytes(payload.try_into().expect("slice length")),
1338 "created-at timestamp",
1339 )?
1340 }
1341 SEGMENT_COPYRIGHT_YEAR => {
1342 if payload.len() != 2 {
1343 bail!("copyright-year segment has invalid length");
1344 }
1345 assign_once(
1346 &mut copyright_year,
1347 u16::from_be_bytes(payload.try_into().expect("slice length")),
1348 "copyright year",
1349 )?
1350 }
1351 SEGMENT_COPYRIGHT_HOLDER => assign_once(
1352 &mut copyright_holder,
1353 decode_optional_text(payload, "copyright holder")?,
1354 "copyright holder",
1355 )?,
1356 SEGMENT_SIGNED_RELEASE_REFERENCE => {
1357 if signed_release_reference.is_some() {
1358 bail!("duplicate signed release reference segment");
1359 }
1360 signed_release_reference = Some(decode_signed_release_reference(payload)?);
1361 }
1362 SEGMENT_BSC_POINTER => {
1363 if bsc_pointer.is_some() {
1364 bail!("duplicate BSC pointer segment");
1365 }
1366 if payload.is_empty() {
1367 bail!("BSC pointer segment must not be empty");
1368 }
1369 bsc_pointer = Some(payload.to_vec());
1370 }
1371 SEGMENT_TONED_CARRIER_MAP => {
1372 if tone_spans.is_some() {
1373 bail!("duplicate toned carrier map segment");
1374 }
1375 tone_spans = Some(decode_toned_carrier_map(payload, None)?);
1376 }
1377 SEGMENT_CACHE_ENCRYPTION => {
1378 if cache_encryption.is_some() {
1379 bail!("duplicate cache encryption segment");
1380 }
1381 cache_encryption = Some(decode_cache_encryption_descriptor(payload)?);
1382 }
1383 _ => bail!("unsupported canonical record descriptor segment type {kind}"),
1384 }
1385
1386 offset = payload_end;
1387 parsed_segments += 1;
1388 }
1389
1390 if parsed_segments != prefix.segment_count {
1391 bail!(
1392 "record descriptor segment count mismatch: declared {}, parsed {}",
1393 prefix.segment_count,
1394 parsed_segments
1395 );
1396 }
1397
1398 let expected = crc32.context("record descriptor CRC32 segment is missing")?;
1399 let range = crc32_range.context("record descriptor CRC32 segment is missing")?;
1400 let mut canonical = bytes[..prefix.payload_len].to_vec();
1401 canonical[range].fill(0);
1402
1403 if compute_descriptor_crc32(&canonical) != expected {
1404 bail!("record descriptor CRC32 mismatch");
1405 }
1406
1407 let b_value = f64::from_bits(prefix.b_value_bits);
1408 if !(b_value.is_finite() && b_value > 0.0) {
1409 bail!("decoded invalid b_value");
1410 }
1411
1412 let record_profile = record_profile.context("record profile segment is missing")?;
1413 let stream_byte_length = stream_byte_length.context("stream byte length segment is missing")?;
1414 let payload_encoding = payload_encoding.context("payload encoding segment is missing")?;
1415 let tone_spans = tone_spans.unwrap_or_default();
1416
1417 match payload_encoding.as_str() {
1418 PAYLOAD_ENCODING_RGB => {
1419 if !tone_spans.is_empty() {
1420 bail!("rgb payload encoding must not include a toned carrier map");
1421 }
1422 }
1423 PAYLOAD_ENCODING_TONED_V1 => {
1424 if tone_spans.is_empty() {
1425 bail!("toned-v1 payload encoding requires a toned carrier map");
1426 }
1427 resolve_tone_spans(&tone_spans, Some(stream_byte_length))?;
1428 }
1429 other => bail!("unsupported canonical payload encoding {other}"),
1430 }
1431
1432 Ok(RecordDescriptor {
1433 version: prefix.version,
1434 checksum_protected: true,
1435 b_value_bits: prefix.b_value_bits,
1436 record_profile,
1437 stream_byte_length,
1438 payload_encoding,
1439 title: title.flatten(),
1440 artist: artist.flatten(),
1441 release_id,
1442 catalog_number: catalog_number.flatten(),
1443 label: label.flatten(),
1444 artwork_credit: artwork_credit.flatten(),
1445 canonical_url: canonical_url.flatten(),
1446 created_at,
1447 copyright_year,
1448 copyright_holder: copyright_holder.flatten(),
1449 signed_release_reference,
1450 bsc_pointer,
1451 tone_spans,
1452 cache_encryption,
1453 })
1454}
1455
1456pub fn compute_descriptor_crc32(bytes: &[u8]) -> u32 {
1457 record_core::crc32_ieee(bytes)
1458}
1459
1460fn decode_optional_text(payload: &[u8], label: &str) -> Result<Option<String>> {
1461 if payload.is_empty() {
1462 return Ok(None);
1463 }
1464 Ok(Some(decode_text(payload, label)?))
1465}
1466
1467fn decode_text(payload: &[u8], label: &str) -> Result<String> {
1468 let value = String::from_utf8(payload.to_vec())
1469 .with_context(|| format!("record descriptor {label} is not valid UTF-8"))?;
1470 if value.chars().any(char::is_control) {
1471 bail!("record descriptor {label} contains control characters");
1472 }
1473 Ok(value)
1474}
1475
1476fn assign_once<T>(destination: &mut Option<T>, value: T, label: &str) -> Result<()> {
1477 if destination.is_some() {
1478 bail!("duplicate {label} segment");
1479 }
1480 *destination = Some(value);
1481 Ok(())
1482}
1483
1484#[derive(Clone, Copy)]
1485struct ByteCursor<'a> {
1486 bytes: &'a [u8],
1487 offset: usize,
1488}
1489
1490impl<'a> ByteCursor<'a> {
1491 fn new(bytes: &'a [u8]) -> Self {
1492 Self { bytes, offset: 0 }
1493 }
1494
1495 fn remaining(self) -> usize {
1496 self.bytes.len().saturating_sub(self.offset)
1497 }
1498
1499 fn read_u8(&mut self, label: &str) -> Result<u8> {
1500 let value = *self
1501 .bytes
1502 .get(self.offset)
1503 .with_context(|| format!("{label} is truncated"))?;
1504 self.offset += 1;
1505 Ok(value)
1506 }
1507
1508 fn read_u16be(&mut self, label: &str) -> Result<u16> {
1509 let end = self
1510 .offset
1511 .checked_add(2)
1512 .with_context(|| format!("{label} offset overflow"))?;
1513 let bytes = self
1514 .bytes
1515 .get(self.offset..end)
1516 .with_context(|| format!("{label} is truncated"))?;
1517 self.offset = end;
1518 Ok(u16::from_be_bytes(
1519 bytes.try_into().expect("length checked"),
1520 ))
1521 }
1522
1523 fn read_varuint(&mut self, label: &str) -> Result<u64> {
1524 let start = self.offset;
1525 let mut value = 0u64;
1526 let mut shift = 0u32;
1527
1528 for byte_index in 0..10 {
1529 let byte = self.read_u8(label)?;
1530 let payload = u64::from(byte & 0x7f);
1531
1532 if shift == 63 && payload > 1 {
1533 bail!("{label} exceeds u64 range");
1534 }
1535
1536 value |= payload
1537 .checked_shl(shift)
1538 .with_context(|| format!("{label} shift overflow"))?;
1539
1540 if byte & 0x80 == 0 {
1541 let consumed = self.offset - start;
1542 if consumed > 1 {
1543 let minimum = 1u64 << (7 * (consumed - 1));
1544 if value < minimum {
1545 bail!("{label} uses non-canonical overlong varuint encoding");
1546 }
1547 }
1548 return Ok(value);
1549 }
1550
1551 shift += 7;
1552 if byte_index == 9 {
1553 bail!("{label} exceeds ten-byte varuint limit");
1554 }
1555 }
1556
1557 unreachable!()
1558 }
1559
1560 fn read_bytes(&mut self, length: usize, label: &str) -> Result<&'a [u8]> {
1561 let end = self
1562 .offset
1563 .checked_add(length)
1564 .with_context(|| format!("{label} length overflow"))?;
1565 let bytes = self
1566 .bytes
1567 .get(self.offset..end)
1568 .with_context(|| format!("{label} is truncated"))?;
1569 self.offset = end;
1570 Ok(bytes)
1571 }
1572}
1573
1574#[cfg(test)]
1575mod tests {
1576 use super::*;
1577 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
1578
1579 fn test_descriptor(secret: Vec<u8>) -> RecordDescriptor {
1580 RecordDescriptor {
1581 version: RECORD_DESCRIPTOR_VERSION,
1582 checksum_protected: true,
1583 b_value_bits: 1.0f64.to_bits(),
1584 record_profile: RECORD_PROFILE_SINGLE45.to_string(),
1585 stream_byte_length: 4096,
1586 payload_encoding: PAYLOAD_ENCODING_RGB.to_string(),
1587 title: Some("Title".to_string()),
1588 artist: Some("Artist".to_string()),
1589 release_id: Some([0x11; RELEASE_ID_LENGTH]),
1590 catalog_number: Some("CAT-1".to_string()),
1591 label: Some("Label".to_string()),
1592 artwork_credit: Some("Credit".to_string()),
1593 canonical_url: Some("https://example.invalid/release".to_string()),
1594 created_at: Some(1_700_000_000),
1595 copyright_year: Some(2006),
1596 copyright_holder: Some("Artist".to_string()),
1597 signed_release_reference: None,
1598 bsc_pointer: Some(vec![1, 2, 3, 4]),
1599 tone_spans: Vec::new(),
1600 cache_encryption: Some(CacheEncryptionDescriptor {
1601 version: CACHE_ENCRYPTION_DESCRIPTOR_VERSION,
1602 algorithm: CacheEncryptionAlgorithm::XChaCha20Poly1305,
1603 key_derivation: CacheKeyDerivation::HkdfSha256,
1604 secret,
1605 }),
1606 }
1607 }
1608
1609 fn test_context() -> CacheEncryptionContext {
1610 CacheEncryptionContext {
1611 protocol_version: 1,
1612 cache_format_version: 1,
1613 cache_store_name: "opus-chunks".to_string(),
1614 cache_key: "0123456789abcdef".to_string(),
1615 chunk_index: 7,
1616 packet_offset: 2048,
1617 plaintext_length: 12,
1618 codec_identifier: "soundkit_opus_packets".to_string(),
1619 }
1620 }
1621
1622 #[test]
1623 fn record_profile_codes_round_trip() {
1624 assert_eq!(record_profile_code("single45").unwrap(), 0);
1625 assert_eq!(record_profile_code("lp").unwrap(), 1);
1626 assert_eq!(record_profile_from_code(0).unwrap(), "single45");
1627 assert_eq!(record_profile_from_code(1).unwrap(), "lp");
1628 assert!(record_profile_from_code(2).is_err());
1629 }
1630
1631 #[test]
1632 fn payload_encoding_codes_round_trip() {
1633 assert_eq!(payload_encoding_code("rgb").unwrap(), 0);
1634 assert_eq!(payload_encoding_code("toned-v1").unwrap(), 1);
1635 assert_eq!(payload_encoding_from_code(0).unwrap(), "rgb");
1636 assert_eq!(payload_encoding_from_code(1).unwrap(), "toned-v1");
1637 assert!(payload_encoding_from_code(2).is_err());
1638 }
1639
1640 #[test]
1641 fn release_id_text_round_trips_through_bytes() {
1642 let bytes = [
1643 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60,
1644 0x70, 0x80,
1645 ];
1646 let text = release_id_to_text(bytes);
1647 assert!(text.starts_with("rel_"));
1648 assert_eq!(text.len(), 4 + 26);
1649 assert_eq!(release_id_to_bytes(&text).unwrap(), bytes);
1650 }
1651
1652 #[test]
1653 fn release_id_rejects_missing_prefix() {
1654 assert!(release_id_to_bytes("01ARZ3NDEKTSV4RRFFQ69G5FAV").is_err());
1655 }
1656
1657 #[test]
1658 fn release_id_rejects_values_above_the_ulid_range() {
1659 assert!(release_id_to_bytes("rel_Z1ARZ3NDEKTSV4RRFFQ69G5FAV").is_err());
1660 }
1661
1662 #[test]
1663 fn release_id_accepts_the_maximum_canonical_ulid() {
1664 let text = "rel_7ZZZZZZZZZZZZZZZZZZZZZZZZZ";
1665 let bytes = release_id_to_bytes(text).unwrap();
1666 assert_eq!(release_id_to_text(bytes), text);
1667 }
1668
1669 #[test]
1670 fn binary_reference_round_trips_through_decoder() {
1671 let mut bytes = Vec::new();
1672 bytes.push(SIGNED_RELEASE_REFERENCE_VERSION);
1673 bytes.extend_from_slice(&[0x11; 32]);
1674 bytes.extend_from_slice(&3u16.to_be_bytes());
1675 bytes.extend_from_slice(b"key");
1676 bytes.extend_from_slice(&[0x22; 64]);
1677
1678 let decoded = decode_signed_release_reference(&bytes).unwrap();
1679 assert_eq!(decoded.release_commitment_sha256, [0x11; 32]);
1680 assert_eq!(decoded.key_id, b"key");
1681 assert_eq!(decoded.signature, vec![0x22; 64]);
1682 }
1683 #[test]
1684 fn toned_carrier_map_round_trips() {
1685 let spans = vec![
1686 ToneSpanDescriptor {
1687 byte_length: 1024,
1688 base: [255, 192, 203],
1689 luma_tolerance: 16,
1690 bits_per_pixel: 21,
1691 ordering: ToneOrdering::ChromaProximity,
1692 },
1693 ToneSpanDescriptor {
1694 byte_length: 513,
1695 base: [20, 40, 80],
1696 luma_tolerance: 8,
1697 bits_per_pixel: 18,
1698 ordering: ToneOrdering::BaseProximity,
1699 },
1700 ];
1701
1702 let bytes = encode_toned_carrier_map(&spans, Some(1537)).unwrap();
1703 let decoded = decode_toned_carrier_map(&bytes, Some(1537)).unwrap();
1704
1705 assert_eq!(decoded, spans);
1706 }
1707
1708 #[test]
1709 fn toned_offsets_are_derived() {
1710 let spans = vec![
1711 ToneSpanDescriptor {
1712 byte_length: 5,
1713 base: [1, 2, 3],
1714 luma_tolerance: 0,
1715 bits_per_pixel: 8,
1716 ordering: ToneOrdering::BaseProximity,
1717 },
1718 ToneSpanDescriptor {
1719 byte_length: 7,
1720 base: [4, 5, 6],
1721 luma_tolerance: 1,
1722 bits_per_pixel: 4,
1723 ordering: ToneOrdering::ChromaProximity,
1724 },
1725 ];
1726
1727 let resolved = resolve_tone_spans(&spans, Some(12)).unwrap();
1728 assert_eq!(resolved[0].byte_offset, 0);
1729 assert_eq!(resolved[1].byte_offset, 5);
1730 assert_eq!(resolved[0].pixel_count, 5);
1731 assert_eq!(resolved[1].pixel_offset, 5);
1732 assert_eq!(resolved[1].pixel_count, 14);
1733 }
1734
1735 #[test]
1736 fn toned_map_rejects_overlong_varuint() {
1737 let bytes = [
1738 TONED_CARRIER_MAP_VERSION,
1739 0,
1740 1,
1741 0x81,
1742 0x00,
1743 0,
1744 0,
1745 0,
1746 0,
1747 8,
1748 TONED_ORDERING_BASE_PROXIMITY,
1749 ];
1750 assert!(decode_toned_carrier_map(&bytes, None).is_err());
1751 }
1752
1753 #[test]
1754 fn cache_encryption_descriptor_round_trips_through_json() {
1755 let descriptor = CacheEncryptionDescriptor {
1756 version: CACHE_ENCRYPTION_DESCRIPTOR_VERSION,
1757 algorithm: CacheEncryptionAlgorithm::XChaCha20Poly1305,
1758 key_derivation: CacheKeyDerivation::HkdfSha256,
1759 secret: vec![7u8; CACHE_ENCRYPTION_SECRET_LENGTH],
1760 };
1761 let json = serde_json::to_string(&descriptor).unwrap();
1762 let decoded: CacheEncryptionDescriptor = serde_json::from_str(&json).unwrap();
1763 assert_eq!(decoded, descriptor);
1764 }
1765
1766 #[test]
1767 fn cache_encryption_secret_must_be_32_bytes() {
1768 let mut descriptor = CacheEncryptionDescriptor {
1769 version: CACHE_ENCRYPTION_DESCRIPTOR_VERSION,
1770 algorithm: CacheEncryptionAlgorithm::XChaCha20Poly1305,
1771 key_derivation: CacheKeyDerivation::HkdfSha256,
1772 secret: vec![0u8; 31],
1773 };
1774 assert!(descriptor.validate().is_err());
1775 descriptor.secret = vec![0u8; 32];
1776 assert!(descriptor.validate().is_ok());
1777 }
1778
1779 #[test]
1780 fn cache_encryption_descriptor_rejects_malformed_base64url() {
1781 let json = r#"{"version":1,"algorithm":"xchacha20-poly1305","keyDerivation":"hkdf-sha256","secret":"not base64"}"#;
1782 assert!(serde_json::from_str::<CacheEncryptionDescriptor>(json).is_err());
1783 }
1784
1785 #[test]
1786 fn cache_encryption_descriptor_rejects_wrong_secret_length() {
1787 let json = format!(
1788 r#"{{"version":1,"algorithm":"xchacha20-poly1305","keyDerivation":"hkdf-sha256","secret":"{}"}}"#,
1789 URL_SAFE_NO_PAD.encode([1u8; 31])
1790 );
1791 let parsed: CacheEncryptionDescriptor = serde_json::from_str(&json).unwrap();
1792 assert!(parsed.validate().is_err());
1793 }
1794
1795 #[test]
1796 fn old_descriptors_without_cache_encryption_still_decode() {
1797 let descriptor = test_descriptor(vec![9u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1798 let json = serde_json::to_string(&descriptor).unwrap();
1799 let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
1800 value.as_object_mut().unwrap().remove("cacheEncryption");
1801 let decoded: RecordDescriptor = serde_json::from_value(value).unwrap();
1802 assert!(decoded.cache_encryption.is_none());
1803 }
1804
1805 #[test]
1806 fn cache_encryption_key_derivation_is_stable_and_bindable() {
1807 let descriptor = test_descriptor(vec![1u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1808 let key_a = derive_cache_encryption_key(&descriptor).unwrap();
1809 let key_b = derive_cache_encryption_key(&descriptor).unwrap();
1810 assert_eq!(key_a, key_b);
1811
1812 let mut other_secret = descriptor.clone();
1813 other_secret.cache_encryption.as_mut().unwrap().secret =
1814 vec![2u8; CACHE_ENCRYPTION_SECRET_LENGTH];
1815 assert_ne!(key_a, derive_cache_encryption_key(&other_secret).unwrap());
1816
1817 let mut other_record = descriptor.clone();
1818 other_record.release_id = Some([0x22; RELEASE_ID_LENGTH]);
1819 assert_ne!(key_a, derive_cache_encryption_key(&other_record).unwrap());
1820 }
1821
1822 #[test]
1823 fn cache_encryption_envelope_round_trips() {
1824 let descriptor = test_descriptor(vec![3u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1825 let context = test_context();
1826 let plaintext = b"opus-packets";
1827 let envelope = encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1828 let decrypted = decrypt_cache_envelope(&descriptor, &context, &envelope).unwrap();
1829 assert_eq!(decrypted, plaintext);
1830 }
1831
1832 #[test]
1833 fn cache_encryption_envelope_rejects_tampering() {
1834 let descriptor = test_descriptor(vec![5u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1835 let context = test_context();
1836 let plaintext = b"opus-packets";
1837 let mut envelope = encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1838
1839 envelope[CacheEncryptionEnvelope::HEADER_LENGTH] ^= 1;
1840 assert!(decrypt_cache_envelope(&descriptor, &context, &envelope).is_err());
1841
1842 let mut nonce_tampered = encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1843 nonce_tampered[60] ^= 1;
1844 assert!(decrypt_cache_envelope(&descriptor, &context, &nonce_tampered).is_err());
1845
1846 let mut binding_tampered =
1847 encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1848 binding_tampered[8] ^= 1;
1849 assert!(decrypt_cache_envelope(&descriptor, &context, &binding_tampered).is_err());
1850 }
1851
1852 #[test]
1853 fn cache_encryption_nonce_is_deterministic_and_content_bound() {
1854 let descriptor = test_descriptor(vec![9u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1855 let context = test_context();
1856 let plaintext = b"opus-packets";
1857
1858 let envelope_a = encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1859 let envelope_b = encrypt_cache_envelope(&descriptor, &context, plaintext).unwrap();
1860 assert_eq!(
1861 envelope_a, envelope_b,
1862 "same (record, plaintext, context) must produce byte-identical envelopes"
1863 );
1864
1865 let other_plaintext = b"opus-packet$";
1866 assert_eq!(other_plaintext.len(), plaintext.len());
1867 let envelope_c = encrypt_cache_envelope(&descriptor, &context, other_plaintext).unwrap();
1868 assert_ne!(
1869 envelope_a, envelope_c,
1870 "different plaintext must not reuse the same nonce/ciphertext"
1871 );
1872 }
1873
1874 #[test]
1875 fn cache_encryption_record_binding_hash_hex_differs_per_record() {
1876 let descriptor_a = test_descriptor(vec![1u8; CACHE_ENCRYPTION_SECRET_LENGTH]);
1877 let mut descriptor_b = descriptor_a.clone();
1878 descriptor_b.release_id = Some([0x33; RELEASE_ID_LENGTH]);
1879
1880 let hash_a = cache_encryption_record_binding_hash_hex(&descriptor_a).unwrap();
1881 let hash_b = cache_encryption_record_binding_hash_hex(&descriptor_b).unwrap();
1882 assert_eq!(hash_a.len(), 64);
1883 assert_ne!(hash_a, hash_b);
1884 }
1885}