Monday, February 21, 2022

Symmetric Encrypt and Decrypt in Javascript


 

In this post we will review the steps to use AES-CBC encryption in javascript. We use the SubtleCrypto API, which is available on on secure context pages. Will also use some functions that are not supported on old browsers.


First, to check if the page is considered a secure context, use the following:



console.log('secure context', window.isSecureContext)



The SubtleCrypto API uses ArrayBuffer for it functions, but as we want to send the data to server, will will convert it from and to a base64 string using the following helper functions:



function arrayBufferToBase64(arrayBuffer) {
const bytes = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))
return window.btoa(bytes)
}

function base64ToArrayBuffer(base64String) {
const chars = window.atob(base64String)
const arrayBuffer = new ArrayBuffer(chars.length)
const bufferView = new Uint8Array(arrayBuffer)
for (let i = 0, strLen = chars.length; i < strLen; i++) {
bufferView[i] = chars.charCodeAt(i)
}
return arrayBuffer
}


Special notes for NodeJS users

As btoa() does not exist in NodeJS, we need to use the Buffer, BUT we must specify the latin1 encoding to have the same encoding as in the browser.


NodeJS version:

function arrayBufferToBase64(arrayBuffer) {
const bytes = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))
return Buffer.from(bytes,'latin1').toString('base64')
}



Now we can generate the AES key.


const ALGORITHM = 'AES-CBC'


async function generateKey() {
return await window.crypto.subtle.generateKey(
{
name: ALGORITHM,
length: 256,
},
true,
['encrypt', 'decrypt'],
)
}



The key can be exported and imported.



async function exportKey(key) {
const exported = await window.crypto.subtle.exportKey(
'raw',
key,
)

return arrayBufferToBase64(exported)
}

async function importKey(base64Key) {
const bytes = base64ToArrayBuffer(base64Key)
return await window.crypto.subtle.importKey(
'raw',
bytes,
'AES-CBC',
true,
['encrypt', 'decrypt'],
)
}



And finally we can encrypt and decrypt using AES-CBC:



async function encrypt(key, clearText) {
const encodedText = new TextEncoder().encode(clearText)
const iv = window.crypto.getRandomValues(new Uint8Array(16))
const cipherText = await window.crypto.subtle.encrypt(
{
name: ALGORITHM,
iv,
},
key,
encodedText,
)

const data = {
cipher: arrayBufferToBase64(cipherText),
iv: arrayBufferToBase64(iv),
}

return JSON.stringify(data)
}

async function decrypt(key, jsonData) {
const data = JSON.parse(jsonData)
const ciphertext = base64ToArrayBuffer(data.cipher)
const iv = base64ToArrayBuffer(data.iv)
const encodedText = await window.crypto.subtle.decrypt(
{
name: 'AES-CBC',
iv,
},
key,
ciphertext,
)

return new TextDecoder().decode(encodedText)
}


Notice that the encryption result includes Initialization Vector (IV), which is a random buffer that is used in the encryption. The IV is returned from the encrypt function unchanged. The decrypt function uses both the cipher result and the IV along with the AES key to do its work. To simplify the usage of both IV and cipher, we use a JSON format.


We can now test our code. The following is an example of using the encryption.



async function unitTest() {
const logPrefix = 'symmetric unit-test'
const key = await generateKey()
console.log(logPrefix, 'key', key)
const clearText = '123456789sdfghjkl%$^&*(XXX'
console.log(logPrefix, 'clearText', clearText)
const cipher = await encrypt(key, clearText)
console.log(logPrefix, 'cipher', cipher)
const exportedKey = await exportKey(key)
console.log(logPrefix, 'exportedKey', exportedKey)
const importedKey = await importKey(exportedKey)
console.log(logPrefix, 'importedKey', importedKey)
const decrypted = await decrypt(importedKey, cipher)
console.log(logPrefix, 'decrypted', decrypted)
}





Final Note


This post is part of a series of posts about encryption. The full posts list is below.




No comments:

Post a Comment