use flamberge_crypto::{crc32, digest};
pub const CHARMAP1: &[u8] = b"n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M";
pub const CHARMAP3: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
pub const CHARMAP4: &[u8] = b"ABCDEFGHIJKLMNPQRSTUVWXYZ123456789";
pub fn encode(data: &[u8], map: &[u8]) -> Vec<u8> {
let n = map.len();
let mut out = Vec::with_capacity(data.len() * 2);
for &b in data {
let q = ((b ^ 0x80) as usize) / n;
let r = (b as usize) % n;
out.push(map[q]);
out.push(map[r]);
}
out
}
pub fn encode_hash(data: &[u8], map: &[u8]) -> Vec<u8> {
encode(&digest::md5(data), map)
}
fn two_bits(field: &[u8], offset: usize) -> u8 {
let byte_number = offset / 4;
let bit_position = 6 - 2 * (offset % 4);
(field[byte_number] >> bit_position) & 3
}
fn six_bits(field: &[u8], offset: usize) -> u8 {
let o = offset * 3;
(two_bits(field, o) << 4) + (two_bits(field, o + 1) << 2) + two_bits(field, o + 2)
}
pub fn encode_pid(hash: &[u8]) -> String {
let mut pid = String::with_capacity(8);
for position in 0..8 {
pid.push(CHARMAP3[six_bits(hash, position) as usize] as char);
}
pid
}
pub fn checksum_pid(s: &str) -> String {
let mut crc = crc32::flamberge(s.as_bytes());
crc ^= crc >> 16;
let l = CHARMAP4.len() as u32;
let mut res = String::from(s);
for _ in 0..2 {
let b = crc & 0xff;
let pos = (b / l) ^ (b % l);
res.push(CHARMAP4[(pos % l) as usize] as char);
crc >>= 8;
}
res
}
pub fn pid_from_serial(s: &[u8], l: usize) -> String {
let crc = crc32::flamberge(s);
let mut arr = vec![0u8; l];
for (i, &b) in s.iter().enumerate() {
arr[i % l] ^= b;
}
let crc_bytes = [
(crc >> 24) as u8,
(crc >> 16) as u8,
(crc >> 8) as u8,
crc as u8,
];
for i in 0..l {
arr[i] ^= crc_bytes[i & 3];
}
let mut pid = String::with_capacity(l);
for &byte in arr.iter() {
let b = byte as usize;
let idx = (b >> 7) + ((b >> 5 & 3) ^ (b & 0x1f));
pid.push(CHARMAP4[idx] as char);
}
pid
}
pub fn book_pid_from_serial(serial: &[u8], rec209: &[u8], token: &[u8]) -> String {
let mut buf = Vec::with_capacity(serial.len() + rec209.len() + token.len());
buf.extend_from_slice(serial);
buf.extend_from_slice(rec209);
buf.extend_from_slice(token);
checksum_pid(&encode_pid(&digest::sha1(&buf)))
}
pub fn eink_pid_from_serial(serial: &str) -> String {
let base = pid_from_serial(serial.as_bytes(), 7);
checksum_pid(&format!("{base}*"))
}
fn generate_device_pid(dsn: &[u8]) -> Option<String> {
if dsn.len() < 4 {
return None;
}
let seed = crc32::flamberge(&dsn[..4]);
let mut pid = [
(seed >> 24) as u8,
(seed >> 16) as u8,
(seed >> 8) as u8,
seed as u8,
(seed >> 24) as u8,
(seed >> 16) as u8,
(seed >> 8) as u8,
seed as u8,
];
let mut index = 0;
for &b in &dsn[..4] {
pid[index] ^= b;
index = (index + 1) % 8;
}
let mut out = String::with_capacity(8);
for &b in &pid {
let idx = ((((b >> 5) & 3) ^ b) & 0x1f) as usize + (b >> 7) as usize;
out.push(CHARMAP4[idx] as char);
}
Some(out)
}
fn db_hex(db: &crate::kindle::KindleDb, name: &str) -> Option<Vec<u8>> {
db.get(name).and_then(|h| hex::decode(h).ok())
}
pub fn k4_pids(rec209: &[u8], token: &[u8], db: &crate::kindle::KindleDb) -> Vec<String> {
let account_token = db_hex(db, "kindle.account.tokens").unwrap_or_default();
let dsn = match db_hex(db, "DSN") {
Some(dsn) => dsn,
None => {
let Some(mazama) = db_hex(db, "MazamaRandomNumber") else {
return Vec::new();
};
let Some(id_string) = db_hex(db, "SerialNumber").or_else(|| db_hex(db, "IDString"))
else {
return Vec::new();
};
let encoded_username = match db_hex(db, "UsernameHash") {
Some(hash) => hash,
None => match db_hex(db, "UserName") {
Some(name) => encode_hash(&name, CHARMAP1),
None => return Vec::new(),
},
};
let encoded_id = encode_hash(&id_string, CHARMAP1);
let mut buf = mazama;
buf.extend_from_slice(&encoded_id);
buf.extend_from_slice(&encoded_username);
encode(&digest::sha1(&buf), CHARMAP1)
}
};
let mut pids = Vec::new();
if let Some(device_pid) = generate_device_pid(&dsn) {
pids.push(checksum_pid(&device_pid));
}
let book_pid = |parts: &[&[u8]]| -> String {
let buf: Vec<u8> = parts.concat();
checksum_pid(&encode_pid(&digest::sha1(&buf)))
};
pids.push(book_pid(&[&dsn, &account_token, rec209, token]));
pids.push(book_pid(&[&account_token, rec209, token]));
pids.push(book_pid(&[&dsn, rec209, token]));
pids
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn encode_pid_is_eight_chars() {
let hash = digest::sha1(b"B00120304050607*rec209token");
assert_eq!(encode_pid(&hash).len(), 8);
}
#[test]
fn checksum_adds_two_chars() {
let base = "ABCD1234";
let full = checksum_pid(base);
assert_eq!(full.len(), 10);
assert!(full.starts_with(base));
}
#[test]
fn k4_pids_from_explicit_dsn() {
let mut db = crate::kindle::KindleDb::new();
db.insert("DSN".into(), hex::encode(b"device-serial-number"));
db.insert("kindle.account.tokens".into(), hex::encode(b"acct-token"));
let pids = k4_pids(b"\x00rec209data", b"tokenbytes", &db);
assert_eq!(pids.len(), 4);
for p in &pids {
assert_eq!(p.len(), 10, "each book PID is 10 chars: {p}");
}
}
#[test]
fn k4_pids_derives_dsn_from_mazama() {
let mut db = crate::kindle::KindleDb::new();
db.insert("MazamaRandomNumber".into(), hex::encode(b"mazama-rand"));
db.insert("SerialNumber".into(), hex::encode(b"B00XYZ"));
db.insert("UserName".into(), hex::encode(b"alice"));
db.insert("kindle.account.tokens".into(), hex::encode(b"acct"));
let pids = k4_pids(b"rec", b"tok", &db);
assert_eq!(pids.len(), 4);
}
#[test]
fn k4_pids_empty_without_dsn_inputs() {
let db = crate::kindle::KindleDb::new();
assert!(k4_pids(b"rec", b"tok", &db).is_empty());
}
#[test]
fn eink_pid_is_ten_chars() {
let pid = eink_pid_from_serial("B001234567890123");
assert_eq!(pid.len(), 10);
let bytes = pid.as_bytes();
assert_eq!(bytes[7], b'*');
for &c in bytes.iter().take(7).chain(bytes.iter().skip(8)) {
assert!(CHARMAP4.contains(&c), "unexpected PID char: {}", c as char);
}
}
}