mp4-rust
mp4-rust copied to clipboard
Audible AAX support
Opening this for some early feedback. My goal is to read and decrypt audio data from Audibles AAX files.
I learned how the adrm boxed worked by looking at FFmpeg and at some sample files with a hex editor. There is still 60 bytes that I don't know what they are for (unknown0) but they seem to be random bytes, maybe more checksums? 🤔
FFmpeg/libavformat also does the actual decryption when reading the track data, but I thought that that was out of scope for this project. Let me know what you think!
The biggest issue right now is the BoxType::UnknownBox(0x61617664) part. aavd boxes are basically identical to mp4a boxes, so I wanted to just reuse that instead of re-implementing the entire Mp4aBox again. It seems like something similar is also true for alac and fLaC as well. I'm very open to suggestions here!
Here is some work in progress code for actually decoding the data:
use aes::{
cipher::{generic_array::GenericArray, BlockDecryptMut, KeyIvInit},
Aes128,
};
use cbc::Decryptor;
use mp4::Mp4Reader;
use sha1::{Digest, Sha1};
use std::fs::File;
use std::io::BufReader;
const AUDIBLE_FIXED_KEY: [u8; 16] = [
0x77, 0x21, 0x4d, 0x4b, 0x19, 0x6a, 0x87, 0xcd, 0x52, 0x00, 0x45, 0xfd, 0x20, 0xa5, 0x1d, 0x67,
];
fn get_reader(path: &str) -> Mp4Reader<BufReader<File>> {
let f = File::open(path).unwrap();
let f_size = f.metadata().unwrap().len();
let reader = BufReader::new(f);
mp4::Mp4Reader::read_header(reader, f_size).unwrap()
}
#[test]
fn test_read_aax() {
let mut mp4 = get_reader("tests/samples/YourFirstListen_ep7.aax");
assert_eq!(mp4.ftyp.major_brand, "aax ".parse().unwrap());
let track = mp4.tracks().get(&1).unwrap();
// Put your activation bytes here!
let activation_bytes = [0x00, 0x00, 0x00, 0x00];
let adrm = track
.trak
.mdia
.minf
.stbl
.stsd
.mp4a
.as_ref()
.and_then(|mp4a| mp4a.adrm.as_ref())
.unwrap();
// Key Derivation
let mut sha = Sha1::new();
sha.update(AUDIBLE_FIXED_KEY);
sha.update(activation_bytes);
let intermediate_key = sha.finalize();
let mut sha = Sha1::new();
sha.update(AUDIBLE_FIXED_KEY);
sha.update(intermediate_key);
sha.update(activation_bytes);
let intermediate_iv = sha.finalize();
let mut sha = Sha1::new();
sha.update(&intermediate_key[..16]);
sha.update(&intermediate_iv[..16]);
let calculated_checksum = sha.finalize();
eprintln!("file_checksum: {:x?}", adrm.file_checksum);
eprintln!("calculated_checksum: {:x?}", calculated_checksum);
assert_eq!(calculated_checksum, adrm.file_checksum.into());
// Decryption setup
let mut aes =
Decryptor::<Aes128>::new_from_slices(&intermediate_key[..16], &intermediate_iv[..16])
.unwrap();
let mut data = adrm.drm_blob.to_owned();
aes.decrypt_block_mut(GenericArray::from_mut_slice(&mut data[0..16]));
aes.decrypt_block_mut(GenericArray::from_mut_slice(&mut data[16..32]));
aes.decrypt_block_mut(GenericArray::from_mut_slice(&mut data[32..48]));
assert_eq!(activation_bytes[0], data[3]);
assert_eq!(activation_bytes[1], data[2]);
assert_eq!(activation_bytes[2], data[1]);
assert_eq!(activation_bytes[3], data[0]);
eprintln!(
"Activation bytes: {:02x}{:02x}{:02x}{:02x}",
data[3], data[2], data[1], data[0]
);
// Read the entire track
for sample_id in 1.. {
if let Some(sample) = mp4.read_sample(1, sample_id).unwrap() {
assert!(!sample.bytes.is_empty());
let mut data = sample.bytes.to_vec();
// Reset the IV for each sample
let mut aes = Decryptor::<Aes128>::new_from_slices(
&intermediate_key[..16],
&intermediate_iv[..16],
)
.unwrap();
// trailing bytes are not encrypted!
let block_count = data.len() / 16;
for j in 0..block_count {
let start = j * 16;
let end = start + 16;
aes.decrypt_block_mut(GenericArray::from_mut_slice(&mut data[start..end]));
}
eprintln!("Decrypted sample {}: {} bytes", sample_id, data.len());
} else {
break;
}
}
}
Now, I think that this works, but I haven't actually tested it yet since I need to figure out how to write the decoded bytes into a new mp4 file with the aac data 😅
(rebased on master, should fix CI)
ping @alfg, do you have any input on this? 🙏
Hey @LinusU, sorry for the late response and thanks for the PR. I will check this out soon.