KOR1K1/svitok

GitHub: KOR1K1/svitok

一款基于纸面 seed 与记忆短语的离线确定性密码管理器,通过实时推导取代密码存储,消除云端保险库泄露风险。

Stars: 2 | Forks: 0

Svitok # Svitok **一款纸笔优先、离线、确定性的密码管理器。** 你的密码不会被存储在任何地方。它们是通过纸上的一串随机 seed 和你脑中的一个短语重新计算出来的。 [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-4a3b28.svg)](LICENSE) ![Platforms](https://img.shields.io/badge/platforms-Android%20%7C%20Windows%20%7C%20macOS%20%7C%20Linux-1c1916) [![Release](https://img.shields.io/github/v/release/KOR1K1/svitok?color=d4643e)](https://github.com/KOR1K1/svitok/releases) ![Built with](https://img.shields.io/badge/built%20with-Rust%20%2B%20Tauri-b84e2c) ![Crypto core deps](https://img.shields.io/badge/crypto%20core%20deps-0-8fa968)
Svitok(俄语 *свиток*,意为“卷轴”)诞生于一个简单又恼人的事件:一个密码在数据泄露中被曝光,结果发现这个带有微小变化的密码已经被重复使用多年了。通常的解决方案是使用一个将所有内容加密存储在云端的保险库应用。而这恰恰相反:**什么都不存储。** 没有会泄露的保险库,因为没有任何密码存放在任何地方——每一个都是按需推导出来的。 ``` password = F(seed, phrase, site, login, counter, length, character policy) ``` **seed** 是你写在纸上的 128 位随机数。**短语** 则保留在你的脑海中。在任何设备上离线输入相同的参数,你就能永久找回完全相同的密码。不需要同步服务器,不需要账户,也不需要联网权限。 ## 目录 - [为什么](#why) - [工作原理](#how-it-works) - [截图](#screenshots) - [功能](#features) - [自动填充](#autofill) - [安装](#install) - [威胁模型](#threat-model) - [备份、同步与防止数据丢失](#backups-sync-and-not-losing-your-data) - [从源码构建](#build-from-source) - [项目结构](#project-layout) - [关注点 / 改进空间](#where-to-look--what-could-be-better) - [安全性](#security) - [致谢与灵感](#credits-and-inspiration) - [捐赠](#donate) - [License](#license) ## 为什么 密码管理器解决了密码重用的问题,这很好。但它们中的大多数将你的密码变成了一个极具吸引力的单一目标:一个加密的 blob,通常会同步到服务器。如果加密技术没问题,你就安全;如果加密有问题,或者终端被入侵,所有信息就会一次性泄露。 Svitok 消除了这个目标。应用内没有东西可偷,不需要同步,也没有可被攻破的服务器。代价是你需要随身携带一张纸(seed)并记住一个短语。这就是交易,而且是经过深思熟虑的。 它还致力于长久存活。其算法足够精简,完全可以仅根据纸面上的规范([`SPEC.md`](SPEC.md))进行转录和重新实现,不需要任何库和网络。即使这个应用的每一份副本都消失了,你依然可以重新计算出你的密码。这就是所谓的“末日抗性”,也是为什么其加密核心具有**零依赖**的原因。 ## 工作原理 输入两个秘密:**seed**(在纸上)和**主短语**(在你的脑海中)。 1. **Master key。** `mk = KDF(seed, phrase)`。KDF 是一种内存密集型、scrypt/ROMix 风格的函数,完全基于 BLAKE2s 构建,因此整个算法都可以记录在纸上。默认参数为 `M20 T21`(32 MiB 内存,2^21 次混合轮次),在 seed 旁边记为 `K M20 T21`。如果你的纸张被盗,内存密集型设计使得暴力破解你的短语成本极高。 2. **各站点密码。** 对于每个条目:`sk = BLAKE2s(key = subkey(mk, "password"), "PW:" | site | 0x1F | login | 0x1F | le32(counter))`,然后通过对 `sk` 使用 ChaCha20 密钥流,采用拒绝采样(无取模偏差)从允许的字符集中选取字符。随后确定性地填补所需字符类别。相同的输入 -> 相同的密码,在任何 CPU 上都是如此。(确切的字节布局见 [`SPEC.md`](SPEC.md)。) 3. **计数器。** 需要在泄露后轮换密码?增加计数器即可。相同的站点,全新的密码。 4. **短语校验。** 因为*任何*短语都会产生*某个* master key,所以不存在内置的“密码错误”提示。Svitok 会存储一个 8 位十六进制的**校验码**(`mk` 的一个子密钥),这样在解锁时就能捕捉到短语中的拼写错误。它不是秘密——没有 seed 你就无法计算出它。 5. **指纹。** 解锁时会显示两个由 `mk` 推导出的字符。将它们写在 seed 旁边;如果它们匹配,说明你的 seed 和短语是正确的。 在推导密码的基础之上,还有一个微型的**加密保险库**,用于存储那些你*无法*推导的内容——TOTP secret、恢复代码、他人的密码、笔记。这个 blob 使用 ChaCha20 + BLAKE2s MAC(先加密后验证,恒定时间比较)进行加密,密钥来源于 `mk`。它足够小,也可以写在纸上,格式与 seed 一样采用 Base32。 所有内容都使用 **Crockford Base32** 编码,带有逐行校验符和最终校验行,因此手动转录时的错误(如 `o`/`0`、`i`/`1`、漏行等)都能被捕捉到。 完整的、实现级别的规范(足以从零开始重新实现 Svitok)位于 [`SPEC.md`](SPEC.md) 中。QR 生成器、KDF、ChaCha20、BLAKE2s、SHA-1/HMAC(用于 TOTP 互操作)以及 Base32 都是手写的;其正确性通过 RFC 测试向量和 `core/tests/` 中的黄金向量来锁定。 ## 截图

Unlock Sites Password 2FA codes

Desktop - sidebar layout

(该应用默认阻止屏幕截图;这些截图是在关闭该功能后截取的。) ## 功能 - 确定性密码推导——不存储任何内容,按需重新计算 - 完全离线工作;发布版本**完全没有任何联网权限** - 一个 seed + 短语 = 每台设备上生成相同的密码 - 用于 TOTP (2FA) 代码、恢复代码、笔记、外部密码的加密微型保险库 - 每个站点可自定义长度和字符策略,带有用于轮换的版本计数器 - **自动填充** —— Android 上的系统自动填充服务,桌面端上的浏览器扩展;密码按请求推导,绝不存储(见[自动填充](#autofill)) - 通过 QR 码在设备间同步你的站点列表(仅靠摄像头,无需网络) - 文本备份(站点列表 + 加密保险库)——粘贴到任何地方;它不是机密 - 可供手动抄写的纸质导出,带有用于发现拼写错误的校验码 - Android:seed 封装在 Keystore 中,仅通过生物识别解锁;seed 和密码屏幕强制禁止屏幕截图 - 桌面端:seed 存放在 OS secret store 中(Windows Credential Manager / macOS Keychain / Linux Secret Service);复制的密码不会进入 Windows 剪贴板历史记录;密钥材料锁定在 RAM(`mlock`/`VirtualLock`)中 - 俄语 / 英语,深色“墨水”主题,移动端和桌面端采用独立的布局 ## 自动填充 由于密码是推导出来的而不是存储的,自动填充不能简单地从数据库中读取一个值 ——它会请求应用为你当前所在的站点计算一次。密钥 和短语永远不会离开应用;只有最终生成的密码会填入输入框。 匹配是通过可注册域名进行的(与浏览器使用的 Public Suffix List 相同), 因此一个保存的 `github.com` 也可以填充 `gist.github.com`,而 `github.com.evil.com` 则会正确地被拒绝填充。 - **Android** —— Svitok 注册为系统自动填充服务。在 Android 设置中开启它,聚焦到登录输入框,选择 Svitok 建议,使用 指纹和你的短语进行确认,它就会自动填充。适用于各种应用,并且(更改一个 Chrome 设置后)也适用于浏览器。 - **桌面端** —— 一个小巧的浏览器扩展通过本地 socket 与正在运行的应用进行通信,中间经过一个不持有任何 secret 的轻量级原生消息宿主。如果在你选择建议时应用是 锁定的,它会置于最前层,要求输入你的短语, 然后进行填充——无需二次点击。扩展通过一个你从“设置”中复制一次的 token 与应用配对。 两个平台的完整设置指南位于 [`docs/AUTOFILL.md`](docs/AUTOFILL.md) 中。 桌面扩展目前需要手动注册原生宿主(安装程序以后会自动处理);扩展本身位于 [`extension/`](extension/)。 ## 安装 从 [**Releases**](https://github.com/KOR1K1/svitok/releases) 获取构建版本: - **Android** —— 通用版 `.apk`(侧载;已签名) - **Windows** —— NSIS `.exe` 安装程序(未签名,因此 SmartScreen 可能会发出警告) - **macOS** —— 通用版 `.dmg`(支持 Intel 和 Apple Silicon) - **Linux** —— `.deb`(Debian/Ubuntu)或便携版 `.AppImage`(适用于大多数发行版;同一个二进制文件可在 X11 和 Wayland 下运行)。在 NixOS(非标准 FHS)上,使用 `nix develop` 进行原生构建——仓库的 `flake.nix` 提供了开发环境——或者通过 `appimage-run`/`nix-ld` 运行 AppImage。 桌面端构建(Windows、macOS、Linux)由 CI 在每个版本标签上生成;Android APK 是单独构建和签名的。计划提交至 F-Droid。故意没有加入应用内的网络自动更新功能:一个离线应用如果回传网络进行自我更新,就背离了其初衷。 ## 威胁模型 在这里保持坦诚比听起来强硬更重要。 **Svitok 实际能防御的威胁:** - **云端保险库泄露。** 没有保险库,也没有云。“他们泄露了密码数据库”这一整类攻击在此都不适用。 - **密码重用。** 每个站点都会获得一个唯一的推导密码。钓鱼攻破一个站点不会交出其余的密码;只需增加计数器即可轮换。 - **丢失短语但 seed 被盗。** 内存密集型 KDF 使得猜测短语代价高昂。这只能*延缓*攻击者,并不能使其变得不可能——薄弱的短语依然会沦陷,这就是为什么应用会强制要求最小长度并屏蔽显而易见的短语。 - **被盗的锁定设备。** seed 存放在 Android Keystore(仅通过生物识别解锁)或桌面端的 OS secret store 中;master key 仅在你解锁后存在于内存中,被锁定在 RAM 中以防被交换到磁盘,并且永远不会跨入 UI/JS 层。 - **偷窥 / 截图。** 默认阻止截屏,即使你关闭了该功能,在 seed 和密码屏幕上也会强制防截屏;密码被隐藏,需按住才能显示;剪贴板在 Android 上被标记为敏感内容,在 Windows 上被排除在剪贴板历史记录之外,并会自动清除。 **无法防范的威胁——任何其他方案也一样:** - **被感染的设备。** 如果你输入短语的机器上带有键盘记录器、屏幕记录器或具有 root 权限的恶意软件,那就全完了。它会在你输入时窃取短语,并在你展示时窃取 seed。你无法在被感染的终端上安全地输入机密——这是所有架构的局限性,而不是这个架构的 bug。 - **恶意的构建版本。** 你必须信任你安装的二进制文件。自己构建(见下文)是唯一真正的答案;可重现构建是一个待办事项 (TODO)。 - **薄弱的短语。** KDF 争取的是时间,而不是奇迹。 - **物理胁迫**,已经解锁的设备落入他人之手,或者在实时会话期间进行的内存取证。 如果你的手机是可信且离线的,Svitok 是一个强大的方案。如果不可信,任何密码管理器都救不了你。 ## 备份、同步与防止数据丢失 这里有两件独立的事情,值得把它们分清楚: - **seed** 就是*那个*核心机密。它存在于纸上(加密后也存在于设备的安全存储中)。如果弄丢了纸张*并且*忘了设备备份在哪,密码就找不回来了。把它写下来——如果你足够重视,可以在不同的地方写两份。永远不要给它拍照或放到云端。 - **站点列表**(包含哪些站点、登录名、计数器、策略)**不是**机密,但你仍然需要它来知道要*推导*什么*。这是元数据。 数据传输的方式: - **在新设备上恢复** —— 输入相同的 seed 和短语。密码会原封不动地回来(这就是设计的核心)。解锁屏幕会显示密钥指纹,以便你确认输入是否正确。 - **QR 同步** —— 一台设备将其站点列表显示为 QR 码(生成器从零编写,支持版本 1-40,最多约 2331 字节),另一台设备用摄像头扫描。仅传输元数据,没有 secret,无需网络。 - **文本备份** —— `Settings -> Backup` 会将站点列表加上加密的保险库复制为一个 blob。把它粘贴到笔记、电子邮件、文件中,任何地方都可以。没有你的 seed 和短语,它毫无用处。在添加、更改或删除条目后,请记得更新它。 磁盘上的文件采用原子方式写入(临时文件 + `fsync` + 重命名),因此保存过程中断电也不会导致 `sites.txt` 被截断或保险库损坏。备份导入在覆盖任何内容*之前*,会先使用你当前的密钥解密并验证保险库。 ## 从源码构建 你需要: - **Rust** (stable) - - **Node.js** 20+ 和 npm - 适用于你的操作系统的 **Tauri** 先决条件 - - 针对 Android:**JDK 21**、Android **SDK** + **NDK** (r28) 以及 Rust Android targets ``` git clone https://github.com/KOR1K1/svitok cd svitok # 运行 test suite (RFC + golden vectors - 这些用于固定 bit-compatibility) cargo test --workspace # 该 CLI (没有 GUI,适合用来探索算法) - 打印 usage信息 cargo run -p svitok ``` ### 桌面应用 ``` cd app npm install npm run tauri dev # dev mode with hot reload npm run tauri build # release build + installer (target/release/bundle/) ``` 注意:KDF 被故意设计得很慢,而 **debug** 构建会让它变得*极其*缓慢。务必始终在 `--release` 构建版本上测试解锁时间。 在 NixOS 上,`nix develop` 会将你带入一个包含 Rust 工具链、Node 和 webkit/GTK 依赖项的 shell 环境(见 `flake.nix`);然后像上面一样运行 `npm run tauri build` 即可。 ### Android 应用 ``` cd app # 为你的机器设置 ANDROID_HOME / NDK_HOME / JAVA_HOME,然后: npx tauri android build --apk ``` 发布版 APK 使用 `gen/android/keystore.properties`(已在 git 中被忽略——它包含密码)引用的 keystore 进行签名。请生成你自己的 keystore 以构建签名版本。 ## 项目结构 ``` core/ no_std crypto core, ZERO dependencies blake2s, chacha20, sha1/hmac, kdf, base32, vault, derive, totp, wipe, domain (PSL) common/ std layer shared by CLI and app: sites.txt/vault.b32 storage, OS RNG, QR generator cli/ terminal version (svitok new / add / pw / totp / vault ...) app/ Tauri v2 app src/ frontend: vanilla TS + Vite, no framework (main.ts, ui.ts, api.ts, i18n.ts, scan.ts) src-tauri/ Rust backend: IPC commands, seed storage, local socket for the extension, platform glue gen/android/ Kotlin: Keystore + BiometricPrompt, FLAG_SECURE, autofill service + JNI bridge host/ native messaging host: relays the browser extension to the app over a local socket extension/ MV3 browser extension for desktop autofill docs/ logo, screenshots, autofill guide; the paper spec is SPEC.md at the root ``` master key 存在于 Rust 的状态(`Mutex`)中,在锁定和丢弃时会被擦除,并且永远不会通过 IPC bridge 跨越进入 JS。只有推导结果和元数据会跨越。 ## 关注点 / 改进空间 适合去研究、审计或贡献的好地方: - **`core/`** —— 真正的加密核心。如果你在这里发现了真正的问题,那才是最重要的问题。`core/tests/golden.rs` 中的黄金向量有意冻结了输出格式——如果你改变了方案,它们就会报错,而且每一张现存的纸条都会失效。这就是它的意义所在。 - **KDF 强度**(`core/src/kdf.rs`)—— 参数记录在纸上,因此它们可以为新的 seed 增长而不会破坏旧的 seed。随着硬件的发展,这值得重新审视。 - **可重现构建** —— 目前还没实现。这是信任层面上价值最高的一项改进。 - **自动填充覆盖范围** —— 如今桌面端的原生宿主需要手动注册;安装程序应该自动完成。原生应用目前通过包名匹配,而不是域名。 - **F-Droid 元数据**,更多语言,以及针对超长列表的动态多帧 QR 码模式。 在进行重大更改之前先开个 issue,这样我们就不会用两种不同的方式做同一件事。关于项目的发展方向,请参阅 [roadmap](ROADMAP.md)。 ## 安全性 如果你发现了一个漏洞,请**不要**发布公开的 issue——使用私有的 GitHub 安全公告(或发送电子邮件)。除非你希望保持匿名,否则我会感谢你的贡献。 加密部分是故意手工编写的(柯克霍夫原则:安全性依赖于 seed 和短语,而不是隐藏算法——这正是为什么将其公开发布是安全的)。手工编写也意味着它还没有经过真正的外部审计。请据此谨慎对待,如果你是密码学专家,欢迎对其进行严苛审查。 ## License [GPL-3.0-or-later](LICENSE)。版权所有 (C) 2026 KOR1K1。 故意采用 Copyleft 协议:任何分支都必须保持开源和可审计。对于一款处理你密码的工具来说,“你可以确切地读懂它的运行原理,任何对它进行分叉的人也能做到这一点”是一个功能,而不是一句客套话。如果你愿意提供帮助,请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。
标签:Rust, Tauri, 可视化界面, 密码学, 密码管理器, 手动系统调用, 无状态密码派生, 桌面应用, 离线工具, 网络流量审计, 通知系统