Building a Password Manager Chrome Extension Securely: Complete Developer Guide
Password security has become one of the most critical concerns in modern web development. With billions of credentials leaked in data breaches each year, users increasingly rely on password manager extensions to protect their digital identities. Building a chrome password manager extension that genuinely protects user credentials requires careful attention to security fundamentals, encryption standards, and Chrome’s extension architecture. This comprehensive guide walks you through creating a secure credential storage extension using modern best practices.
Creating a secure password manager chrome extension is both a challenging and rewarding endeavor. Unlike simple productivity extensions, password managers handle extremely sensitive data that users entrust with their most private information. This responsibility demands developers understand cryptography fundamentals, secure coding practices, and the unique security model of Chrome extensions. By following this guide, you’ll build an extension that protects credentials while providing a seamless user experience.
Understanding the Security Requirements
Before writing any code, you must understand what makes a password manager secure. Users rely on their credential storage extension to protect sensitive information including passwords, usernames, API keys, and other authentication credentials. Any vulnerability in your extension could expose this data to attackers, making security your paramount concern throughout development.
Core Security Principles
A secure password manager chrome extension must satisfy several fundamental security requirements. First, all sensitive data must be encrypted at rest using strong encryption algorithms. Second, the master password used to unlock the vault should never be stored in plaintext and should be processed using key derivation functions that resist brute-force attacks. Third, the extension must protect against common web vulnerabilities including cross-site scripting, cross-site request forgery, and injection attacks.
The chrome.storage API provides the foundation for storing encrypted credential data, but it does not provide encryption itself. You must implement encryption using the Web Crypto API, which is available in Chrome’s extension context. This API provides access to cryptographic primitives including AES-GCM for symmetric encryption and PBKDF2 for key derivation, both essential for secure credential storage.
Threat Model
Understanding the threats your extension faces helps inform security decisions. Your password manager may encounter several attack vectors. Remote attackers might try to intercept credentials during transmission if your extension syncs data. Malware on the user’s machine could attempt to inject code into your extension’s context. Phishing attacks might try to trick users into revealing their master password through fake login prompts. Additionally, malicious websites could attempt to exploit vulnerabilities in your extension to access stored credentials.
Each of these threats requires specific countermeasures. Using HTTPS for all network communications protects against interception. Implementing content security policies prevents code injection. Designing clear, unmistakable user interfaces helps users recognize phishing attempts. Regularly auditing your code and using modern development practices prevents exploitation of vulnerabilities.
Project Structure and Manifest Configuration
Let’s set up the project structure for a secure password manager chrome extension using Manifest V3. This structure separates concerns and makes the codebase maintainable and auditable.
Directory Layout
Create a well-organized directory structure that separates your HTML, JavaScript, and assets. A typical password manager extension includes the following directories and files:
password-manager/
├── manifest.json
├── background/
│ └── service-worker.js
├── popup/
│ ├── popup.html
│ ├── popup.css
│ └── popup.js
├── content/
│ └── content-script.js
├── lib/
│ ├── crypto.js
│ ├── storage.js
│ └── password-generator.js
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── _locales/
└── en/
└── messages.json
This structure separates the background service worker from the popup interface, keeping each component focused on its specific responsibilities. The lib directory contains reusable modules for encryption and storage operations.
Manifest V3 Configuration
Your manifest.json defines the extension’s capabilities and permissions. For a secure password manager, you’ll need to carefully specify which permissions your extension requires:
{
"manifest_version": 3,
"name": "SecureVault Password Manager",
"version": "1.0.0",
"description": "A secure password manager for Chrome",
"permissions": [
"storage",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"background": {
"service_worker": "background/service-worker.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/content-script.js"]
}
]
}
Notice that we request minimal permissions. The storage permission is required for saving encrypted credentials, and scripting allows the content script to interact with web forms. The host permissions allow the extension to detect login forms on websites, but Manifest V3’s runtime permission system provides additional security by requiring user consent before accessing specific sites.
Implementing Cryptographic Functions
The cryptographic module is the heart of your secure password extension. This code handles all encryption and decryption operations, making it critical to get right. We’ll use the Web Crypto API, which provides browser-native cryptographic functions that are both performant and secure.
Key Derivation
Never store the master password directly. Instead, derive an encryption key from the master password using a key derivation function designed to resist brute-force attacks. PBKDF2 (Password-Based Key Derivation Function 2) is the standard approach:
// lib/crypto.js
const SALT_LENGTH = 16;
const ITERATIONS = 100000;
const HASH_ALGORITHM = 'SHA-256';
const KEY_LENGTH = 256;
async function deriveKey(masterPassword, salt) {
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(masterPassword);
const keyMaterial = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveKey']
);
return await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: ITERATIONS,
hash: HASH_ALGORITHM
},
keyMaterial,
{ name: 'AES-GCM', length: KEY_LENGTH },
false,
['encrypt', 'decrypt']
);
}
function generateSalt() {
return crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
}
This implementation uses 100,000 iterations, which provides a good balance between security and usability. The salt ensures that even if two users have the same master password, their derived keys will be different. Always use cryptographically secure random values for salt generation.
Encryption and Decryption
For encrypting stored credentials, AES-GCM (Advanced Encryption Standard in Galois/Counter Mode) provides both confidentiality and integrity verification. This means you’ll immediately detect any tampering with encrypted data:
async function encryptData(data, key) {
const encoder = new TextEncoder();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: iv },
key,
encoder.encode(JSON.stringify(data))
);
return {
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedBuffer))
};
}
async function decryptData(encryptedObject, key) {
const iv = new Uint8Array(encryptedObject.iv);
const data = new Uint8Array(encryptedObject.data);
const decryptedBuffer = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv },
key,
data
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decryptedBuffer));
}
The initialization vector (IV) must be unique for each encryption operation. By generating a random IV for each encryption, we ensure that encrypting the same data twice produces different ciphertext, preventing pattern analysis attacks.
Credential Storage Module
The storage module manages how credentials are saved and retrieved from chrome.storage. This module wraps the chrome.storage API with encryption handling, providing a clean interface for the rest of your extension.
Secure Storage Implementation
// lib/storage.js
const STORAGE_KEY = 'secure_vault_data';
const ENCRYPTED_KEY = 'encrypted_credentials';
class SecureStorage {
constructor() {
this.masterKey = null;
this.isUnlocked = false;
}
async initialize(masterPassword) {
const result = await chrome.storage.local.get(['salt', ENCRYPTED_KEY]);
let salt;
if (result.salt) {
salt = new Uint8Array(result.salt);
} else {
salt = generateSalt();
await chrome.storage.local.set({ salt: Array.from(salt) });
}
this.masterKey = await deriveKey(masterPassword, salt);
this.isUnlocked = true;
}
async saveCredential(credential) {
if (!this.isUnlocked) {
throw new Error('Vault is locked');
}
const encrypted = await encryptData(credential, this.masterKey);
const result = await chrome.storage.local.get(ENCRYPTED_KEY);
const credentials = result[ENCRYPTED_KEY] || [];
credentials.push(encrypted);
await chrome.storage.local.set({ [ENCRYPTED_KEY]: credentials });
}
async getCredentials() {
if (!this.isUnlocked) {
throw new Error('Vault is locked');
}
const result = await chrome.storage.local.get(ENCRYPTED_KEY);
const encryptedList = result[ENCRYPTED_KEY] || [];
const credentials = [];
for (const encrypted of encryptedList) {
try {
const decrypted = await decryptData(encrypted, this.masterKey);
credentials.push(decrypted);
} catch (error) {
console.error('Failed to decrypt credential:', error);
}
}
return credentials;
}
lock() {
this.masterKey = null;
this.isUnlocked = false;
}
}
const secureStorage = new SecureStorage();
This implementation ensures that credentials are never stored in plaintext. All data is encrypted before being saved to chrome.storage, and the encryption key is held in memory only while the vault is unlocked. When the user locks the vault, the key is discarded from memory.
Building the Popup Interface
The popup interface provides the user-facing part of your secure password extension. It should be intuitive and provide clear feedback about the vault’s locked or unlocked state.
Popup HTML Structure
<!-- popup/popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="popup.css">
<title>SecureVault</title>
</head>
<body>
<div class="container">
<header>
<h1>SecureVault</h1>
<span id="status" class="status locked">Locked</span>
</header>
<div id="unlock-view">
<form id="unlock-form">
<input
type="password"
id="master-password"
placeholder="Enter master password"
required
autocomplete="current-password"
>
<button type="submit">Unlock Vault</button>
</form>
<p id="unlock-error" class="error hidden"></p>
</div>
<div id="vault-view" class="hidden">
<div class="actions">
<button id="add-credential" class="primary">Add Password</button>
<button id="generate-password">Generate Password</button>
<button id="lock-vault">Lock Vault</button>
</div>
<div id="credentials-list" class="credentials-list"></div>
</div>
</div>
<script src="../lib/crypto.js"></script>
<script src="../lib/storage.js"></script>
<script src="popup.js"></script>
</body>
</html>
Popup JavaScript
// popup/popup.js
document.addEventListener('DOMContentLoaded', async () => {
const unlockView = document.getElementById('unlock-view');
const vaultView = document.getElementById('vault-view');
const statusEl = document.getElementById('status');
const unlockForm = document.getElementById('unlock-form');
const unlockError = document.getElementById('unlock-error');
// Check if vault is already initialized
const stored = await chrome.storage.local.get(['salt']);
if (!stored.salt) {
// First time setup - show create password view
statusEl.textContent = 'Setup Required';
}
unlockForm.addEventListener('submit', async (e) => {
e.preventDefault();
const password = document.getElementById('master-password').value;
try {
await secureStorage.initialize(password);
showVaultView();
} catch (error) {
unlockError.textContent = 'Invalid master password';
unlockError.classList.remove('hidden');
}
});
function showVaultView() {
unlockView.classList.add('hidden');
vaultView.classList.remove('hidden');
statusEl.textContent = 'Unlocked';
statusEl.classList.remove('locked');
statusEl.classList.add('unlocked');
loadCredentials();
}
async function loadCredentials() {
const credentials = await secureStorage.getCredentials();
const list = document.getElementById('credentials-list');
list.innerHTML = '';
for (const cred of credentials) {
const item = document.createElement('div');
item.className = 'credential-item';
item.innerHTML = `
<span class="site">${escapeHtml(cred.site)}</span>
<span class="username">${escapeHtml(cred.username)}</span>
<button class="copy-password" data-password="${escapeHtml(cred.password)}">
Copy
</button>
`;
list.appendChild(item);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Lock vault button
document.getElementById('lock-vault').addEventListener('click', () => {
secureStorage.lock();
vaultView.classList.add('hidden');
unlockView.classList.remove('hidden');
statusEl.textContent = 'Locked';
statusEl.classList.remove('unlocked');
statusEl.classList.add('locked');
});
});
Auto-Fill Functionality
One of the most valuable features of a credential storage extension is automatic form filling. The content script detects login forms and offers to fill them with stored credentials.
Content Script Implementation
// content/content-script.js
const FORM_SELECTORS = [
'form[action*="login"]',
'form[action*="signin"]',
'form[id*="login"]',
'form[id*="signin"]',
'input[type="password"]'
];
function findLoginForm() {
for (const selector of FORM_SELECTORS) {
const forms = document.querySelectorAll(selector);
for (const form of forms) {
const usernameInput = form.querySelector('input[type="text"], input[type="email"], input[name*="user"], input[name*="email"]');
const passwordInput = form.querySelector('input[type="password"]');
if (usernameInput && passwordInput) {
return { form, usernameInput, passwordInput };
}
}
}
return null;
}
function injectCredentialFill() {
const loginForm = findLoginForm();
if (!loginForm) return;
const { form, usernameInput, passwordInput } = loginForm;
const fillButton = document.createElement('button');
fillButton.type = 'button';
fillButton.textContent = '🔐 Fill Password';
fillButton.className = 'vault-fill-button';
fillButton.addEventListener('click', async () => {
const tab = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.runtime.sendMessage({
action: 'get-credentials',
url: window.location.hostname
}, (credentials) => {
if (credentials && credentials.length > 0) {
usernameInput.value = credentials[0].username;
passwordInput.value = credentials[0].password;
}
});
});
form.appendChild(fillButton);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', injectCredentialFill);
} else {
injectCredentialFill();
}
The content script communicates with the background service worker to retrieve credentials for the current site. This architecture keeps sensitive operations in the service worker while allowing the content script to interact with page DOM elements.
Best Practices and Security Considerations
Building a secure password extension requires ongoing attention to security throughout development and maintenance.
Never Store Master Password
Your extension should never store the master password in any form. The password is used to derive an encryption key, but the key itself is held in memory only while the vault is unlocked. Even chrome.storage.local should never contain the master password or any direct derivation of it.
Implement Proper Memory Handling
JavaScript doesn’t provide direct memory control, but you can minimize the exposure of sensitive data. Clear sensitive data from variables when no longer needed, and avoid logging sensitive information to console. In the background service worker, the key should be cleared when the worker terminates.
Use Content Security Policy
A strong Content Security Policy prevents cross-site scripting attacks that could otherwise inject malicious code into your extension. Define strict CSP headers in your manifest:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'"
}
}
Regular Security Audits
Password managers are high-value targets for attackers. Regularly audit your code for vulnerabilities, keep dependencies updated, and consider commissioning third-party security reviews. Subscribe to security advisories for any libraries you use.
User Education
Even the most secure extension can be compromised if users choose weak master passwords. Implement password strength meters when users create their master password, and encourage the use of unique, randomly generated passwords for each site.
Conclusion
Building a secure password manager chrome extension requires careful attention to security at every level. By implementing proper encryption using the Web Crypto API, storing credentials securely with chrome.storage, and following secure development practices, you can create an extension that genuinely protects users’ credentials.
Remember that security is not a one-time achievement but an ongoing process. Stay informed about new vulnerabilities, keep your dependencies updated, and always prioritize user security in your design decisions. With the foundations laid out in this guide, you’re well-equipped to build a chrome password manager extension that users can trust with their most sensitive credentials.
The complete source code for this secure password manager extension provides a starting point for your own implementation. Extend it with features like password strength analysis, secure note storage, and cross-device synchronization while maintaining the security principles outlined throughout this guide.
Related Articles
- Chrome Extension Security Best Practices 2025 - Comprehensive security guidelines for extensions
- Content Security Policy for Chrome Extensions - Implement robust CSP policies
- Chrome Extension Local Storage Encryption - Secure your local storage with encryption
Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.