I'm experimenting with encryption and decryption of mp3 files. I have a python code doing an AES encryption and trying to decrypt the encrypted output with crypto library of node.js. My python code is:
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)
and a javascript code for decryption:
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!');
});
However, I am getting an error while attempting to decrypt with the following message:
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'
}
In the Python code, in the written file, first the size of the plaintext file is stored (on 8 bytes, with little endian), then the 16 bytes IV and finally the ciphertext. This structure is not considered at all in the NodeJS code. Instead, a random IV is used for decryption. Actually, the first 8 bytes and the IV should be read from the encrypted file in the NodeJS code and the rest (i.e. the ciphertext) should be decrypted using the extracted IV.
Additionally, the key must not be Base64 encoded.
Also, different paddings are used in both codes. In the Python code, an unreliable padding with blanks is applied, in the NodeJS code PKCS7 padding.
The most reasonable change would be to switch to PKCS7 padding in the Python code. For this, PyCryptodome provides a padding-module. The advantage is that the padding is automatically removed during decryption. The stored plaintext file size is then not needed.
Alternatively, the padding can be disabled in the NodeJS code and can be removed in an additional step at the end using the plaintext file size.
The following NodeJS code implements the latter approach:
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!');
});