mp4-rust icon indicating copy to clipboard operation
mp4-rust copied to clipboard

Audible AAX support

Open LinusU opened this issue 2 years ago • 3 comments

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!

LinusU avatar Mar 19 '23 05:03 LinusU

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 😅

LinusU avatar Mar 19 '23 06:03 LinusU

(rebased on master, should fix CI)

ping @alfg, do you have any input on this? 🙏

LinusU avatar Jun 20 '23 06:06 LinusU

Hey @LinusU, sorry for the late response and thanks for the PR. I will check this out soon.

alfg avatar Jun 21 '23 03:06 alfg