Aes-256-cbc decode example

Hello!
I have working encoder/decoder on my php backend with key and vector in plain text.
Trying to create a simple POC decoder to pass some secret info from my backend to CF worker via client request.
Do you guys have a working example how to decode simple string in my case?
Thanks!

Hi, not sure if this is what you are after but wrote a small wrapper for a current project.

I’m concatenating the iv with the as hex with the base64 encoded buffer if I remember correctly which isn’t that pretty. So, probably want to change it a bit but hopefully it’s good enough for insperation.

const hexKey = <KEY>
let key;

function hexStringToUint8Array(hexString) {
  const arrayBuffer = new Uint8Array(hexString.length / 2);

  for (let i = 0; i < hexString.length; i += 2) {
    const byteValue = parseInt(hexString.substr(i, 2), 16);
    arrayBuffer[i / 2] = byteValue;
  }

  return arrayBuffer;
}

function base64ToArraybuffer(base64) {
  const binary = atob(base64.replace(/-/g, '+').replace(/_/g, '/'));
  const len = binary.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

function arraybufferToString(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

async function getKey() {
  if (key) {
    return key;
  }

  const keyBuffer = hexStringToUint8Array(hexKey);
  key = await crypto.subtle.importKey(
    'raw',
    keyBuffer,
    {
      name: 'AES-CBC',
    },
    false,
    ['decrypt'],
  );

  return key;
}

async function decrypt(data) {
  const key = await getKey();

  // Slice the iv from the data
  const nonce = hexStringToUint8Array(data.slice(0, 32));

  try {
    const dataBuffer = base64ToArraybuffer(data.slice(32));
    const decrypted = await crypto.subtle.decrypt(
      {
        name: 'AES-CBC',
        iv: nonce,
      },
      key,
      dataBuffer,
    );

    return JSON.parse(arraybufferToString(decrypted));
  } catch (err) {
    console.log(`Decryption failed: ${err.message}`);
    throw err;
  }
}

module.exports = {
  decrypt,
};

Well, I’ve tried to reuse some of your functions and still can’t make it work.
My php functions looks like this and I can’t change it unfortunately:

   function aes_encrypt($data, $vector = null) {
          return $data ? bin2hex(base64_decode(openssl_encrypt($data, 'aes-256-cbc', ENCRYPTION_KEY, null, md5(md5($vector ?? ENCRYPTION_KEY), true)))) : false;
   }
   function aes_decrypt($data, $vector = null) {
          $data = @pack('H*', $data);
          $data = base64_encode($data);
          return openssl_decrypt($data, 'aes-256-cbc', ENCRYPTION_KEY, null, md5(md5($vector ?? ENCRYPTION_KEY), true));
   }

So, I’ve recreated pack('H*') with this function (works fine, tested):

function pack_hex(source) {
    var source = source.length % 2 ? source + '0' : source,
        result = '';
    for( var i = 0; i < source.length; i = i + 2 ) result += String.fromCharCode( parseInt( source.substr( i , 2 ), 16 ) );
    return result;
}

and my decrypt function still triggers some errors in CF console (“Error: internal error” at crypto.subtle.decrypt line):

async function decrypt(data, key, iv) {
    data = pack_hex(data);
    data = btoa(data); // DOESN'T HELP HERE
    data = base64ToArraybuffer(data); // stringToArray() DOESN'T WORK TOO
    const decypher = await crypto.subtle.decrypt({ name: 'AES-CBC', iv: iv }, key, data);
    return arrayToString(decypher);
}

My PHP skills are unfortunately no good so I won’t be able to help there… What I can share is the corresponding node.js code to encrypt if that’s of any help:
const crypto = require(‘crypto’);

const algorithm = 'aes-256-cbc';
const hexKey = 'key';
const key = crypto.createSecretKey(hexStringToUint8Array(hexKey));

/**
 * Helper function to validate the the encryption works on node side..
 */
function hexStringToUint8Array(hexString) {
  if (hexString.length % 2 !== 0) {
    throw 'Invalid hexString';
  }

  const arrayBuffer = new Uint8Array(hexString.length / 2);

  for (let i = 0; i < hexString.length; i += 2) {
    const byteValue = parseInt(hexString.substr(i, 2), 16);
    if (isNaN(byteValue)) {
      throw 'Invalid hexString';
    }
    arrayBuffer[i / 2] = byteValue;
  }

  return arrayBuffer;
}

function encrypt(text) {
  const nonce = crypto.randomBytes(16);

  const cipher = crypto.createCipheriv(algorithm, key, nonce);
  let crypted = cipher.update(text, 'utf8', 'base64');
  crypted += cipher.final('base64');

  // Stick the nonce in the beginning of the encrypted message
  const cryptedAndNonce = nonce.toString('hex') + crypted;
  // Make it url friendly
  return cryptedAndNonce
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
}

function decrypt(text) {
  const nonce = hexStringToUint8Array(text.slice(0, 32));

  const decipher = crypto.createDecipheriv(algorithm, key, nonce);
  let dec = decipher.update(text.slice(32), 'base64', 'utf8');
  dec += decipher.final('utf8');
  return dec;
}

module.exports = {
  decrypt,
  encrypt,
};

And… the key needs to be the right length. I create the keys like this:

const crypto = require(‘crypto’);

const key = crypto.createSecretKey(crypto.randomBytes(32));

console.log(key.export().toString(‘hex’));

Hi - did you get this working? I too need to decrypt / encrypt data between a cloudflare worker and PHP. I’m also getting “Error: internal error” when executing crypto.subtle.decrypt.

I get an encrypted message and iv from the PHP server, and I know the key. However when I try to decrypt the message the closes I’ve got to getting this working now is an “OperationError” with the mesage “Error: Decryption failed. This could be due to a ciphertext authentication failure, bad padding, incorrect CryptoKey, or another algorithm-specific reason.”

Here’s some example code based on this, that generates the error. I can’t figure out what I’ve got wrong.

PHP:

$enc_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
return openssl_encrypt('This is a message that I am trying to decode', 'aes-256-cbc', 'hMYmJ6p^M6bDlJ1rhZ0jQn*XL6D$Tgd&', null, $enc_iv). "::" . base64_encode($enc_iv);

When this is retrieved by the CF worker, the payload is split into the message and iv as:

raw_enc_message: 'nFNKTdD7c/6DAVcjEfQ7V14FnJftXVjf5PKHhx5GuNW9Mf21IXTmsoHzzYay1fVQ'
raw_iv:  '/s0XIWCNiiTF46hxHdqHWw=='

CF Worker JS:

const key= 'hMYmJ6p^M6bDlJ1rhZ0jQn*XL6D$Tgd&'
const encrypted_message= new TextEncoder().encode(raw_enc_message)
const iv= Uint8Array.from(atob(raw_iv), c => c.charCodeAt(0))
const decrypt_key= await crypto.subtle.importKey('raw',new TextEncoder().encode(key),'AES-CBC',true,['encrypt', 'decrypt'])
const decrypted_message= await crypto.subtle.decrypt({name: "AES-CBC",length: "256",iv: iv.buffer} , decrypt_key, encrypted_message)

I think I’ve got a working proof of concept now. The issue - base64 encoding in Javascript just sucks!

So I created a worker that displays a form and you can enter a plain message to encrypt it. Submit the form and it is returned with the encrypted message concatenated with the iv (using hex strings) in the ‘encrypted’ message field. I can then generate an encrypted message in PHP to the same format, paste it in the worker form and decrypt it.

Javascript:

const mypassword='XXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxx' // 32 characters - need to hide this

const enc = new TextEncoder("utf-8")
const dec = new TextDecoder("utf-8")

function hexStringToUint8Array(hexString) {
  if (hexString.length % 2 !== 0) {
    throw 'Invalid hexString';
  }

  const arrayBuffer = new Uint8Array(hexString.length / 2);

  for (let i = 0; i < hexString.length; i += 2) {
    const byteValue = parseInt(hexString.substr(i, 2), 16);
    if (isNaN(byteValue)) {
      throw 'Invalid hexString';
    }
    arrayBuffer[i / 2] = byteValue;
  }

  return arrayBuffer;
}

function bytesToHexString(bytes)
{
    if (!bytes)
        return null;

    bytes = new Uint8Array(bytes);
    var hexBytes = [];

    for (let i = 0; i < bytes.length; ++i) {
        let byteString = bytes[i].toString(16);
        if (byteString.length < 2)
            byteString = "0" + byteString;
        hexBytes.push(byteString);
    }

    return hexBytes.join("");
}

function arraybufferToString(buf) {
  return String.fromCharCode.apply(null, new Uint8Array(buf));
}

async function getPasswordKey(password, keyUsage) {
  return crypto.subtle.importKey('raw', enc.encode(password), 'AES-CBC', false, keyUsage)
}

async function encryptData(secretData, password) {
  try {
    const iv = crypto.getRandomValues(new Uint8Array(16));
    const passwordKey = await getPasswordKey(password, ['encrypt'])
    const encryptedContent = await crypto.subtle.encrypt(
      {
        name: 'AES-CBC',
        iv: iv,
      },
      passwordKey,
      enc.encode(secretData),
    )

    // put the iv on the end with a :: delimiter
    return (bytesToHexString(encryptedContent) + '::' + bytesToHexString(iv))

  } catch (e) {
    throw e
  }
}

async function decryptData(encryptedData, password) {
  try {
    // split the IV off from the end (delimited using ::)
    encryptedComponents= encryptedData.split("::")

    const data = hexStringToUint8Array(encryptedComponents[0])
    const iv = hexStringToUint8Array(encryptedComponents[1])
    
    const passwordKey = await getPasswordKey(password, ['decrypt'])
    const decryptedContent = await crypto.subtle.decrypt(
      {
        name: 'AES-CBC',
        iv: iv,
      },
      passwordKey,
      data,
    )
    return arraybufferToString(decryptedContent)
  } catch (e) {
    throw e
  }
}

/**
 * rawHtmlResponse returns HTML inputted directly
 * into the worker script
 * @param {string} html
 */
function rawHtmlResponse(html) {
  const init = {
    headers: {
      "content-type": "text/html;charset=UTF-8",
    },
  }
  return new Response(html, init)
}
/**
 * readRequestBody reads in the incoming request body
 * Use await readRequestBody(..) in an async function to get the string
 * @param {Request} request the incoming request to read from
 */
async function readRequestBody(request) {
  const { headers } = request
  const contentType = headers.get("content-type") || ""

  if (contentType.includes("application/json")) {
    return JSON.stringify(await request.json())
  }
  else {
    const formData = await request.formData()
    const body = {}
    for (const entry of formData.entries()) {
      if(entry[1]) {
        body[entry[0]] = entry[1]
        // DECRYTPION
        if(entry[0] == 'secret') {
            try {
              body['result'] = await decryptData(entry[1], mypassword)
            } catch (e) {
              throw e
            }
        }

        // ENCRYPTION
        if(entry[0] == 'plain') {
          try {
            body['result'] = await encryptData(entry[1], mypassword)
          } catch (e) {
            throw e
          }
        }

      }
    }
    return JSON.stringify(body)
  }
}

const someForm = `
  <!DOCTYPE html>
  <html>
  <body>
  <h1>Crypto Test</h1>
  <p>Enter either a Plain Message to encrypt, or an Encryted Message to decrypt</p>
  <form action="/demos/requests" method="post">
    <div>
      <label for="plain">Plain Message</label>
      <input size="50" name="plain" id="plain" value="[PLAINVALUE]">
    </div>
    <div>
      <label for="plain">Encrypted Message</label>
      <input size="50" name="secret" id="secret" value="[ENCRYPTVALUE]">
    </div>
    <div>
      <button>Submit</button>
    </div>
  </form>
  </body>
  </html>
  `

async function handleRequest(request) {
  const reqBody = await readRequestBody(request)
  let reqData= JSON.parse(reqBody)

  let init = {
    "headers": {
      "content-type": "text/html;charset=UTF-8",
      "Cache-Control": "no-cache"
    },
    "status": 200,
  }

  let html

  if (reqData.plain) {
      html= someForm
        .replace('[ENCRYPTVALUE]', reqData.result)
        .replace('[PLAINVALUE]', '')

  }

  if (reqData.secret) {
      html= someForm
        .replace('[PLAINVALUE]', reqData.result)
        .replace('[ENCRYPTVALUE]', '')
  }

  return new Response(html, init)
}

addEventListener("fetch", event => {
  const { request } = event
  const { url } = request

  if (request.method === "POST") {
    return event.respondWith(handleRequest(request))
  }
  else {
    let html= someForm
        .replace('[PLAINVALUE]', '')
        .replace('[ENCRYPTVALUE]', '')
    return event.respondWith(rawHtmlResponse(html))
  }
})

PHP:

function external_encrypt($value) {
        $key= 'XXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxx';  // 32 characters - need to hide this
        $enc_iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));

        return bin2hex(openssl_encrypt($value, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $enc_iv)) . "::" . (bin2hex($enc_iv));
    }

function external_decrypt($value) {
        $key= 'XXXXxxxxXXXXxxxxXXXXxxxxXXXXxxxx';  // 32 characters - need to hide this
        $encryptedComponents= explode('::', $value);

        return openssl_decrypt(hex2bin($encryptedComponents[0]), 'aes-256-cbc', $key, OPENSSL_RAW_DATA, hex2bin($encryptedComponents[1]));
}
1 Like

Hello! I’ve successfully created working POC. You need couple of helpers here and there and check the console for output and wrap your own request handler please:

Worker:

addEventListener('fetch', event => {
    event.respondWith(handleRequest(event.request))
})

const ALGORITHM = 'AES-CBC';
const VECTOR = 'YOURVECTORINBASE64HERE';
const ENCRYPTION_KEY = '32CHARACTERSLONGHEXKEYHERE';

async function handleRequest(request) {

    const AES_VECTOR = str2ab(atob(VECTOR));
    const AES_KEY = await import_key(ENCRYPTION_KEY);
    
    let test_string = 'YOURTESTSTRINGHERE';
    let encrypted = await aes_encrypt(test_string, AES_KEY, AES_VECTOR);
    let decrypted = await aes_decrypt(encrypted, AES_KEY, AES_VECTOR);

    console.log(test_string, encrypted, decrypted);

    return new Response('hello world', {status: 200})
}

async function import_key(key) {
    return await crypto.subtle.importKey(
        'raw',
        str2ab(key),
        ALGORITHM,
        true,
        ['encrypt', 'decrypt'],
    );
}

async function aes_encrypt(data, key, vi) {
    data = str2ab(data+'');
    cypher = await crypto.subtle.encrypt({ name: ALGORITHM, iv: vi }, key, data);
    cypher = ab2str(cypher);
    cypher = bin2hex(cypher);
    return cypher;
}

async function aes_decrypt(data, key, vi) {  
    data = str2ab(pack_hex(data));
    let decypher = await crypto.subtle.decrypt({ name: ALGORITHM, iv: vi }, key, data);
    decypher = ab2str(decypher);
    return decypher;
}

function ab2str(buf) {
    return String.fromCharCode.apply(null, new Uint8Array(buf));
}

function str2ab(str) {
    let buf = new ArrayBuffer(str.length);
    let bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

function bin2hex(s) {
    var i, l, o = '', n; s += '';
    for (i = 0, l = s.length; i < l; i++) {
        n = s.charCodeAt(i).toString(16);
        o += n.length < 2 ? '0' + n : n;
    }
  return o
}

function pack_hex(source) {
    var source = source.length % 2 ? source + '0' : source,
        result = '';
    for( var i = 0; i < source.length; i = i + 2 ) result += String.fromCharCode( parseInt( source.substr( i , 2 ), 16 ) );
    return result;
}
1 Like