想深入了解威胁模型、参数选型理由与所有工程实践细节?
版本:1.0 发布日期:2026 年 5 月 3 日 适用版本:ByteGuard iOS v2.3.x 及以上 联系:[email protected]
ByteGuard 是一款完全在用户 Apple 设备(iPhone / iPad / Mac)本地运行的密码管理器。本白皮书面向密码学审计方、独立安全研究者与高安全要求的用户,详述 ByteGuard 的密钥层级、加密算法、设备级保护机制以及威胁模型。
我们遵循 Kerckhoffs 原则:系统的安全性应仅依赖密钥保密,不依赖算法或架构保密。本文档公开的所有内容(算法、参数、密钥层级、存储位置)都不会削弱实际安全性;恰恰相反,公开是审计与信任的前提。
| 威胁 | 防御方式 |
|---|---|
| 服务器入侵 / 数据泄露 | ByteGuard 不运行任何后端服务器;同步完全走用户自己的 iCloud Private DB |
| 设备丢失 | 字段级 AES-256-GCM 加密 + Keychain kSecAttrAccessibleWhenUnlockedThisDeviceOnly |
| 网络中间人 | 主密码、Secret Key、明文 Vault DEK 永不离开设备 RAM;同步上传的是已 AES-256-GCM 加密的字段密文 + KEK 包裹后的 wrapped DEK(应用层加密,独立于 CloudKit 自身加密) |
| 钓鱼登录 | 通行密钥(Passkey / FIDO2)私钥永不离设备 |
| 弱主密码暴力破解 | Argon2id 64MB 内存成本 × 3 次迭代(OWASP 移动端推荐) |
| GPU 离线攻击 | Argon2id 内存硬度对 GPU 极不友好 |
| 时序攻击(密码验证) | 常量时间字节 XOR 比较 |
| 内存残留 | Vault DEK 锁屏/挂起/切库时三次覆写(zero → random → zero);Master Key / KEK 仅作为解锁函数 local 变量短暂存在,由 Swift ARC 自动释放 |
我们对如下场景不做承诺——任何宣称能防御这些场景的密码管理器都在夸大其词:
这些威胁不在本白皮书的安全模型内。
ByteGuard 采用五层密钥层级(每条目密钥隔离):
主密码 + Secret Key (128-bit) ──Argon2id──→ Master Key (32B)
│
├─HKDF "auth-v1"──→ Auth Hash (验证用)
│
└─HKDF "kek-v1"──→ KEK (32B)
│
↓ AES-GCM unwrap
Vault DEK (32B, 全库唯一, 随机生成)
│
↓ HKDF "type-{T}-item-{ID}-v1"
Item Key (32B, 每条独立)
│
↓ AES-256-GCM
字段密文
| 输入 | 长度 | 来源 | 持久化位置 |
|---|---|---|---|
| 主密码 | 用户输入 | 用户输入 | 永不持久化 |
| Secret Key | 128-bit (16B) | libsodium randomBytes 创建库时生成 |
iOS Keychain(可选 iCloud Keychain 同步),同时以 BIP39 12 词形式展示给用户备份 |
| Salt | 256-bit (32B) | libsodium randomBytes 创建库时生成 |
SwiftData(密码库元数据) |
派生算法:Argon2id(libsodium 实现)
输入:Argon2id(password ‖ secretKey.base64, salt) → 32 bytes
参数:
参数选型依据:OWASP Password Storage Cheat Sheet 移动端推荐。在 iPhone 14 Pro 上典型耗时 100~250 ms,即对用户无感、对 GPU 攻击者极昂贵。
安全特性:
两者都从 Master Key 通过 HKDF-SHA256 派生(RFC 5869):
| 派生密钥 | 用途 | HKDF info 字符串 | 输出长度 |
|---|---|---|---|
| Auth Hash | 验证主密码是否正确 | "vault-auth-v1" |
32B |
| KEK | 解封 Vault DEK | "vault-kek-v1" |
32B |
职责严格分离:
密码验证使用 常量时间 XOR 比较,杜绝时序攻击。
生成方式:库创建时由 libsodium randomBytes 生成 32 字节随机密钥;用 KEK 通过 AES-256-GCM 加密(即 Wrapped DEK)后存储到 SwiftData。
为什么 DEK 全库唯一而不是 per-item 随机? 持久化只存 1 个 wrapped DEK(不用 N 份),存储与同步开销随条目数线性下降但不暴涨。每条目密钥隔离由下一层 Item Key 完成。
修改主密码的关键性质:
派生算法:HKDF-SHA256(Vault DEK, info="type-{T}-item-{ID}-v1") → 32B
T:数据类型(login / card / note / passkey / identity 等)ID:条目唯一 ID安全性:
算法:AES-256-GCM(CryptoKit 实现)
对每个敏感字段单独加密:用户名、密码、URL、自定义字段、卡号、CVV(仅会话内存中存在)、备注、附件等。
唯一 IV:CryptoKit 自动为每次加密生成唯一 96-bit IV,相同明文永不产生相同密文。
禁用持久化的字段:
为支持 Face ID / Touch ID 快速解锁,Vault DEK 的副本会在用户启用生物识别时存入 iOS Keychain:
访问控制 (ACL):
kSecAttrAccessible = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
kSecAccessControlFlags = .biometryCurrentSet
含义:
WhenUnlockedThisDeviceOnly:设备未解锁时不可读;不参与 iCloud Keychain 同步biometryCurrentSet:必须通过 Face ID / Touch ID 验证;当用户在系统设置中修改/重录生物识别数据时,iOS 自动删除该条目iOS Keychain 条目由 Secure Enclave 内的 device key 加密。当应用调用 SecItemCopyMatching 时:
关键事实:
iOS 的 biometryCurrentSet 语义保证:用户重新录入 Face ID(哪怕只是补一个面孔),iOS 自动删除该 Keychain 条目。
ByteGuard 启动时通过 LAContext.evaluatedPolicyDomainState 检测此变化:
如果 biometryCurrentSet 不可用(模拟器、未注册生物识别),DEK 缓存直接抛错,不降级到无生物保护的普通 Keychain 存储。
设计理由:
所有用户数据存储在 iOS SwiftData 数据库。ByteGuard 不依赖 SwiftData 的存储加密——即使 SwiftData 文件被攻击者拿到原始字节,所有敏感字段都已是 AES-256-GCM 密文。
iCloud 同步默认关闭,由用户在设置中显式启用。启用后通过 SwiftData ModelConfiguration(cloudKitDatabase: .private(...)) 接入 CloudKit Private Database。
关于 CloudKit 加密的事实陈述(避免常见误解):
关键性质:
看到「Vault DEK 的 wrapped 副本会上传 iCloud」可能让人本能担心:把"密钥"放云上不是反直觉吗?这一节正面回答这个疑问。
跨设备同步在密码学上要求:用户在新 iPhone / iPad 首次登录后,必须能解密所有已存储的字段。如果 wrapped DEK 不同步,新设备永远拿不到 DEK,所有密文等同于丢失。
Envelope encryption(KEK 包裹 DEK)就是为了解决这个矛盾的标准方案 —— 把"用户主密码"与"数据加密密钥"解耦,前者只在用户脑里,后者用前者派生的 KEK 包装后可以放任何地方(包括公开服务器)。
要从 wrapped DEK 还原明文 DEK,攻击者必须依次突破:
wrapped DEK = AES-256-GCM(KEK, plaintext_DEK, IV)
↑
需要 KEK 才能解密
↑
KEK = HKDF-SHA256(Master Key, "vault-kek-v1")
↑
需要 Master Key
↑
Master Key = Argon2id(主密码 ‖ Secret Key, salt) · 64MB × 3 iter
↑
三重壁垒同时具备:
① 用户的主密码(攻击者不知道)
② 128-bit Secret Key(用户专属,Apple 也拿不到,从未离开设备明文形态)
③ 每次猜测要做 64MB × 3 iter 的 Argon2id 计算
假设主密码强度只有 60 bits(一个像样但不极端的密码),加上 128-bit Secret Key 的熵 = 188 bits。
即使量子计算成熟、Argon2id 被加速 10^15 倍,仍需 10^31 年 —— 宇宙寿命的 10^21 倍。
所有主流密码管理器都把 wrapped 主密钥(或等价物)放云端:
| 厂商 | 同步方式 | wrapped 主密钥位置 | KDF |
|---|---|---|---|
| 1Password | 自有云 | 上传 | PBKDF2 / 受 Account Password + Secret Key 保护 |
| Bitwarden | 自有云 | 上传 | PBKDF2 / Argon2id(用户可选) |
| Dashlane | 自有云 | 上传 | Argon2id |
| Apple Keychain | iCloud Keychain E2EE | 上传(Apple ADP 加密层) | Apple 内部 |
| ByteGuard | iCloud Private DB | 上传 | Argon2id 64MB × 3 + 128-bit Secret Key |
ByteGuard 的密码学强度处于行业第一梯队,关键差异:
Wrapped DEK 上 iCloud 不仅不削弱安全,反而是 envelope encryption 模式的核心安全前提。
安全等级完全依赖于:① 主密码强度 ② Secret Key 永不泄露 ③ Argon2id 参数。CloudKit 是否被入侵、Apple 是否能解密 CloudKit、网络中间人是否截获密文 —— 在这套设计下都无关紧要。
iOS AutoFill 是独立的进程上下文,无法直接访问主 App 的内存中 DEK。ByteGuard v2 方案:
biometryCurrentSet 保护写入 App Group 共享 Keychain好处:AutoFill 不接触 Vault DEK 主密钥,最小权限原则。
SecureMemory.zero(&buffer) 对敏感字节执行三次覆写 + 内存屏障:
memset)arc4random_buf)memset)load(as: UInt8) 阻止编译器优化掉清零清零范围(精确事实):
_currentDEK 实例变量):在 lockVault / clearAllKeychainData 时显式调用 SecureMemory.zero 三次覆写unlockVault / performUnlockOperations 函数内的 local 变量短暂存在,函数返回后由 Swift ARC 自动释放。ARC 释放只保证引用计数归零,不保证字节立即被覆写 — 字节会停留在 heap 直到被新分配覆盖。这是当前实现的限制。sodium_memzero 自行清理防御范围:编译器优化、heap dump(越狱/调试器场景)、应用挂起后内存被换页到磁盘。不防御:Master Key / KEK 在 ARC 释放之前的短暂窗口(毫秒~秒级)内被实时内存读取的攻击。
密码验证使用按字节 XOR 累加:
var result: UInt8 = 0
for i in 0..<computedHash.count {
result |= computedHash[i] ^ storedHash[i]
}
guard result == 0 else { throw .invalidPassword }
确保比较耗时与匹配位置/数量无关,杜绝时序侧信道。
所有随机数(Salt、Vault DEK、Secret Key、IV)来源:
libsodium randomBytes.buf()(底层调用 macOS / iOS 的 arc4random_buf,最终来自内核 CSPRNG)AES.GCM.seal() 内部 IV 由 CryptoKit 自动生成绝不使用 Math.random()、Date()、PID 或其他低熵来源。
应用内置的密码生成器:
SecRandomCopyBytes(内核 CSPRNG)零知识架构意味着以下场景我们也做不到——这是设计目标,不是限制:
| 场景 | 状态 |
|---|---|
| 重置主密码 | ❌ 不能 — 没有第二条解密路径 |
| 找回丢失的 Secret Key | ❌ 不能 — 我们从未持有任何 Secret Key 数据 |
| 配合执法机构解密用户数据 | ❌ 不能 — 服务器无密钥、无密文 |
| 在加密中预留后门 | ❌ 不能 — 算法与参数全部公开,可独立审计 |
| 收集用户密码用于风控 / 训练 | ❌ 不会,且技术上做不到 |
| 推送广告或第三方 SDK | ❌ 应用内不接入任何分析、广告、崩溃上报 SDK |
用户同时丢失主密码和 Secret Key → 数据永久无法解密。这是设计强约束,不是产品缺陷。
| 用途 | 算法 | 实现 | 标准 |
|---|---|---|---|
| 主密码 KDF | Argon2id 64MB / 3 iter | libsodium | RFC 9106 |
| 密钥派生 | HKDF-SHA256 | CryptoKit | RFC 5869 |
| 对称加密 | AES-256-GCM | CryptoKit | NIST SP 800-38D |
| 密码强度评估 | zxcvbn 移植 | 内部 | — |
| 泄露检测 | k-anonymity HIBP API | 内部 | HIBP API v3 |
| 助记词 | BIP39 12 词(128-bit 熵) | 自实现 | BIP-0039 |
| 通行密钥 | WebAuthn / FIDO2 ECDSA P-256 | iOS AuthenticationServices | FIDO2 / CTAP 2.2 |
所有 HKDF info 字符串带 -v1 后缀,便于未来无缝迁移到 v2 算法(例如 SHA-3、AES-256-GCM-SIV)。每个数据库迁移路径都会保留旧密文的解密能力。
| 参数 | 值 |
|---|---|
| Argon2id 内存 | 64 MB |
| Argon2id 迭代 | 3 |
| Argon2id 输出 | 32 bytes |
| Salt 长度 | 32 bytes |
| Secret Key 长度 | 16 bytes (128-bit) |
| Master Key 长度 | 32 bytes (256-bit) |
| KEK 长度 | 32 bytes (256-bit) |
| Vault DEK 长度 | 32 bytes (256-bit) |
| Item Key 长度 | 32 bytes (256-bit) |
| AES-GCM IV 长度 | 12 bytes (96-bit) |
| AES-GCM Tag 长度 | 16 bytes (128-bit) |
| Auth Hash 长度 | 32 bytes |
| BIP39 助记词 | 12 词 |
| info | 用途 |
|---|---|
vault-auth-v1 |
Auth Hash 派生 |
vault-kek-v1 |
KEK 派生 |
type-{T}-item-{ID}-v1 |
Item Key 派生(按数据类型 + 条目 ID) |
vault-dek-v1 |
(已弃用)旧版 DEK 派生 |
版本历史
| 版本 | 日期 | 变更 |
|---|---|---|
| 1.0 | 2026-05-03 | 白皮书首版 |
反馈与漏洞披露
发现安全问题请邮件至 [email protected]。我们承诺: