use zeroize::Zeroizing;
use crate::{Error, Result};
#[cfg(feature = "deflate")]
pub(crate) fn compress(plaintext: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
crate::deflate::deflate(plaintext)
}
#[cfg(not(feature = "deflate"))]
pub(crate) fn compress(_plaintext: &[u8]) -> Result<Zeroizing<Vec<u8>>> {
Err(Error::UnsupportedAlgorithm)
}
#[cfg(feature = "deflate")]
pub(crate) fn decompress(compressed: &[u8], max_len: usize) -> Result<Zeroizing<Vec<u8>>> {
decompress_with(compressed, max_len, crate::deflate::inflate)
}
#[cfg(feature = "deflate")]
fn decompress_with(
compressed: &[u8],
max_len: usize,
inflate: impl FnOnce(&[u8], usize) -> Result<Zeroizing<Vec<u8>>>,
) -> Result<Zeroizing<Vec<u8>>> {
if max_len == 0 {
return Err(Error::DecryptionFailed);
}
#[cfg(test)]
crate::test_counters::inflate_calls::tick();
inflate(compressed, max_len).map_err(|_| Error::DecryptionFailed)
}
#[cfg(not(feature = "deflate"))]
pub(crate) fn decompress(_compressed: &[u8], _max_len: usize) -> Result<Zeroizing<Vec<u8>>> {
Err(Error::UnsupportedAlgorithm)
}
#[cfg(all(test, not(feature = "deflate")))]
mod tests_without_deflate {
use super::*;
#[test]
fn both_directions_are_unsupported_without_deflate() {
assert_eq!(compress(b"x"), Err(Error::UnsupportedAlgorithm));
assert_eq!(
decompress(&[0x03, 0x00], 1),
Err(Error::UnsupportedAlgorithm)
);
}
}
#[cfg(all(test, feature = "deflate"))]
mod tests {
#![allow(clippy::panic)]
use super::*;
use crate::header::Header;
use crate::jwa::{ContentEncAlg, KeyMgmtAlg};
use crate::jwe::api::DecryptPolicy;
use crate::jwe::content::{self, Cek};
use crate::jwe::core::{self as pipeline, JweParts, RecipientParts};
use crate::jwk::Jwk;
use crate::test_counters::inflate_calls;
use crate::util::b64url_encode;
fn inflate_all(compressed: &[u8]) -> Zeroizing<Vec<u8>> {
decompress(compressed, usize::MAX).unwrap_or_else(|e| panic!("{e}"))
}
#[test]
fn compressed_plaintext_round_trips() {
let text = b"You can trust us to stick with you through thick and thin".repeat(40);
for plaintext in [&b""[..], b"a", &text[..], &[0u8; 70_000][..]] {
let compressed = compress(plaintext).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(&inflate_all(&compressed)[..], plaintext);
}
let compressed = compress(&text).unwrap_or_else(|e| panic!("{e}"));
assert!(compressed.len() < text.len() / 4, "not compressed");
}
const EMPTY_STREAM: [u8; 2] = [0x03, 0x00];
#[test]
fn the_bound_is_inclusive() {
let plaintext = b"0123456789".repeat(25_000);
let compressed = compress(&plaintext).unwrap_or_else(|e| panic!("{e}"));
let out = decompress(&compressed, 250_000).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(&out[..], &plaintext[..]);
assert_eq!(
decompress(&compressed, 249_999),
Err(Error::DecryptionFailed)
);
let longer = b"0123456789".repeat(25_001);
let compressed = compress(&longer[..250_001]).unwrap_or_else(|e| panic!("{e}"));
assert_eq!(
decompress(&compressed, 250_000),
Err(Error::DecryptionFailed)
);
assert_eq!(
decompress(&compressed, 250_001).map(|v| v.len()),
Ok(250_001)
);
}
#[test]
fn a_zero_bound_rejects_every_stream() {
assert_eq!(decompress(&EMPTY_STREAM, 0), Err(Error::DecryptionFailed));
let empty = compress(b"").unwrap_or_else(|e| panic!("{e}"));
assert_eq!(decompress(&empty, 0), Err(Error::DecryptionFailed));
assert_eq!(decompress(b"", 0), Err(Error::DecryptionFailed));
assert_eq!(decompress(&EMPTY_STREAM, 1).map(|v| v.len()), Ok(0));
}
#[test]
fn every_inflate_error_is_decryption_failed() {
let valid = compress(b"hello hello hello").unwrap_or_else(|e| panic!("{e}"));
let malformed: [&[u8]; 6] = [
b"", &[0x07], &[0x01, 0x05, 0x00, 0x00, 0x00], &[0x01, 0x05, 0x00, 0xFA, 0xFF, 1], &[0x00], &valid[..valid.len() - 1], ];
for input in malformed {
assert_eq!(
decompress(input, 1 << 20),
Err(Error::DecryptionFailed),
"{input:?}"
);
}
}
#[derive(Default)]
struct Bits {
out: Vec<u8>,
len: usize,
}
impl Bits {
fn bit(&mut self, bit: u32) {
self.zeros(1);
if let Some(last) = self.out.last_mut() {
*last |= u8::from(bit & 1 == 1) << ((self.len - 1) % 8);
}
}
fn value(&mut self, value: u32, count: u32) {
(0..count).for_each(|i| self.bit(value >> i));
}
fn code(&mut self, code: u32, count: u32) {
(0..count).rev().for_each(|i| self.bit(code >> i));
}
fn zeros(&mut self, count: usize) {
self.len += count;
self.out.resize(self.len.div_ceil(8), 0);
}
}
fn bomb(matches: usize) -> Vec<u8> {
let mut w = Bits::default();
w.value(1, 1); w.value(0b10, 2); w.value(286 - 257, 5); w.value(0, 5); w.value(18 - 4, 4); let order = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1];
for symbol in order {
w.value(
match symbol {
18 => 1,
1 | 2 => 2,
_ => 0,
},
3,
);
}
let zeros = |w: &mut Bits, n: u32| {
w.code(0, 1); w.value(n - 11, 7);
};
zeros(&mut w, 97);
w.code(0b11, 2); zeros(&mut w, 138);
zeros(&mut w, 20);
w.code(0b11, 2); zeros(&mut w, 28);
w.code(0b10, 2); w.code(0b10, 2); w.code(0b10, 2); w.zeros(2 * matches); w.code(0b11, 2); w.out
}
fn gib_bomb() -> Vec<u8> {
let bomb = bomb((1 << 30) / 258 + 1);
assert!(bomb.len() < 1100 * 1024, "bomb is {} bytes", bomb.len());
bomb
}
#[test]
fn the_bomb_is_valid_deflate() {
let out = inflate_all(&bomb(1000));
assert_eq!(out.len(), 1 + 258 * 1000);
assert!(out.iter().all(|&b| b == b'a'));
}
const KEY: [u8; 16] = [0x5A; 16];
fn dir_key() -> Jwk {
let json = format!(r#"{{"kty":"oct","k":"{}"}}"#, b64url_encode(&KEY));
Jwk::from_json(json.as_bytes()).unwrap_or_else(|e| panic!("{e}"))
}
fn policy() -> DecryptPolicy {
DecryptPolicy::new(&[KeyMgmtAlg::Dir], &[ContentEncAlg::A128GCM])
.unwrap_or_else(|e| panic!("{e}"))
}
fn zip_jwe(payload: &[u8]) -> JweParts {
let json = r#"{"alg":"dir","enc":"A128GCM","zip":"DEF"}"#;
let protected_b64 = b64url_encode(json.as_bytes());
let iv = vec![7; 12];
let aad = content::aad(&protected_b64, None);
let cek = Cek::from_slice(&KEY);
let (ciphertext, tag) = content::encrypt(&ContentEncAlg::A128GCM, &cek, &iv, &aad, payload)
.unwrap_or_else(|e| panic!("{e}"));
JweParts {
protected_b64,
protected: Some(Header::from_json(json.as_bytes()).unwrap_or_else(|e| panic!("{e}"))),
shared: None,
recipients: vec![RecipientParts {
header: None,
encrypted_key: Vec::new(),
}],
iv,
ciphertext,
tag,
aad: None,
}
}
#[test]
fn a_bad_tag_never_inflates() {
let bomb = gib_bomb();
let mut parts = zip_jwe(&bomb);
parts.tag[0] ^= 1;
let (result, inflations) =
inflate_calls::count(|| pipeline::decrypt(parts, &dir_key(), &policy()));
assert_eq!(result.err(), Some(Error::DecryptionFailed));
assert_eq!(inflations, 0);
let parts = zip_jwe(&bomb);
let (result, inflations) =
inflate_calls::count(|| pipeline::decrypt(parts, &dir_key(), &policy()));
assert_eq!(result.err(), Some(Error::DecryptionFailed));
assert_eq!(inflations, 1);
}
#[test]
fn a_zero_bound_never_inflates() {
let parts = zip_jwe(&EMPTY_STREAM);
let zero = policy().max_decompressed_len(0);
let (result, inflations) =
inflate_calls::count(|| pipeline::decrypt(parts, &dir_key(), &zero));
assert_eq!(result.err(), Some(Error::DecryptionFailed));
assert_eq!(inflations, 0);
}
#[test]
fn inflate_gets_exactly_the_policy_bound() {
let bomb = gib_bomb();
for bound in [
DecryptPolicy::DEFAULT_MAX_DECOMPRESSED_LEN,
1,
4096,
1 << 20,
] {
let mut seen = Vec::new();
let result = decompress_with(&bomb, bound, |input, max| {
seen.push(max);
crate::deflate::inflate(input, max)
});
assert_eq!(result, Err(Error::DecryptionFailed), "bound {bound}");
assert_eq!(seen, [bound]);
}
assert_eq!(policy().max_decompressed_len, 250_000);
}
#[test]
fn the_output_buffer_is_at_most_the_policy_bound() {
let matches = 2000;
let stream = bomb(matches);
let bound = 1 + 258 * matches;
assert!(1032 * stream.len() > bound);
let exact = policy().max_decompressed_len(bound);
let decrypted = pipeline::decrypt(zip_jwe(&stream), &dir_key(), &exact)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(decrypted.plaintext.len(), bound);
assert!(decrypted.plaintext.capacity() <= bound);
let under = policy().max_decompressed_len(bound - 1);
let result = pipeline::decrypt(zip_jwe(&stream), &dir_key(), &under);
assert_eq!(result.err(), Some(Error::DecryptionFailed));
}
#[test]
fn a_huge_policy_bound_reserves_at_most_the_default_bound() {
const GIB: usize = 1 << 30;
let mut stored = vec![1, 0xE8, 0x03, 0x17, 0xFC];
stored.extend(std::iter::repeat_n(b'x', 1000));
assert!(1032 * stored.len() > DecryptPolicy::DEFAULT_MAX_DECOMPRESSED_LEN);
let huge = policy().max_decompressed_len(GIB);
let decrypted = pipeline::decrypt(zip_jwe(&stored), &dir_key(), &huge)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(&decrypted.plaintext[..], &[b'x'; 1000][..]);
assert_eq!(
decrypted.plaintext.capacity(),
DecryptPolicy::DEFAULT_MAX_DECOMPRESSED_LEN
);
let matches = 2000;
let stream = bomb(matches);
let decrypted = pipeline::decrypt(zip_jwe(&stream), &dir_key(), &huge)
.unwrap_or_else(|e| panic!("{e}"));
let len = decrypted.plaintext.len();
assert_eq!(len, 1 + 258 * matches);
assert!(decrypted.plaintext.capacity() <= 2 * len);
assert!(decrypted.plaintext.capacity() <= 1032 * stream.len());
}
}