TL;DR
Storing recently used accounts improves user experience but requires careful security considerations. This guide outlines a secure approach using encrypted storage, focusing on local browser storage with strong encryption and limited retention.
1. Understand the Risks
Simply storing usernames and passwords (even partially) is dangerous. If compromised, attackers gain easy access to user accounts. We’ll focus on storing only identifiers that can be used to retrieve account details from a secure server-side store.
2. Choose an Encryption Method
Use a robust encryption library suitable for the browser environment. The Web Crypto API is generally preferred as it’s built-in and avoids external dependencies. Here’s how you can generate a key:
const cryptoKey = window.crypto.subtle.generateKey({
name: "AES-GCM",
length: 256,
}, true, ["encrypt", "decrypt"]);
Important: Never store the encryption key directly in your code or browser storage! The key is derived from user credentials (e.g., password) using a Key Derivation Function (KDF).
3. Implement Key Derivation
Use a KDF like PBKDF2 to derive an encryption key from the user’s master password. This makes it much harder for attackers to crack even if they obtain the storage.
async function deriveKey(password, salt) {
const encoder = new TextEncoder();
const encodedPassword = encoder.encode(password);
const keyMaterial = await window.crypto.subtle.importKey("raw", encodedPassword,
{ name: "PBKDF2", hash: "SHA-256" }, true, ["deriveKey"]);
const derivedKey = await window.crypto.subtle.deriveKey(keyMaterial, salt, { name: "PBKDF2", hash: "SHA-256", length: 256 });
return derivedKey;
}
Generate a unique random salt for each user and store it securely alongside the encrypted data.
4. Store Account Identifiers, Not Credentials
Instead of storing usernames/passwords directly, store unique identifiers (e.g., UUIDs) that correspond to account details on your server. Encrypt these identifiers before saving them.
async function encryptData(data, key) {
const encoder = new TextEncoder();
const encodedData = encoder.encode(JSON.stringify(data));
const iv = window.crypto.getRandomValues(new Uint8Array(12)); // Initialization Vector
const encryptedData = await window.crypto.subtle.encrypt({
name: "AES-GCM",
iv: iv,
}, key, encodedData);
return { ciphertext: new Uint8Array(encryptedData), iv };
}
Store the ciphertext and iv in browser storage (e.g., LocalStorage or SessionStorage).
5. Use Browser Storage Carefully
- LocalStorage: Persists across sessions, but is accessible to all scripts on the same domain. Suitable for limited recent accounts with strong encryption.
- SessionStorage: Only lasts for the current session; more secure than LocalStorage but data is lost when the browser closes.
Example using LocalStorage:
localStorage.setItem('recentAccounts', JSON.stringify({ciphertext, iv}));
6. Implement Decryption
When retrieving accounts, decrypt the identifiers using the same key derived from the user’s password.
async function decryptData(ciphertext, iv, key) {
const encrypted = new Uint8Array(ciphertext);
const decrypted = await window.crypto.subtle.decrypt({
name: "AES-GCM",
iv: iv,
}, key, encrypted);
return JSON.parse(new TextDecoder().decode(decrypted));
}
7. Limit Retention & Implement Auto-Deletion
Don’t store an unlimited number of accounts! Set a reasonable limit (e.g., 5-10) and automatically delete older entries.
Implement a timer to clear the storage after a period of inactivity or when the user logs out.
8. Server-Side Security
The server storing account details must be highly secure. Use strong authentication, authorization, and encryption at rest. Regularly audit your server’s security practices.
9. Consider Multi-Factor Authentication (MFA)
Even with encrypted storage, MFA adds an extra layer of protection against compromised passwords.