加密细节
MimiKey 使用的算法与参数、密钥层级、信封与 sealed box 格式、全部 AAD 域字符串,以及可复现的测试向量。
概览
本页是 MimiKey 密码学设计的参考说明,面向安全研究者与需要自行实现兼容客户端的开发者。设计目标与威胁模型见安全模型。
所有二进制值(密钥、nonce、密文、签名)在 JSON 中一律使用 base64url、无填充(RFC 4648 §5)编码。
算法与参数
| 用途 | 算法 | 参数 |
|---|---|---|
| 两秘密派生(2SKD) | Argon2id,版本 0x13 | 内存 65536 KiB(64 MiB),迭代 3,并行度 1,输出 32 字节,盐 16 字节 |
| 子密钥派生 | HKDF-SHA256 | 输出 32 字节;salt 与 info 见域字符串 |
| 对称加密 | XChaCha20-Poly1305 | 256 位密钥,192 位随机 nonce,128 位认证标签;单次明文上限 256 KiB |
| 密钥协商 | X25519 | 用于账号身份密钥、共享库包裹、第三方结果与补丁的 sealed box |
| 签名 | Ed25519 | 第三方授权的每授权审批钥 |
| 账号登录 | SRP-6a | RFC 5054 2048 位群,SHA-256,标识为规范化邮箱 |
| 指纹 | SHA-256 | 取前 8 字节,小写十六进制(16 个字符) |
| 授权码绑定 | PKCE S256 | base64url(SHA-256(code_verifier)),verifier 43–128 字符 |
Secret Key 为 16 字节(128 位)随机数,显示为 PM- 前缀加 26 个 Crockford Base32 字符。主密码长度要求 12–1024 字节(UTF-8)。
密钥层级
主密码 ─┐
Secret Key ─┼─ Argon2id(64 MiB, t=3, p=1, 账号盐 16B) ──┬─ 登录材料 ── SRP 验证器(存服务器)
账号盐 ─┘ (三个域分别派生,互不可推) │
├─ 个人库钥 ── HKDF("item/{item_id}") ── 条目子钥 ── 条目信封
│
└─ 身份材料 ── 密封 ── 账号身份私钥(X25519)
│
┌──────────────────────────────────────┤
│ │
打开:共享库密钥包裹 打开:第三方修改补丁
│
共享库钥(随机 32B)── HKDF("item/{item_id}") ── 条目信封
第三方 SDK(每次安装、每次授权):
seal 密钥对(X25519)── 打开:设备密封给它的结果
审批密钥对(Ed25519)── 签名:每一个数据调用
| 密钥 | 来源 | 存放 |
|---|---|---|
| 登录材料 | 2SKD,登录域 | 仅内存;服务器只存 SRP 验证器 |
| 个人库钥 | 2SKD,库域 | 仅内存,从不传输或包裹 |
| 身份材料 | 2SKD,身份域 | 仅内存 |
| 账号身份私钥 | 随机 X25519 | 用身份材料密封后存服务器 |
| 共享库钥 | 随机 32 字节 | 逐成员包裹给其身份公钥后存服务器 |
| 条目子钥 | HKDF(库钥, item/{item_id}) | 每次加解密时派生 |
| 第三方 seal 私钥、审批私钥 | 接入方 SDK 在设备上随机生成 | 接入方 App 的系统钥匙串,仅本机 |
2SKD 输入格式
三份材料使用相同的 Argon2id 参数,仅前缀域不同。Argon2id 的 password 输入按以下字节拼接:
domain || 0x00 || u32_be(len(master_password_utf8)) || master_password_utf8 || secret_key(16 字节)
| 材料 | domain |
|---|---|
| 登录材料 | password-manager/account-login/v1 |
| 个人库钥 | password-manager/account-vault/v1 |
| 身份材料 | password-manager/account-identity/v1 |
salt 为账号的 16 字节 SRP 盐。修改主密码或找回账号时盐会随机换新,因此三份材料全部改变:个人库的条目会在客户端逐条重新加密,身份私钥按新材料重新密封。
信封格式
所有对称加密的结果都是同一种 JSON 信封:
{
"format": 2,
"nonce": "8Ovm4dzX0s3Iw765tK-qpaCblpGMh4J9",
"ciphertext": "9eSfVW9LOkKE0BLuJRYysVYZYiJuhwLy…"
}
| 字段 | 说明 |
|---|---|
format | 固定为 2 |
nonce | 24 字节随机 nonce |
ciphertext | 密文与 16 字节 Poly1305 标签的拼接 |
XChaCha20-Poly1305 采用标准构造:用 HChaCha20 以 nonce 前 16 字节派生子密钥,再以 IETF ChaCha20-Poly1305 加密,其 12 字节 nonce 为 4 个零字节加上 24 字节 nonce 的后 8 字节。AAD 参与认证但不加密。
条目
条目子钥为 HKDF-SHA256(ikm = 库钥, salt = "password-manager/v2", info = "item/{item_id}"),AAD 为:
password-manager/item/v2/{vault_id}/{item_id}/{revision}
AAD 绑定库、条目和修订号:密文不能被挪到其他条目、其他库,也不能回填到其他修订号。条目明文是如下 JSON(以 UTF-8 编码):
{
"title": "示例银行",
"username": "me@example.com",
"password": "correct horse battery staple",
"url": "https://bank.example.com/login",
"notes": "",
"kind": "login",
"extra": {},
"updated_at": "2026-10-23T00:00:00.000Z"
}
kind 取 login、credit_card、bank_card、api_key、server、secure_note 之一,缺省视为 login;extra 存放该类型特有的字段(例如卡号、有效期)。
Sealed box 格式
需要把数据交给「只有某个公钥持有者才能打开」的场景(共享库密钥包裹、第三方结果、第三方修改补丁)使用 sealed box:
1. 生成临时 X25519 密钥对 (esk, epk)
2. shared = X25519(esk, 收件人公钥);结果为全零时拒绝
3. key = HKDF-SHA256(ikm = shared, salt = 域字符串, info = AAD),32 字节
4. envelope = XChaCha20-Poly1305(key, 随机 24B nonce, AAD, 明文)
{
"format": 1,
"epk": "PHld72bSpQfUtjDl4WAtULZ7pgXRKD8lMW0U1d-OABo",
"envelope": {
"format": 2,
"nonce": "8Ovm4dzX0s3Iw765tK-qpaCblpGMh4J9",
"ciphertext": "9eSfVW9LOkKE0BLuJRYysVYZYiJuhwLy…"
}
}
| 字段 | 说明 |
|---|---|
format | 固定为 1 |
epk | 32 字节临时公钥 |
envelope | 上文的信封,format 为 2 |
在第三方授权 API 中,sealed box 以字符串形式传输:result_enc 与 patch_enc 的值是上述 JSON 序列化后的文本。
AAD 与域字符串
不同用途的域与 AAD 彼此独立,一种用途的密文不能在另一种用途下被打开。
| 用途 | 密钥派生 | AAD |
|---|---|---|
| 条目 | HKDF salt password-manager/v2,info item/{item_id} | password-manager/item/v2/{vault_id}/{item_id}/{revision} |
| 账号身份私钥 | 密钥 = 身份材料 | password-manager/identity-key/v1 |
| 共享库密钥包裹 | sealed box,salt password-manager/vault-key-wrap/v1 | password-manager/vault-key-wrap/v1/{vault_id}/{recipient_account_id} |
| 第三方结果 | sealed box,salt password-manager/oauth-seal/v1 | password-manager/oauth-result/v1/{grant_id}/{request_id} |
| 第三方修改补丁 | sealed box,salt password-manager/oauth-seal/v1 | password-manager/oauth-patch/v1/{grant_id}/{request_id} |
| 生物识别解锁包(Mac) | 包裹密钥由 Secure Enclave 保护 | password-manager/device-unlock/v1 + 换行 + 绑定信息 |
| 备份清单 | HKDF salt password-manager/v2,info backup/manifest/v1/{backup_id} | password-manager/backup/manifest/v1/{vault_id}/{backup_id} |
| 旧模式 2 库钥包裹(已停用) | sealed box,salt password-manager/oauth-seal/v1 | password-manager/oauth-vault-key/v1/{client_id}/{account_id}/{vault_id} |
第三方结果与补丁
结果(result_enc) | 补丁(patch_enc) | |
|---|---|---|
| 由谁密封 | 执行请求的 MimiKey 设备 | 接入方 SDK |
| 收件人 | 该授权的 seal 公钥 | 账号身份公钥(GET /api/oauth/account 的 identity_public_key) |
| 由谁打开 | 接入方 SDK | 你已解锁的 MimiKey 设备 |
| 明文 | {"item":{"id","revision","record"}},list_items 为 {"v":2,"items":[…]} | {"patch":{"字段名":"新值"}} |
修改补丁的 AAD 需要 request_id,因此 update_item 分两步:先创建草稿拿到 request_id,再上传补丁,见 API 参考。
第三方调用签名
每个数据调用都要用该授权的审批私钥(Ed25519)签名下列规范串(UTF-8,\n 分隔,无尾随换行):
{METHOD}\n{PATH}\n{TIMESTAMP}\n{NONCE}\n{SHA256_HEX(BODY)}
PATH不含查询串;BODY为请求体原始字节,空体的 SHA-256 为e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855。- 签名为 64 字节,base64url 编码后放入
x-pm-signature。 - 审批钥指纹 =
SHA-256(公钥 32 字节)前 8 字节十六进制,显示在同意卡上供核对。
完整的请求头约定见 API 参考。
测试向量
以下向量由 Rust 参考实现以固定种子生成,Swift SDK 的单元测试逐字节校验。它们使用公开的测试密钥,不对应任何真实账号。完整文件随 Swift SDK 一同提供:Tests/MimiKeySDKTests/Vectors/oauth_vectors.json。
审批签名
| 字段 | 值 |
|---|---|
| 种子(Ed25519 私钥) | 3-bt9PsCCRAXHiUsMzpBSE9WXWRrcnmAh46VnKOqsbg |
| 公钥 | lAfBINSvrkhw1qvNeOF5ug8mgbUoRkv3Fjn79UVUfIk |
| 指纹 | d1984a0a3442c41c |
空请求体:
GET
/api/oauth/account
1761177600
n-0123456789abcdef
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
签名:KngAgL1nChfn4NugEnlNlesKA8EtmrhpsL1GU9pU_Z0AcwkQRzP9iasdaFfe3nEVcvCGUT92eWHzzZCEX849DQ
带请求体(体为 {"op":"pick_item","vault_id":null,"hint":{"url":"https://bank.example.com"},"reason":"登录示例银行完成对账"}):
POST
/api/oauth/requests
1761177600
n-0123456789abcdef
a60bf0d8213f0871138d7ed45ba0cfdbd8aa81238a24262b2e5ce9ab985c8a07
签名:-PzhO-9DzOmyskR2C-Sq85OOWMiGgCs_bRMy1vI3_T_PN2RAJrWcysXhn_7A1GBo5_6RaVqlD_axrK2gncNkCQ
Sealed box
向量文件的 result 与 patch 两节给出固定的收件人密钥、临时私钥、nonce、AAD、明文与期望的 sealed box JSON,可同时验证密封与打开两个方向:
| 字段 | 值 |
|---|---|
| 收件人公钥 | 9Xwg4YSHxLN9dr2JSRYL_wfRh_0iafGiNzQtT5blNys |
| 收件人指纹 | 8d9b63107169f06e |
grant_id | 6f1d2c3b-4a59-4e68-8b7c-9d0e1f2a3b4c |
request_id | a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
| 补丁明文 | {"patch":{"password":"new-password-✓"}} |
PKCE
服务端使用 RFC 7636 附录 B 的向量自检:verifier dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk 对应 challenge E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM。
备份清单摘要
备份清单的认证输入是按固定字段顺序紧凑编码的 JSON(不是 RFC 8785):
{"backup_format":1,"format":2,"vault_id":"00000000-0000-4000-8000-000000000001","backup_id":"00000000-0000-4000-8000-000000000002","items":[{"id":"00000000-0000-4000-8000-000000000003","revision":1,"envelope":{"format":2,"nonce":"AA","ciphertext":"BB"}}]}
SHA-256:31406c915e36b94924581c9d2a7a01f716fe1d2d36833ac54fe7c38a734d34fb
注意 信封的 nonce 每次随机生成,因此除上述固定种子的向量外,同一明文每次加密得到的密文都不同;请以「能否打开」而不是「密文是否相同」来验证互通性。