[FFmpeg-devel] [PATCH v2] Add support for Audible AAX (and AAX+) files
James Almer
jamrial at gmail.com
Sun Jul 12 05:59:05 CEST 2015
On 11/07/15 11:38 PM, Vesselin Bontchev wrote:
> diff --git a/libavformat/mov.c b/libavformat/mov.c
> index 6d59863..62495a8 100644
> --- a/libavformat/mov.c
> +++ b/libavformat/mov.c
> @@ -26,6 +26,7 @@
> #include <inttypes.h>
> #include <limits.h>
> #include <stdint.h>
> +#include <ctype.h>
>
> #include "libavutil/attributes.h"
> #include "libavutil/channel_layout.h"
> @@ -37,6 +38,8 @@
> #include "libavutil/dict.h"
> #include "libavutil/display.h"
> #include "libavutil/opt.h"
> +#include "libavutil/aes.h"
> +#include "libavutil/hash.h"
> #include "libavutil/timecode.h"
> #include "libavcodec/ac3tab.h"
> #include "avformat.h"
> @@ -807,6 +810,126 @@ static int mov_read_mdat(MOVContext *c, AVIOContext *pb, MOVAtom atom)
> return 0; /* now go for moov */
> }
>
> +
> +static int hexchar2int(char c) {
> + if (c >= '0' && c <= '9') return c - '0';
> + if (c >= 'a' && c <= 'f') return c - 'a' + 10;
> + if (c >= 'A' && c <= 'F') return c - 'A' + 10;
> + return -1;
> +}
> +
> +static void hex_encode(unsigned char *s, int len, unsigned char *o)
> +{
> + char itoa16_private[16] = "0123456789abcdef";
> + int i;
> + for (i = 0; i < len; ++i) {
> + o[0] = itoa16_private[s[i] >> 4];
> + o[1] = itoa16_private[s[i] & 15];
> + o += 2;
> + }
> +}
> +
> +#define DRM_BLOB_SIZE 56
> +
> +static int aax_parser(MOVContext *c, AVIOContext *pb)
> +{
> + unsigned char activation_bytes[4];
> +
> + // extracted from libAAX_SDK.so and AAXSDKWin.dll files!
> + unsigned char fixed_key[] = { 0x77, 0x21, 0x4d, 0x4b, 0x19, 0x6a, 0x87, 0xcd,
> + 0x52, 0x00, 0x45, 0xfd, 0x20, 0xa5, 0x1d, 0x67 };
> + unsigned char intermediate_key[20] = {0};
> + unsigned char intermediate_iv[20] = {0};
> + unsigned char input[64] = {0};
> + unsigned char output[64] = {0};
> + unsigned char file_checksum[20] = {0};
> + unsigned char file_checksum_encoded[41] = {0};
> + unsigned char file_key_encoded[41] = {0};
> + unsigned char file_iv_encoded[41] = {0};
> + unsigned char calculated_checksum[20];
> + struct AVHashContext *ctx;
> + struct AVAES *aes_decrypt;
> + int a, b, i;
> + const char *magic = "drm";
> + char *s;
> +
> + av_hash_alloc(&ctx, "SHA160");
If you only need sha1 then it may be a better idea to use the sha functions directly,
rather than the hash API. The latter is mainly meant for cases where you might need
more than one type of hash algorithm.
Unless you plan to use the special final() functions (To get an hex, bin, or base64
digest), using it for a single algorithm just adds one more layer for no real gain.
See libavutil/sha.h
> + aes_decrypt = av_aes_alloc();
> + if (!aes_decrypt) {
> + return AVERROR(ENOMEM);
> + }
> +
> + /* drm blob processing */
> + avio_seek(pb, 0x246, 0);
> + avio_read(pb, input, 3);
> + if (strncmp(input, magic, 3)) {
> + av_log(c->fc, AV_LOG_FATAL, "[aax] drm blob is missing from this file!\n");
> + exit(-1);
> + }
> + avio_seek(pb, 0x251, 0);
> + avio_read(pb, input, DRM_BLOB_SIZE);
> + avio_seek(pb, 0x28d, 0);
> + avio_read(pb, file_checksum, 20);
> + hex_encode(file_checksum, 20, file_checksum_encoded);
> + av_log(c->fc, AV_LOG_DEBUG, "[aax] file checksum == %s\n", file_checksum_encoded);
> +
> + /* extract activation data */
> + s = getenv("activation_bytes");
> + if (!s || strlen(s) < 8) {
> + av_log(c->fc, AV_LOG_FATAL, "[aax] export activation_bytes=<value> is missing!\n");
> + exit(-1); // XXX exit more gracefully
> + }
> + av_log(c->fc, AV_LOG_DEBUG, "[aax] activation_bytes == %s!\n", s);
> + for (i = 0; i < 4 && isxdigit(*s); i++) {
> + a = hexchar2int(*s++);
> + b = hexchar2int(*s++);
> + activation_bytes[i] = (a << 4) | b;
> + }
> +
> + /* AAX (and AAX+) key derivation */
> + av_hash_init(ctx);
> + av_hash_update(ctx, fixed_key, 16);
> + av_hash_update(ctx, activation_bytes, 4);
> + av_hash_final(ctx, intermediate_key);
> + av_hash_init(ctx);
> + av_hash_update(ctx, fixed_key, 16);
> + av_hash_update(ctx, intermediate_key, 20);
> + av_hash_update(ctx, activation_bytes, 4);
> + av_hash_final(ctx, intermediate_iv);
> + av_hash_init(ctx);
> + av_hash_update(ctx, intermediate_key, 16);
> + av_hash_update(ctx, intermediate_iv, 16);
> + av_hash_final(ctx, calculated_checksum);
> + if (memcmp(calculated_checksum, file_checksum, 20)) {
> + av_log(c->fc, AV_LOG_ERROR, "[aax] mismatch in checksums, terminating!\n");
> + exit(-1);
> + }
> + av_aes_init(aes_decrypt, intermediate_key, 128, 1);
> + av_aes_crypt(aes_decrypt, output, input, DRM_BLOB_SIZE >> 4, intermediate_iv, 1);
> + for (i = 0; i < 4; i++) {
> + if (activation_bytes[i] != output[3 - i]) {
> + av_log(c->fc, AV_LOG_ERROR, "[aax] error in drm blob decryption, terminating!\n");
> + exit(-1);
> + }
> + }
> + memcpy(c->file_key, output + 8, 16);
> + memcpy(input, output + 26, 16);
> + av_hash_init(ctx);
> + av_hash_update(ctx, input, 16);
> + av_hash_update(ctx, c->file_key, 16);
> + av_hash_update(ctx, fixed_key, 16);
> + av_hash_final(ctx, c->file_iv);
As i said, what you do gain from using the hash API is the possibility of requesting
the final digest in hex form directly with av_hash_final_hex(), which may come in
handy here.
> + hex_encode(c->file_key, 16, file_key_encoded);
> + av_log(c->fc, AV_LOG_DEBUG, "[aax] file key == %s\n", file_key_encoded);
> + hex_encode(c->file_iv, 16, file_iv_encoded);
> + av_log(c->fc, AV_LOG_DEBUG, "[aax] file iv == %s\n", file_iv_encoded);
> +
> + av_free(aes_decrypt);
> + av_free(ctx);
For the hash API you need to call av_hash_freep(). av_free() is fine if you use the
sha functions directly.
> +
> + 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)
> {
> @@ -814,6 +937,7 @@ static int mov_read_ftyp(MOVContext *c, AVIOContext *pb, MOVAtom atom)
> int comp_brand_size;
> char* comp_brands_str;
> uint8_t type[5] = {0};
> + int64_t current_pos = avio_tell(pb);
> int ret = ffio_read_size(pb, type, 4);
> if (ret < 0)
> return ret;
> @@ -822,6 +946,13 @@ static int mov_read_ftyp(MOVContext *c, AVIOContext *pb, MOVAtom atom)
> c->isom = 1;
> av_log(c->fc, AV_LOG_DEBUG, "ISO: File Type Major Brand: %.4s\n",(char *)&type);
> av_dict_set(&c->fc->metadata, "major_brand", type, 0);
> + /* Recognize Audible AAX files */
> + if (!strncmp((char*)&type, "aax", 3)) {
> + av_log(c->fc, AV_LOG_DEBUG, "[aax] aax file detected!\n");
> + c->aax_mode = 1;
> + aax_parser(c, pb);
> + avio_seek(pb, current_pos, 0);
> + }
> minor_ver = avio_rb32(pb); /* minor version */
> av_dict_set_int(&c->fc->metadata, "minor_version", minor_ver, 0);
>
> @@ -4336,6 +4467,35 @@ static int should_retry(AVIOContext *pb, int error_code) {
> return 1;
> }
>
> +/* Audible AAX (and AAX+) bytestream decryption
> + *
> + * export activation_bytes=CAFED00D # only 4 bytes ;)
> + *
> + * ffmpeg -i test.aax -vn -c:a copy -v debug output.mp4
> + */
> +static int aax_filter(uint8_t *input, int size, MOVContext *c)
> +{
> + int blocks = 0;
> + unsigned char key[16];
> + unsigned char iv[16];
> + struct AVAES *aes_decrypt;
> +
> + aes_decrypt = av_aes_alloc();
> + if (!aes_decrypt) {
> + return AVERROR(ENOMEM);
> + }
> +
> + memcpy(key, c->file_key, 16);
> + memcpy(iv, c->file_iv, 16);
> + blocks = size >> 4;
> + av_aes_init(aes_decrypt, key, 128, 1);
> + av_aes_crypt(aes_decrypt, input, input, blocks, iv, 1);
> +
> + av_free(aes_decrypt);
> +
> + return 0;
> +}
> +
> static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
> {
> MOVContext *mov = s->priv_data;
> @@ -4430,6 +4590,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;
> }
More information about the ffmpeg-devel
mailing list