I've successfully encrypted data with crypto API. Once it's done, I save the initialization-vector and the encrypted data as a single base64 string.
When decrypting, I revert these two information to Uint8Array that match the originals. But the decryption always fails with the following error:
error decrypt Error: OperationError
Here is the code:
// generate key
generateKey (){
crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
// encrypt
async encrypt(data, secretKey) {
const initializationVector = crypto.getRandomValues(new Uint8Array(96));
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: initializationVector,
tagLength: 128,
},
secretKey,
encodedData
);
const encryptedDataBase64 = btoa(new Uint8Array(encryptedBuffer));
const initializationVectorBase64 = btoa(initializationVector);
return `${encryptedDataBase64}.${initializationVectorBase64}`;
}
// convert base64 string to uint8array
base64ToUint8Array(base64String) {
return new Uint8Array(
atob(base64String)
.split(",")
.map((n) => +n)
);
}
//decrypt
async decrypt(encryptedData, secretKey) {
const { 0: data, 1: iv } = encryptedData.split(".");
const initializationVector = base64ToUint8Array(iv);
const _data = base64ToUint8Array(data);
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: initializationVector,
tagLength: 128,
},
secretKey,
_data
);
return new TextDecoder().decode(decryptedData)
}
I've checked the initialization-vector and the data Uint8Array during the encryption and during the decryption. They match their original versions. So I don't know where I'm doing something wrong here.
Thanks for your help!
The conversion from ArrayBuffer
to Base64 and vice versa is not correct. Also, when creating the IV or instantiating the Uint8Array
, the length must be specified in bytes and not bits. A possible fix is:
(async () => {
var key = await generateKey();
var plaintext = {"data": "The quick brown fox jumps over the lazy dog"};
var ciphertext = await encrypt(plaintext, key);
console.log(ciphertext.replace(/(.{48})/g,'$1\n'));
var decrypted = await decrypt(ciphertext, key);
console.log(JSON.parse(decrypted));
})();
// generate key
function generateKey (){
return crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
// encrypt
async function encrypt(data, secretKey) {
const initializationVector = crypto.getRandomValues(new Uint8Array(12)); // Fix: length in bytes
const encodedData = new TextEncoder().encode(JSON.stringify(data));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: initializationVector,
tagLength: 128,
},
secretKey,
encodedData
);
const encryptedDataBase64 = ab2b64(encryptedBuffer); // Fix: Apply proper ArrayBuffer to Base64 conversion
const initializationVectorBase64 = ab2b64(initializationVector); // Fix: Apply proper ArrayBuffer to Base64 conversion
return `${encryptedDataBase64}.${initializationVectorBase64}`;
}
// decrypt
async function decrypt(encryptedData, secretKey) {
const { 0: data, 1: iv } = encryptedData.split(".");
const initializationVector = b642ab(iv); // Fix: Apply proper Base64 to ArrayBuffer conversion
const _data = b642ab(data); // Fix: Apply proper Base64 to ArrayBuffer conversion
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: initializationVector,
tagLength: 128,
},
secretKey,
_data
);
return new TextDecoder().decode(decryptedData)
}
// https://stackoverflow.com/a/11562550/9014097 or https://stackoverflow.com/a/9458996/9014097
function ab2b64(arrayBuffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
// https://stackoverflow.com/a/41106346 or https://stackoverflow.com/a/21797381/9014097
function b642ab(base64string){
return Uint8Array.from(atob(base64string), c => c.charCodeAt(0));
}