use std::io::Read;
use base64::Engine;
use flamberge_crypto::{aes, kdf};
use crate::{Result, SchemeError};
pub(crate) fn decrypt_member(book_key: &[u8; 16], data: &[u8]) -> Result<Vec<u8>> {
if data.len() < 16 || data.len() % 16 != 0 {
return Err(invalid(
"EPUB encrypted member is not a whole number of AES blocks",
));
}
let (iv, ciphertext) = data.split_at(16);
let plain = aes::cbc_decrypt(book_key, iv, ciphertext)?;
let plain = kdf::pkcs7_unpad(&plain, 16)?;
Ok(raw_inflate(&plain).unwrap_or(plain))
}
pub(crate) fn raw_inflate(data: &[u8]) -> Option<Vec<u8>> {
let mut out = Vec::new();
flate2::read::DeflateDecoder::new(data)
.read_to_end(&mut out)
.ok()
.map(|_| out)
}
pub(crate) fn decode_b64(s: &str) -> Result<Vec<u8>> {
let compact: String = s.chars().filter(|c| !c.is_ascii_whitespace()).collect();
base64::engine::general_purpose::STANDARD
.decode(compact.as_bytes())
.map_err(|e| invalid(&format!("invalid base64 wrapped key: {e}")))
}
pub(crate) fn invalid(msg: &str) -> SchemeError {
SchemeError::Format(flamberge_formats::FormatError::Invalid(msg.to_string()))
}
#[cfg(test)]
pub(crate) mod test_support {
use std::collections::BTreeMap;
use std::io::{Read, Write};
use flamberge_crypto::aes;
use zip::write::SimpleFileOptions;
use zip::{CompressionMethod, ZipWriter};
pub(crate) const ADEPT_NS: &str = "http://ns.adobe.com/adept";
pub(crate) const ENC_NS: &str = "http://www.w3.org/2001/04/xmlenc#";
pub(crate) fn pkcs7_pad(data: &[u8], block: usize) -> Vec<u8> {
let pad = block - data.len() % block;
let mut out = data.to_vec();
out.extend(std::iter::repeat_n(pad as u8, pad));
out
}
pub(crate) fn raw_deflate(data: &[u8]) -> Vec<u8> {
let mut e = flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
e.write_all(data).unwrap();
e.finish().unwrap()
}
pub(crate) fn encrypt_member(book_key: &[u8; 16], content: &[u8], deflate: bool) -> Vec<u8> {
let body = if deflate {
raw_deflate(content)
} else {
content.to_vec()
};
let iv = [0x5Au8; 16];
let ct = aes::cbc_encrypt(book_key, &iv, &pkcs7_pad(&body, 16)).unwrap();
let mut out = iv.to_vec();
out.extend_from_slice(&ct);
out
}
pub(crate) fn rights_xml(key_b64: &str) -> Vec<u8> {
format!(
"<?xml version=\"1.0\"?>\
<adept:rights xmlns:adept=\"{ADEPT_NS}\">\
<adept:licenseToken><adept:encryptedKey>{key_b64}</adept:encryptedKey>\
</adept:licenseToken></adept:rights>"
)
.into_bytes()
}
pub(crate) fn encryption_xml(paths: &[&str]) -> Vec<u8> {
let mut body = format!("<encryption xmlns:enc=\"{ENC_NS}\">");
for p in paths {
body.push_str(&format!(
"<enc:EncryptedData><enc:CipherData>\
<enc:CipherReference URI=\"{p}\"/>\
</enc:CipherData></enc:EncryptedData>"
));
}
body.push_str("</encryption>");
body.into_bytes()
}
pub(crate) fn build_zip(members: &[(&str, Vec<u8>)]) -> Vec<u8> {
let mut buf = Vec::new();
{
let mut w = ZipWriter::new(std::io::Cursor::new(&mut buf));
for (name, bytes) in members {
let method = if *name == "mimetype" {
CompressionMethod::Stored
} else {
CompressionMethod::Deflated
};
w.start_file(
*name,
SimpleFileOptions::default().compression_method(method),
)
.unwrap();
w.write_all(bytes).unwrap();
}
w.finish().unwrap();
}
buf
}
pub(crate) fn read_zip(bytes: &[u8]) -> BTreeMap<String, Vec<u8>> {
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)).unwrap();
let mut out = BTreeMap::new();
for i in 0..archive.len() {
let mut f = archive.by_index(i).unwrap();
let name = f.name().to_owned();
let mut b = Vec::new();
f.read_to_end(&mut b).unwrap();
out.insert(name, b);
}
out
}
}