We have a legacy tool written in ActionScript which encrypts input using AES-CBC and a hard-coded shared key. I'm trying to write the corresponding decrypt function in Typescript but have not been successful. The AS3 side uses the Hurlant as3crypto library with NullPad and an empty (eg length 0) IV. Also worth noting, the shared secret is only 15 bytes instead of 16. as3crypto doesn't seem to mind that, and I've also tried with a 16 byte key without success. My goal is to fix the aesDecrypt() function in Javascript so that I can successfully decrypt the output of the AS3 aesEncrypt() function.
Below I have the encypt (as well as working decrypt counterpart) in AS3, followed by my attempted decrypt (and corresponding encrypt) function in Typescript. In this example, the input "test" is encrypted as "ryhkw3BmJ85+qBr0E9bYqw==" in AS3, but the Javascript decrypt does not yield "test".
AS3
<?xml version="1.0"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" visible="false">
<fx:Script><![CDATA[
import com.hurlant.crypto.Crypto;
import com.hurlant.crypto.symmetric.ICipher;
import com.hurlant.crypto.symmetric.IPad;
import com.hurlant.crypto.symmetric.IVMode;
import com.hurlant.crypto.symmetric.NullPad;
import com.hurlant.util.Hex;
import mx.utils.Base64Decoder;
import mx.utils.Base64Encoder;
static var KEY:String = "vI^diTubIwH]Gag";
internal static function aesEncrypt(decoded:String):String
{
var pad:IPad = new NullPad();
var mode:ICipher = Crypto.getCipher("aes-cbc", Hex.toArray(Hex.fromString(KEY)), pad);
pad.setBlockSize(mode.getBlockSize());
var iv:ByteArray = new ByteArray();
if (mode is IVMode) {
(mode as IVMode).IV = iv;
}
var encoder:Base64Encoder = new Base64Encoder();
var ba:ByteArray = Hex.toArray(Hex.fromString(decoded));
mode.encrypt(ba);
encoder.reset();
encoder.encodeBytes(ba);
return encoder.toString();
}
internal static function aesDecrypt(encoded:String):String
{
var pad:IPad = new NullPad();
var mode:ICipher = Crypto.getCipher("aes-cbc", Hex.toArray(Hex.fromString(KEY)), pad);
pad.setBlockSize(mode.getBlockSize());
var iv:ByteArray = new ByteArray();
if (mode is IVMode) {
(mode as IVMode).IV = iv;
}
var decoder:Base64Decoder = new Base64Decoder();
decoder.reset();
decoder.decode(encoded);
var ba:ByteArray = decoder.toByteArray();
mode.decrypt(ba);
return Hex.toString(Hex.fromArray(ba));
}
trace(aesEncrypt('test'));
trace(aesDecrypt('ryhkw3BmJ85+qBr0E9bYqw=='));
]]></fx:Script>
</s:Application>
Node.js
let crypto = require('crypto');
function aesEncrypt(cleartext:string, cipherType:string = 'AES', keyBitLength:number = 128, mode:string = 'CBC', cryptkey:string = 'vI^diTubIwH]Gag') {
const algorithm = `${cipherType.toLowerCase()}-${keyBitLength}-${mode.toLowerCase()}`;
const blockSize = keyBitLength / 8;
const key = Buffer.from(cryptkey);
const paddedKey = nullPad(key, blockSize);
const iv = Buffer.alloc(blockSize, 0);
const cipher = crypto.createCipheriv(algorithm, paddedKey, iv);
cipher.setAutoPadding(false);
const encodedInputBuffer = Buffer.from(cleartext, 'utf8');
const encodedInput = encodedInputBuffer.toString('hex');
const paddedInputBuffer = nullPad(Buffer.from(encodedInput), blockSize);
const encrypted = Buffer.concat([cipher.update(paddedInputBuffer), cipher.final()])
return encrypted.toString('base64');
}
function aesDecrypt(encoded:string, cipherType:string = 'AES', keyBitLength:number = 128, mode:string = 'CBC', cryptkey:string = 'vI^diTubIwH]Gag') {
const algorithm = `${cipherType.toLowerCase()}-${keyBitLength}-${mode.toLowerCase()}`;
const blockSize = keyBitLength / 8;
const key = Buffer.from(cryptkey);
const paddedKey = nullPad(key, blockSize);
let iv = Buffer.alloc(blockSize, 0);
const decipher = crypto.createDecipheriv(algorithm, paddedKey, iv);
decipher.setAutoPadding(false);
const decodedInput = Buffer.from(encoded, 'base64').toString( 'binary');
let decrypted = Buffer.concat([decipher.update(decodedInput, 'binary'), decipher.final()]);
return decrypted.toString().trim();
}
function nullPad(input:Buffer, length:number) {
const nullPad = Buffer.alloc(length);
let padLength = length - (input.length % length);
if (padLength == length) {
padLength = 0;
}
if(padLength > 0) {
input = Buffer.concat([input, nullPad.slice(0, padLength)]);
}
return input;
}
// should output "ryhkw3BmJ85+qBr0E9bYqw=="
console.log("Encrypt 'test'\t\t\t\t>>>\t" + aesEncrypt("test"));
// should output "test"
console.log("Decrypt 'ryhkw3BmJ85+qBr0E9bYqw=='\t>>>\t" + aesDecrypt("ryhkw3BmJ85+qBr0E9bYqw=="));
// working roundtrips
console.log("Decrypt '+3C9rjmO7W7hIQqcJMPgXQ=='\t>>>\t" + Buffer.from(aesDecrypt("+3C9rjmO7W7hIQqcJMPgXQ=="), 'hex').toString());
console.log("Decrypt 'elaKkhcvG75EBFvZwB1KiA=='\t>>>\t" + aesDecrypt("elaKkhcvG75EBFvZwB1KiA=="));
Unfortunately we can't update the legacy code for the ActionScript tool, as much as we'd like to (especially since the empty IV and null padding are both bad, not to mention the non-standard key length). I'd be very grateful if anyone could help show me how to fix the Javascript side to be able to decrypt. I'd prefer a pure Node-crypto solution, but happy to use CryptoJS or Forge if either would work (I've been unsuccessful using them for this task as well).