Skip to content

zduu/web-2fa

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

47 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Web 2FA Authenticator

一个纯前端的 2FA 验证器(TOTP / HOTP),支持离线运行、PWA 安装、端到端加密的多设备同步。

License Cloudflare Pages


✨ 特点

  • 🚀 即点即用:普通用户打开页面即可本地添加 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 + 离线:支持安装到桌面/主屏,断网也能生成验证码

🚀 三种使用场景

1. 普通访客「即点即用」

打开页面 → 「+」添加账户 → 粘贴密钥或扫描二维码 → 立刻显示验证码。

完全本地(localStorage),无需登录、无需创建任何“项目”,关闭浏览器后数据仍在。点击卡片即可复制验证码,长按或右键可进行编辑、删除、复制 otpauth 链接等操作。 如果你已经启用了主密码,本应用还支持在兼容浏览器里额外绑定一把 Passkey,用生物识别或设备 PIN 快速解锁本地数据。 如果要从其他验证器迁入,可在「设置 → 数据」里直接导入 Aegis 明文 JSON、Bitwarden CSV/JSON、andOTP 加密备份,或扫描 otpauth:// / otpauth-migration:// 二维码。 如果要迁移到其他支持 Google 批量导入的验证器,可在「设置 → 数据」里使用“批量二维码”导出当前项目。

2. 管理员管理多设备同步

管理员登录后,设置入口(右上齿轮)→「项目」→「新建项目」,填项目名、Sync ID、Sync Secret 三项即可创建端到端加密的同步空间:

  • 推送 / 拉取 / 自动同步全部一键完成
  • 多设备只需配置相同的 Sync ID + Secret
  • 支持创建多个项目(个人 / 工作 / 测试),并提供「📊 全部汇总视图」聚合查看

3. 管理员高阶运维

进入「关于」页,连续点击版本号 v0.2.0 7 次显示高级入口,再输入 Admin Key 登录后,才会出现“分享”“管理员”标签页:

  • 查看全部分享记录,并统一复制 / 撤销分享
  • 查看分享访问次数、最后访问时间与 User-Agent 摘要
  • 生成分享后立即展示离线二维码、链接与有效期提示;可选再加一层接收方口令
  • 查看最近 30 天 API 写操作审计日志(含拒绝请求)
  • 列出云端所有同步项目(KV sync:*
  • 批量解密预览(可输入多个 Sync Secret 尝试)
  • 多格式导出(otpauth / JSON / CSV,可按项目分文件 / 仅导出选中)
  • RSA 公钥密钥托管 + 私钥找回
  • 批量密钥迁移(旧 Secret → 新 Secret)

📱 Android APK(本地可用,可选云同步)

如果你的目标是“手机上像本地应用一样运行,断网也能生成验证码,联网时再同步”,这个仓库支持 Android APK 模式

  • APK 内运行同一套前端核心逻辑,数据仍优先保存在手机本地 localStorage
  • web-2fa-local-debug.apk 是纯本地版:完全离线可用,不启用云端 API
  • web-2fa-sync-debug.apk 是同步版:配置 Cloudflare Pages 站点地址后,会把 /api/* 请求发到该站点,支持端到端加密的云同步、分享和管理员功能
  • 断网时本地验证码不受影响;恢复网络后可手动推送 / 拉取,或使用自动同步
  • 云端仍只保存密文,Sync Secret 不会上传

APK 产出方式

APK 不依赖本地机器构建,统一通过 GitHub Actions 产出:

  1. 推送到 main / master,创建 v* tag,或在 Actions 页面手动触发 Android APK
  2. Workflow 会临时安装 Capacitor / TypeScript / Android SDK
  3. 构建完成后,在该次 Actions 的 Artifacts 中下载 web-2fa-android-release-apks
  4. Artifact 和 Release 都会包含两个 APK:
    • web-2fa-sync-release.apk:带云同步能力
    • web-2fa-local-release.apk:纯本地离线版
  5. 创建 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 更新继承配置

要让每次发布的新 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 云同步配置

云端链接在同步版 APK 内自行设置,不在 GitHub Actions / workflow 里写死:

  1. 安装并打开 web-2fa-sync-release.apk
  2. 进入 设置 → 数据 → 云端地址
  3. 填写 Cloudflare Pages 地址,例如 https://your-app.pages.dev
  4. 可选填写公开站点地址;留空时默认同云端 API 地址

保存后,推送、拉取、分享和管理员接口都会使用这个地址。web-2fa-local-release.apk 始终是纯本地离线版,不显示云端地址入口,也不会连接云端。

如果 Cloudflare Pages 启用了访问口令,移动 APK 仍可通过 Admin Key 调用同步接口;Functions 已允许 https://localhost / capacitor://localhost 的跨源 API 请求。需要限制来源时,可在 Pages 环境变量中设置 CORS_ORIGIN(多个来源用逗号分隔)。


⚙️ 部署到 Cloudflare Pages

一、创建 KV 命名空间

在 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 项目
  • SettingsBuildBuild watch paths

这样做的效果是:

  • android/ 下各级目录和文件的改动不会触发新的 Pages 构建
  • Web 源码、functions/、根目录页面文件的改动仍会正常触发构建

注意:

  • 这只能减少无效构建触发,不能改变 Pages 读取仓库根目录这一事实。
  • 也就是说,它解决的是“不要因为 Android 改动而重建”,不是“从仓库结构上彻底隔离 android 目录”。

三、绑定 KV

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,管理员页将无法启用访问口令

GitHub 私有仓库外部备份

如需在 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 KeySync Secret 都不会写入备份文件。若未设置 GITHUB_BACKUP_TOKENGITHUB_BACKUP_REPO,该逻辑完全不启用。

Token 建议使用 GitHub fine-grained personal access token,并按最小权限创建:

  • Repository access: 只选择目标备份私有仓库
  • Repository permissions: Contents / Code 设为 Read and write
  • Metadata: Read-only 是 GitHub 自动附带权限

不需要授权 ActionsWorkflowsAdministrationSecretsPagesIssuesPull requestsDeploymentsCodespaces 或安全告警等权限。只有备份路径写到 .github/workflows/ 时才需要 Workflows: Read and write,默认路径不需要。若设置 GITHUB_BACKUP_BRANCH,该分支必须已存在。

默认路径里的 {id} 会替换为 Sync ID,ID 中的 / 会替换为 -;如果你固定设置一个不含 {id} 的路径,多个同步项目会写入同一个文件。GitHub 写入失败时,KV 同步仍会成功,接口会返回 X-Note: github-backup-failed 供前端提示外部备份异常。

使用 GitHub 备份文件

备份文件是同步密文副本,不包含 Sync SecretAdmin Key 或明文验证码。只要还记得原来的 Sync IDSync 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 模式下读写都需要)
                                          │
                                    ┌─────┴──────┐
                                    │  访客或    │
                                    │  其他用户  │
                                    └────────────┘

双层防护

  1. 写入永远要鉴权(ADMIN_KEY)—— 即使知道你的 Sync ID,没 Admin Key 也无法覆盖
  2. strict 读取也鉴权 —— 普通访客根本拿不到任何 KV 密文
  3. Sync Secret 端到端加密 —— 即便密文泄露,没 Sync Secret 也解不开

分享说明:

  • 完整模式由接收方浏览器解密,属于端到端加密;接收方可以查看 Secret。
  • 默认安全模式由服务端短暂解密并计算当前验证码,接收方拿不到 Secret,但服务端并非零知识。
  • “最多访问次数”基于 Cloudflare KV 读改写,只是尽力限制;并发请求可能超过设定次数,不能视为强一次性保证。
  • 链接恢复材料默认不保存。显式开启后,无口令分享的服务端管理员可恢复解密密钥。

🔌 API

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.2 重构说明(与 v0.1 对比)

维度 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 账户,离线本地存储。

我想多设备同步,最少要做什么?

  1. 部署 + 配置 KV
  2. 设置环境变量 ADMIN_KEY
  3. 在前端“关于”页连续点击版本号 7 次,显示高级入口后输入 Admin Key
  4. 进入"项目"→ 新建项目,填项目名 / Sync ID / Sync Secret
  5. 在另一台设备登录管理员、填同样的 Sync ID + Secret

我不想让别人看到我的云端数据怎么办?

保持默认 SYNC_MODE = strict。strict 模式下,没有 Admin Key 的人发 GET /api/sync/:id 会被拒绝(401),根本拿不到密文。

重复扫描同一个二维码会怎样?

同一项目内会自动去重:

  • 如果这个账户已经存在,会跳过,不会新增第二条
  • 如果这个账户之前被删除过,但只是逻辑删除,会自动恢复
  • Google Authenticator 导出的批量迁移码也适用这套规则

📄 License

MIT

About

No description, website, or topics provided.

Resources

Stars

6 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors