Add support for Audible AAX (and AAX+) files
authorVesselin Bontchev <vesselin.bontchev@yandex.com>
Sat, 11 Jul 2015 18:02:47 +0000 (18:02 +0000)
committerMichael Niedermayer <michael@niedermayer.cc>
Sun, 19 Jul 2015 18:28:39 +0000 (20:28 +0200)
Signed-off-by: Michael Niedermayer <michael@niedermayer.cc>
doc/general.texi
doc/muxers.texi
libavformat/isom.h
libavformat/mov.c

index dc22d90..617be66 100644 (file)
@@ -226,6 +226,8 @@ library:
 @item 4xm                       @tab   @tab X
     @tab 4X Technologies format, used in some games.
 @item 8088flex TMV              @tab   @tab X
+@item AAX                       @tab   @tab X
+    @tab Audible Enhanced Audio format, used in audiobooks.
 @item ACT Voice                 @tab   @tab X
     @tab contains G.729 audio
 @item Adobe Filmstrip           @tab X @tab X
index 8e3eb4b..ccd222c 100644 (file)
@@ -667,6 +667,13 @@ point on IIS with this muxer. Example:
 ffmpeg -re @var{<normal input/transcoding options>} -movflags isml+frag_keyframe -f ismv http://server/publishingpoint.isml/Streams(Encoder1)
 @end example
 
+@subsection Audible AAX
+
+Audible AAX files are encrypted M4B files, and they can be decrypted by specifying a 4 byte activation secret.
+@example
+ffmpeg -activation_bytes 1CEB00DA -i test.aax -vn -c:a copy output.mp4
+@end example
+
 @section mp3
 
 The MP3 muxer writes a raw MP3 stream with the following optional features:
index 5d48989..aee9d6e 100644 (file)
@@ -198,6 +198,14 @@ typedef struct MOVContext {
     MOVFragmentIndex** fragment_index_data;
     unsigned fragment_index_count;
     int atom_depth;
+    unsigned int aax_mode;  ///< 'aax' file has been detected
+    uint8_t file_key[20];
+    uint8_t file_iv[20];
+    void *activation_bytes;
+    int activation_bytes_size;
+    void *audible_fixed_key;
+    int audible_fixed_key_size;
+    struct AVAES *aes_decrypt;
 } MOVContext;
 
 int ff_mp4_read_descr_len(AVIOContext *pb);
index 9d6b2e4..92bf2f8 100644 (file)
@@ -37,6 +37,8 @@
 #include "libavutil/dict.h"
 #include "libavutil/display.h"
 #include "libavutil/opt.h"
+#include "libavutil/aes.h"
+#include "libavutil/sha.h"
 #include "libavutil/timecode.h"
 #include "libavcodec/ac3tab.h"
 #include "avformat.h"
@@ -807,6 +809,120 @@ static int mov_read_mdat(MOVContext *c, AVIOContext *pb, MOVAtom atom)
     return 0; /* now go for moov */
 }
 
+#define DRM_BLOB_SIZE 56
+
+static int mov_read_adrm(MOVContext *c, AVIOContext *pb, MOVAtom atom)
+{
+    uint8_t intermediate_key[20];
+    uint8_t intermediate_iv[20];
+    uint8_t input[64];
+    uint8_t output[64];
+    uint8_t file_checksum[20];
+    uint8_t calculated_checksum[20];
+    struct AVSHA *sha;
+    int i;
+    int ret = 0;
+    uint8_t *activation_bytes = c->activation_bytes;
+    uint8_t *fixed_key = c->audible_fixed_key;
+
+    c->aax_mode = 1;
+
+    sha = av_sha_alloc();
+    if (!sha)
+        return AVERROR(ENOMEM);
+    c->aes_decrypt = av_aes_alloc();
+    if (!c->aes_decrypt) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    /* drm blob processing */
+    avio_read(pb, output, 8); // go to offset 8, absolute postion 0x251
+    avio_read(pb, input, DRM_BLOB_SIZE);
+    avio_read(pb, output, 4); // go to offset 4, absolute postion 0x28d
+    avio_read(pb, file_checksum, 20);
+
+    av_log(c->fc, AV_LOG_INFO, "[aax] file checksum == "); // required by external tools
+    for (i = 0; i < 20; i++)
+        av_log(sha, AV_LOG_INFO, "%02x", file_checksum[i]);
+    av_log(c->fc, AV_LOG_INFO, "\n");
+
+    /* verify activation data */
+    if (!activation_bytes || c->activation_bytes_size != 4) {
+        av_log(c->fc, AV_LOG_FATAL, "[aax] activation_bytes option is missing!\n");
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+    if (c->activation_bytes_size != 4) {
+        av_log(c->fc, AV_LOG_FATAL, "[aax] activation_bytes value needs to be 4 bytes!\n");
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+
+    /* verify fixed key */
+    if (c->audible_fixed_key_size != 16) {
+        av_log(c->fc, AV_LOG_FATAL, "[aax] audible_fixed_key value needs to be 16 bytes!\n");
+        ret = AVERROR(EINVAL);
+        goto fail;
+    }
+
+    /* AAX (and AAX+) key derivation */
+    av_sha_init(sha, 160);
+    av_sha_update(sha, fixed_key, 16);
+    av_sha_update(sha, activation_bytes, 4);
+    av_sha_final(sha, intermediate_key);
+    av_sha_init(sha, 160);
+    av_sha_update(sha, fixed_key, 16);
+    av_sha_update(sha, intermediate_key, 20);
+    av_sha_update(sha, activation_bytes, 4);
+    av_sha_final(sha, intermediate_iv);
+    av_sha_init(sha, 160);
+    av_sha_update(sha, intermediate_key, 16);
+    av_sha_update(sha, intermediate_iv, 16);
+    av_sha_final(sha, calculated_checksum);
+    if (memcmp(calculated_checksum, file_checksum, 20)) { // critical error
+        av_log(c->fc, AV_LOG_ERROR, "[aax] mismatch in checksums!\n");
+        ret = AVERROR_INVALIDDATA;
+        goto fail;
+    }
+    av_aes_init(c->aes_decrypt, intermediate_key, 128, 1);
+    av_aes_crypt(c->aes_decrypt, output, input, DRM_BLOB_SIZE >> 4, intermediate_iv, 1);
+    for (i = 0; i < 4; i++) {
+        // file data (in output) is stored in big-endian mode
+        if (activation_bytes[i] != output[3 - i]) { // critical error
+            av_log(c->fc, AV_LOG_ERROR, "[aax] error in drm blob decryption!\n");
+            ret = AVERROR_INVALIDDATA;
+            goto fail;
+        }
+    }
+    memcpy(c->file_key, output + 8, 16);
+    memcpy(input, output + 26, 16);
+    av_sha_init(sha, 160);
+    av_sha_update(sha, input, 16);
+    av_sha_update(sha, c->file_key, 16);
+    av_sha_update(sha, fixed_key, 16);
+    av_sha_final(sha, c->file_iv);
+
+fail:
+    av_free(sha);
+
+    return ret;
+}
+
+// Audible AAX (and AAX+) bytestream decryption
+static int aax_filter(uint8_t *input, int size, MOVContext *c)
+{
+    int blocks = 0;
+    unsigned char iv[16];
+
+    memcpy(iv, c->file_iv, 16); // iv is overwritten
+    blocks = size >> 4; // trailing bytes are not encrypted!
+    av_aes_init(c->aes_decrypt, c->file_key, 128, 1);
+    av_aes_crypt(c->aes_decrypt, input, input, blocks, iv, 1);
+
+    return 0;
+}
+
 /* read major brand, minor version and compatible brands and store them as metadata */
 static int mov_read_ftyp(MOVContext *c, AVIOContext *pb, MOVAtom atom)
 {
@@ -3637,6 +3753,7 @@ static const MOVParseTableEntry mov_default_parse_table[] = {
 { MKTAG('e','l','s','t'), mov_read_elst },
 { MKTAG('e','n','d','a'), mov_read_enda },
 { MKTAG('f','i','e','l'), mov_read_fiel },
+{ MKTAG('a','d','r','m'), mov_read_adrm },
 { MKTAG('f','t','y','p'), mov_read_ftyp },
 { MKTAG('g','l','b','l'), mov_read_glbl },
 { MKTAG('h','d','l','r'), mov_read_hdlr },
@@ -4058,6 +4175,8 @@ static int mov_read_close(AVFormatContext *s)
     }
     av_freep(&mov->fragment_index_data);
 
+    av_freep(&mov->aes_decrypt);
+
     return 0;
 }
 
@@ -4477,6 +4596,9 @@ static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
     pkt->flags |= sample->flags & AVINDEX_KEYFRAME ? AV_PKT_FLAG_KEY : 0;
     pkt->pos = sample->pos;
 
+    if (mov->aax_mode)
+        aax_filter(pkt->data, pkt->size, mov);
+
     return 0;
 }
 
@@ -4590,6 +4712,12 @@ static const AVOption mov_options[] = {
         AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, .flags = FLAGS },
     { "export_xmp", "Export full XMP metadata", OFFSET(export_xmp),
         AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, .flags = FLAGS },
+    { "activation_bytes", "Secret bytes for Audible AAX files", OFFSET(activation_bytes),
+        AV_OPT_TYPE_BINARY, .flags = AV_OPT_FLAG_DECODING_PARAM },
+    { "audible_fixed_key", // extracted from libAAX_SDK.so and AAXSDKWin.dll files!
+        "Fixed key used for handling Audible AAX files", OFFSET(audible_fixed_key),
+        AV_OPT_TYPE_BINARY, {.str="77214d4b196a87cd520045fd20a51d67"},
+        .flags = AV_OPT_FLAG_DECODING_PARAM },
     { NULL },
 };