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.
This commit is contained in:
Kacper Michajłow committed 2026-06-27 19:04:03 +02:00
1 parent fc26cd23c0
commit 449944e25f
4 files changed
+308 -19

No files matched your search

+164
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#include <string.h>
#include <libavformat/avio.h>
#include <libavutil/aes.h>
#include <libavutil/error.h>
#include <libavutil/mem.h>
#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);
}
+32
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <libavformat/avio.h>
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);
+2 -1
View File
@@ -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(
+110 -18
View File
@@ -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);
}