nodejs 加密 createDecipheriv 抛出 'ERR_OSSL_EVP_WRONG_FINAL_BLOCK_LENGTH'

nodejs crypto createDecipheriv throws 'ERR_OSSL_EVP_WRONG_FINAL_BLOCK_LENGTH'

我正在试验 mp3 文件的加密和解密。我有一个 python 代码执行 AES 加密并尝试使用 node.js 的加密库解密加密输出。我的 python 代码是:

from Crypto.Cipher import AES
import hashlib

# code from https://eli.thegreenplace.net/2010/06/25/
# aes-encryption-of-files-in-python-with-pycrypto
def encrypt_file(key, in_filename, out_filename=None, chunksize=64*1024):    
    if not out_filename:
        out_filename = in_filename + '.enc'

    #iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
    iv = os.urandom(16)
    encryptor = AES.new(key, AES.MODE_CBC, iv)
    filesize = os.path.getsize(in_filename)

    with open(in_filename, 'rb') as infile:
        with open(out_filename, 'wb') as outfile:
            outfile.write(struct.pack('<Q', filesize))
            outfile.write(iv)

            while True:
                chunk = infile.read(chunksize)
                if len(chunk) == 0:
                    break
                elif len(chunk) % 16 != 0:
                    chunk += (' ' * (16 - len(chunk) % 16)).encode('ascii')

                outfile.write(encryptor.encrypt(chunk))

if __name__ == '__main__':
    password = 'helloWorld!'
    enc_key = hashlib.sha256(password.encode(encoding='utf-8',errors='strict')).digest()
    print(base64.b64encode(enc_key).decode('ascii'))
    audio_file_name = "GetachewMekuryaSaxphone_IBSA8Sz.mp3"
    enc_output = audio_file_name + ".enc"
    encrypt_file(enc_key, audio_file_name, enc_output)

和一个javascript解密代码:

var fs = require('fs');
var crypto = require('crypto');
var password = 'helloWorld!'
const enc_key = crypto.createHash('sha256').update(String(password)).digest('base64').substr(0, 32);
console.log(enc_key);
let iv = crypto.randomBytes(16);
var cipher = crypto.createCipheriv('aes-256-cbc', enc_key, iv);
var decipher = crypto.createDecipheriv('aes-256-cbc',enc_key, iv);
var input = fs.createReadStream('GetachewMekuryaSaxphone_IBSA8Sz.mp3.enc');
var output = fs.createWriteStream('GetachewMekuryaSaxphone_IBSA8Sz_DEC.mp3');

input.pipe(decipher).pipe(output);

output.on('finish', function() {
  console.log('Encrypted file written to disk!');
});

但是,我在尝试解密时收到错误消息:

events.js:292
      throw er; // Unhandled 'error' event
      ^

Error: error:0606506D:digital envelope routines:EVP_DecryptFinal_ex:wrong final block length
    at Decipheriv._flush (internal/crypto/cipher.js:141:29)
    at Decipheriv.prefinish (_stream_transform.js:142:10)
    at Decipheriv.emit (events.js:315:20)
    at prefinish (_stream_writable.js:619:14)
    at finishMaybe (_stream_writable.js:627:5)
    at Decipheriv.Writable.end (_stream_writable.js:571:5)
    at ReadStream.onend (_stream_readable.js:676:10)
    at Object.onceWrapper (events.js:421:28)
    at ReadStream.emit (events.js:327:22)
    at endReadableNT (_stream_readable.js:1223:12)
Emitted 'error' event on Decipheriv instance at:
    at emitErrorNT (internal/streams/destroy.js:100:8)
    at emitErrorCloseNT (internal/streams/destroy.js:68:3)
    at processTicksAndRejections (internal/process/task_queues.js:84:21) {
  library: 'digital envelope routines',
  function: 'EVP_DecryptFinal_ex',
  reason: 'wrong final block length',
  code: 'ERR_OSSL_EVP_WRONG_FINAL_BLOCK_LENGTH'
}

在Python代码中,在写入的文件中,首先存储明文文件的大小(8字节,小端),然后是16字节的IV,最后是密文。 NodeJS 代码中根本没有考虑这种结构。相反,随机 IV 用于解密。实际上,前 8 个字节和 IV 应该从 NodeJS 代码中的加密文件中读取,其余(即密文)应该使用提取的 IV 解密。

此外,密钥不得使用 Base64 编码。

此外,两种代码中使用了不同的填充。在 Python 代码中,应用了带有空格的不可靠填充,在 NodeJS 代码中 PKCS7 填充。
最合理的更改是在 Python 代码中切换到 PKCS7 填充。为此,PyCryptodome 提供了 padding-module。优点是在解密过程中会自动删除填充。然后不需要存储的明文文件大小。
或者,可以在 NodeJS 代码中禁用填充,并可以在最后使用纯文本文件大小的附加步骤中将其删除。

以下 NodeJS 代码实现了后一种方法:

var fs = require('fs');
var crypto = require('crypto');
var password = 'helloWorld!'

var pathEncryptedFile = '<path to .mp3.enc input file>';
var pathDecryptedFile = '<path to .dec.mp3 output file>';

// Derive key
var enc_key = crypto.createHash('sha256').update(password).digest();

// Read IV and size
// Remeber: Encrypted file structure: plain file length (8 bytes) | iv (16 bytes) | ciphertext
var fd = fs.openSync(pathEncryptedFile, 'r');
var size = Buffer.alloc(8);
fs.readSync(fd, size, 0, 8, 0) // Read plaintext file size
var size = size.readUIntLE(0, 6)
var iv = Buffer.alloc(16);
fs.readSync(fd, iv, 0, 16, 8) // Read iv
fs.closeSync(fd)

// Decrypt (ignore the first 8 + 16 bytes)
var input = fs.createReadStream(pathEncryptedFile, { start: 24 });
var output = fs.createWriteStream(pathDecryptedFile);
var decipher = crypto.createDecipheriv('aes-256-cbc', enc_key, iv);
decipher.setAutoPadding(false); // Disable unpadding
input.pipe(decipher).pipe(output);

output.on('finish', function () {
    // Unpad manually using the plaintext file size
    var fd = fs.openSync(pathDecryptedFile, 'r+');
    fs.ftruncateSync(fd, size);
    fs.closeSync(fd)
    console.log('Encrypted file written to disk!');
});