Never crash: crypt has no panic paths

AES blocks come from `as_chunks`, the IV from `split_first_chunk`, and the R6 hash takes its
AES key and IV with `first_chunk`, so no `expect` is needed. The crate now denies the panic lints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-04 11:55:59 -04:00
parent 8f1f4e7ffb
commit 95d8ba3101
2 changed files with 24 additions and 20 deletions

View File

@ -13,9 +13,8 @@ enum Cipher {
impl Cipher {
fn new(key: &[u8]) -> Self {
if key.len() >= 32 {
let k: [u8; 32] = key[..32].try_into().expect("32 bytes");
Cipher::A256(Box::new(Aes256::new(&Array::from(k))))
if let Some(k) = key.first_chunk::<32>() {
Cipher::A256(Box::new(Aes256::new(&Array::from(*k))))
} else {
let mut k = [0u8; 16];
let n = key.len().min(16);
@ -55,14 +54,12 @@ pub fn aes_cbc_encrypt(key: &[u8], iv: &[u8; 16], data: &[u8], pad: bool) -> Vec
buf.resize(buf.len().div_ceil(16) * 16, 0);
}
let mut prev = *iv;
for chunk in buf.chunks_mut(16) {
let mut b: [u8; 16] = chunk.try_into().expect("16 bytes");
for b in buf.as_chunks_mut::<16>().0 {
for (x, p) in b.iter_mut().zip(prev) {
*x ^= p;
}
c.encrypt(&mut b);
chunk.copy_from_slice(&b);
prev = b;
c.encrypt(b);
prev = *b;
}
buf
}
@ -70,11 +67,10 @@ pub fn aes_cbc_encrypt(key: &[u8], iv: &[u8; 16], data: &[u8], pad: bool) -> Vec
/// Decrypt; with `pad`, PKCS#5 padding is removed when valid.
pub fn aes_cbc_decrypt(key: &[u8], iv: &[u8; 16], data: &[u8], pad: bool) -> Vec<u8> {
let c = Cipher::new(key);
let whole = data.len() / 16 * 16;
let mut out = Vec::with_capacity(whole);
let blocks = data.as_chunks::<16>().0;
let mut out = Vec::with_capacity(blocks.len() * 16);
let mut prev = *iv;
for chunk in data[..whole].chunks(16) {
let cipher: [u8; 16] = chunk.try_into().expect("16 bytes");
for &cipher in blocks {
let mut b = cipher;
c.decrypt(&mut b);
for (x, p) in b.iter_mut().zip(prev) {

View File

@ -12,6 +12,8 @@
//! The crate knows nothing about the COS object model. Callers pass in the values of the
//! `/Encrypt` dictionary and the first element of the trailer `/ID`.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
mod aes_cbc;
mod rc4;
@ -256,16 +258,19 @@ impl SecurityHandler {
h.update(num.to_le_bytes());
h.update(generation.to_le_bytes());
h.update(data);
let iv: [u8; 16] = h.finalize()[..16].try_into().expect("16 bytes");
let digest = h.finalize();
let mut iv = [0u8; 16];
for (d, s) in iv.iter_mut().zip(digest.iter()) {
*d = *s;
}
let mut out = iv.to_vec();
out.extend(aes_cbc_encrypt(&key, &iv, data, true));
out
} else {
if data.len() < 16 {
let Some((iv, body)) = data.split_first_chunk::<16>() else {
return Vec::new(); // only (part of) an IV: empty plaintext
}
let (iv, body) = data.split_at(16);
aes_cbc_decrypt(&key, iv.try_into().expect("16 bytes"), body, true)
};
aes_cbc_decrypt(&key, iv, body, true)
}
}
}
@ -429,15 +434,18 @@ fn hash_r6(r: i64, pw: &[u8], salt: &[u8], udata: &[u8]) -> Vec<u8> {
k1.extend_from_slice(&k);
k1.extend_from_slice(udata);
}
let e = aes_cbc_encrypt(&k[..16], k[16..32].try_into().expect("16 bytes"), &k1, false);
let m = e[..16].iter().map(|b| u32::from(*b)).sum::<u32>() % 3;
// `k` is always a SHA-2 digest (32, 48 or 64 bytes): key = its first 16 bytes, IV = the next 16.
let Some((key, rest)) = k.split_first_chunk::<16>() else { break };
let Some(iv) = rest.first_chunk::<16>() else { break };
let e = aes_cbc_encrypt(key, iv, &k1, false);
let m = e.iter().take(16).map(|b| u32::from(*b)).sum::<u32>() % 3;
k = match m {
0 => Sha256::digest(&e).to_vec(),
1 => Sha384::digest(&e).to_vec(),
_ => Sha512::digest(&e).to_vec(),
};
round += 1;
if round >= 64 && u32::from(*e.last().expect("non-empty")) <= round - 32 {
if round >= 64 && e.last().is_none_or(|b| u32::from(*b) <= round - 32) {
break;
}
}