Here is the minimum code needed to replicate the issue, the error I'm getting is generic and not descriptive at all DOMException: The operation failed for an operation-specific reason in short it failed because it failed.
This is the code to run the test:
encryptTextWithAES("Hello world!", "password1")
.then((encryptedText) => {
console.log (encryptedText);
decryptTextWithAES(encryptedText, "password1")
.then((decryptedText) => {
console.log(decryptedText);
});
});
This is the code that performs the procedure:
// Function to derive a key from a password and salt using PBKDF2
async function deriveKeyFromPassword(password, salt) {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
encoder.encode(password),
{ name: 'PBKDF2' },
false,
['deriveBits', 'deriveKey']
);
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: encoder.encode(salt),
iterations: 100000, //todo: is size ok??
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
);
return derivedKey;
}
// Function to convert an ArrayBuffer to a Base64 string
function arrayBufferToBase64(arrayBuffer) {
const uint8Array = new Uint8Array(arrayBuffer);
const byteArray = Array.from(uint8Array);
return btoa(byteArray.map(byte => String.fromCharCode(byte)).join(''));
}
// Function to convert a Base64 string to an ArrayBuffer
function base64ToArrayBuffer(base64String) {
const binaryString = atob(base64String);
const byteArray = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
byteArray[i] = binaryString.charCodeAt(i);
}
return byteArray.buffer;
}
// Function to encrypt text using AES-GCM with a password
async function encryptTextWithAES(text, password) {
// Generate a random salt
const salt = window.crypto.getRandomValues(new Uint8Array(16)); //todo: is size ok??
// Derive a key from the password and salt using PBKDF2
const key = await deriveKeyFromPassword(password, salt);
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const data = encoder.encode(text);
const encryptedData = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data
);
const encryptedText = `${arrayBufferToBase64(encryptedData)}:${arrayBufferToBase64(iv)}:${arrayBufferToBase64(salt)}`;
return encryptedText;
}
// Function to decrypt text using AES-GCM with a password
async function decryptTextWithAES(encryptedText, password) {
const [encryptedDataBase64, ivBase64, saltBase64] = encryptedText.split(':');
const encryptedData = base64ToArrayBuffer(encryptedDataBase64);
const iv = base64ToArrayBuffer(ivBase64);
const salt = base64ToArrayBuffer(saltBase64);
// Derive a key from the password and salt using PBKDF2
const key = await deriveKeyFromPassword(password, salt);
const decryptedData = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
encryptedData
)
const decoder = new TextDecoder();
const decryptedText = decoder.decode(decryptedData);
return decryptedText;
}
I tested this also on different tools, thinking the problem was my environment (low memory??) and I always get the same outcome. It is those kind of bugs that require a new pair of eyes to pin-point what is wrong. In another answer someon suggested to enable a feature on Firefox, but this is not an acceptable solution since I cannot ask the users to do so, would be overly complicated to explain and not user friendly.