From 449944e25f306d97df714e13e9036bce60161e6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= Date: Sat, 9 May 2026 09:40:56 +0200 Subject: [PATCH] demux/avio_crypto: add AES decryption This mirrors `crypto+` protocol from ffmpeg, which is used in HLS demuxer. Unfortunatelly ffmpeg doesn't use custom io_open in their crypto.c, so we spin our own decryption. --- demux/avio_crypto.c | 164 +++++++++++++++++++++++++++++++++++++++++++ demux/avio_crypto.h | 32 +++++++++ meson.build | 3 +- stream/stream_curl.c | 128 ++++++++++++++++++++++++++++----- 4 files changed, 308 insertions(+), 19 deletions(-) create mode 100644 demux/avio_crypto.c create mode 100644 demux/avio_crypto.h diff --git a/demux/avio_crypto.c b/demux/avio_crypto.c new file mode 100644 index 0000000000..29b3771a30 --- /dev/null +++ b/demux/avio_crypto.c @@ -0,0 +1,164 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#include + +#include +#include +#include +#include + +#include "avio_crypto.h" + +#include "common/common.h" +#include "misc/bstr.h" +#include "mpv_talloc.h" + +#define BLOCKSIZE 16 +// In-place decrypt scratch. Holds unconsumed plaintext followed by at most one +// held-back ciphertext block. Any value >= 2*BLOCKSIZE works, this is just a +// reasonable batch size. +#define IO_BUFFER_SIZE (4 * 1024) +#define AVIO_BUFFER_SIZE (64 * 1024) + +struct crypto_priv { + AVIOContext *inner; + struct AVAES *aes; + uint8_t iv[BLOCKSIZE]; + + // [0 .. plain_pos) consumed plaintext (free space) + // [plain_pos .. plain_end) plaintext to deliver + // [plain_end .. ct_end) held-back ciphertext (< BLOCKSIZE) + uint8_t buf[IO_BUFFER_SIZE]; + int plain_pos; + int plain_end; + int ct_end; + bool inner_eof; +}; + +static void priv_destructor(void *ptr) +{ + struct crypto_priv *p = ptr; + av_free(p->aes); +} + +static int crypto_read(void *opaque, uint8_t *buf, int size) +{ + struct crypto_priv *p = opaque; + + if (size <= 0) + return 0; + + while (p->plain_pos == p->plain_end) { + // Compact held-back ciphertext to the start of the buffer. + int held = p->ct_end - p->plain_end; + if (held > 0 && p->plain_end > 0) + memmove(p->buf, p->buf + p->plain_end, held); + p->plain_pos = p->plain_end = 0; + p->ct_end = held; + + // Refill until we have two blocks pending (so the final one can stay + // held back for PKCS#7 stripping) or hit EOF. + while (!p->inner_eof && p->ct_end < 2 * BLOCKSIZE) { + int n = avio_read(p->inner, p->buf + p->ct_end, + sizeof(p->buf) - p->ct_end); + if (n <= 0) { + p->inner_eof = true; + break; + } + p->ct_end += n; + } + + int blocks = p->ct_end / BLOCKSIZE; + if (!p->inner_eof) + blocks--; + if (blocks <= 0) + return AVERROR_EOF; + + av_aes_crypt(p->aes, p->buf, p->buf, blocks, p->iv, 1); + p->plain_end = blocks * BLOCKSIZE; + + // Final batch: drop PKCS#7 padding bytes from the tail. + if (p->inner_eof && p->plain_end == p->ct_end) { + int padding = p->buf[p->plain_end - 1]; + if (padding < 1 || padding > BLOCKSIZE) + return AVERROR_INVALIDDATA; + p->plain_end -= padding; + } + } + + int copy = MPMIN(p->plain_end - p->plain_pos, size); + memcpy(buf, p->buf + p->plain_pos, copy); + p->plain_pos += copy; + return copy; +} + +int mp_avio_crypto_open(AVIOContext **out_pb, AVIOContext *inner, + bstr key, bstr iv) +{ + if (!out_pb || !inner || key.len != BLOCKSIZE || iv.len != BLOCKSIZE) + return AVERROR(EINVAL); + + int r; + void *avio_buf = NULL; + struct crypto_priv *p = talloc_zero(NULL, struct crypto_priv); + talloc_set_destructor(p, priv_destructor); + + p->inner = inner; + p->aes = av_aes_alloc(); + if (!p->aes) { + r = AVERROR(ENOMEM); + goto fail; + } + if (av_aes_init(p->aes, key.start, BLOCKSIZE * 8, 1) < 0) { + r = AVERROR(EINVAL); + goto fail; + } + memcpy(p->iv, iv.start, BLOCKSIZE); + + avio_buf = av_malloc(AVIO_BUFFER_SIZE); + if (!avio_buf) { + r = AVERROR(ENOMEM); + goto fail; + } + + AVIOContext *pb = avio_alloc_context(avio_buf, AVIO_BUFFER_SIZE, 0, p, + crypto_read, NULL, NULL); + if (!pb) { + r = AVERROR(ENOMEM); + goto fail; + } + pb->seekable = 0; + + *out_pb = pb; + return 0; + +fail: + av_free(avio_buf); + talloc_free(p); + return r; +} + +void mp_avio_crypto_close(AVIOContext **pb) +{ + if (!pb || !*pb) + return; + struct crypto_priv *p = (*pb)->opaque; + av_freep(&(*pb)->buffer); + avio_context_free(pb); + talloc_free(p); +} diff --git a/demux/avio_crypto.h b/demux/avio_crypto.h new file mode 100644 index 0000000000..3528b9e99b --- /dev/null +++ b/demux/avio_crypto.h @@ -0,0 +1,32 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see . + */ + +#pragma once + +#include + +struct bstr; + +// AES-128-CBC + PKCS#7 decryption layer over `inner`. `key` and `iv` are 16 +// bytes each; other lengths return AVERROR(EINVAL). Neither buffer is retained +// past this call. +// +// The returned wrapper is read-only and non-seekable, and does not own +// `inner`. +int mp_avio_crypto_open(AVIOContext **out_pb, AVIOContext *inner, + struct bstr key, struct bstr iv); +void mp_avio_crypto_close(AVIOContext **pb); diff --git a/meson.build b/meson.build index 636fde3fa1..4d757a3be9 100644 --- a/meson.build +++ b/meson.build @@ -627,7 +627,8 @@ libcurl = dependency('libcurl', version: '>= 7.88.0', required: get_option('libc features += {'libcurl': libcurl.found()} if features['libcurl'] dependencies += [libcurl] - sources += files('stream/stream_curl.c') + sources += files('demux/avio_crypto.c', + 'stream/stream_curl.c') endif cdda_opt = get_option('cdda').require( diff --git a/stream/stream_curl.c b/stream/stream_curl.c index a571e83cd6..fa0b081e41 100644 --- a/stream/stream_curl.c +++ b/stream/stream_curl.c @@ -36,6 +36,7 @@ #include "common/global.h" #include "common/msg.h" #include "cookies.h" +#include "demux/avio_crypto.h" #include "demux/demux.h" #include "misc/bstr.h" #include "misc/dispatch.h" @@ -1005,6 +1006,7 @@ struct curl_avio_cookie { struct stream *stream; struct mp_cancel *cancel; const char *location; // final URL after redirects, exposed via the "location" opt + AVIOContext *transport; }; static const AVClass curl_avio_cookie_class = { @@ -1080,10 +1082,10 @@ static int64_t curl_avio_seek(void *opaque, int64_t pos, int whence) return pos; } -int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, - void **cookie_out, const char *url, int flags, - AVDictionary **options, - const char *whitelist, const char *blacklist) +static int open_curl_transport(struct demuxer *demuxer, AVIOContext **pb_out, + void **cookie_out, const char *url, int flags, + AVDictionary **options, + const char *whitelist, const char *blacklist) { *pb_out = NULL; *cookie_out = NULL; @@ -1108,12 +1110,6 @@ int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, struct curl_open_args oa = {0}; if (options && *options) { AVDictionaryEntry *e; - // Nested IO plumbs whitelist/blacklist through the AVDictionary, - // use that if set, same as FFmpeg's implementation. - if ((e = av_dict_get(*options, "protocol_whitelist", NULL, 0))) - whitelist = e->value; - if ((e = av_dict_get(*options, "protocol_blacklist", NULL, 0))) - blacklist = e->value; // lavf's http demuxer exposes initial/final byte offsets as AVOptions // Some demuxers, like lavf/hls.c assume it is always available, even for // custom IO... Add support for this. @@ -1181,16 +1177,112 @@ int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, return 0; } +static void close_curl_transport(AVIOContext *pb, struct curl_avio_cookie *c) +{ + av_freep(&pb->buffer); + avio_context_free(&pb); + free_stream(c->stream); + talloc_free(c->cancel); + talloc_free(c); +} + +// Open a `crypto+...` URL by opening the inner URL with curl and layering +// AES-128-CBC decryption on top using the `key`/`iv` hex strings from the +// AVDictionary. Returns AVERROR(ENOSYS) when curl can't handle the inner or +//the AES options are missing. +static int open_curl_crypto(struct demuxer *demuxer, AVIOContext **pb_out, + void **cookie_out, const char *inner_url, + int flags, AVDictionary **options, + const char *whitelist, const char *blacklist) +{ + if (flags & AVIO_FLAG_WRITE) + return AVERROR(ENOSYS); + if (!options || !*options) + return AVERROR(ENOSYS); + + AVDictionaryEntry *key_e = av_dict_get(*options, "key", NULL, 0); + AVDictionaryEntry *iv_e = av_dict_get(*options, "iv", NULL, 0); + if (!key_e || !iv_e) + return AVERROR(ENOSYS); + + void *tmp = talloc_new(NULL); + AVIOContext *transport = NULL; + void *cookie = NULL; + AVIOContext *wrapper = NULL; + + bstr key = {0}, iv = {0}; + bstr_decode_hex(tmp, bstr0(key_e->value), &key); + bstr_decode_hex(tmp, bstr0(iv_e->value), &iv); + + int r = open_curl_transport(demuxer, &transport, &cookie, inner_url, flags, + options, whitelist, blacklist); + if (r < 0) + goto done; + + r = mp_avio_crypto_open(&wrapper, transport, key, iv); + if (r < 0) { + MP_ERR(demuxer, "Failed to set up crypto stream: %s\n", av_err2str(r)); + close_curl_transport(transport, cookie); + goto done; + } + + // Consume from the dict so demuxer-side mp_avdict_print_unset stays quiet. + av_dict_set(options, "key", NULL, 0); + av_dict_set(options, "iv", NULL, 0); + + ((struct curl_avio_cookie *)cookie)->transport = transport; + *pb_out = wrapper; + *cookie_out = cookie; + +done: + talloc_free(tmp); + return r; +} + +int mp_curl_avio_open(struct demuxer *demuxer, AVIOContext **pb_out, + void **cookie_out, const char *url, int flags, + AVDictionary **options, + const char *whitelist, const char *blacklist) +{ + *pb_out = NULL; + *cookie_out = NULL; + + if (flags & AVIO_FLAG_WRITE) + return AVERROR(ENOSYS); + + // Nested IO plumbs whitelist/blacklist through the AVDictionary, use that + // if set, same as FFmpeg's implementation. + if (options && *options) { + AVDictionaryEntry *e; + if ((e = av_dict_get(*options, "protocol_whitelist", NULL, 0))) + whitelist = e->value; + if ((e = av_dict_get(*options, "protocol_blacklist", NULL, 0))) + blacklist = e->value; + } + + bstr rest = bstr0(url); + if (bstr_eatstart0(&rest, "crypto+") || bstr_eatstart0(&rest, "crypto:")) { + if (!is_protocol_allowed(demuxer->log, (bstr)bstr0_lit("crypto"), whitelist, blacklist)) + return AVERROR(EINVAL); + return open_curl_crypto(demuxer, pb_out, cookie_out, rest.start, + flags, options, whitelist, blacklist); + } + + return open_curl_transport(demuxer, pb_out, cookie_out, url, flags, + options, whitelist, blacklist); +} + void mp_curl_avio_close(AVIOContext *pb, void *cookie) { struct curl_avio_cookie *c = cookie; - if (pb) { - av_freep(&pb->buffer); - avio_context_free(&pb); - } - if (c) { - free_stream(c->stream); - talloc_free(c->cancel); - talloc_free(c); + if (!c) + return; + + AVIOContext *transport = c->transport; + if (transport) { + mp_avio_crypto_close(&pb); + } else { + transport = pb; } + close_curl_transport(transport, c); }