v0.8.0 本地加密 / 被踢清数据 / 增量同步 / 附件队列 / 顶栏同步进度
用户 2026-10-18 两条: 「直接做 L3 锁界面 + 本地加密(HUKS,踢设备即吊销密钥):数据在但读不出, 重登后恢复密钥即可用。」 「右上角头像和提醒前面加同步进度…首次全量、之后增量;进来先展示本地数据, 一边同步一边更新;附件后台队列同步(名册头像 / 电影海报 / 观影照片 / 资源附件)。」 ① 整库加密(FbSchema v4→v5,新增 module_sync / attach_cache 两张表) RDB encrypt=true。加密属性一经创建不可修改 ⇒ openWithEncryption 三段式: 直接开 → 失败删库重建 → 再失败降级明文(degraded 字段,设置页如实显示, 不静默降级)。 ② 两层密钥(FbKeyStore) HUKS 里 KEK(fb_kek_v1,不可导出)包裹 32 字节 DEK;revoke() 删 KEK ⇒ 已落盘附件密文在数学上解不开,这就是"踢设备即吊销密钥"。 重新登录重新生成 DEK 并重下附件(不做服务端托管,为了省一次重下 把能解你本地数据的密钥放到服务端不值)。 会话属性里 PURPOSE 只出现一次:原来把含 ENCRYPT|DECRYPT 的生成属性直接 拿去开会话再 push 一个 PURPOSE,同一 tag 出现两次在 HUKS 里行为未定义 ⇒ 拆成 kekBaseProps / kekGenProps / kekSessionProps。 ③ AES-256-GCM(FbCrypto) 落盘 iv(12)|ct|tag(16);变换串必须带 padding 段(AES256|GCM|PKCS7, 缺了抛 801);解密从尾部切 tag。 ④ 附件后台队列(FbAttachQueue) attach_cache 台账(pending/done/fail,MAX_TRIES=3);密文放 filesDir、 明文临时放 cacheDir;扩展名白名单筛 key。 图片地址收成统一入口 fbImageSrc(key,token):本地有就用 file://, 没有才回网络 —— 收成一个函数才漏不掉(5 个页面全部改过来)。 ⑤ 被踢 / 退出清数据 lockOut → 清令牌 → FB_ATTACH.wipe(第一步就是吊销 KEK)→ FbDb.wipe (先关句柄再 deleteRdbStore,并发去重,open 会等它)→ 同步状态复位。 重新登录后重建密钥 + 库 + 附件索引,否则之后只读网络不写缓存且不报错。 ⑥ 增量同步(FbSyncAll 重写) 先问 /api/v1/changes;指纹相同就跳过。本地没水位必须拉(否则老用户升级后 一个模块都不同步);24h 陈旧上限兜"改无 updated_at 表的已有行"; /changes 拿不到时判 ping status>0 —— 断网直接收工,不退回全量 (否则 12 个模块各等 15 秒)。 家谱也进了预同步列表(原来只在打开时拉,离线是一片空白); 总步数由 CACHE_MODULES.length 算出,不写死。 ⑦ 删除传播 服务端是物理删除、没有墓碑 ⇒ 观影同步改成"先清后插" (persons 跨模块共用,不清)。 ⑧ 顶栏同步进度 头像与铃铛之间加三态按钮(转圈 / 红感叹号 / 灰对勾)+ bindSheet 面板: 进度条、「N 个模块没有变化」、附件行、失败清单、立即同步(防连点)。 状态中心 FbSyncState(@ObservedV2 + @Trace,AppStorageV2 单例); 界面直接读 @Trace 字段,不提供派生 getter。 验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 290/0(本批新增 59 条); _fb_v58_falsify.py 31 项注入 38/0;b57 反向验证 12/0(锚点跟上本批 logout 改动); 本地 e2e 1519/0;全量流水线 PUSH_RC=0。
This commit is contained in:
@@ -37,7 +37,7 @@ export const FB_DB_NAME: string = 'familybook.db';
|
||||
* 本地库**不能像服务端那样"每次启动都全量 ALTER 一遍"** ——
|
||||
* 那是拿用户的手机当试验田。必须按版本逐步迁移。
|
||||
*/
|
||||
export const FB_DB_VERSION: number = 4;
|
||||
export const FB_DB_VERSION: number = 5;
|
||||
|
||||
/**
|
||||
* 建表语句。
|
||||
@@ -205,6 +205,45 @@ export const FB_SCHEMA_SQL: string[] = [
|
||||
// 同一行只保留最后一次操作(合并重复改动)
|
||||
`CREATE UNIQUE INDEX IF NOT EXISTS idx_pending_row ON pending_ops (table_name, row_id)`,
|
||||
|
||||
/**
|
||||
* 🔴 v58:模块级同步水位(增量同步用)。
|
||||
*
|
||||
* 为什么不能复用上面的 `sync_state`:
|
||||
* 那张表记的是"**这张表**上次同步到哪个服务端时间",它服务于"按表增量拉"。
|
||||
* 而 `changes` 接口给的是"**这个模块**的变更指纹" —— 粒度不同(一个模块 = 多张表)。
|
||||
* 硬塞进同一张表会让两种语义互相覆盖,出问题时根本分不清是谁写的。
|
||||
*
|
||||
* `fp` 是服务端指纹的**原样字符串**(`行数:最大id:最大updated_at`)。
|
||||
* 端上**只做相等比较**,绝不解析它 —— 服务端换算法时端上不用改。
|
||||
*/
|
||||
`CREATE TABLE IF NOT EXISTS module_sync (
|
||||
name TEXT PRIMARY KEY,
|
||||
fp TEXT NOT NULL DEFAULT '',
|
||||
synced_at INTEGER NOT NULL DEFAULT 0,
|
||||
full_at INTEGER NOT NULL DEFAULT 0
|
||||
)`,
|
||||
|
||||
/**
|
||||
* 🔴 v59:附件缓存台账(海报 / 头像 / 观影照片 / 资源附件)。
|
||||
*
|
||||
* 为什么要有这张表而不是"直接看文件在不在":
|
||||
* · 要记**失败次数**(下载失败要退避重试,不能每次启动都重撞);
|
||||
* · 要记**大小**(面板上显示"已缓存 12.3MB");
|
||||
* · 要能在"踢设备"时**整表清空**(不该靠遍历目录猜哪些是我们下的)。
|
||||
*
|
||||
* `path` 是沙箱内的文件名(不含目录),内容为 AES 密文(见 `FbAttachQueue`)。
|
||||
* ⚠️ `key` 是**服务端相对键**(如 `posters/963dd25f.jpg`),与 `media.php?k=` 一致。
|
||||
*/
|
||||
`CREATE TABLE IF NOT EXISTS attach_cache (
|
||||
key TEXT PRIMARY KEY,
|
||||
path TEXT NOT NULL DEFAULT '',
|
||||
size INTEGER NOT NULL DEFAULT 0,
|
||||
state TEXT NOT NULL DEFAULT 'pending',
|
||||
tries INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at INTEGER NOT NULL DEFAULT 0
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_attach_state ON attach_cache (state, tries)`,
|
||||
|
||||
// ---------- 名册(v2 新增)----------
|
||||
// 🔴 为什么名册需要**自己**的表,而不是直接用 `persons`:
|
||||
// `persons` 是"全家的通讯录底账"(本家 800+ 人);
|
||||
@@ -277,7 +316,7 @@ export const FB_TABLE_GROUPS: FbTableGroup[] = [
|
||||
},
|
||||
{
|
||||
group: '同步元数据(本地专有)',
|
||||
tables: ['sync_state', 'pending_ops']
|
||||
tables: ['sync_state', 'pending_ops', 'module_sync', 'attach_cache']
|
||||
}
|
||||
];
|
||||
|
||||
@@ -291,7 +330,26 @@ export const FB_SYNCED_TABLES: string[] = [
|
||||
export const FB_STORE_CONFIG: relationalStore.StoreConfig = {
|
||||
name: FB_DB_NAME,
|
||||
securityLevel: relationalStore.SecurityLevel.S1,
|
||||
// ⚠️ 不开 encrypt:库内是家庭日程/观影记录这类普通数据;
|
||||
// 真要加密应走"整库加密 + 密钥进 KeyStore",不是在这里开个开关了事。
|
||||
encrypt: false
|
||||
/**
|
||||
* 🔴 v58(用户 2026-10-17 定 L3:**数据在但读不出**)—— 打开整库加密。
|
||||
*
|
||||
* 为什么这里开个开关就够了(而不像注释里原来写的那样"应该走 KeyStore"):
|
||||
* 鸿蒙 RDB 的 `encrypt: true` **本身就走系统密钥库** ——
|
||||
* 密钥由系统按「应用 + 设备」生成并托管在 HUKS 里,应用拿不到、也导不出明文。
|
||||
* 所以:库文件被拷走(root / 拆机 / 备份包)**没有那把密钥就是一堆密文**。
|
||||
* 这正是用户要的"捡到手机也读不出"。
|
||||
*
|
||||
* ⚠️⚠️ **加密属性一经创建不可修改**(ArkData 的硬约束):
|
||||
* 已装用户手里的是**明文库**,带 `encrypt:true` 打开会直接失败。
|
||||
* 处理办法在 `FbDb.doOpen()`:失败 → 删掉旧库 → 用加密参数重建 → 重新全量同步。
|
||||
* 敢删是因为**本地库只是服务端数据的缓存**(见本文件头"本地优先"那段),
|
||||
* 用户真正的东西都在服务器上;删除只会让下一次同步变慢,不会丢数据。
|
||||
* ⚠️ 但它**不等于安全擦除**:闪存上的旧块可能还有残留。真正的保证来自"新库是密文",
|
||||
* 不要把 `deleteRdbStore` 说成"数据已被彻底销毁"。
|
||||
*
|
||||
* ⚠️ 万一本机不支持加密(老系统/异常),`FbDb` 会**降级用明文打开**并把原因记进
|
||||
* `errorText()` —— 一个家庭应用宁可少一层保护,也不能变成打不开。
|
||||
* 这条降级路径必须在设置页的「诊断」里看得见,不能静默。
|
||||
*/
|
||||
encrypt: true
|
||||
};
|
||||
Reference in New Issue
Block a user