I'm working on building an encrypt/decrypt feature using AES, and a fixed AES 256-bit key size. I have decided on using AES-GCM, as it seems the most performant and secure.
I would like to know whether the following use of the browser's Crypto.js library is correctly used.
async function generateAESGCMKey() {
const key = await window.crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256, // 256-bit key length
},
true, // Extractable
['encrypt', 'decrypt'] // Key usages
);
const encodedKey = btoa(String.fromCharCode(...new Uint8Array(await window.crypto.subtle.exportKey('raw', key))));
// The btoa function encodes the raw key data into base64
return encodedKey;
}
async function encryptText(plainText, base64Key) {
// Convert base64 key to ArrayBuffer
const keyBuffer = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
// Import the key
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBuffer,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
// Generate a random initialization vector (IV)
const iv = crypto.getRandomValues(new Uint8Array(12));
// Encrypt the plain text
const plainTextBuffer = new TextEncoder().encode(plainText);
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
cryptoKey,
plainTextBuffer
);
// Combine the IV and encrypted buffer for decryption later
const resultBuffer = new Uint8Array(12 + encryptedBuffer.byteLength);
resultBuffer.set(iv);
resultBuffer.set(new Uint8Array(encryptedBuffer), 12);
// Convert the result to a base64 string
const resultBase64 = btoa(String.fromCharCode(...resultBuffer));
return resultBase64;
}
async function decryptText(base64Combined, base64Key) {
// Convert base64 combined input to ArrayBuffer
const combinedBuffer = Uint8Array.from(atob(base64Combined), c => c.charCodeAt(0));
// Extract IV and encrypted data
const iv = combinedBuffer.slice(0, 12);
const encryptedData = combinedBuffer.slice(12);
// Convert base64 key to ArrayBuffer
const keyBuffer = Uint8Array.from(atob(base64Key), c => c.charCodeAt(0));
// Import the key
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyBuffer,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
// Decrypt the encrypted data
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
cryptoKey,
encryptedData
);
// Convert decrypted buffer to a string
const decryptedText = new TextDecoder().decode(decryptedBuffer);
return decryptedText;
}
(async function () {
// key size will be fixed every time
// "oVmzkV09WrEfWGn8fDrp80k2n/aRPHYfu9nPohYneRE=" - generated by SS PQE KEM
// S6l5o2KhMr6E/9G9gmlpO7UD8iAdWGiCreRRzNPVHLk= - generated by generateAESGCMKey
const key = await generateAESGCMKey();
console.log(key)
// plainText is of variable size
const plainText = "This is a super secret message, that should not be shared";
const encryptedText = await encryptText(plainText, key);
console.log("Encrypted text:", encryptedText);
const decryptedText = await decryptText(encryptedText, key);
console.log("Decrypted text:", decryptedText);
})();
Are there any edge-cases I should be wary of, or anything missing?
I usually work with these functions on a higher level, and haven't really worked with code this close to metal.