2Password Chrome 浏览器插件开发实战:从 Manifest V3 到 Touch ID 生物认证的完整实现
这是一篇 Chrome 浏览器扩展的完整开发实战。2Password 是一个完全本地运行、数据自主可控的浏览器密码管理器插件——安装到 Chrome 后,它能自动识别登录表单、一键填充账号密码、生成 TOTP 验证码,并通过 macOS Touch ID 保护所有数据。不依赖任何云服务,没有后端服务器,所有加密和存储都在浏览器本地完成。本文从 Chrome 扩展的架构设计(Manifest V3 + Service Worker + Content Script + Native Messaging)到每项核心功能的实现细节,完整拆解整个浏览器插件系统。
一、项目背景与技术选型
最终效果一览(左:自动填充 + 右:添加/编辑 + 下:自动登录成功)

密码管理器的核心诉求只有两个字:安全和便捷。这两者天然矛盾——越安全往往越不方便。2Password 的设计目标就是在不依赖云端的前提下,通过浏览器扩展的原生能力和系统级集成来平衡这对矛盾。
2Password 是一个跨平台密码管理器系列,此前已经完成了两个平台的实现:
- macOS 桌面端. ——项目起源,从 1Password 迁移到自建方案的完整记录
- Android 移动端. ——将核心加密方案移植到手机
而本文介绍的 Chrome 浏览器扩展是第三个平台,也是最复杂的——它需要在浏览器的沙箱环境中实现与桌面端同等的安全能力,同时还要自动识别网页表单、在任意网站上完成密码填充。技术选型上,几个关键决策:
- Chrome Manifest V3:Chrome 扩展的最新规范版本,使用 Service Worker 替代 V2 的 background page,更安全、更省资源
- Web Crypto API:浏览器原生加密 API(
window.crypto.subtle),无需第三方加密库,性能最优 - IndexedDB:浏览器内置的 NoSQL 数据库,支持大容量结构化存储,相比 localStorage 容量更大、支持索引查询
- Native Messaging:Chrome 官方的原生通信机制,通过 stdin/stdout 与系统级程序交换 JSON 消息,让扩展能调用系统 API
- Swift:macOS 原生编程语言,直接调用 LocalAuthentication(生物识别)和 Security(Keychain)框架
为什么不用 WebAuthn / FIDO2?因为这些标准依赖服务端,而 2Password 的核心原则是纯本地、零服务端。macOS Keychain + Touch ID 是纯本地方案中最安全的选择。
关于 IndexedDB:上文提到 2Password 用 IndexedDB 存储密码数据。IndexedDB 是浏览器内置的 NoSQL 数据库——可以理解为 Chrome 自带的"本地 SQLite",只不过它存的是 JavaScript 对象而非 SQL 表格。相比更常见的 localStorage,它有三个关键优势:容量大(数百 MB 到数 GB,而 localStorage 只有约 5MB)、支持索引查询(可以按分类、网站快速搜索)、异步操作(不阻塞页面)。数据存在哪?以 macOS Chrome 为例,文件位于 ~/Library/Application Support/Google/Chrome/Default/IndexedDB/chrome-extension_[扩展ID].indexeddb.leveldb/。每个扩展的数据完全隔离;卸载扩展时自动删除;不会通过 Chrome Sync 同步到其他设备——正好符合"零云端"原则。2Password 的数据库叫 password_manager_db,包含 passwords(加密的密码记录)和 history(变更历史)两个对象仓库。
二、整体架构
Chrome 扩展由三大部分组成:Popup(用户界面)、Background(后台服务,即 Service Worker)、Content Script(页面注入脚本,可以读取和修改用户正在浏览的网页 DOM,但与页面自身 JS 隔离运行)。2Password 还多了一层 Native Host(系统原生程序),形成了四层架构:
三、Touch ID 生物认证实现
这是整个项目最有趣的技术点。Chrome 扩展运行在沙箱中,无法直接调用 macOS 的 Touch ID API。Chrome 提供了 Native Messaging 机制来解决这个问题——扩展可以启动一个系统级的原生程序(Swift 编译的二进制文件),通过操作系统的标准输入/输出(stdin/stdout)进行 JSON 通信。这是 Chrome 扩展突破沙箱限制的唯一官方方式。
3.1 完整调用链:从点击到指纹验证
用户在 Popup 中点击"解锁"后,一条消息要经过 4 层才能到达 Touch ID 硬件。让我们从最上层逐步往下看:
关键细节:Background 层使用的是 chrome.runtime.connectNative()(长连接端口模式),而非 sendNativeMessage()(一次性模式)。这是因为 connectNative 支持超时控制和端口断开监听,更可靠。但 Swift 程序仍然是"处理一条消息就退出"——每次认证请求都是一次新的进程启动。
3.2 Native Messaging 二进制协议
Chrome 与 Native Host 之间的通信协议很简单但严格:每条消息由 4 字节小端序长度头 + JSON 消息体组成。Chrome 会自动处理这个协议的编码和解码,开发者只需要关注 JSON 消息本身。
3.3 Swift 端 Keychain 查询与 Touch ID 弹出
Swift 程序收到请求后,核心操作是构建一个 Keychain 查询字典。关键技术在于 kSecUseAuthenticationContext 参数——它是 Touch ID 弹出的开关:
核心原理:macOS Keychain 支持对每条记录设置访问控制。当存储时指定了 SecAccessControlCreateFlagUserPresence,后续每次读取都必须通过生物识别或设备密码。Swift 程序通过传入 LAContext 告诉系统"我要做身份验证",系统就会自动弹出认证 UI。这意味着密钥永远不会离开系统安全边界——Chrome 扩展拿到的只是验证通过后的密钥值,无法绕过验证直接获取。
3.4 配置文件与安装
Chrome 通过一个 JSON 配置文件来发现 Native Host:
{
"name": "com.2password.touchid",
"description": "Touch ID auth for 2Password",
"path": "/Users/.../NativeMessagingHosts/touchid_host",
"type": "stdio",
"allowed_origins": ["chrome-extension://YOUR_EXT_ID/"]
}
这个文件必须放在 ~/Library/Application Support/Google/Chrome/NativeMessagingHosts/,且 allowed_origins 中的扩展 ID 必须与实际匹配,否则 Chrome 会拒绝连接——这是 Chrome 的安全机制,防止恶意网站利用 Native Messaging。
3.5 数据安全模型
理解 2Password 的安全性,关键是搞清楚什么被保护、用什么保护、存在哪:
攻击场景分析:假设攻击者获得了用户电脑的文件访问权限:
- 能拿到什么? IndexedDB 中的密文、chrome.storage.local 中的 salt
- 拿不到什么? deviceKey(在 Keychain 中,Touch ID 保护)
- 能破解吗? 没有 deviceKey 就无法派生 AES 密钥,密文无法解密。即使用 PBKDF2 暴力枚举所有可能的 deviceKey(2^256 种),100K 迭代的代价使其在物理上不可行
- AES-GCM 的额外保障:即使知道部分明文(如已知某个密码格式),GCM 模式的认证标签会防止任何密文篡改
四、加密体系设计
密码管理器的加密方案需要回答三个问题:密钥从哪来、怎么加密、密钥怎么存。
术语速览:AES-256-GCM — 当前最广泛使用的对称加密方案,256 位密钥,GCM 模式会自动生成"认证标签"来检测密文是否被篡改。IV(初始化向量) — 加密时附加的一段随机字节,确保相同明文每次加密结果不同。PBKDF2 — "基于密码的密钥派生函数",通过反复哈希迭代(10 万次)来增加暴力破解成本。Salt(盐值) — 随机数据,与密钥拼接后一起哈希,防止"彩虹表攻击"。
4.1 密钥派生链路
2Password 采用"设备密钥 → PBKDF2 → AES 密钥"的两层派生方案:
为什么要拼接固定字符串 "PasswordManagerExtension_v2"?这是一种"域名隔离"技术——即使两个应用碰巧生成了相同的 deviceKey,因为拼接字符串不同,最终派生的 AES 密钥也不同,防止密钥碰撞。
4.2 加密存储格式
每条密码在 IndexedDB 中的存储结构:
注意只有 password、notes、mfaSecret 三个字段加密。title、username、website 保留明文,因为自动填充需要根据网站域名匹配密码,搜索功能也需要搜索标题。
五、智能表单识别与自动填充
自动填充的难点不在于填充本身,而在于准确识别哪个输入框是用户名、哪个是密码。不同网站的表单结构千差万别,2Password 设计了一套评分系统来解决这个问题。
DOM(Document Object Model):浏览器将 HTML 文档解析成的树形结构,每个标签对应一个"节点"。自动填充本质上就是:找到代表用户名和密码的 DOM 节点,修改它们的值。不同网站用不同标签名和属性,所以需要"识别"。
5.1 字段评分系统
每个输入框会根据多个特征获得一个分数,最终选择得分最高的作为目标:
5.2 自动填充时序
从用户按下快捷键到密码填充完成,整个时序如下:
填充时有一个关键细节:不是简单地设置 input.value,而是依次触发 focus → value → input event → change event → blur。这是因为很多现代前端框架(React、Vue)监听的是事件而非属性变化。如果不触发这些事件,框架的状态不会更新,提交时可能会发送空值。
具体来说,fillInput() 函数只做 5 件事:
function fillInput(input, value) { input.focus(); // 1. 聚焦输入框(激活框架状态) input.value = value; // 2. 设置值 input.dispatchEvent(new Event('input', {bubbles: true})); // 3. 模拟输入事件(React onChange) input.dispatchEvent(new Event('change')); // 4. 模拟变更事件(Vue v-model) input.blur(); // 5. 失焦(触发 blur 验证) }
其中 bubbles: true 是关键——React 依赖事件冒泡来捕获 input 事件。如果不设置 bubbles,React 的合成事件系统不会接收到这个事件。
5.3 三轮降级检测算法
不同网站的登录表单千差万别:有的用标准 autocomplete 属性,有的用自定义组件,有的连 <form> 标签都没有。2Password 采用三轮逐步降级的策略,从最可靠的特征开始,到最后的兜底方案:
这种设计确保了"能用高精度方法就不降级"的原则。第一轮通过 autocomplete 属性能精准识别绝大多数标准表单;只有在标准方法失败时,才会使用更激进的兜底策略。
5.4 MFA 自动填充:MutationObserver 监听
很多网站在用户输入密码后会跳转到 MFA 验证页面。2Password 使用 MutationObserver(浏览器提供的 DOM 变化监听 API,可以检测页面中元素的添加、删除或属性变化)来监听这种页面变化,自动填充 TOTP 验证码:
5.5 网站匹配算法
自动填充面板打开时,密码列表需要按"当前网站优先"排序。匹配算法支持三级域名匹配:
5.6 登录按钮智能检测
自动填充的最后一步是点击登录按钮。2Password 用两层策略查找按钮:
第一层:语义匹配。遍历页面所有 button、input[type="submit"]、a[role="button"],检查文本内容是否包含 登录/login/sign in/提交/submit 等关键词。支持中英文。
第二层:表单兜底。如果语义匹配没找到,查找当前表单内的 button[type="submit"] 或 input[type="submit"]。
MFA 页面的按钮检测更严格——先排除包含"返回/back"关键词的按钮(避免误点),再查找包含"验证/verify/confirm"的按钮。
六、TOTP MFA 实现
2Password 内置了 TOTP(基于时间的一次性密码,Time-based One-Time Password)生成器,完全兼容 Google Authenticator。实现原理:
七、密码生成器与强度检测
2Password 内置了密码生成器,支持自定义长度和字符类型,并提供实时强度评估。密码生成使用 crypto.getRandomValues()(CSPRNG,密码学安全伪随机数生成器,从操作系统硬件熵源收集随机性,生成的随机数无法被预测),而非 Math.random(),确保密码的随机性达到密码学安全级别。
7.1 密码生成算法
7.2 密码强度评估
强度评估采用多维度评分(满分 8 分),从"弱"到"非常强"共四个等级:
| 评分维度 | 条件 | 得分 |
|---|---|---|
| 长度 | ≥8 / ≥12 / ≥16 / ≥20 | 各 +1(最高 +4) |
| 字符多样性 | 大写 / 小写 / 数字 / 符号 | 各 +1(最高 +4) |
评分与颜色映射:0-1 分 → 弱(红) | 2-3 分 → 中等(橙) | 4-5 分 → 强(绿) | 6-8 分 → 非常强(蓝)
八、会话保持机制
每次打开扩展都要 Touch ID 认证会非常烦。2Password 通过 chrome.storage.session(Chrome 扩展专用的内存级键值存储,浏览器关闭后数据自动清除)实现会话保持:解锁一次后,浏览器关闭前都不需要再次认证。
这里有一个容易踩的坑:Popup 解锁后,如果 Content Script 通过 Background 请求密码列表,Background 必须知道已解锁。所以 Popup 解锁后必须通过 syncUnlockState 消息同步密钥给 Background,而不是让 Background 自己再触发一次 Touch ID(否则用户会看到两次认证弹窗)。
九、数据完整性与历史追踪
每次密码的增删改都会自动记录历史,支持查看旧密码值。这对回溯"上个月我改了什么密码"非常有用。
十、技术要点总结
| 技术点 | 实现方案 | 设计考量 |
|---|---|---|
| 加密算法 | AES-256-GCM (Web Crypto API) | 浏览器原生,零依赖,GCM 模式自带完整性校验 |
| 密钥派生 | PBKDF2-SHA256, 100K 迭代 | 防暴力破解,deviceKey + salt + 固定字符串三重保护 |
| Touch ID | Native Messaging + Swift + Keychain | Chrome 沙箱外运行,系统级安全,单次请求响应 |
| 数据存储 | IndexedDB (密码) + chrome.storage (密钥) | IndexedDB 大容量,session storage 自动过期 |
| 表单识别 | 多特征评分系统 (autocomplete/attrs/label) | 覆盖主流框架,从最可靠到兜底逐级降级 |
| MFA | TOTP (HMAC-SHA1, 30s 周期) | 兼容 Google Authenticator,secret 加密存储 |
| 会话保持 | chrome.storage.session + 状态同步 | 避免双次 Touch ID,浏览器关闭自动失效 |
| 密码生成 | crypto.getRandomValues (CSPRNG) | 密码学安全随机数,多维度强度评估 |
2Password 虽然是一个个人项目,但在安全设计上借鉴了业界最佳实践。从加密方案到密钥管理,从表单识别到会话保持,每个细节都经过仔细考量。整个项目零外部依赖,纯原生 JavaScript 实现,代码量不到 2000 行,但功能完整度不输主流密码管理器的核心功能。
核心价值:完全本地运行 + Touch ID 系统级保护 + 浏览器内自动填充 + MFA 一站式管理。代码开源,数据自主,安全透明。
相关文章
- 从 1Password 到自建密码管理器:2Password 电脑端实现 — 2Password 项目的起源,介绍 macOS 桌面端的设计思路与实现
- 密码管理器 2Password 安卓实现 — 基于 Kotlin + Jetpack Compose 的 Android 端实现,加密方案与本文一脉相承
📜 版权声明
本文作者:王梓 | 原文链接:https://www.bthlt.com/note/369920595-Teg2Password Chrome 浏览器插件开发实战
出处:葫芦的运维日志 | 转载请注明出处并保留原文链接


📜 留言板
留言提交后需管理员审核通过才会显示