1. 项目概述:为什么我们需要一个“Theodor”?
在数字生活里,密码就像空气,无处不在却又常常被我们忽视,直到它出问题。我猜你和我一样,曾经用过“生日+姓名缩写”的万能密码,或者在十几个不同的网站和服务里,循环使用两三个稍微变体的密码。更糟糕的是,把这些密码记在手机备忘录、电脑文本文档,甚至——别笑——一张随手可及的便利贴上。这种“便利”带来的风险是巨大的:一旦一个网站的数据泄露,你所有的账户都可能像多米诺骨牌一样接连倒下。这就是为什么,作为一个在信息安全领域摸爬滚打了十多年的从业者,我决定亲手打造一个属于自己的密码管理器,并给它起名叫“Theodor”。
“Theodor”这个名字,源于我对安全与秩序的一种个人化寄托。它不是一个简单的密码存储箱,而是一个集成了现代加密技术、跨平台同步和便捷用户交互的私人数字保险库。市面上有LastPass、1Password、Bitwarden等优秀产品,但它们要么是闭源的,你无法完全信任其“不会作恶”;要么是订阅制,长期使用成本不低;要么在某些功能上不符合我的个人工作流。自己动手,意味着我能完全掌控我的数据流向、加密算法的选择,以及功能的定制。这个项目,就是要把一个资深从业者对密码管理的所有理解、对安全边界的把控,以及实际使用中的痛点,全部凝结成一个可运行、可复现的工具。无论你是想学习现代应用开发、深入理解加密原理,还是仅仅想拥有一个完全受自己控制的密码解决方案,这篇关于“Theodor”的构建手记,都将为你提供一条清晰的路径。
2. 核心架构设计与技术选型
2.1 整体思路:安全、可控与用户体验的三角平衡
构建一个密码管理器,核心目标是在“绝对安全”、“完全可控”和“良好体验”之间找到一个稳固的平衡点。我的设计思路遵循以下几个核心原则:
- 零知识架构:这是底线。服务器或任何第三方(包括我自己部署的服务端)在任何时候都不能以明文形式接触到用户的主密码或存储的密码数据。所有加密和解密操作都必须在客户端(用户的设备)完成。
- 端到端加密:数据在离开用户设备前就已经被加密,密文在网络上传输和服务器存储。只有拥有正确主密钥的用户才能解密。
- 开源与可审计:所有代码公开,加密算法使用行业标准、经过广泛验证的库,杜绝“自己造轮子”可能引入的隐蔽漏洞。
- 跨平台与数据同步:在现代多设备环境下,密码管理器必须在手机、电脑、平板间无缝同步。这引入了网络和冲突处理的复杂性,但必不可少。
- 本地优先,云端备份:优先保证本地操作的流畅性和安全性,将加密后的数据同步到云端仅作为备份和多设备同步的手段。
基于这些原则,“Theodor”的架构自然分成了三个清晰的部分:客户端(跨平台)、同步服务器和加密核心。服务器只负责存储和转发加密后的二进制数据块,它“看不懂”也“碰不了”用户的任何一条具体密码。
2.2 技术栈选型背后的考量
技术选型直接决定了项目的可行性、安全性和开发效率。以下是我的选择及其背后的深层逻辑:
前端/客户端 (跨平台应用):
- 选择:Flutter。这是最关键的选择之一。我需要一个能同时高质量覆盖iOS、Android、Windows、macOS 和 Linux的解决方案。React Native 在桌面端体验欠佳,原生开发(Swift + Kotlin + SwiftUI + Java FX)则意味着至少维护4个代码库,成本过高。Flutter 的单一代码库和自绘引擎能保证所有平台 UI 和逻辑的高度一致,其性能对于密码管理器这类工具型应用绰绰有余。Dart 语言的强类型和现代语法也有助于构建稳定可靠的前端逻辑。
后端/同步服务器:
- 选择:Go (Golang) + 轻量级框架(如 Gin)。同步服务器的核心需求是高并发、低延迟、低资源消耗,并且要稳定可靠。Go 语言在并发处理(goroutine)和网络服务方面的先天优势非常明显,编译为单一可执行文件也便于部署。它没有像 Python(Django/Flask)或 Node.js 那样的运行时和庞大依赖,部署更简洁,内存占用更小,非常适合这种专注于数据中转的“哑巴”服务器。
数据库:
- 选择:SQLite (客户端) + PostgreSQL (服务器)。这是一个经典组合。客户端使用 SQLite 存储本地加密后的数据缓存和配置,无需网络连接即可工作,速度快且无需单独部署数据库服务。服务器端使用 PostgreSQL,因为它对复杂查询(未来可能需要的审计日志分析)、数据一致性和可靠性支持得更好,适合存储所有用户的加密数据块。
加密核心库:
- 这是安全的心脏,绝不能妥协。
- 选择:libsodium (通过 Flutter 插件
flutter_sodium和 Go 的golang.org/x/crypto)。Libsodium 是一个现代、易用、且难以误用的加密库,它封装了 NaCl 网络和加密库的精华。我直接使用它提供的crypto_secretbox_easy和crypto_secretbox_open_easy进行对称加密,使用Argon2id作为密钥派生函数(KDF)。这避免了手动组合 AES-GCM 和 HMAC 等底层原语可能带来的风险。在 Flutter 和 Go 中统一使用 libsodium/NaCl 系算法,能确保两端加密解密行为完全一致。
数据同步协议:
- 选择:基于 RESTful API 的增量同步。考虑到实现的复杂度和可靠性,我没有选择 WebSocket 实时同步,而是采用 HTTP 长轮询或简单的定时拉取。核心是设计一个类似 Git 的“版本”机制:每个客户端的每次修改都生成一个带时间戳和哈希的“数据版本”。同步时,客户端将本地最新版本号发给服务器,服务器返回所有更新的版本数据,客户端进行合并(冲突解决策略采用“最后写入获胜”或手动合并)。这种设计简单、鲁棒,且易于调试。
注意:为什么不用现成的云服务(如 Firebase)?当然可以,Firebase 的 Firestore 和 Authentication 能极大加快开发。但这就违背了“完全可控”的初衷。Firebase 是谷歌的服务,数据存储位置、备份策略不完全透明。自己搭建服务器,虽然运维成本增加,但数据物理存储位置(比如自己的云主机)、访问日志、备份频率完全由自己掌控,这种心理上的安全感是第三方服务无法提供的。
3. 核心安全机制深度解析
密码管理器的灵魂在于其安全模型。这里,我将拆解“Theodor”如何将主密码转化为保护你所有秘密的铜墙铁壁。
3.1 从主密码到加密密钥:密钥派生过程
这是最关键的一步。用户输入的主密码(如“MyCatName2024!”)绝对不能直接用作加密密钥。因为它可能熵值不够(不够随机),且需要被转换成特定长度的密钥。这个过程就是密钥派生(KDF)。
- 为什么用 Argon2id?过去常用 PBKDF2,但它对 GPU/ASIC 攻击的抵抗较弱。Argon2 是 2015 年密码哈希竞赛的获胜者,而Argon2id是混合模式,能同时抵抗侧信道攻击和GPU破解,是目前公认最安全的 KDF 选择。它通过消耗大量内存和计算资源,使得暴力破解的成本极高。
- 具体派生流程:
- 输入:用户的主密码(字符串) + 一个随机生成的、每个用户唯一的盐(Salt)。盐的作用是确保即使两个用户密码相同,派生出的密钥也完全不同,并能防止预计算攻击(彩虹表)。
- 参数设置:Argon2id 需要配置时间成本(迭代次数)、内存成本(内存消耗)和并行度。我的设置是:
t=3, m=65536KB (64MB), p=4。这个设置在主流设备上派生密钥需要约 0.5-1 秒,用户体验可以接受,但足以让攻击者尝试单个密码的成本变得极高。 - 输出:一个 256 位(32字节)的主密钥(Master Key)。这个主密钥才是后续用来加密所有数据的“万能钥匙”。
// Flutter 侧伪代码示意 import 'package:flutter_sodium/flutter_sodium.dart'; Future<Uint8List> deriveMasterKey(String masterPassword, Uint8List salt) async { // 将字符串密码转换为字节 final passwordBytes = Uint8List.fromList(utf8.encode(masterPassword)); // 使用 Argon2id 进行密钥派生 final masterKey = await Sodium.cryptoPwHash( keyLen: 32, // 输出32字节密钥 password: passwordBytes, salt: salt, opsLimit: Sodium.cryptoPwHashOpsLimitInteractive, // 对应中等计算成本 memLimit: Sodium.cryptoPwHashMemLimitInteractive, // 对应64MB内存 alg: Sodium.cryptoPwHashAlgArgon2Id13, // 指定Argon2id算法 ); return masterKey; }3.2 数据加密与存储:保险柜的运作方式
得到主密钥后,我们用它来加密实际的密码条目。每个密码条目(包含网站、用户名、密码、备注等)在加密前会被序列化为一个 JSON 字符串。
加密过程:
- 为每条记录生成一个随机的Nonce(一次性数字)。Nonce 的作用是保证即使加密相同的明文数据,使用相同的密钥,每次产生的密文也完全不同,防止攻击者分析密文模式。
- 使用XChaCha20-Poly1305加密算法(libsodium 的
crypto_secretbox_easy默认或推荐使用)。这是 ChaCha20 流密码加上 Poly1305 消息认证码(MAC)的组合。它比 AES-GCM 在某些平台上更快,且同样能同时提供保密性和完整性验证(确保密文在传输中未被篡改)。 - 加密函数:
密文 = crypto_secretbox_easy(明文JSON, nonce, 主密钥)。输出是密文+内置的认证标签。
本地存储结构:
- 本地 SQLite 数据库中,每条记录存储的内容包括:
记录ID、加密后的密文(Blob类型)、对应的Nonce、创建/修改时间戳、版本号。 - 主密钥永远不会被存储。它只在用户输入主密码后,在内存中生成并使用。应用进入后台一段时间或关闭后,内存中的主密钥会被安全擦除。
- 盐和加密参数会安全地存储在本地(例如,与用户配置一起),用于下次登录时重新派生主密钥。
- 本地 SQLite 数据库中,每条记录存储的内容包括:
3.3 同步安全:密文之旅
数据同步是安全链条上的重要一环。由于我们采用“零知识”架构,同步过程相对清晰且安全。
- 客户端上传:当需要同步时,客户端将本地新增或修改的记录的密文、Nonce、版本号打包,通过HTTPS通道发送到服务器。服务器只是将这些二进制数据存入数据库,与用户ID关联。它无法解密其中的内容。
- 客户端拉取:另一台设备登录后,从服务器拉取该用户ID下的所有密文记录和版本信息。拉取到本地后,使用本地派生的主密钥尝试解密。如果主密码正确,解密成功;如果错误,则解密失败(因为 Poly1305 认证会失败),数据无法使用。
- 冲突解决:当两台设备离线修改了同一条记录后同时同步,就会产生冲突。服务器会保存两个版本。客户端拉取到冲突后,可以根据策略自动解决(如保留时间戳最新的),或者将冲突标记出来,等待用户下次打开应用时手动选择保留哪一个。这个冲突解决逻辑在客户端进行,服务器只负责存储所有版本。
实操心得:内存安全处理。在 Dart/Flutter 和 Go 中,包含敏感信息(如主密钥、解密后的临时明文)的变量,在使用完毕后,应尽可能快地将其内存覆盖或置空。虽然高级语言有垃圾回收,但主动清空敏感内存区域是一个好习惯。例如,可以将
Uint8List的内容用随机数据覆盖一遍再丢弃引用。
4. 客户端应用实现详解
4.1 Flutter 界面与状态管理
用户界面需要清晰、直观且响应迅速。我采用 Flutter 的现代架构。
- 状态管理选择:Riverpod。它比 Provider 更灵活、更安全,且编译时安全。非常适合管理“Theodor”的全局状态,如:用户登录状态、当前解密的密码库、UI主题、同步状态等。
- 核心页面流:
- 启动/解锁页:首次进入,检测本地是否存在加密的数据库。如果存在,显示主密码输入框。如果不存在,引导用户创建主密码并生成盐。
- 密码库主列表页:解密成功后进入,以卡片或列表形式展示所有密码条目。支持搜索、按分类筛选。每个条目只显示网站图标和用户名,点击后进入详情页或直接复制密码。
- 密码条目详情/编辑页:查看或修改一条记录的详细信息。密码默认隐藏,点击“眼睛”图标显示。提供“复制用户名”、“复制密码”按钮,密码在复制后一定时间(如60秒)后会自动从剪贴板清除(通过后台任务实现)。
- 生成密码页:内置密码生成器,允许用户指定长度、包含大写字母、小写字母、数字、特殊符号,并排除易混淆字符(如
1, l, I, 0, O)。
- 生物识别认证集成:在移动端,使用
local_auth插件集成指纹(Touch ID/Face ID)或面部识别。关键点:生物识别只用于“解锁”本地存储的、已用主密钥加密过的数据库文件。主密钥的派生仍然需要主密码。生物识别只是一种便捷方式,替代每次输入主密码去解密本地缓存。首次设置时,必须输入主密码。之后,应用可以将派生出的主密钥,用设备本身的硬件安全区域(如 iOS 的 Keychain/KeyStore)进行二次加密存储,生物识别通过后,再从安全区域取出主密钥来解密数据库。这样即使设备丢失,没有主密码,攻击者也无法从设备物理存储中提取出有效的主密钥。
4.2 本地数据库操作
使用sqflite插件操作 SQLite。
// 数据库帮助类示例 class PasswordDatabase { static final PasswordDatabase _instance = PasswordDatabase._internal(); Database? _db; Future<Database> get database async { if (_db != null) return _db!; _db = await _initDB('passwords.db'); return _db!; } Future<Database> _initDB(String filePath) async { final dbPath = await getDatabasesPath(); final path = join(dbPath, filePath); return await openDatabase( path, version: 1, onCreate: _onCreate, // 设置密码?不,数据库文件本身已是密文。这里openDatabase的password参数用于加密SQLite文件本身, // 但我们选择不用。因为我们的每条记录在存入前都已加密,整个数据库文件即使被窃取,也是一堆无法识别的密文。 // 加上SQLite层加密可能会影响性能,且增加复杂度。 ); } Future<void> _onCreate(Database db, int version) async { await db.execute(''' CREATE TABLE IF NOT EXISTS items( id TEXT PRIMARY KEY, encrypted_data BLOB NOT NULL, nonce BLOB NOT NULL, category TEXT, updated_at INTEGER NOT NULL, version INTEGER NOT NULL ) '''); // 创建索引以加速查询(尽管查询时是先解密再过滤,但分类和更新时间索引对同步有帮助) await db.execute('CREATE INDEX IF NOT EXISTS idx_category ON items(category)'); await db.execute('CREATE INDEX IF NOT EXISTS idx_updated ON items(updated_at)'); } // 插入一条已加密的记录 Future<int> insertEncryptedItem(EncryptedItem item) async { final db = await database; return await db.insert('items', item.toMap()); } // 获取所有记录(用于同步或全量解密展示) Future<List<EncryptedItem>> getAllEncryptedItems() async { final db = await database; final List<Map<String, dynamic>> maps = await db.query('items'); return List.generate(maps.length, (i) => EncryptedItem.fromMap(maps[i])); } }4.3 密码生成器实现
一个安全的密码生成器不应使用简单的随机函数。我使用 cryptographically secure random number generator (CSPRNG)。
import 'dart:math'; import 'package:flutter_sodium/flutter_sodium.dart'; String generatePassword({ int length = 16, bool includeLowercase = true, bool includeUppercase = true, bool includeNumbers = true, bool includeSymbols = true, String excludeChars = '1lI0O', }) { final buffer = StringBuffer(); final allChars = StringBuffer(); const lowercase = 'abcdefghijklmnopqrstuvwxyz'; const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; const numbers = '0123456789'; const symbols = '!@#\$%^&*()_+-=[]{}|;:,.<>?'; if (includeLowercase) allChars.write(lowercase); if (includeUppercase) allChars.write(uppercase); if (includeNumbers) allChars.write(numbers); if (includeSymbols) allChars.write(symbols); final availableChars = allChars.toString().replaceAll(RegExp('[$excludeChars]'), ''); if (availableChars.isEmpty) { throw ArgumentError('No character set available after exclusions.'); } // 使用 libsodium 的随机数生成器,它是密码学安全的 final randomBytes = Sodium.randombytes_buf(length); final random = Random.secure(); // 备用方案,但Sodium的更优 for (int i = 0; i < length; i++) { // 从安全随机字节中获取一个索引 final index = randomBytes[i] % availableChars.length; buffer.write(availableChars[index]); } return buffer.toString(); }5. 服务端同步API实现
服务端使用 Go 语言编写,职责非常明确:用户认证(非解密性认证)、接收密文数据、存储密文数据、按需返回密文数据。
5.1 API 端点设计
使用 JWT (JSON Web Token) 进行无状态的用户会话管理。用户注册/登录时,使用主密码派生出的一个“认证密钥”(可与主密钥不同,单独派生)或使用独立的账号密码来获取 JWT。
// 主要API端点示例 (使用Gin框架) func setupRouter() *gin.Engine { r := gin.Default() r.Use(middleware.CORS()) // 处理跨域 api := r.Group("/api/v1") { auth := api.Group("/auth") { auth.POST("/register", handlers.Register) // 注册,在服务器创建用户记录,存储用户公钥信息(如果未来需要)和盐 auth.POST("/login", handlers.Login) // 登录,验证凭证,返回JWT } data := api.Group("/data") data.Use(middleware.JWTAuth()) // JWT认证中间件 { data.POST("/sync/upload", handlers.UploadData) // 客户端上传加密数据块 data.GET("/sync/download", handlers.DownloadData) // 客户端拉取加密数据块 data.GET("/sync/version", handlers.GetVersion) // 获取最新版本号,用于增量同步 } } return r }5.2 数据模型与存储
服务器端的数据库模型极其简单。
-- PostgreSQL 表结构 CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), username VARCHAR(255) UNIQUE NOT NULL, auth_salt BYTEA NOT NULL, -- 用于认证密钥派生的盐 auth_hash BYTEA NOT NULL, -- 认证密钥的哈希值(如Argon2id哈希) encryption_salt BYTEA NOT NULL, -- 客户端用于派生主密钥的盐,由客户端注册时生成并上传 created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE encrypted_blobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, item_id TEXT NOT NULL, -- 客户端生成的记录ID encrypted_data BYTEA NOT NULL, -- 加密后的数据 nonce BYTEA NOT NULL, -- 加密使用的Nonce version INTEGER NOT NULL, -- 版本号,用于冲突检测 updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- 同一个item_id可能有多个版本(冲突时),用(user_id, item_id, version)做唯一约束或主键 UNIQUE(user_id, item_id, version) ); CREATE INDEX idx_blobs_user_item ON encrypted_blobs(user_id, item_id); CREATE INDEX idx_blobs_user_updated ON encrypted_blobs(user_id, updated_at);handlers.UploadData处理函数的核心逻辑就是验证 JWT,获取user_id,然后将客户端传来的item_id,encrypted_data,nonce,version原封不动地存入encrypted_blobs表。handlers.DownloadData则根据user_id和客户端传来的“上次同步版本号”,查询出所有更新的记录返回。
注意事项:服务器端的安全性。虽然服务器不处理明文,但仍需做好基础安全:
- HTTPS 强制:所有 API 必须通过 HTTPS 访问。
- 输入验证与限流:对上传的数据大小、频率做限制,防止 DoS 攻击。
- SQL 注入防护:使用参数化查询或 ORM。
- JWT 安全:使用强密钥,设置合理的过期时间,在服务端维护一个可选的令牌黑名单(用于注销)。
- 日志与监控:记录访问日志,但切勿记录任何加密数据内容。
6. 部署、测试与日常使用
6.1 客户端打包与发布
Flutter 打包:
- Android:生成签名的 APK 或 App Bundle,上传到 Google Play Store。在 Play Console 中需要填写详细的安全和隐私说明。
- iOS:使用 Xcode 归档并导出为 IPA 文件,通过 App Store Connect 提交至苹果审核。苹果对涉及密码、数据安全的应用审核非常严格,需要准备清晰的数据流说明和隐私政策。
- Windows/macOS/Linux:使用
flutter build windows/macos/linux生成可执行文件。可以通过 GitHub Releases 或自己的网站分发。对于 macOS,可能还需要进行公证(Notarization)以避免安全警告。
服务器部署:
- 选择一家云服务商(如 DigitalOcean, Linode, AWS Lightsail),创建一台虚拟机(VPS)。
- 在服务器上安装 Docker 和 Docker Compose。
- 编写
docker-compose.yml文件,定义 PostgreSQL 和你的 Go 后端服务。 - 配置 Nginx 作为反向代理,处理 HTTPS(使用 Let‘s Encrypt 免费证书)。
- 使用 systemd 或 supervisor 管理 Docker Compose 服务的自动启动和守护。
- 定期备份 PostgreSQL 数据库和服务器上的重要配置文件。
6.2 安全测试与审计要点
自己用的工具,安全测试更不能马虎。
- 静态代码分析:使用 Dart 的
dart analyze和 Go 的go vet、staticcheck检查代码问题。 - 依赖安全检查:定期运行
dart pub outdated和go list -u -m all检查依赖更新,特别是加密库的更新。 - 渗透测试(简易版):
- 客户端:尝试使用逆向工程工具(如反编译 Flutter 产物)查看是否能找到硬编码的密钥或逻辑漏洞。检查本地 SQLite 数据库文件,确认存储的确实是密文。
- 服务器:使用
curl或 Postman 模拟恶意请求,尝试越权访问其他用户的数据(通过修改 JWT 或请求参数)。测试 SQL 注入和路径遍历。
- 核心加密逻辑验证:编写单元测试和集成测试,确保 Flutter 和 Go 两端使用相同的盐和主密码能派生出相同的密钥,并能正确加解密。测试边界情况,如空密码、超长密码、特殊字符密码。
- 网络抓包:在同步过程中,使用 Wireshark 或 Charles Proxy 抓包,确认传输的数据是否为不可读的二进制密文,且所有请求均通过 HTTPS。
6.3 日常使用心得与维护
- 主密码是命根子:一定要设置一个高强度、独一无二且自己能记住的主密码。可以考虑使用“助记词短语”,如“CorrectHorseBatteryStaple42!”。务必记住它,我没有设置任何密码找回功能,因为那会引入安全弱点。忘记主密码等于丢失所有数据。
- 定期导出备份:虽然有多设备同步,但我仍建议每隔几个月,在客户端使用“导出”功能,将解密后的数据以加密格式(例如,用主密码再次加密成一个文件)备份到本地硬盘或离线存储设备中。
- 生物识别只是便利:要清楚 Face ID/Touch ID 解锁的是本地的“保险箱钥匙”,而不是替代主密码。在重启手机后或长时间未使用,通常需要重新输入主密码。
- 保持更新:订阅 libsodium、Flutter、Go 等相关安全公告。一旦有严重漏洞披露,需要尽快更新代码和重新发布客户端。
7. 常见问题与排查实录
在实际开发和使用的过程中,我遇到了不少典型问题,这里记录下来,希望能帮你绕过这些坑。
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 客户端登录失败,提示“解密错误” | 1. 主密码输入错误。 2. 本地存储的盐损坏或丢失。 3. 加密算法参数不一致(如升级后Argon2参数改变)。 | 1. 确认主密码无误(注意大小写和特殊字符)。 2. 检查本地数据库或配置文件中存储的“盐”值是否被意外修改。如果是新设备,确保从服务器正确拉取了用户配置(包含盐)。 3. 检查代码版本,确保客户端派生密钥时使用的 Argon2 参数(opsLimit, memLimit)与创建账户时完全一致。重要:这些参数一旦设定,必须永久与用户账户绑定,不能更改。 |
| 同步冲突导致数据丢失 | 多设备离线修改同一条记录后同时同步,冲突解决策略选择了“最后写入获胜”,覆盖了想要的版本。 | 1. 检查服务器端的encrypted_blobs表,是否还保存着旧版本的数据(如果设计是保存历史版本)。2. 在客户端实现更智能的冲突解决UI,当检测到冲突时,不是自动覆盖,而是弹出对话框让用户手动选择保留哪个版本,或者将冲突条目标记为“待解决”状态。 3. 定期备份,以防万一。 |
| 移动端应用在后台被系统清理后,需要重新输入主密码 | 为了安全,应用在退到后台一段时间后,主动清除了内存中的主密钥。这是正常行为。 | 这是设计使然,并非错误。可以适当延长应用在后台保持状态的时间,但不应禁用此安全特性。确保生物识别解锁功能正常工作,它可以快速地从设备安全区域恢复出主密钥,无需手动输入。 |
| 密码生成器生成的密码某些网站不接受 | 1. 包含了该网站禁止的特殊字符。 2. 密码长度超出限制。 3. 密码以数字或符号开头,某些网站有奇怪规则。 | 1. 在密码生成器设置中,自定义“排除的字符”,将常见问题字符(如&,+,%, 空格)加入排除列表。2. 在生成前,先了解目标网站的密码规则(长度、字符类型要求)。 3. 生成后,手动微调一两个字符以满足特定网站的要求,然后保存。 |
| 服务器CPU或内存占用突然飙升 | 1. 可能受到暴力破解登录尝试(针对认证接口)。 2. 某个用户在上传异常大量的数据。 | 1. 在服务器端实施登录尝试频率限制(如每分钟5次)。 2. 在 /sync/upload接口实施请求体和频率限制。3. 检查服务器日志,定位是哪个用户或IP地址的请求异常。 4. 考虑对数据库 encrypted_blobs表按user_id和updated_at建立索引,优化查询。 |
开发“Theodor”的过程,是一次将安全理念、软件工程和用户体验深度融合的实践。它让我对“信任”和“控制”在数字世界中的含义有了更具体的理解。这个项目没有终点,随着新的加密标准出现(如抗量子密码学)和用户需求的变化,它还需要不断地迭代和进化。如果你也走上了自建密码管理器的道路,我希望这份详尽的记录能成为你可靠的路线图。记住,安全是一个过程,而不是一个产品。保持警惕,持续学习,才是守护数字身份的根本。