Loading...
Security & Local-First schedule5 min read • Published on 2026-11-09

MD-Vault 架构思考:为纯文本极客打造的零依赖本地优先 2FA 密码看板

XI
Xiang C
•
#Security #WebCrypto #TOTP #Local-First #Open Source

MD-Vault 架构思考:为纯文本极客打造的零依赖本地优先 2FA 密码看板

很长一段时间里,我都习惯直接在文本文件里记录日常账号和密码——它最轻便、最透明,也永远不会被任何软件格式所绑定。

但随着积累的账号越来越多,查找账号密码成了一件挺麻烦的事;更让人头疼的是,在日常复制粘贴时,有时还会手误改动甚至删除了密码,导致下次登录时莫名碰壁。

为了解决这个真实又琐碎的日常痛点,我让 AI 帮我做了一个本地的密码管理小应用——这便是 MD-Vault 的真实起点。在后续的打磨中,我们进一步让纯文本兼具了直观看板交互、一键复制防误触,以及基于浏览器原生 WebCrypto 实现毫秒级的离线 2FA/TOTP 动态口令计算。


1. 核心设计:用单文件 HTML 查看本地 Markdown

很多密码工具的问题在于,一旦用了它,数据就被关进了专有的私有格式里。MD-Vault 则遵循一个原则:代码与数据完全分离。

MD-Vault Dashboard
MD-Vault Dashboard

  • 密码始终保存在你的 Markdown 文档中: 真实的账号、密码与备用码依然记录在你本地磁盘的 .md 文件里。你随时可以用任何文本编辑器(VS Code、Obsidian、Vim 甚至系统自带记事本)直接查阅和编辑,没有任何私有格式绑定。
  • 通过 index.html 本地加载与展示: index.html 本身只是一个不到 30KB 的纯静态单文件应用,里面不包含任何用户的密码数据。日常使用非常简单:
    1. 双击用任意现代浏览器打开 index.html;
    2. 点击右上角选择自己的密码文件,或者直接把 .md 文件拖拽扔进浏览器窗口;
    3. 浏览器在本地内存中瞬时完成解析,生成清晰规整的响应式卡片。
  • 纯本地运行,零网络依赖: 所有解析、搜索过滤以及一键复制防手误功能,全部在浏览器的本地内存中完成。全程无需启动后端服务,也不发任何外部网络请求,即使电脑断网也能 100% 正常使用。

2. 动态验证码:在浏览器里用 WebCrypto 计算 TOTP

在用文本记录密码时,另一个不便之处在于两步验证(2FA/TOTP):以往每次遇到需要验证码的网站,都得停下手头的工作,拿出手机打开验证器应用查看 6 位动态码。

既然浏览器原生提供了强大的 WebCrypto 密码学接口,完全可以让页面在本地根据密钥和时间戳直接算出动态验证码。

在 Markdown 模板中,只需按照约定写下密钥:

markdown
- **GitHub**:
  - username: `[email protected]`
  - password: `my-super-secret-pass`
  - otpauth: `otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub`

2.1 时间步进与 HMAC-SHA1 计算

标准 TOTP(RFC 6238)的核心是拿当前 Unix 时间戳除以 30 秒,得到当前的时间步(Time Step):

T=⌊Epoch Time30⌋T = \left\lfloor \frac{\text{Epoch Time}}{30} \right\rfloor

随后将时间步长转换为 8 字节大端序计数器,与解码后的 Base32 密钥进行 HMAC-SHA1 运算:

HS=HMAC-SHA1(Secret,T)\text{HS} = \text{HMAC-SHA1}(\text{Secret}, T)

2.2 动态截断与 6 位数字代码提取

从 20 字节的哈希结果中取最后一个字节的低 4 位作为动态偏移量(Offset):

Offset=HS[19] & 0x0F\text{Offset} = \text{HS}[19] \ & \ 0x0F

提取从该偏移量起始的 4 个字节,抹去最高符号位并取模得到标准 6 位数字:

Code=(HS[Offset] & 0x7F)≪24 ∣ (HS[Offset+1] & 0xFF)≪16 ∣ (HS[Offset+2] & 0xFF)≪8 ∣ (HS[Offset+3] & 0xFF)\text{Code} = (\text{HS}[\text{Offset}] \ & \ 0x7F) \ll 24 \ | \ (\text{HS}[\text{Offset}+1] \ & \ 0xFF) \ll 16 \ | \ (\text{HS}[\text{Offset}+2] \ & \ 0xFF) \ll 8 \ | \ (\text{HS}[\text{Offset}+3] \ & \ 0xFF)

TOTP=Code(mod106)\text{TOTP} = \text{Code} \pmod{10^6}

浏览器端的具体实现非常精炼:

typescript
// 基于原生 WebCrypto 的 TOTP 计算函数
export async function generateTotp(secretBase32: string): Promise<{ code: string; remainingSeconds: number }> {
  const epoch = Math.floor(Date.now() / 1000);
  const timeStep = Math.floor(epoch / 30);
  const remainingSeconds = 30 - (epoch % 30);

  const keyBuffer = base32ToBuffer(secretBase32);
  const cryptoKey = await window.crypto.subtle.importKey(
    'raw',
    keyBuffer,
    { name: 'HMAC', hash: { name: 'SHA-1' } },
    false,
    ['sign']
  );

  const timeBuffer = new ArrayBuffer(8);
  const dataView = new DataView(timeBuffer);
  dataView.setUint32(4, timeStep, false);

  const hmacSignature = await window.crypto.subtle.sign('HMAC', cryptoKey, timeBuffer);
  const hash = new Uint8Array(hmacSignature);

  const offset = hash[hash.length - 1] & 0x0f;
  const binary =
    ((hash[offset] & 0x7f) << 24) |
    ((hash[offset + 1] & 0xff) << 16) |
    ((hash[offset + 2] & 0xff) << 8) |
    (hash[offset + 3] & 0xff);

  const otp = binary % 1000000;
  return {
    code: otp.toString().padStart(6, '0'),
    remainingSeconds
  };
}

页面以 1 秒为周期刷新倒计时,剩余时间不足 5 秒时平缓变色提醒,点击即可直接复制当前的 6 位动态验证码。


3. 本地安全与日常使用建议

为了让日常使用既顺手又安心,MD-Vault 在设计上遵循了几个简单的原则:

  1. 零外部网络请求:整个页面的样式、图标和逻辑全部内嵌在单个 HTML 文件中,不引入任何外部 CDN 资源、统计脚本或远程字体,断网也能正常运转;
  2. 配合系统级加密更放心:因为凭据本身是普通的 Markdown 文本,如果担心电脑上的明文存储风险,可以随手将密码文件放在系统的加密磁盘映像、BitLocker / FileVault 目录中,或者使用 git-crypt 进行透明加密;
  3. 纯粹的本地开源工具:MD-Vault 是一个完全免费、轻量的本地工具,数据始终在您自己手里,永远不会有账号注册或云端同步的打扰。

4. 留给未来的想法(AI 的建议清单)

在完成当前这个轻量版本后,AI 在代码之余,还帮我构思了一份挺有意思的“特性拓展建议”。虽然为了保持目前的极简小巧暂时没有做,但我把它们记录在了项目的待办清单中,留待日后有实际需要时再慢慢拓展:

  • 界面防窥遮挡:在办公室或投屏共享屏幕时,给密码加一层模糊滤镜,鼠标悬停时才显影,或者直接一键盲拷,防止被旁人无意看到;
  • 剪贴板定时清空:复制密码后开启 30 秒倒计时,超时自动清空剪贴板,避免密码长时间留在输入法或剪贴板历史记录里;
  • 页面内直接编辑回写:利用现代浏览器的 File System Access API,允许直接在网页上新增或修改卡片,并自动保存回本地的 .md 文件,省去切回文本编辑器的步骤;
  • 单文件密码自解密:探索用 WebCrypto 的 AES-GCM 算法,在打开页面时输入主密码,只在内存中解密渲染,本地磁盘上只存加密后的密文文件;
  • 终端或快捷启动器扩展:写一个简单的 Raycast 插件或命令行脚本,共用同一份 Markdown 文件,在终端敲一行命令直接把 2FA 动态码拷进剪贴板。

5. 在线体验与 GitHub 仓库

shieldOpen Source Lab

纯文本极客的零依赖本地优先 2FA 密码看板

厌倦了中心化密码库被拖库撞库?体验高密度纯文本 Markdown 凭据管理,原生浏览器 WebCrypto 实时计算 2FA。