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:
1 parent
fc26cd23c0
commit
449944e25f
4 files changed
+308
-19
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user