TL;DR
This guide shows you how to securely store sensitive data (like passwords or API keys) in a user’s browser using HTML LocalStorage, protected by asymmetric encryption. We’ll use JavaScript’s Web Crypto API to generate key pairs and encrypt/decrypt the data.
Steps
- Generate Key Pair:
- First, check if a key pair already exists in LocalStorage. This avoids regenerating keys on every page load.
- If no key pair exists, generate one using the
crypto.subtle.generateKeyfunction. We’ll use RSA with a key size of 2048 bits for good security. - Encrypt Data:
- Convert the data you want to store into an
ArrayBufferusingTextEncoder. - Use the public key from the key pair to encrypt the data with
crypto.subtle.encrypt. We’ll use ‘RSA-OAEP’ as the encryption algorithm. - Store Encrypted Data in LocalStorage:
- Convert the encrypted data (which is an
ArrayBuffer) into a base64 string usingbtoa. This makes it safe to store as text in LocalStorage. - Save the base64 encoded string in LocalStorage with a descriptive key name.
- Decrypt Data:
- Retrieve the base64 encoded data from LocalStorage using
localStorage.getItem. - Decode the base64 string back into an
ArrayBufferusingatob. - Use the private key from the key pair to decrypt the data with
crypto.subtle.decrypt. - Example Usage:
- To store a password:
await saveData('mySecretPassword', 'superSecurePassword'); - To retrieve the password:
const password = await decryptData('mySecretPassword'); - Important Considerations:
- Key Security: The private key is stored in the browser. While encrypted, it’s still vulnerable if the user’s machine is compromised. This method is suitable for protecting against casual access but not against determined attackers.
- Browser Support: Web Crypto API has good support across modern browsers. Check MDN for compatibility details.
- Error Handling: The code snippets above lack comprehensive error handling (e.g., what happens if decryption fails?). Add
try...catchblocks to handle potential errors gracefully.
async function getKeyPair() {
const existingKeyPair = JSON.parse(localStorage.getItem('keyPair'));
if (existingKeyPair) {
return existingKeyPair;
}
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSA-OAEP',
modulusLength: 2048,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: { name: 'SHA-256' },
},
true, // extractable
['encrypt', 'decrypt']
);
localStorage.setItem('keyPair', JSON.stringify(keyPair));
return keyPair;
}
async function encryptData(data) {
const keyPair = await getKeyPair();
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const encrypted = await crypto.subtle.encrypt(
{
name: 'RSA-OAEP',
},
keyPair.publicKey,
dataBuffer
);
return new Uint8Array(encrypted);
}
async function saveData(data, storageKey) {
const encryptedData = await encryptData(data);
const base64String = btoa(String.fromCharCode(...encryptedData));
localStorage.setItem(storageKey, base64String);
}
async function decryptData(storageKey) {
const base64String = localStorage.getItem(storageKey);
if (!base64String) return null;
const encryptedData = Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
const keyPair = await getKeyPair();
const decrypted = await crypto.subtle.decrypt(
{
name: 'RSA-OAEP',
},
keyPair.privateKey,
encryptedData
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}

