mod errors;
pub use errors::DecodeError;
mod combine;
mod format;
mod materials;
mod streams;
use alloc::vec::Vec;
use materials::Material;
pub fn encode(plaintext: &[u8], seed: &[u8], slug: &[u8; 32]) -> Vec<u8> {
assert!(!seed.is_empty(), "laquna: seed must be non-empty");
let payload_input = compress(plaintext);
let material = materials::derive(seed, slug);
let mask = build_mask(&material, payload_input.len());
let payload = combine::xor_buffers(&payload_input, &mask);
format::assemble(payload, slug)
}
#[cfg_attr(not(test), allow(dead_code))] pub fn decode(encoded: &[u8], seed: &[u8]) -> Result<Vec<u8>, DecodeError> {
decode_with_limit(encoded, seed, None)
}
pub fn decode_with_limit(
encoded: &[u8],
seed: &[u8],
max_decoded_len: Option<usize>,
) -> Result<Vec<u8>, DecodeError> {
if seed.is_empty() {
return Err(DecodeError::EmptySeed);
}
let (payload, slug) = format::disassemble(encoded)?;
let material = materials::derive(seed, &slug);
let mask = build_mask(&material, payload.len());
let payload_input = combine::xor_buffers(&payload, &mask);
decompress(&payload_input, max_decoded_len)
}
fn build_mask(material: &Material, length: usize) -> Vec<u8> {
let mut anchor = streams::anchor::AnchorGenerator::new(material);
let mut extendable = streams::extendable::ExtendableGenerator::new(material);
let mut network = streams::network::NetworkGenerator::new(material);
let stream_a = anchor.produce(length);
let stream_b = extendable.produce(length);
let stream_c = network.produce(length);
combine::mix(&stream_a, &stream_b, &stream_c)
}
fn compress(plaintext: &[u8]) -> Vec<u8> {
use zstd::bulk::Compressor;
use zstd::stream::raw::CParameter;
let mut compressor = Compressor::new(3).expect("compression level 3 is valid");
compressor
.set_parameter(CParameter::ChecksumFlag(true))
.expect("enabling the content-checksum frame flag is supported");
compressor
.compress(plaintext)
.expect("compression of an in-memory buffer does not fail")
}
fn decompress(
payload_input: &[u8],
max_decoded_len: Option<usize>,
) -> Result<Vec<u8>, DecodeError> {
use ruzstd::io::Read;
use ruzstd::StreamingDecoder;
if let Some(limit) = max_decoded_len {
check_frame_header_within(payload_input, limit)?;
}
let mut decoder =
StreamingDecoder::new(payload_input).map_err(|_| DecodeError::DecompressionFailed)?;
let mut out = Vec::new();
let mut buf = [0u8; 4096];
loop {
let read = decoder
.read(&mut buf)
.map_err(|_| DecodeError::DecompressionFailed)?;
if read == 0 {
break;
}
if let Some(limit) = max_decoded_len {
if out.len() + read > limit {
return Err(DecodeError::DecompressionFailed);
}
}
out.extend_from_slice(&buf[..read]);
}
let frame = decoder.into_frame_decoder();
if let Some(stored) = frame.get_checksum_from_data() {
match frame.get_calculated_checksum() {
Some(computed) if computed == stored => {}
_ => return Err(DecodeError::DecompressionFailed),
}
}
Ok(out)
}
fn check_frame_header_within(payload_input: &[u8], limit: usize) -> Result<(), DecodeError> {
let Ok((frame, _)) = ruzstd::frame::read_frame_header(payload_input) else {
return Ok(());
};
let header = &frame.header;
let limit = limit as u64;
let declares_content_size =
matches!(header.descriptor.frame_content_size_bytes(), Ok(n) if n > 0);
if declares_content_size && header.frame_content_size() > limit {
return Err(DecodeError::DecompressionFailed);
}
if let Ok(window) = header.window_size() {
if window > limit.max(ruzstd::frame::MIN_WINDOW_SIZE) {
return Err(DecodeError::DecompressionFailed);
}
}
Ok(())
}
#[cfg(test)]
mod decode_limit_tests {
use super::*;
use std::time::{Duration, Instant};
const SEED: &[u8] = b"did:plc:example||tools.example.post||3kabcd2lqr7s2y";
const SLUG: [u8; 32] = [0x5a; 32];
fn artifact_from_zstd(raw: &[u8]) -> Vec<u8> {
let material = materials::derive(SEED, &SLUG);
let mask = build_mask(&material, raw.len());
format::assemble(combine::xor_buffers(raw, &mask), &SLUG)
}
fn rle_frame(window_descriptor: u8, block_len: u32, blocks: usize) -> Vec<u8> {
let mut f = vec![0x28, 0xB5, 0x2F, 0xFD, 0x00, window_descriptor];
for i in 0..blocks {
let last = u32::from(i + 1 == blocks);
let header = (block_len << 3) | (1 << 1) | last; f.extend_from_slice(&header.to_le_bytes()[..3]);
f.push(0xAB);
}
f
}
const WINDOW_64_KIB: u8 = 6 << 3;
const WINDOW_512_KIB: u8 = 9 << 3;
const WINDOW_64_MIB: u8 = 16 << 3;
const BLOCK_128_KIB: u32 = 128 * 1024;
#[test]
fn encoded_record_at_limit_decodes_and_one_over_fails() {
let plaintext: Vec<u8> = (0..10_000u32).map(|i| (i * 7919 % 251) as u8).collect();
let artifact = encode(&plaintext, SEED, &SLUG);
assert_eq!(decode_with_limit(&artifact, SEED, Some(10_000)).unwrap(), plaintext);
assert_eq!(decode_with_limit(&artifact, SEED, Some(10_001)).unwrap(), plaintext);
assert_eq!(decode_with_limit(&artifact, SEED, None).unwrap(), plaintext);
assert_eq!(
decode_with_limit(&artifact, SEED, Some(9_999)),
Err(DecodeError::DecompressionFailed)
);
}
#[test]
fn undeclared_size_frame_at_limit_decodes_and_one_over_fails() {
let total = 16 * 64 * 1024;
let artifact = artifact_from_zstd(&rle_frame(WINDOW_64_KIB, 64 * 1024, 16));
let out = decode_with_limit(&artifact, SEED, Some(total)).unwrap();
assert_eq!(out.len(), total);
assert!(out.iter().all(|&b| b == 0xAB));
assert!(matches!(
decode_with_limit(&artifact, SEED, Some(total - 1)),
Err(DecodeError::DecompressionFailed)
));
}
#[test]
fn rle_bomb_small_window_fails_fast() {
let small = artifact_from_zstd(&rle_frame(WINDOW_512_KIB, BLOCK_128_KIB, 4));
assert_eq!(decode(&small, SEED).unwrap().len(), 4 * 128 * 1024);
let bomb = artifact_from_zstd(&rle_frame(WINDOW_512_KIB, BLOCK_128_KIB, 8192));
assert!(bomb.len() < 64 * 1024);
let start = Instant::now();
assert!(matches!(
decode_with_limit(&bomb, SEED, Some(1_000_000)),
Err(DecodeError::DecompressionFailed)
));
assert!(start.elapsed() < Duration::from_secs(2), "took {:?}", start.elapsed());
}
#[test]
fn rle_bomb_large_window_refused_by_header() {
let small = artifact_from_zstd(&rle_frame(WINDOW_64_MIB, BLOCK_128_KIB, 2));
assert_eq!(decode(&small, SEED).unwrap().len(), 2 * 128 * 1024);
let raw = rle_frame(WINDOW_64_MIB, BLOCK_128_KIB, 8192);
assert_eq!(
check_frame_header_within(&raw, 1_000_000),
Err(DecodeError::DecompressionFailed)
);
let bomb = artifact_from_zstd(&raw);
let start = Instant::now();
assert!(matches!(
decode_with_limit(&bomb, SEED, Some(1_000_000)),
Err(DecodeError::DecompressionFailed)
));
assert!(start.elapsed() < Duration::from_secs(2), "took {:?}", start.elapsed());
}
#[test]
fn window_over_limit_refused_even_when_content_fits() {
let artifact = artifact_from_zstd(&rle_frame(WINDOW_64_MIB, BLOCK_128_KIB, 2));
assert_eq!(decode_with_limit(&artifact, SEED, None).unwrap().len(), 256 * 1024);
assert!(matches!(
decode_with_limit(&artifact, SEED, Some(1_000_000)),
Err(DecodeError::DecompressionFailed)
));
}
#[test]
fn declared_content_size_over_limit_refused_by_header() {
let plaintext = vec![0u8; 50_000];
let raw = compress(&plaintext);
assert_eq!(check_frame_header_within(&raw, 50_000), Ok(()));
assert_eq!(
check_frame_header_within(&raw, 49_999),
Err(DecodeError::DecompressionFailed)
);
}
}