- 🚀 即点即用:普通用户打开页面即可本地添加 2FA,默认不上传
- 🔐 端到端加密同步:每个项目用
Sync Secret派生 AES-GCM 密钥,云端只存密文 - 🪪 Passkey 快捷解锁:在支持 WebAuthn
prf的浏览器中,可把 Passkey 作为本地主密码之外的额外解锁方式 ↕️ 项目内拖拽排序:支持拖拽调整卡片顺序,并在本地持久化保存- 🎨 三档主题:支持暗色 / 亮色 / 跟随系统,分享页也同步主题色
- 🛡 管理员能力默认隐藏:管理员入口默认不显示,需在“关于”页连续点击版本号 7 次后再输入
ADMIN_KEY - 🔒 分享仅管理员可用:普通用户不显示分享入口,管理员可统一查看全部分享记录与访问日志
- 📱 分享支持离线 QR:生成链接后会弹出二维码、链接和有效期提示,手机扫码更直接
- 🔐 分享口令可选:可额外设置接收方访问口令;不设置时仍可直接通过链接访问
- 🧰 可选管理员分享互通:生成分享时可显式保存链接恢复材料;默认关闭,避免服务端同时持有密文与解密材料
- 🔳 支持批量迁移二维码导出:当前项目可直接导出为
otpauth-migration://多张二维码,便于迁移到 Google Authenticator 等应用 - 🧾 管理员审计日志:所有 API 写操作会记录最近 30 天的 method/path/status/IP 摘要/UA 摘要
- 📷 导入更完整:支持手动输入、
otpauth://、otpauth-migration://、Aegis 明文 JSON、Bitwarden CSV/JSON、andOTP 加密备份 - ♻️ 重复导入自动去重:同一项目重复扫同一个二维码时,会自动跳过;若之前只是“逻辑删除”,会直接恢复
- ♿ 键盘与对话框可访问性增强:列表语义、Tab 语义、模态焦点陷阱已补齐
- 📱 PWA + 离线:支持安装到桌面/主屏,断网也能生成验证码
打开页面 → 「+」添加账户 → 粘贴密钥或扫描二维码 → 立刻显示验证码。
完全本地(localStorage),无需登录、无需创建任何“项目”,关闭浏览器后数据仍在。点击卡片即可复制验证码,长按或右键可进行编辑、删除、复制 otpauth 链接等操作。
如果你已经启用了主密码,本应用还支持在兼容浏览器里额外绑定一把 Passkey,用生物识别或设备 PIN 快速解锁本地数据。
如果要从其他验证器迁入,可在「设置 → 数据」里直接导入 Aegis 明文 JSON、Bitwarden CSV/JSON、andOTP 加密备份,或扫描 otpauth:// / otpauth-migration:// 二维码。
如果要迁移到其他支持 Google 批量导入的验证器,可在「设置 → 数据」里使用“批量二维码”导出当前项目。
管理员登录后,设置入口(右上齿轮)→「项目」→「新建项目」,填项目名、Sync ID、Sync Secret 三项即可创建端到端加密的同步空间:
- 推送 / 拉取 / 自动同步全部一键完成
- 多设备只需配置相同的 Sync ID + Secret
- 支持创建多个项目(个人 / 工作 / 测试),并提供「📊 全部汇总视图」聚合查看
进入「关于」页,连续点击版本号 v0.2.0 7 次显示高级入口,再输入 Admin Key 登录后,才会出现“分享”“管理员”标签页:
- 查看全部分享记录,并统一复制 / 撤销分享
- 查看分享访问次数、最后访问时间与 User-Agent 摘要
- 生成分享后立即展示离线二维码、链接与有效期提示;可选再加一层接收方口令
- 查看最近 30 天 API 写操作审计日志(含拒绝请求)
- 列出云端所有同步项目(KV
sync:*) - 批量解密预览(可输入多个
Sync Secret尝试) - 多格式导出(
otpauth/ JSON / CSV,可按项目分文件 / 仅导出选中) - RSA 公钥密钥托管 + 私钥找回
- 批量密钥迁移(旧 Secret → 新 Secret)
如果你的目标是“手机上像本地应用一样运行,断网也能生成验证码,联网时再同步”,这个仓库支持 Android APK 模式:
- APK 内运行同一套前端核心逻辑,数据仍优先保存在手机本地
localStorage web-2fa-local-debug.apk是纯本地版:完全离线可用,不启用云端 APIweb-2fa-sync-debug.apk是同步版:配置 Cloudflare Pages 站点地址后,会把/api/*请求发到该站点,支持端到端加密的云同步、分享和管理员功能- 断网时本地验证码不受影响;恢复网络后可手动推送 / 拉取,或使用自动同步
- 云端仍只保存密文,
Sync Secret不会上传
APK 不依赖本地机器构建,统一通过 GitHub Actions 产出:
- 推送到
main/master,创建v*tag,或在 Actions 页面手动触发Android APK - Workflow 会临时安装 Capacitor / TypeScript / Android SDK
- 构建完成后,在该次 Actions 的
Artifacts中下载web-2fa-android-release-apks - Artifact 和 Release 都会包含两个 APK:
web-2fa-sync-release.apk:带云同步能力web-2fa-local-release.apk:纯本地离线版
- 创建
v*tag 时会自动发布 GitHub Release;手动触发时勾选create_release也会发布 Release
说明:
- Web 云端版继续使用根目录源码 +
wrangler pages dev/deploy - APK 版仍然通过
scripts/build-local-web.mjs生成dist-local/,但默认由 CI 执行 dist-local/只是临时构建产物,不入库- 本地
package.json已移除 APK 构建用的 Capacitor 依赖与脚本,避免要求开发机安装整套 Android 打包链 - APK 使用固定 release 签名,并用 GitHub Actions run number 递增
versionCode,所以安装新版会覆盖旧版并保留本地数据、主密码状态和同步版 APK 内填写的云端地址
要让每次发布的新 APK 能直接覆盖安装并继承旧数据,必须在 GitHub Secrets 固定同一把 Android 签名密钥:
| Secret | 作用 |
|---|---|
ANDROID_KEYSTORE_BASE64 |
release keystore 文件的 base64 内容 |
ANDROID_KEYSTORE_PASSWORD |
keystore 密码 |
ANDROID_KEY_ALIAS |
key alias |
ANDROID_KEY_PASSWORD |
key 密码 |
首次生成 keystore 示例:
keytool -genkeypair -v -keystore web-2fa-release.keystore -alias web2fa -keyalg RSA -keysize 2048 -validity 10000
base64 -i web-2fa-release.keystore | pbcopy把 base64 内容保存到 ANDROID_KEYSTORE_BASE64,其余密码和 alias 保存到对应 Secrets。以后不要更换这把 keystore;更换后 Android 会认为是另一个签名,无法覆盖安装旧 APK。
注意:如果手机上已经安装过早期的 debug APK,第一次切换到 release 签名 APK 时无法直接覆盖安装,因为签名不同。需要先在旧 APK 内导出数据,再卸载旧版、安装 release APK 并导入数据。之后只要继续使用同一把 release keystore,新 APK 就可以直接覆盖安装并继承数据。
云端链接在同步版 APK 内自行设置,不在 GitHub Actions / workflow 里写死:
- 安装并打开
web-2fa-sync-release.apk - 进入
设置 → 数据 → 云端地址 - 填写 Cloudflare Pages 地址,例如
https://your-app.pages.dev - 可选填写公开站点地址;留空时默认同云端 API 地址
保存后,推送、拉取、分享和管理员接口都会使用这个地址。web-2fa-local-release.apk 始终是纯本地离线版,不显示云端地址入口,也不会连接云端。
如果 Cloudflare Pages 启用了访问口令,移动 APK 仍可通过 Admin Key 调用同步接口;Functions 已允许 https://localhost / capacitor://localhost 的跨源 API 请求。需要限制来源时,可在 Pages 环境变量中设置 CORS_ORIGIN(多个来源用逗号分隔)。
在 Cloudflare Dashboard 创建一个 KV 命名空间,记下 ID。
wrangler pages deploy或在 Pages 控制台连接 Git 仓库自动部署。
如果你不想改现有的 Build command / Build output directory,但又想避免 android/ 的改动频繁触发 Pages 无效构建,可以在 Cloudflare Pages 控制台配置 Build watch paths:
- Include paths:
* - Exclude paths:
android/**
配置位置:
- Workers & Pages
- 选择你的 Pages 项目
Settings→Build→Build watch paths
这样做的效果是:
android/下各级目录和文件的改动不会触发新的 Pages 构建- Web 源码、
functions/、根目录页面文件的改动仍会正常触发构建
注意:
- 这只能减少无效构建触发,不能改变 Pages 读取仓库根目录这一事实。
- 也就是说,它解决的是“不要因为 Android 改动而重建”,不是“从仓库结构上彻底隔离 android 目录”。
Pages 项目 → Settings → Functions → KV Bindings:变量名 AUTH_KV → 选择刚才创建的 KV。
| 变量名 | 必选 | 作用 |
|---|---|---|
ADMIN_KEY |
推荐 | 管理员主密钥。一个值搞定:同步写入鉴权、云端浏览鉴权、分享写入鉴权 |
SYNC_MODE |
可选 | strict(默认)/ open。strict 下读取也需要 ADMIN_KEY,普通访客无法下载任何同步密文 |
ACCESS_GATE |
可选 | 全站访问口令内容(独立于 ADMIN_KEY)。管理员页只控制是否启用,口令值始终来自这里 |
GATE_COOKIE_SECRET |
推荐 | 访问门 cookie 标签的服务端随机盐;未设置时回退使用 ADMIN_KEY |
GATE_RATE_LIMIT_SALT |
可选 | 访问门限流键的哈希盐;未设置时回退使用 ADMIN_KEY/ACCESS_GATE |
AUDIT_IP_SALT |
可选 | 审计 IP 摘要的哈希盐;未设置时回退使用 ADMIN_KEY,均未配置时不记录 IP 摘要 |
SHARE_TTL |
可选 | 分享默认有效期(秒,<=0 表示永久) |
SYNC_TOKEN |
兼容 | 旧版别名,等价于 ADMIN_KEY |
KV_ADMIN_KEY |
兼容 | 旧版别名,等价于 ADMIN_KEY(云端浏览专用) |
GITHUB_BACKUP_TOKEN |
可选 | GitHub 私有仓库备份 token;不设置则不启用外部备份 |
GITHUB_BACKUP_REPO |
可选 | GitHub 备份仓库,格式 owner/repo;需与 token 同时设置 |
GITHUB_BACKUP_BRANCH |
可选 | 写入分支;留空使用仓库默认分支 |
GITHUB_BACKUP_PATH |
可选 | 备份文件路径模板,默认 .web-2fa-backup/sync/{id}.web2fa-backup.json |
建议:只配
ADMIN_KEY,再决定SYNC_MODE。其他都默认。
补充:
- 进入网站后,管理员现在可直接在“管理员”页里启用/关闭访问口令
- 站内只保存“是否启用”的开关,口令内容始终读取 Cloudflare Pages 的
ACCESS_GATE - 若未设置
ACCESS_GATE,管理员页将无法启用访问口令
如需在 Cloudflare KV 之外再保留一份平台外备份,可在 Cloudflare Pages 环境变量中设置:
GITHUB_BACKUP_TOKEN=github fine-grained token
GITHUB_BACKUP_REPO=owner/private-repo
GITHUB_BACKUP_BRANCH=backup # 可选
GITHUB_BACKUP_PATH=.web-2fa-backup/sync/{id}.web2fa-backup.json
启用后,每次 PUT /api/sync/:id 写入成功,Functions 会把同一份同步密文写到 GitHub 仓库。备份文件内容仍然是客户端用 Sync Secret 加密后的密文,GitHub token、Admin Key 和 Sync Secret 都不会写入备份文件。若未设置 GITHUB_BACKUP_TOKEN 和 GITHUB_BACKUP_REPO,该逻辑完全不启用。
Token 建议使用 GitHub fine-grained personal access token,并按最小权限创建:
- Repository access: 只选择目标备份私有仓库
- Repository permissions:
Contents/Code设为Read and write Metadata: Read-only是 GitHub 自动附带权限
不需要授权 Actions、Workflows、Administration、Secrets、Pages、Issues、Pull requests、Deployments、Codespaces 或安全告警等权限。只有备份路径写到 .github/workflows/ 时才需要 Workflows: Read and write,默认路径不需要。若设置 GITHUB_BACKUP_BRANCH,该分支必须已存在。
默认路径里的 {id} 会替换为 Sync ID,ID 中的 / 会替换为 -;如果你固定设置一个不含 {id} 的路径,多个同步项目会写入同一个文件。GitHub 写入失败时,KV 同步仍会成功,接口会返回 X-Note: github-backup-failed 供前端提示外部备份异常。
备份文件是同步密文副本,不包含 Sync Secret、Admin Key 或明文验证码。只要还记得原来的 Sync ID 和 Sync Secret,即使 Cloudflare Pages / KV 不在线,也可以在本地离线解密。
最简单方式:
macOS 双击 decrypt-github-backup.command。这个文件是自包含的,可以单独复制到其他目录使用。按提示输入备份文件路径、原来的 Sync Secret、输出格式和输出目录即可;备份 JSON 文件可以直接拖进终端窗口来填路径。Sync Secret 输入时不会显示,默认读取 .web-2fa-backup/sync/google.web2fa-backup.json,默认把明文 JSON 写到 decrypted-backups/。
支持的格式:
json:完整明文记录,默认格式otpauth:每行一个otpauth://URI,便于导入其他验证器csv:表格格式
如果备份文件里的 syncId 缺失,脚本会提示你手动输入。解密结果包含真实 2FA Secret,请只在可信设备上执行并妥善保管输出文件。
cp wrangler.toml.example wrangler.toml # 编辑 KV ID 和环境变量
npm run dev:https # 推荐 HTTPS(摄像头/剪贴板需要)说明:
- 本地不设置
ADMIN_KEY也能跑前端,但管理员相关接口会不可用 - 想测试扫码、复制、PWA 安装,优先使用
npm run dev:https
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 设备 A │ │ 云端 (KV) │ │ 设备 B │
│ │ ── Sync Secret ──→ │ │ ←── Sync Secret ── │ │
│ 原始数据 │ 加密推送 │ 密文数据 │ 拉取并解密 │ 本地数据 │
└──────────────┘ └──────────────┘ └──────────────┘
↑
│ ADMIN_KEY 鉴权(strict 模式下读写都需要)
│
┌─────┴──────┐
│ 访客或 │
│ 其他用户 │
└────────────┘
- 写入永远要鉴权(ADMIN_KEY)—— 即使知道你的 Sync ID,没 Admin Key 也无法覆盖
- strict 读取也鉴权 —— 普通访客根本拿不到任何 KV 密文
- Sync Secret 端到端加密 —— 即便密文泄露,没 Sync Secret 也解不开
分享说明:
- 完整模式由接收方浏览器解密,属于端到端加密;接收方可以查看 Secret。
- 默认安全模式由服务端短暂解密并计算当前验证码,接收方拿不到 Secret,但服务端并非零知识。
- “最多访问次数”基于 Cloudflare KV 读改写,只是尽力限制;并发请求可能超过设定次数,不能视为强一次性保证。
- 链接恢复材料默认不保存。显式开启后,无口令分享的服务端管理员可恢复解密密钥。
GET /api/sync/:id # 拉取(strict 模式需鉴权)
PUT /api/sync/:id # 推送(始终需鉴权)
DELETE /api/sync/:id # 删除项目密文
GET /api/share/:id # 获取分享密文(公开)
PUT /api/share/:id?ttl=3600 # 创建分享
DELETE /api/share/:id # 撤销分享
GET /api/share/list # 列出所有分享 SID(需鉴权)
GET /api/share/stat # 获取分享访问统计(需鉴权)
GET /api/sharekey/:id # 获取可选托管的分享解密材料(需鉴权)
PUT /api/sharekey/:id # 显式托管分享解密材料(需鉴权)
GET /api/vault/:id # 取出密钥托管密文(需鉴权)
PUT /api/vault/:id # 存放密钥托管密文(需鉴权)
POST /api/admin/list-all # 列出所有 sync:* 项目(需鉴权)
GET /api/admin/audit # 获取最近审计日志(需鉴权)
GET /api/admin/access-gate # 读取站内访问口令状态(需鉴权)
PUT /api/admin/access-gate # 保存站内访问口令配置(需鉴权)
GET /api/gate # 检查 ACCESS_GATE
POST /api/gate # 提交访问口令
DELETE /api/gate # 退出请求头:
X-Token: ADMIN_KEY(推荐)或 SYNC_TOKEN(兼容)X-KV-Admin-Key: ADMIN_KEY 或 KV_ADMIN_KEY(云端浏览也接受)
web-2fa/
├── index.html # 极简 shell
├── shared.html # 分享查看页
├── styles.css # 设计系统
├── app.js # 入口
├── assets/
│ └── icons/ # PWA / favicon / Apple Touch 图标
├── src/
│ ├── core/
│ │ ├── totp.js # TOTP/HOTP/otpauth/migration
│ │ ├── crypto.js # AES-GCM/PBKDF2/RSA-OAEP
│ │ ├── migration-formats.js # Aegis/Bitwarden/andOTP 迁移格式解析
│ │ ├── passkey.js # Passkey PRF / 本地快捷解锁
│ │ ├── storage.js # localStorage + 主密码加密
│ │ ├── qrgen.js # 分享/导出用离线二维码包装
│ │ ├── qrgen-vendor.js # vendored QR encoder
│ │ └── version.js # APP_VERSION 单一来源
│ ├── sync/
│ │ ├── sync.js # 推送/拉取/合并/自动同步(页面隐藏自动暂停)
│ │ ├── projects.js # 项目 CRUD/切换
│ │ ├── vault.js # RSA 托管/找回/迁移
│ │ └── cloud.js # 云端浏览/批量解密/导出
│ ├── share/
│ │ └── share.js # 二维码分享/撤销/列表
│ ├── ui/
│ │ ├── home.js # 主页 + tick + 卡片交互
│ │ ├── add.js # 添加面板(Tab 切换)
│ │ ├── scanner.js # QR 扫描
│ │ ├── drawer.js # 设置抽屉 + 所有面板
│ │ ├── theme.js # 主题偏好(dark/light/auto)
│ │ ├── modal.js # 模态/Prompt/ActionSheet
│ │ ├── toast.js # Toast + 复制/下载
│ │ ├── ring.js # SVG 圆形进度环
│ │ ├── avatar.js # Issuer 字母头像
│ │ ├── prefs.js # 显示密度偏好
│ │ └── import-export.js # 导入/导出(含加密包与 migration QR)
│ └── admin/
│ └── unlock.js # 管理员密码校验
├── functions/ # Cloudflare Pages Functions
│ ├── _middleware.js
│ ├── _lib/
│ │ ├── auth.js # 共享鉴权工具(恒时比较 + 多字段兼容)
│ │ └── audit.js # API 写请求审计日志
│ └── api/
│ ├── health.js
│ ├── sync-trash.js
│ ├── sync-backup/[id].js
│ ├── sync/[id].js
│ ├── share/[id].js
│ ├── share/list.js
│ ├── share/stat.js
│ ├── sharekey/[id].js
│ ├── vault/[id].js
│ ├── gate.js
│ └── admin/
│ ├── list-all.js
│ └── audit.js
├── tests/ # Vitest 核心算法/合并逻辑测试
├── .github/workflows/ci.yml # CI:测试 + Functions 构建
├── sw.js # Service Worker
└── manifest.webmanifest
| 维度 | v0.1 | v0.2 |
|---|---|---|
| 前端文件 | 单文件 app.js 2752 行 |
ES Module 化,src/ 下 17 个模块 |
| 概念数量 | Server Token / Sync Secret / KV Admin Key / Vault 公私钥 五种 | Admin Key + Sync Secret 两种(旧字段仍兼容) |
| 默认体验 | 一进来满屏按钮和概念 | 极简:FAB + 齿轮,按需展开 |
| 隐藏交互 | 三击标题设 Server Token | 「关于」页版本号连点 7 次后显示管理员入口 |
| 云端可见性 | GET 永远开放 | strict 模式(默认)GET 也鉴权 |
| 视觉 | 网格 + 线条进度条 | 玻璃拟态 + 圆形 SVG 进度环 + 渐变 |
| 鉴权代码 | 复制粘贴在多个 endpoint | 抽到 functions/_lib/auth.js,恒时比较降低时序攻击面 |
| 自动同步 | 60s 固定轮询 | 页面隐藏时自动暂停拉取 |
数据完全兼容:升级后旧 localStorage 数据自动可用,无需迁移。
详见 CONCEPTS.md。
能。打开页面即可添加 2FA 账户,离线本地存储。
- 部署 + 配置 KV
- 设置环境变量
ADMIN_KEY - 在前端“关于”页连续点击版本号 7 次,显示高级入口后输入 Admin Key
- 进入"项目"→ 新建项目,填项目名 / Sync ID / Sync Secret
- 在另一台设备登录管理员、填同样的 Sync ID + Secret
保持默认 SYNC_MODE = strict。strict 模式下,没有 Admin Key 的人发 GET /api/sync/:id 会被拒绝(401),根本拿不到密文。
同一项目内会自动去重:
- 如果这个账户已经存在,会跳过,不会新增第二条
- 如果这个账户之前被删除过,但只是逻辑删除,会自动恢复
- Google Authenticator 导出的批量迁移码也适用这套规则
MIT