Reputation: 6257
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!
Upvotes: 1
Views: 2501
Reputation: 49251
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));
}
Upvotes: 1