I'm trying to make AES-256 encryption work across node.js and actionscript, but every approach I'm trying is leading to a deadend. Below are two different attempts which both fail (for different reasons).
One important thing to note is that in both cases- the IV, Key, and Ciphertext are matching perfectly.
Excuse the code repetition but I figured it's better to just show exactly what I'm working with...
1) Default Padding
When using the default Node.JS padding and PKCS5 in as3, I get an Error: PKCS#5:unpad: Invalid padding value. expected [105], found [30].
Node.JS
var CIPHER_METHOD = "aes-256-cbc";
function aesEncryptStringToHex(input, key, iv) {
var aesCipher = crypto.createCipher(CIPHER_METHOD, key, iv);
var plainText = new Buffer(input, 'utf8').toString('hex');
var output;
output = aesCipher.update(input, 'utf8', 'hex') + aesCipher.final('hex');
console.log('IV: ' + iv.toString('hex'));
console.log('Key: ' + key.toString('hex'));
console.log('Plaintext: ' + plainText);
console.log('Ciphertext: ' + output);
sendToFlash(iv.toString('hex') + output);
}
AS3
private function aesDecryptToBytes(cipherBA:ByteArray, key:ByteArray):ByteArray {
var IV:ByteArray = new ByteArray();
var finalBytes:ByteArray = new ByteArray();
var retBytes:ByteArray;
var aesKey:AESKey;
var cbcMode:CBCMode;
var pad:PKCS5;
var testOnly:ByteArray = new ByteArray();
testOnly.writeUTFBytes('Hello World');
if(key.length != 32) {
throw new Error("INVALID KEY!");
}
if(cipherBA.length < 17) {
throw new Error("INVALID CONTENT!");
}
cipherBA.readBytes(IV,0,16);
cipherBA.readBytes(finalBytes, 0);
IV.position = finalBytes.position = 0;
trace('IV:', Hex.fromArray(IV));
trace('Key:', Hex.fromArray(key));
trace('Ciphertext:', Hex.fromArray(finalBytes));
trace('Decrypted Plaintext Should Be:', Hex.fromArray(testOnly));
pad = new PKCS5();
aesKey = new AESKey(key);
cbcMode = new CBCMode(aesKey,pad);
cbcMode.IV = IV;
pad.setBlockSize(cbcMode.getBlockSize());
cbcMode.decrypt(finalBytes);
retBytes = finalBytes;
retBytes.position = 0;
trace('But instead it is:', Hex.fromArray(retBytes));
return(retBytes);
}
When using "HELLO WORLD!" for the input and the same key for both, I get
Output on Node.JS side
IV: 87134386f7bf12dffc9b87b49da86d10
Key: 56036ce4ddab006af7b0924ddad511adbea3fba97f672db4040102a1978e41f7
Plaintext: 48454c4c4f20574f524c4421
Ciphertext: d68db4542be683a80bceb0b8ca900d5c
Output on AS3 side
IV: 87134386f7bf12dffc9b87b49da86d10
Key: 56036ce4ddab006af7b0924ddad511adbea3fba97f672db4040102a1978e41f7
Ciphertext: d68db4542be683a80bceb0b8ca900d5c
Decrypted Plaintext Should Be: 48454c4c4f20574f524c4421
Error: PKCS#5:unpad: Invalid padding value. expected [105], found [30]
2) Custom and Null Padding
When disabling the default Node.JS padding and filling with null characters, and then using NullPad on as3, I get no errors but the decryption fails
Node.JS
var CIPHER_METHOD = "aes-256-cbc";
var AES_BLOCK_SIZE = 16;
var AES_PAD_STARTER = Array(16).join('\0');
function aesEncryptStringToHex(input, key, iv) {
var aesCipher = crypto.createCipher(CIPHER_METHOD, key, iv);
var plainText = new Buffer(input, 'utf8').toString('hex');
var padLength = AES_BLOCK_SIZE - (input.length % AES_BLOCK_SIZE);
var output;
aesCipher.setAutoPadding(false);
input += AES_PAD_STARTER.substr(0, padLength);
output = aesCipher.update(input, 'utf8', 'hex') + aesCipher.final('hex');
console.log('IV: ' + iv.toString('hex'));
console.log('Key: ' + key.toString('hex'));
console.log('Plaintext: ' + plainText);
console.log('Ciphertext: ' + output);
sendToFlash(iv.toString('hex') + output);
}
AS3
private function aesDecryptToBytes(cipherBA:ByteArray, key:ByteArray):ByteArray {
var IV:ByteArray = new ByteArray();
var finalBytes:ByteArray = new ByteArray();
var retBytes:ByteArray;
var aesKey:AESKey;
var cbcMode:CBCMode;
var pad:NullPad;
var testOnly:ByteArray = new ByteArray();
testOnly.writeUTFBytes("HELLO WORLD!");
if(key.length != 32) {
throw new Error("INVALID KEY!");
}
if(cipherBA.length < 17) {
throw new Error("INVALID CONTENT!");
}
cipherBA.readBytes(IV,0,16);
cipherBA.readBytes(finalBytes, 0);
IV.position = finalBytes.position = 0;
trace('IV:', Hex.fromArray(IV));
trace('Key:', Hex.fromArray(key));
trace('Ciphertext:', Hex.fromArray(finalBytes));
trace('Decrypted Plaintext Should Be:', Hex.fromArray(testOnly));
pad = new NullPad();
aesKey = new AESKey(key);
cbcMode = new CBCMode(aesKey,pad);
cbcMode.IV = IV;
pad.setBlockSize(cbcMode.getBlockSize());
cbcMode.decrypt(finalBytes);
retBytes = finalBytes;
retBytes.position = 0;
trace('But instead it is:', Hex.fromArray(retBytes));
return(retBytes);
}
When using "HELLO WORLD!" for the input and the same key for both, I get
Output on Node.JS side
IV: cfa6cfee9f81d81d7e3b651e57b6f42d
Key: 56036ce4ddab006af7b0924ddad511adbea3fba97f672db4040102a1978e41f7
Plaintext: 48454c4c4f20574f524c4421
Ciphertext: 8daf432aad551e333818c42d3190dca5
Output on AS3 side
IV: cfa6cfee9f81d81d7e3b651e57b6f42d
Key: 56036ce4ddab006af7b0924ddad511adbea3fba97f672db4040102a1978e41f7
Ciphertext: 8daf432aad551e333818c42d3190dca5
Decrypted Plaintext Should Be: 48454c4c4f20574f524c4421
But instead it is: 70a4716a7a7d7156bca075efe90041a3
Note that trying retBytes.readUTFBytes(retBytes.length) yields garbage as well.
Any way to make AES encryption work across both platforms?!
EDIT: For the sake of posterity, some node code which works for encrypting and decrypting, with comments to illustrate the potential gotchas:
const CIPHER_METHOD = "aes-256-cbc";
const AES_BLOCK_SIZE = 16;
let nullPad = new Buffer(AES_BLOCK_SIZE).fill(0);
function aesEncrypt(input, key, iv) {
if(iv === undefined) {
//create a random iv.. this is the norm for encryption
iv = crypto.randomBytes(AES_BLOCK_SIZE);
}
let aesCipher = crypto.createCipheriv(CIPHER_METHOD, key, iv);
let padLength = AES_BLOCK_SIZE - (input.length % AES_BLOCK_SIZE);
//don't pad if it's an entire block's worth
if(padLength === AES_BLOCK_SIZE) {
padLength = 0;
}
//we're controlling the padding manually here so we can match it in other environments
aesCipher.setAutoPadding(false);
//for now, just a simple null pad. Need to add it before encryption
//if it were pcks#7 or something, the length would not need to be returned for later use
if(padLength > 0) {
input = Buffer.concat([input, nullPad.slice(0, padLength)]);
}
//encrypt it
let cipherText = Buffer.concat([aesCipher.update(input), aesCipher.final()])
return {
cipherText: cipherText,
iv: iv,
padLength: padLength,
}
}
function aesDecrypt(cipherText, key, iv, padLength) {
if(iv === undefined) {
//strip the iv off the front
iv = cipherText.slice(0,AES_BLOCK_SIZE);
cipherText = cipherText.slice(AES_BLOCK_SIZE);
}
let aesCipher = crypto.createDecipheriv(CIPHER_METHOD, key, iv);
//turn off padding so we can match it in other environments
aesCipher.setAutoPadding(false);
//decrypt it
let plaintext = Buffer.concat([aesCipher.update(cipherText), aesCipher.final()]);
//for now, just a simple null padding. Need to strip it after decryption
//if it were pcks#7 or something, the length would be auto-determined
plaintext = plaintext.slice(0,plaintext.length - padLength);
return plaintext;
}
function testRun(original, key) {
//cipher is an object containing ciphertext, iv, and padLength
let cipher = aesEncrypt(original, key);
//treat the iv separately from the ciphertext. This is nice though hurlant doesn't support that afaik
let decryptedSeparate = aesDecrypt(cipher.cipherText, key, cipher.iv, cipher.padLength);
//combine the iv with the ciphertext directly. aesDecrypt will strip it automatically
let combinedCipherIv = Buffer.concat([cipher.iv, cipher.cipherText]);
let decryptedCombined = aesDecrypt(combinedCipherIv, key, undefined, cipher.padLength);
//Show the results!
console.log("Original: " + original.toString('utf8'));
console.log("Encrypted: " + cipher.cipherText.toString('utf8'));
console.log("Padding size: " + cipher.padLength);
console.log("Plaintext from combined: " + decryptedCombined.toString('utf8'));
console.log("Plaintext from separate: " + decryptedSeparate.toString('utf8'));
}
//key should be something more secure than whatever happens to be in memory at the moment ;)
let key = new Buffer(32);
//original is just binary data... doesn't have to be a string, though it's easier to see in the console for testing
//this test is for no padding
let original = new Buffer("0123456789ABCDEF", 'utf8');
testRun(original, key);
console.log("");
//this test is with some padding
original = new Buffer("HELLO WORLD", 'utf8');
testRun(original, key);
Well, I kind of have the same problem between node.js and Objective-C, but in teh nodeJS code you should be using crypto.CreateCypheriv
, not just createCypher, and maybe try not just with key and IV as buffer but also change the key and IV to bynary on both ends as working with arrays may sometimes be troublesome... I hope this gives you some ideas to solve your problem