短文,个人笔记用。
昨天因为某个需求需要手动解码下下来的加密的TS块。理论上很简单,我随便就搜到了Python:
from binascii import unhexlify
from Crypto.Cipher import AES
key = "614c9b1fa9ea1b1be878929c592d20e0"
key = unhexlify(key)
iv = 630
iv = iv.to_bytes(16, 'big')
decipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = open('enc', 'rb').read()
decrypted = decipher.decrypt(encrypted)
open('dec_python', 'wb').write(decrypted)
Node.js(抄袭自Minyami):
const fs = require('fs');
const crypto = require('crypto');
let key = '614c9b1fa9ea1b1be878929c592d20e0';
let iv = (630).toString(16);
const algorithm = "aes-128-cbc";
if (iv.length % 2 == 1) {
iv = "0" + iv;
}
const keyBuffer = Buffer.alloc(16);
const ivBuffer = Buffer.alloc(16);
keyBuffer.write(key, "hex");
ivBuffer.write(iv, 16 - iv.length / 2, "hex");
const input = 'enc'
const output = 'dec_node'
const decipher = crypto.createDecipheriv(algorithm, keyBuffer, ivBuffer);
const i = fs.createReadStream(input);
const o = fs.createWriteStream(output);
i.pipe(decipher).pipe(o);
OpenSSL:
openssl enc -aes-128-cbc -d -K 614c9b1fa9ea1b1be878929c592d20e0 -iv 276 -in enc -out dec_openssl.ts
多种方法。本来这事儿就完了,解码出来的文件也都可以正常播放;但是我闲着没事对比了下几种方式出来的文件,发现居然hash都不一致?!
Padding
以结果不match搜索,很快就知道了Python和Node的异同:Node这个decipher比较高级,默认就有auto Padding(对于解码,就是auto unpadding)。这个可以通过decipher.setAutoPadding(false);
来取消掉。相对,Python我这个就比较底层,所以要手动unpad:
from Crypto.Util.Padding import unpad
decrypted_unpad = unpad(decrypted, AES.block_size)
open('dec_python_unpad', 'wb').write(decrypted_unpad)
我们这样生成了unpad(JS默认,Pythonunpad
)和没unpad(Python默认,JS设为false)的两组文件,两两hash相等。
不过,对于我们的应用(解码HLS的视频块),是否真的需要unpad呢?rfc8216#section-4.3.2.4提到了要用标准的PKCS7 padding,那么我们就理解为需要unpad吧。
OpenSSL的IV补全问题
OK这俩一致了,那么OpenSSL是怎么回事?OpenSSL也是默认pad/unpad的,可以通过-nopad
参数来取消。但是无论加还是不加都和上面两个产生的结果不一致。另外还有-nosalt
,但是似乎对AES-128-CBC算法并没有区别。
我在网上搜了半天也没什么头绪,不过发现运行时OpenSSL有个提示:hex string is too short, padding with zero bytes to length
。会不会有关系?我于是测试了下把IV改成0276、000276等等,发现结果居然都不一样!不是自动在前面填充0吗,为啥会有区别?百思不得其解下,我用这个warning作为关键词搜了半天,终于搜到了一个相关的文章——虽然这个文章没有直接说只是给你演示了一波,但是可以看出,原来天杀的他是在后面填充〇而不是前面!虽然现在看来,这也很正常,但是由于HLS这里的IV用的是默认的sequence number(一个int),所以我直觉地天真以为他肯定是在前面填充〇了。这个文章里引用了SO某答案里的一句:
It is always best to provide the exact size inputs to encryption functions, specify 32 hex digits for an AES 128-bit key.
zaph from https://stackoverflow.com/a/39908983/3939155
深表赞同。我要是一开始这么做也不会踩坑了。
其实这个文章也不需要那么大费周章对比,因为OpenSSL enc有个argument是-p
,可以直接print相关的key/IV等:
G:\2>openssl enc -aes-128-cbc -d -K 614c9b1fa9ea1b1be878929c592d20e0 -iv 276 -in enc -out dec_openssl -p
hex string is too short, padding with zero bytes to length
salt=2831EC7600000000
key=614C9B1FA9EA1B1BE878929C592D20E0
iv =27600000000000000000000000000000
就直接可以看到问题所在了。
用FFMPEG解密
顺便一提,如果你直接搜索解密HLS加密的ts文件,搜到的绝大部分都是教你用ffmpeg;但是ffmpeg解码一个m3u8文件倒是很方便,但是直接解码单独ts文件怎么做却几乎没人提及。
我搜了半天终于搜到一个superuser的问题的评论里提到方法:
ffmpeg -key <key> -iv <iv> -i crypto:dec.ts
这里key和iv也都是hexstring(同上,自己手动填充到足bytes吧!),注意后面的-i要加crypto协议。
如果要输出后面就-c copy out.ts
就行了。但是注意,因为ffmpeg的操作,即使是copy,也会在容器级别有很多操作,所以和上面三种方法纯解码的是无法直接对比的。我只能说肉眼收货解码是成功的就是了。
IV对最后结果的影响
在最开始我用OpenSSL输入IV长度不够导致被错误 append zeros的时候,解码出来的文件也是可以完全正常播放的。因为AES-128-CBC的原理就是用第一个block明文+IV来加密成密文,然后再用第一个block的密文当做第二个block的IV来继续加密。所以,即使IV错误,也只会导致第一个block解码错误。由于mepg-ts是一个容错性非常强的格式,几乎完全不会导致解码结果有任何问题。但是既然我们都有IV了,那还是正确处理吧。