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]));
}
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;
}