Securely encrypting data in a Chrome Extension without sending it to a server (key management problem) ??

I am building a Chrome extension where some user data (for example tab metadata or browsing-related information) must remain on the user’s device due to Chrome Web Store privacy policies. The extension is not allowed to send this data to my server.
Because of this restriction, I need to store the data locally using something like chrome.storage or IndexedDB. However, storing the data in plain text is not acceptable from a security perspective.
My current approach is to encrypt the data before storing it locally and decrypt it when needed.
For encryption I am using the Web Crypto API with AES-GCM, and deriving the key from a user password using PBKDF2.
Here is a simplified version of the encryption logic I am currently using:
Copyconst ALGO = 'AES-GCM';
const KEY_LENGTH = 256;
const IV_LENGTH = 12;
export async function deriveKey(password, salt) {
const enc = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveKey']
);
return crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{
name: ALGO,
length: KEY_LENGTH
},
false,
['encrypt', 'decrypt']
);
}
export async function encryptData(data, masterPassword) {
const enc = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
const salt = crypto.getRandomValues(new Uint8Array(16));
const key = await deriveKey(masterPassword, salt);
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: ALGO, iv },
key,
enc.encode(JSON.stringify(data))
);
return {
cipher: bufferToBase64(encryptedBuffer),
iv: bufferToBase64(iv),
salt: bufferToBase64(salt)
};
}
export async function decryptData(encryptedPayload, masterPassword) {
const { cipher, iv, salt } = encryptedPayload;
const key = await deriveKey(masterPassword, base64ToBuffer(salt));
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: ALGO, iv: base64ToBuffer(iv) },
key,
base64ToBuffer(cipher)
);
const dec = new TextDecoder();
return JSON.parse(dec.decode(decryptedBuffer));
}
function bufferToBase64(buffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)));
}
function base64ToBuffer(base64) {
return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
}
Example usage:
Copyconst password = '1234';
const encrypted = await encryptData(plainTab, password);
My main concern is key management.
Since this is a browser extension without a backend, I cannot store a secret key on a server. At the same time, if a key is hardcoded or stored directly in the extension code, it can be extracted by inspecting the extension.
So my questions are:
What is the recommended approach for encrypting sensitive data locally in a Chrome extension?
How should the encryption key be generated and managed securely if everything runs on the client side?
Is deriving the key from a user-provided master password (using PBKDF2 or similar) considered a good practice in this scenario?
Are there established patterns used by password managers or other privacy-focused extensions for this problem?
I am trying to design something similar to how password managers encrypt vault data locally before storing it.
Environment:
Chrome Extension (Manifest V3)
JavaScript
Web Crypto API
Storage via chrome.storage or IndexedDB
Have you solved a similar problem in a Chrome extension or another client-only application?
If you have any ideas, better approaches, or security recommendations, please leave a reply or reach out to me via email codeguyakash[at]gmail[dot]com. I’d love to learn from your experience.


