Build a Password Manager Chrome Extension: Security-First Approach

24 min read

Build a Password Manager Chrome Extension: Security-First Approach

Build a Password Manager Chrome Extension: Security-First Approach

Password security remains one of the most critical challenges in modern web development. With the average person managing over 100 online accounts, the need for a reliable, secure password manager Chrome extension has never been more pressing. Building a password vault extension requires careful attention to security architecture, encryption standards, and user experience. This comprehensive guide walks you through creating a production-ready credential manager chrome extension using a security-first approach that protects your users’ most sensitive data.

In this tutorial, we will cover everything from setting up the project structure and implementing robust encryption to building intuitive user interfaces for credential management. Whether you are a seasoned Chrome extension developer or just starting your journey into browser extension development, this guide provides the foundational knowledge and practical code examples needed to build a secure password manager extension that can compete with industry-standard solutions.


Understanding the Security Requirements

Before writing any code, you must understand the security requirements that govern password manager extensions. Unlike regular Chrome extensions, password managers handle extremely sensitive data that requires the highest levels of protection. Users trust these extensions with the keys to their digital lives, and any security lapse can result in devastating consequences including identity theft, financial loss, and privacy breaches.

The security requirements for a secure password Chrome extension start with encryption at rest. All stored credentials must be encrypted using industry-standard cryptographic algorithms before they ever touch the disk. This means even if an attacker gains access to the user’s device or browser storage, they cannot read the stored passwords without the encryption key. Additionally, the extension must implement secure communication channels between its various components, proper memory management to prevent sensitive data from lingering in memory, and robust authentication mechanisms to verify the user’s identity before granting access to stored credentials.

Chrome’s extension platform provides several storage APIs, but not all are suitable for sensitive data. The chrome.storage.local API stores data locally on the user’s device, while chrome.storage.sync synchronizes data across the user’s devices through their Google account. However, neither of these APIs provides automatic encryption. Your extension must implement its own encryption layer before storing any sensitive information. The Chrome platform also provides the chrome.storage.managed API for enterprise-managed configurations, but this is not designed for user credential storage.

Cryptographic Foundation

The foundation of any secure password manager lies in its cryptographic implementation. For a Chrome extension, you have several options for implementing encryption, each with trade-offs between security, performance, and ease of use. The most recommended approach for modern extensions is to use the Web Crypto API, which provides native browser support for cryptographic operations without requiring external dependencies.

The Web Crypto API supports various encryption algorithms, but for password manager applications, AES-GCM (Advanced Encryption Standard in Galois/Counter Mode) stands out as the preferred choice. AES-GCM provides both confidentiality and integrity protection, meaning it ensures that encrypted data cannot be read by unauthorized parties and cannot be tampered with without detection. The Galois/Counter Mode also provides authenticated encryption, which automatically verifies that the decrypted data has not been modified.

For key derivation, you should never use passwords directly as encryption keys. Instead, you must derive a secure key from the user’s master password using a key derivation function (KDF). PBKDF2 (Password-Based Key Derivation Function 2) is widely supported and recommended, though Argon2 provides better resistance against GPU-based cracking attacks. When implementing key derivation, use a high iteration count—at least 100,000 iterations for PBKDF2—to make brute-force attacks computationally expensive.


Project Setup and Manifest Configuration

Every Chrome extension begins with a manifest.json file that defines the extension’s capabilities, permissions, and structure. For a password manager, the manifest requires careful configuration to balance functionality with security. You must request only the permissions necessary for the extension to function, as overly broad permissions trigger additional scrutiny during Chrome Web Store review and increase the potential impact of any security vulnerability.

Create a new directory for your extension and initialize the manifest file with the following configuration:

{
  "manifest_version": 3,
  "name": "SecureVault Password Manager",
  "version": "1.0.0",
  "description": "A security-first password manager for Chrome",
  "permissions": [
    "storage",
    "activeTab",
    "scripting"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"],
      "run_at": "document_idle"
    }
  ]
}

The permissions configuration reflects a security-conscious approach. The storage permission allows the extension to save encrypted credentials, while activeTab and scripting enable the autofill functionality that injects credentials into web forms. The <all_urls> host permission is necessary for the extension to function across all websites, but this broad permission means you must implement additional security measures to protect the data your extension handles.

Note that in Manifest V3, background pages have been replaced with service workers, which are event-driven and do not persist in memory. This change has important implications for password managers, as you cannot keep decryption keys in memory between user interactions. Instead, you must re-derive or retrieve the key each time the user authenticates, adding a slight usability trade-off for improved security.


Core Encryption Module

With the manifest configured, you can now implement the cryptographic foundation of your password manager. Create a JavaScript module that handles all encryption and decryption operations using the Web Crypto API. This module will be the backbone of your extension’s security architecture.

The encryption module should export functions for generating encryption keys from master passwords, encrypting credential data, and decrypting stored passwords. Here’s a comprehensive implementation that follows security best practices:

// crypto.js - Core cryptographic operations

const ALGORITHM = 'AES-GCM';
const KEY_LENGTH = 256;
const PBKDF2_ITERATIONS = 100000;
const SALT_LENGTH = 16;
const IV_LENGTH = 12;

export class CryptoManager {
  constructor() {
    this.key = null;
  }

  async deriveKey(masterPassword, salt) {
    const encoder = new TextEncoder();
    const passwordKey = await crypto.subtle.importKey(
      'raw',
      encoder.encode(masterPassword),
      'PBKDF2',
      false,
      ['deriveKey']
    );

    return crypto.subtle.deriveKey(
      {
        name: 'PBKDF2',
        salt: salt,
        iterations: PBKDF2_ITERATIONS,
        hash: 'SHA-256'
      },
      passwordKey,
      { name: ALGORITHM, length: KEY_LENGTH },
      false,
      ['encrypt', 'decrypt']
    );
  }

  async initialize(masterPassword) {
    const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
    this.key = await this.deriveKey(masterPassword, salt);
    return salt;
  }

  async encrypt(plaintext) {
    if (!this.key) {
      throw new Error('Crypto manager not initialized');
    }

    const encoder = new TextEncoder();
    const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
    
    const encryptedData = await crypto.subtle.encrypt(
      { name: ALGORITHM, iv: iv },
      this.key,
      encoder.encode(plaintext)
    );

    const combined = new Uint8Array(iv.length + encryptedData.byteLength);
    combined.set(iv);
    combined.set(new Uint8Array(encryptedData), iv.length);
    
    return this.arrayBufferToBase64(combined);
  }

  async decrypt(encryptedData) {
    if (!this.key) {
      throw new Error('Crypto manager not initialized');
    }

    const combined = this.base64ToArrayBuffer(encryptedData);
    const iv = combined.slice(0, IV_LENGTH);
    const ciphertext = combined.slice(IV_LENGTH);

    const decrypted = await crypto.subtle.decrypt(
      { name: ALGORITHM, iv: iv },
      this.key,
      ciphertext
    );

    const decoder = new TextDecoder();
    return decoder.decode(decrypted);
  }

  arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
  }

  base64ToArrayBuffer(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes;
  }
}

This implementation provides military-grade encryption using AES-256-GCM, which offers both confidentiality and integrity verification. The key derivation uses PBKDF2 with 100,000 iterations and a random salt for each encryption operation, making rainbow table attacks ineffective. The initialization vector (IV) is randomly generated for each encryption operation, ensuring that encrypting the same plaintext twice produces different ciphertexts.


Secure Storage Architecture

With encryption in place, you need to implement the storage architecture that will hold your users’ credentials. The storage layer must handle not only saving and retrieving encrypted data but also managing the metadata necessary for organizing passwords, implementing search functionality, and maintaining data integrity.

Create a storage manager that handles all interactions with Chrome’s storage API while maintaining encryption:

// storage.js - Secure storage management

import { CryptoManager } from './crypto.js';

const STORAGE_KEYS = {
  SALT: 'vault_salt',
  VAULT: 'vault_data',
  SETTINGS: 'vault_settings'
};

export class SecureStorage {
  constructor() {
    this.crypto = new CryptoManager();
    this.isUnlocked = false;
  }

  async setup(masterPassword) {
    const { vault_salt } = await chrome.storage.local.get(STORAGE_KEYS.SALT);
    
    let salt;
    if (vault_salt) {
      salt = this.crypto.base64ToArrayBuffer(vault_salt);
    } else {
      salt = await this.crypto.initialize(masterPassword);
      await chrome.storage.local.set({
        [STORAGE_KEYS.SALT]: this.crypto.arrayBufferToBase64(salt)
      });
    }

    if (!vault_salt) {
      await this.crypto.initialize(masterPassword);
    } else {
      await this.crypto.initializeWithExistingSalt(masterPassword, salt);
    }

    this.isUnlocked = true;
  }

  async saveCredential(credential) {
    if (!this.isUnlocked) {
      throw new Error('Vault is locked');
    }

    const encryptedData = await this.crypto.encrypt(JSON.stringify(credential));
    const { vault } = await chrome.storage.local.get(STORAGE_KEYS.VAULT);
    
    const credentials = vault ? JSON.parse(await this.crypto.decrypt(vault)) : [];
    credentials.push({
      ...credential,
      id: crypto.randomUUID(),
      createdAt: new Date().toISOString()
    });

    const encryptedVault = await this.crypto.encrypt(JSON.stringify(credentials));
    await chrome.storage.local.set({
      [STORAGE_KEYS.VAULT]: encryptedVault
    });
  }

  async getCredentials() {
    if (!this.isUnlocked) {
      throw new Error('Vault is locked');
    }

    const { vault } = await chrome.storage.local.get(STORAGE_KEYS.VAULT);
    if (!vault) {
      return [];
    }

    const decrypted = await this.crypto.decrypt(vault);
    return JSON.parse(decrypted);
  }

  async searchCredentials(query) {
    const credentials = await this.getCredentials();
    const lowerQuery = query.toLowerCase();
    
    return credentials.filter(cred => 
      cred.title.toLowerCase().includes(lowerQuery) ||
      cred.username.toLowerCase().includes(lowerQuery) ||
      cred.url.toLowerCase().includes(lowerQuery)
    );
  }
}

This storage architecture ensures that credentials are never stored in plaintext. Every time you save or retrieve data, it passes through the encryption layer. The vault remains encrypted on disk and is only decrypted in memory when the user provides their master password. Even if an attacker gains access to the Chrome storage, they will only find encrypted data that is computationally expensive to crack.


Building the Popup Interface

The popup interface serves as the primary interaction point between users and your password manager. It must balance usability with security, providing quick access to stored credentials while implementing measures to prevent unauthorized access. Design the popup with a login-first approach that requires authentication before displaying any sensitive information.

Create the popup HTML with a clean, functional interface:

<!-- popup.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SecureVault</title>
  <link rel="stylesheet" href="popup.css">
</head>
<body>
  <div class="container">
    <header>
      <h1>SecureVault</h1>
    </header>

    <div id="auth-section">
      <form id="unlock-form">
        <input type="password" id="master-password" 
               placeholder="Enter master password" required>
        <button type="submit" class="primary-btn">Unlock</button>
      </form>
      <div id="setup-section" style="display: none;">
        <p>Set up your master password</p>
        <form id="setup-form">
          <input type="password" id="new-master-password" 
                 placeholder="Create master password" required>
          <input type="password" id="confirm-password" 
                 placeholder="Confirm password" required>
          <button type="submit" class="primary-btn">Create Vault</button>
        </form>
      </div>
    </div>

    <div id="vault-section" style="display: none;">
      <div class="search-bar">
        <input type="text" id="search-input" placeholder="Search credentials...">
      </div>
      <div id="credentials-list"></div>
      <button id="add-credential-btn" class="secondary-btn">Add New Credential</button>
    </div>

    <div id="add-form-section" style="display: none;">
      <form id="credential-form">
        <input type="text" id="cred-title" placeholder="Title" required>
        <input type="url" id="cred-url" placeholder="Website URL" required>
        <input type="text" id="cred-username" placeholder="Username/Email" required>
        <input type="password" id="cred-password" placeholder="Password" required>
        <button type="submit" class="primary-btn">Save</button>
        <button type="button" id="cancel-btn" class="secondary-btn">Cancel</button>
      </form>
    </div>
  </div>
  <script type="module" src="popup.js"></script>
</body>
</html>

The popup implements a security-first design pattern. When opened, it shows only the authentication interface, requiring the user to unlock the vault before accessing any stored credentials. This prevents shoulder surfing and ensures that closing the popup locks the vault immediately.


Implementing Autofill Functionality

One of the most valuable features of a password manager is the ability to automatically fill login forms on websites. This requires a content script that detects login forms, communicates with the background script, and injects the correct credentials. Implementing this feature securely requires careful attention to avoid exposing credentials to web pages.

Create a content script that handles form detection and autofill:

// content.js - Form detection and autofill

(function() {
  'use strict';

  // Listen for messages from the extension
  chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.action === 'fillCredentials') {
      fillForm(message.credentials);
      sendResponse({ success: true });
    }
    return true;
  });

  // Detect login forms on the page
  function detectLoginForm() {
    const selectors = [
      'form[action*="login"]',
      'form[action*="signin"]',
      'form[id*="login"]',
      'form[id*="signin"]',
      'input[type="email"][name*="user"]',
      'input[type="text"][name*="user"]',
      'input[type="password"]'
    ];

    const forms = document.querySelectorAll('form');
    for (const form of forms) {
      const passwordInputs = form.querySelectorAll('input[type="password"]');
      if (passwordInputs.length > 0) {
        return form;
      }
    }
    return null;
  }

  // Fill the login form with credentials
  function fillForm(credentials) {
    const form = detectLoginForm();
    if (!form) {
      console.log('No login form detected');
      return false;
    }

    // Find username field
    const usernameInputs = form.querySelectorAll('input[type="text"], input[type="email"]');
    for (const input of usernameInputs) {
      const name = input.name.toLowerCase();
      if (name.includes('user') || name.includes('email') || name.includes('login')) {
        input.value = credentials.username;
        input.dispatchEvent(new Event('input', { bubbles: true }));
        break;
      }
    }

    // Find password field and fill it
    const passwordInputs = form.querySelectorAll('input[type="password"]');
    for (const input of passwordInputs) {
      input.value = credentials.password;
      input.dispatchEvent(new Event('input', { bubbles: true }));
    }

    return true;
  }
})();

The content script follows the principle of minimal exposure. It never stores or logs credentials and only receives them temporarily for the autofill operation. The script uses event dispatching to trigger any JavaScript listeners that might be attached to form fields, ensuring compatibility with complex login forms that use client-side validation.


Background Service Worker

The background service worker coordinates communication between the popup, content scripts, and storage. It handles authentication state, manages the crypto manager, and processes credential requests. In Manifest V3, service workers have a limited lifetime and must be designed to handle this constraint.

// background.js - Background service worker

import { SecureStorage } from './storage.js';

let storage = new SecureStorage();

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.action) {
    case 'checkVault':
      checkVaultExists().then(sendResponse);
      return true;
    
    case 'unlock':
      unlockVault(message.password).then(result => sendResponse(result));
      return true;
    
    case 'setup':
      setupVault(message.password).then(result => sendResponse(result));
      return true;
    
    case 'getCredentials':
      getCredentials().then(creds => sendResponse({ credentials: creds }));
      return true;
    
    case 'saveCredential':
      saveCredential(message.credential).then(result => sendResponse(result));
      return true;
    
    case 'search':
      searchCredentials(message.query).then(results => sendResponse({ results }));
      return true;
  }
});

async function checkVaultExists() {
  const { vault_salt } = await chrome.storage.local.get('vault_salt');
  return { exists: !!vault_salt };
}

async function unlockVault(password) {
  try {
    await storage.setup(password);
    const credentials = await storage.getCredentials();
    return { success: true, credentials };
  } catch (error) {
    return { success: false, error: 'Invalid password' };
  }
}

async function setupVault(password) {
  await storage.setup(password);
  return { success: true };
}

async function getCredentials() {
  return await storage.getCredentials();
}

async function saveCredential(credential) {
  await storage.saveCredential(credential);
  return { success: true };
}

async function searchCredentials(query) {
  return await storage.searchCredentials(query);
}

Security Best Practices and Considerations

Building a secure password chrome extension requires adherence to additional security best practices beyond the implementation details covered above. These practices address the unique threats facing browser extensions and ensure that your password manager provides robust protection for user credentials.

First, implement automatic locking after periods of inactivity. Users may walk away from their computers without closing the browser, leaving the password vault unlocked. Configure your extension to automatically lock after a configurable timeout period, requiring re-authentication before accessing stored credentials again.

Second, implement secure memory handling. JavaScript’s garbage collector does not provide guarantees about when objects are removed from memory. For highly sensitive data, consider using techniques like overwriting sensitive strings with random data when they are no longer needed. While JavaScript makes perfect memory control difficult, these measures add layers of defense.

Third, implement tamper detection. Use Chrome’s runtime protection features and implement checks that detect if the extension has been modified. Any detected tampering should immediately lock the vault and warn the user.

Fourth, consider implementing biometric authentication where available. Chrome on certain platforms supports the Web Authentication API, which can integrate with Windows Hello, Touch ID, or other platform biometric systems. This provides convenient second-factor authentication without the need for password entry.

Fifth, implement secure sync if you plan to offer cross-device synchronization. Any synced data must remain encrypted with a key derived from the user’s master password, ensuring that even Google cannot access the plaintext credentials. The sync encryption key should never leave the user’s devices.


Testing Your Password Manager

Comprehensive testing is essential for a password manager, where bugs can have severe security implications. Test both the functional behavior and the security properties of your implementation. Functional tests should verify that credentials can be saved, retrieved, searched, and autofilled correctly across various scenarios.

Security testing should verify that encrypted data cannot be decrypted without the correct master password, that the extension handles incorrect passwords gracefully without revealing information, and that credentials are properly cleared from memory after use. Use automated tools to scan for common vulnerabilities like XSS in your popup and content scripts, and manually test edge cases that might expose sensitive data.


Conclusion

Building a password manager Chrome extension is a rewarding project that teaches valuable skills in Chrome extension development, security architecture, and cryptographic implementation. The security-first approach outlined in this guide ensures that your extension handles user credentials with the protection they deserve.

Remember that security is not a feature but a foundation. Every line of code you write should be evaluated through the lens of security, considering what happens when things go wrong. By implementing proper encryption, secure storage, careful permission management, and robust authentication, you can build a password manager that genuinely protects your users’ digital identities.

The complete source code structure outlined in this guide provides a solid foundation for a production-ready extension. From here, you can add features like password generation, secure notes, folders for organization, automatic form detection improvements, and cross-device synchronization. Each addition should maintain the security-first philosophy that forms the core of this implementation.

No previous article
No next article