第一步:定义数据契约
一、核心密码表:
| 字段名 (键) |
数据类型 |
说明与作用 |
零知识安全要求 |
id |
String (UUID) |
条目标识。 每一条密码记录的全局唯一ID。客户端生成,服务端只做存储。 |
公开信息,无安全风险。 |
user_id |
String (UUID) |
用户标识。 归属的账号ID,用于权限隔离。 |
公开信息。 |
encrypted_data |
Text / BLOB |
加密后的核心数据。 里面打包包含了标题、网址、账号、密码、备注等所有明文信息。 |
绝对密文。 服务端看到的必须是一堆乱码,无法解析。 |
iv |
String (Base64) |
初始化向量。 每次使用算法加密时,必须生成一个随机的偏移量。解密时需要用到它。 |
明文存储,它是解密的钥匙孔,不是钥匙。 |
auth_tag |
String (Base64) |
认证标签。 |
AES-GCM 加密后,不仅会产生乱码(Ciphertext),还会产生一段约 16 字节的 Auth Tag。 |
revision |
Integer |
版本号。 每次客户端修改这条密码,版本号 +1。这是多设备同步的核心,用来判断谁的数据最新。 |
公开信息。 |
deleted |
Integer (0 或 1) |
软删除标记。 当你在手机上删除了一个密码,不要在数据库里真删,而是标记为 1。这样另一台设备拉取同步时,才知道要“在本地删掉这条数据”。 |
公开信息。 |
updated_at |
Timestamp |
最后修改时间。 辅助版本号处理极端冲突,或用于 UI 展示排序。 |
公开信息。 |
二、客户端侧的“明文打包”逻辑
React Native 客户端当用户填完一个表单点击保存时,流程如下:
- 客户端将这些零散的字段组合成一个 JSON 对象(这部分服务端永远看不到):
"url": "https://github.com",
"account": "your_account_or_email",
"password": "your_password",
"totp_seed": "JBSWY3DPEHPK3PXP",
{ "name": "安全问题", "value": "我的小学名字" }
- 客户端使用主密码派生出的密钥(Key)和随机生成的向量(IV),将上面这个 JSON 对象整体加密成一段密文(比如
U2FsdGVkX19+...)。
- 客户端将这段密文塞进
encrypted_data 字段,连同 id, iv, revision 等发给 Go 语言写的后端 API。
三、 辅助表:
User 表
| 字段名 |
数据类型 |
说明与作用 |
id |
String (UUID) |
用户唯一标识。 |
username |
String |
用户名,可作为账号。 |
master_password_hash |
String |
验证哈希。 注意,这不是主密码本身,而是主密码经过极高强度哈希运算后的结果,仅用于服务端验证登录,不能用于解密数据。 |
crypto_salt |
String |
加密盐值。 用户注册时客户端生成的一串随机字符。换新手机登录时,必须先从服务端拉取这个盐值,结合用户手打的主密码,才能在本地还原出真正的加密密钥。 |
User_Devices 表 |
|
|
| 字段名 |
数据类型 |
说明与作用 |
device_id |
String (UUID) |
主键。 设备唯一标识。 |
user_id |
String (UUID) |
所属用户。 必须关联 User 表,否则服务器不知道该设备属于谁。 |
device_name |
String |
设备名称。 例如 “iPhone 15 Pro” 或 “Windows 桌面”。 |
login_time |
Timestamp |
使用时间戳 (Timestamp) 方便后端自动清理过期设备或前端进行排序。 |
last_active |
Timestamp |
最后活跃时间。 每次同步数据时更新。长时间未活跃的设备可被视为已失效。 |