mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
166 lines
7.5 KiB
Rust
166 lines
7.5 KiB
Rust
use super::*;
|
||
|
||
fn hex(s: &str) -> Vec<u8> {
|
||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||
}
|
||
|
||
#[test]
|
||
fn rc4_reference_vectors() {
|
||
assert_eq!(rc4(b"Key", b"Plaintext"), hex("bbf316e8d940af0ad3"));
|
||
assert_eq!(rc4(b"Wiki", b"pedia"), hex("1021bf0420"));
|
||
assert_eq!(rc4(b"Secret", b"Attack at dawn"), hex("45a01f645fc35b383552544b9bf5"));
|
||
}
|
||
|
||
#[test]
|
||
fn aes_cbc_nist_sp800_38a_vectors() {
|
||
let iv: [u8; 16] = hex("000102030405060708090a0b0c0d0e0f").try_into().unwrap();
|
||
let plain = hex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51");
|
||
let k128 = hex("2b7e151628aed2a6abf7158809cf4f3c");
|
||
let c128 = aes_cbc_encrypt(&k128, &iv, &plain, false);
|
||
assert_eq!(c128, hex("7649abac8119b246cee98e9b12e9197d5086cb9b507219ee95db113a917678b2"));
|
||
assert_eq!(aes_cbc_decrypt(&k128, &iv, &c128, false), plain);
|
||
let k256 = hex("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4");
|
||
let c256 = aes_cbc_encrypt(&k256, &iv, &plain, false);
|
||
assert_eq!(c256, hex("f58c4c04d6e5f1ba779eabfb5f7bfbd69cfc4e967edb808d679f777bc6702c7d"));
|
||
assert_eq!(aes_cbc_decrypt(&k256, &iv, &c256, false), plain);
|
||
}
|
||
|
||
#[test]
|
||
fn aes_padding_is_added_and_tolerantly_removed() {
|
||
let key = [7u8; 16];
|
||
let iv = [1u8; 16];
|
||
for n in [0, 1, 15, 16, 17, 100] {
|
||
let data: Vec<u8> = (0..n as u8).collect();
|
||
let c = aes_cbc_encrypt(&key, &iv, &data, true);
|
||
assert_eq!(c.len() % 16, 0);
|
||
assert_eq!(aes_cbc_decrypt(&key, &iv, &c, true), data);
|
||
}
|
||
// Garbage (bad padding, partial block) decrypts without panicking.
|
||
let _ = aes_cbc_decrypt(&key, &iv, &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17], true);
|
||
}
|
||
|
||
const ID: &[u8] = b"0123456789abcdef";
|
||
|
||
fn new(alg: Algorithm, user: &str, owner: &str, perms: i32) -> SecurityHandler {
|
||
create(
|
||
&NewEncryption { algorithm: alg, user_password: user, owner_password: owner, permissions: perms, encrypt_metadata: true, seed: [42; 32] },
|
||
ID,
|
||
)
|
||
.unwrap()
|
||
}
|
||
|
||
const ALL: [Algorithm; 4] = [Algorithm::Rc4_40, Algorithm::Rc4_128, Algorithm::Aes128, Algorithm::Aes256];
|
||
|
||
#[test]
|
||
fn created_encryption_authenticates_owner_and_user() {
|
||
for alg in ALL {
|
||
let made = new(alg, "user pw", "owner pw", 0b0100); // print only
|
||
let d = made.dict().clone();
|
||
let owner = SecurityHandler::open(d.clone(), ID, Some("owner pw")).unwrap();
|
||
assert_eq!(owner.auth(), Auth::Owner, "{alg:?}");
|
||
assert_eq!(owner.file_key(), made.file_key(), "{alg:?}");
|
||
assert!(owner.permissions().modify(), "owners can do everything");
|
||
let user = SecurityHandler::open(d.clone(), ID, Some("user pw")).unwrap();
|
||
assert_eq!(user.auth(), Auth::User, "{alg:?}");
|
||
assert_eq!(user.file_key(), made.file_key(), "{alg:?}");
|
||
assert!(user.permissions().print() && !user.permissions().modify() && !user.permissions().copy(), "{alg:?}");
|
||
assert_eq!(SecurityHandler::open(d.clone(), ID, Some("nope")).unwrap_err(), CryptError::WrongPassword, "{alg:?}");
|
||
assert_eq!(SecurityHandler::open(d, ID, None).unwrap_err(), CryptError::WrongPassword, "{alg:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn empty_user_password_opens_without_asking() {
|
||
for alg in ALL {
|
||
let d = new(alg, "", "secret", -1).dict().clone();
|
||
let h = SecurityHandler::open(d, ID, None).unwrap();
|
||
assert_eq!(h.auth(), Auth::User, "{alg:?}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strings_and_streams_round_trip_and_are_scrambled() {
|
||
for alg in ALL {
|
||
let h = new(alg, "", "o", -1);
|
||
let text = b"Confidential quarterly numbers".to_vec();
|
||
let enc = h.encrypt_string(12, 0, &text);
|
||
assert_ne!(enc, text, "{alg:?}");
|
||
assert_eq!(h.decrypt_string(12, 0, &enc), text, "{alg:?}");
|
||
if alg != Algorithm::Aes256 {
|
||
// R2–R4 derive a key per object; R6 uses the file key everywhere (§7.6.3.3).
|
||
assert_ne!(h.decrypt_string(13, 0, &enc), text, "object keys differ per object ({alg:?})");
|
||
}
|
||
let data: Vec<u8> = (0..1000u32).map(|i| (i * 7) as u8).collect();
|
||
let enc = h.encrypt_stream(5, 0, &data, StreamKind::Normal);
|
||
assert_eq!(h.decrypt_stream(5, 0, &enc, StreamKind::Normal), data, "{alg:?}");
|
||
// Deterministic: the same object encrypts to the same bytes (reproducible saves).
|
||
assert_eq!(h.encrypt_stream(5, 0, &data, StreamKind::Normal), enc);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn identity_crypt_filters_leave_data_alone() {
|
||
let mut d = new(Algorithm::Aes256, "", "o", -1).dict().clone();
|
||
// Embedded-file-only encryption: strings and normal streams are not encrypted.
|
||
d.str_f = b"Identity".to_vec();
|
||
d.stm_f = b"Identity".to_vec();
|
||
d.ef_f = b"StdCF".to_vec();
|
||
let h = SecurityHandler::open(d, ID, None).unwrap();
|
||
assert!(!h.encrypts_strings());
|
||
assert_eq!(h.decrypt_stream(3, 0, b"plain", StreamKind::Normal), b"plain");
|
||
let enc = h.encrypt_stream(3, 0, b"attachment", StreamKind::EmbeddedFile);
|
||
assert_ne!(enc, b"attachment");
|
||
assert_eq!(h.decrypt_stream(3, 0, &enc, StreamKind::EmbeddedFile), b"attachment");
|
||
assert_eq!(h.decrypt_stream(3, 0, b"x", StreamKind::Named(b"Identity")), b"x");
|
||
}
|
||
|
||
#[test]
|
||
fn r6_passwords_are_saslprepped() {
|
||
// U+00AD (soft hyphen) is mapped to nothing by SASLprep; "ª" normalises to "a" (NFKC).
|
||
let d = new(Algorithm::Aes256, "SªSL\u{AD}prep", "o", -1).dict().clone();
|
||
assert!(SecurityHandler::open(d.clone(), ID, Some("SaSLprep")).is_ok());
|
||
assert!(SecurityHandler::open(d, ID, Some("SaSL-prep")).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn unsupported_handlers_are_reported() {
|
||
let mut d = new(Algorithm::Aes128, "", "o", -1).dict().clone();
|
||
d.filter = b"Adobe.PubSec".to_vec();
|
||
assert!(matches!(SecurityHandler::open(d.clone(), ID, None), Err(CryptError::Unsupported(_))));
|
||
d.filter = b"Standard".to_vec();
|
||
d.r = 7;
|
||
assert!(matches!(SecurityHandler::open(d, ID, None), Err(CryptError::Unsupported(_))));
|
||
}
|
||
|
||
#[test]
|
||
fn permission_bits_follow_table_22() {
|
||
let p = Permissions { bits: -3904, owner: false }; // 0xFFFFF0C0: only reserved bits set
|
||
assert!(!p.print() && !p.modify() && !p.copy() && !p.annotate() && !p.assemble());
|
||
let p = Permissions { bits: -1, owner: false };
|
||
assert!(p.print() && p.modify() && p.copy() && p.annotate() && p.assemble() && p.fill_forms());
|
||
let p = Permissions { bits: 0, owner: true };
|
||
assert!(p.modify(), "owners ignore /P");
|
||
}
|
||
|
||
proptest::proptest! {
|
||
#[test]
|
||
fn malformed_dictionaries_never_panic(o in proptest::collection::vec(proptest::num::u8::ANY, 0..60),
|
||
u in proptest::collection::vec(proptest::num::u8::ANY, 0..60),
|
||
r in 0i64..9, len in -10i64..300) {
|
||
let d = EncryptDict { filter: b"Standard".to_vec(), v: 4, r, length_bits: len, o, u, p: -1, encrypt_metadata: true, ..Default::default() };
|
||
let _ = SecurityHandler::open(d, ID, Some("x"));
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn owner_authentication_recovers_the_user_password() {
|
||
for alg in [Algorithm::Rc4_40, Algorithm::Rc4_128, Algorithm::Aes128] {
|
||
let d = new(alg, "user pw", "owner pw", -1).dict().clone();
|
||
let h = SecurityHandler::open(d.clone(), ID, Some("owner pw")).unwrap();
|
||
assert_eq!(h.recovered_user_password().as_deref(), Some(&b"user pw"[..]), "{alg:?}");
|
||
assert_eq!(SecurityHandler::open(d, ID, Some("user pw")).unwrap().recovered_user_password(), None);
|
||
}
|
||
let d = new(Algorithm::Rc4_128, "", "owner", -1).dict().clone();
|
||
assert_eq!(SecurityHandler::open(d, ID, Some("owner")).unwrap().recovered_user_password().as_deref(), Some(&b""[..]));
|
||
}
|