mod content;
mod db;
use std::collections::BTreeMap;
use flamberge_formats::ocf;
use crate::{DecryptedBook, KeyStore, Result, SchemeError};
use content::{check, decrypt_member, CheckResult};
const ZIP_MAGIC: &[u8; 4] = b"PK\x03\x04";
pub fn decrypt(input: &[u8], keys: &KeyStore) -> Result<DecryptedBook> {
if !input.starts_with(ZIP_MAGIC) {
return Err(SchemeError::NotThisScheme);
}
let db_bytes = keys.kobo_db.as_deref().ok_or_else(|| {
invalid(
"Kobo decryption needs the library SQLite DB (KoboReader.sqlite); none was provided",
)
})?;
let db::VolumeKeys {
keys: wrapped_keys,
title,
} = db::read_volume(db_bytes, keys.kobo_volumeid.as_deref())?;
let wrapped: BTreeMap<&str, &[u8]> = wrapped_keys
.iter()
.map(|k| (k.elementid.as_str(), k.wrapped.as_slice()))
.collect();
let members = ocf::read_all_members(input)?;
for user_key in &keys.kobo_keys {
if let Some(replacements) = try_key(user_key, &wrapped, &members) {
let data = ocf::repackage(input, &replacements)?;
return Ok(DecryptedBook {
data,
extension: "epub".to_string(),
title,
});
}
}
Err(SchemeError::NoKeyWorked)
}
fn try_key(
user_key: &[u8; 16],
wrapped: &BTreeMap<&str, &[u8]>,
members: &[(String, Vec<u8>)],
) -> Option<BTreeMap<String, Vec<u8>>> {
let mut replacements = BTreeMap::new();
for (name, ciphertext) in members {
let Some(&page_key) = wrapped.get(name.as_str()) else {
continue; };
let plain = decrypt_member(user_key, page_key, ciphertext).ok()?;
if check(name, &plain) == CheckResult::Failed {
return None;
}
replacements.insert(name.clone(), plain);
}
Some(replacements)
}
fn invalid(msg: impl Into<String>) -> SchemeError {
SchemeError::Format(flamberge_formats::FormatError::Invalid(msg.into()))
}
#[cfg(test)]
mod tests;