使用 AES 和自定义密钥使用 WebCrypto API 加密和解密视频

Encrypt and decrypt a video with WebCrypto API using AES and a custom key

我想知道如何使用 AES 和自定义密钥使用 WebCrypto API 加密和解密视频。我只找到这段代码,只说明如何加密视频而不说明如何解密,也使用随机密钥。提前谢谢你。

  function processFile(evt) {
    var file = evt.target.files[0],
        reader = new FileReader();

    reader.onload = function(e) {
        var data = e.target.result,
            iv = crypto.getRandomValues(new Uint8Array(16));

        crypto.subtle.generateKey({ 'name': 'AES-CBC', 'length': 256 }, false, ['encrypt', 'decrypt'])
            .then(key => crypto.subtle.encrypt({ 'name': 'AES-CBC', iv }, key, data) )
            .then(encrypted => {
                console.log(encrypted);
                alert('The encrypted data is ' + encrypted.byteLength + ' bytes long'); // encrypted is an ArrayBuffer
            })
            .catch(console.error);
    }

    reader.readAsArrayBuffer(file);   
}

您将在此处找到有关如何使用 AES-GCM 生成密钥、导入密钥、加密和解密的完整示例: https://github.com/diafygi/webcrypto-examples/blob/master/README.md#aes-gcm

您应该使用 GCM,因为它是一种经过身份验证的加密模式。 WebCrypto 没有流接口,因此您必须分块处理,否则非常直接。

您可能想使用 ECDH 来交换 AES 密钥。同一页面也有相关示例。

这里有一个使用 aes-256-cbc 解密和播放 HLS 视频的演示:

https://kaizhu256.github.io/node-demo-hls-encrypted/index.html

它是通过在 hls.js (https://github.com/video-dev/hls.js/blob/v0.8.9/dist/hls.js) 中破解 ajax-call 来解密 xhr.response 然后再传递给视频播放来实现的:

--- assets.hls.v0.8.9.js    2018-08-04 03:59:42.000000000 +0700
+++ assets.hls.v0.8.9.crypto.js 2018-08-04 03:59:42.000000000 +0700
@@ -1,3 +1,97 @@
+var local;
+(function () {
+    (function () {
+        local = local || {};
+        local.base64ToBuffer = function (b64, mode) {
+        /*
+         * this function will convert b64 to Uint8Array
+         * https://gist.github.com/wang-bin/7332335
+         */
+            /*globals Uint8Array*/
+            var bff, byte, chr, ii, jj, map64, mod4;
+            b64 = b64 || '';
+            bff = new Uint8Array(b64.length); // 3/4
+            byte = 0;
+            jj = 0;
+            map64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+            mod4 = 0;
+            for (ii = 0; ii < b64.length; ii += 1) {
+                chr = map64.indexOf(b64[ii]);
+                if (chr >= 0) {
+                    mod4 %= 4;
+                    if (mod4 === 0) {
+                        byte = chr;
+                    } else {
+                        byte = byte * 64 + chr;
+                        bff[jj] = 255 & (byte >> ((-2 * (mod4 + 1)) & 6));
+                        jj += 1;
+                    }
+                    mod4 += 1;
+                }
+            }
+            // optimization - create resized-view of bff
+            bff = bff.subarray(0, jj);
+            // mode !== 'string'
+            if (mode !== 'string') {
+                return bff;
+            }
+            // mode === 'string' - browser js-env
+            if (typeof window === 'object' && window && typeof window.TextDecoder === 'function') {
+                return new window.TextDecoder().decode(bff);
+            }
+            // mode === 'string' - node js-env
+            Object.setPrototypeOf(bff, Buffer.prototype);
+            return String(bff);
+        };
+        local.cryptoAes256CbcByteDecrypt = function (key, data, onError, mode) {
+        /*
+         * this function will aes-256-cbc decrypt with the hex-key, Uint8Array data
+         * example usage:
+            key = '0000000000000000000000000000000000000000000000000000000000000000';
+            local.cryptoAes256CbcByteEncrypt(key, new Uint8Array([1,2,3]), function (error, data) {
+                console.assert(!error, error);
+                local.cryptoAes256CbcByteDecrypt(key, data, console.log);
+            });
+         */
+            /*globals Uint8Array*/
+            var cipher, crypto, ii, iv, tmp;
+            // init key
+            tmp = key;
+            key = new Uint8Array(32);
+            for (ii = 0; ii < key.length; ii += 2) {
+                key[ii] = parseInt(tmp.slice(2 * ii, 2 * ii + 2), 16);
+            }
+            // base64
+            if (mode === 'base64') {
+                data = local.base64ToBuffer(data);
+            }
+            if (!(data instanceof Uint8Array)) {
+                data = new Uint8Array(data);
+            }
+            // init iv
+            iv = data.subarray(0, 16);
+            // optimization - create resized-view of data
+            data = data.subarray(16);
+            crypto = typeof window === 'object' && window.crypto;
+            /* istanbul ignore next */
+            if (!(crypto && crypto.subtle && typeof crypto.subtle.importKey === 'function')) {
+                setTimeout(function () {
+                    crypto = require('crypto');
+                    cipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
+                    onError(null, Buffer.concat([cipher.update(data), cipher.final()]));
+                });
+                return;
+            }
+            crypto.subtle.importKey('raw', key, {
+                name: 'AES-CBC'
+            }, false, ['decrypt']).then(function (key) {
+                crypto.subtle.decrypt({ iv: iv, name: 'AES-CBC' }, key, data).then(function (data) {
+                    onError(null, new Uint8Array(data));
+                }).catch(onError);
+            }).catch(onError);
+        };
+    }());
+}());
 (function webpackUniversalModuleDefinition(root, factory) {
    if(typeof exports === 'object' && typeof module === 'object')
        module.exports = factory();
@@ -10919,6 +11013,22 @@
           }
           stats.loaded = stats.total = len;
           var response = { url: xhr.responseURL, data: data };
+          if (data && window.modeMediaEncrypted && window.mediaEncryptedKey) {
+              var self = this;
+              local.cryptoAes256CbcByteDecrypt(
+                window.mediaEncryptedKey,
+                data,
+                function (error, data) {
+                  response.data = typeof xhr.response === 'string'
+                    ? new TextDecoder().decode(data)
+                    : data;
+                  stats.loaded = stats.total = data.byteLength;
+                  self.callbacks.onSuccess(response, stats, context, xhr);
+                },
+                typeof xhr.response === 'string' && 'base64'
+              );
+              return;
+          }
           this.callbacks.onSuccess(response, stats, context, xhr);
         } else {
           // if max nb of retries reached or if http status between 400 and 499 (such error cannot be recovered, retrying is useless), return error