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);
}