用户 2026-10-18 六条(全部已在真机逐条验证):
① 登录页「先不登录 / 离线使用」彻底删除
用户:「把那些不登录、看离线数据之类的提示全部去掉。以后就不再拥有这样的
功能了,把这些功能也全部删除。」
⇒ 两个按钮 + onSkip + allowSkip 参数 + Index 里的 skip 分支一起删净。
这是产品决定(不是隐藏):该路径本身就是安全缺口,v58 上了整库加密后更要堵死。
② 标语统一为「一家人用岁月写成的一本书」
用户:「这也是我们"家书"这个名字的由来。所以你不要再乱换这个标语了,
只要是想出现标语或者副标题的地方,就用这样一句话。」
· 端上 FB_DEFAULT_SLOGAN 是唯一定义处(登录页副标题 + 抽屉回落都用它);
· 抽屉左上角原来是家庭名,现在显示**标语**;
· 三层优先级(个人 slogan → 家庭 motto → 应用默认口号)**在服务端算**
(Family::slogan,与网页端首页 heroSlogan 同一套),/login 与 /me 都返回。
用户还说:「如果这个用户自己有标语……这个地方就是用户自己自定义的标语。」
真机实测:登录者杨永全自己写了「修身,齐家」⇒ 抽屉就显示他那句。
③ 首页新增「最近纪念日」(放在近期任务之后、往年的今天之前)
用户:「未来最近的三个纪念日,包括每个人的生日,以及手动填写的各个纪念日。
以倒计时形式展示:距离哪个纪念日还有多少天,距离谁谁谁的生日还有多少天。」
· /home.recent_days = Anniversary::upcoming(fam,pid,370) 取前 3 ——
取满一年:窗口太小会让"刚过完生日"的家庭一个都取不到,模块直接空着;
· 数据源 all() = 派生生日 + 自建纪念日,正好覆盖用户说的两样;
· 倒计时:0 天显示「就是今天」(写「还有 0 天」读起来像已经过了)。
④ 悬浮栏图标也上色(用户:「除了下面的汉字,上面图标的颜色也变成跟汉字一样的颜色」)
🔴🔴 真根因两层,都是"无报错失效":
· 第一层:Image.fillColor() **只对 SVG 生效**,而本工程 30+ 图标**全是 PNG**
⇒ 全工程 24 处图标染色一直是空操作(文字走 fontColor 所以只有文字变了)。
· 第二层(真机才现形):改用 colorFilter 后,矩阵写成了"系数乘 RGB"
(R' = R_原 × 系数) —— 对白色图标成立,但本工程图标是**纯黑**,
黑 × 任何系数 = 仍是黑 ⇒ 真机上图标像素依旧是 (0,0,0)。
⇒ 最终形态:`fbTint` 用 **alpha 当载体**(R' = (r/255) × A_原):
系数是归一化约定(官方灰度示例系数和为 1,量纲确定);
形状完全由原图 alpha 决定 ⇒ 黑/白图标都成立,描边抗锯齿自然过渡。
真机像素实测:日程项图标与文字同为 (176,120,64)≈#B0763A;
未选中项图标 (44,39,35) 深灰(不再是纯黑)。
⚠️ 全工程 24 处 fillColor 一并改掉;FbIconButton.tint 类型改成 string,
让"误传 Resource"变成编译错误而不是又一个静默失效。
⑤ 顶栏:真实头像 + 拉开按钮间距
用户:「右上角的头像应该是真实的……现在提醒的按钮距离头像的按钮太近了。」
· /home 给 me_avatar、/login 给 avatar_url(avatar_url() 按线上存储驱动拼好);
· 三颗按钮(同步 / 铃铛 / 头像)统一用 FbMetric.gapTopActions = 8。
⑥ 日程四处
· 「日期下面有几个点」⇒ 月视图与周视图的圆点全部删除,
连 FbDot / dots() / count / dotColor 一起清(不留死代码);
· 「农历全部都是初一」⇒ 真根因是端上 lunarOf() **硬编码 return '初一'**,
而服务端 lunarRange() 本来就每天给真实农历(title)⇒ 改取 title;
· 「列表这个视图,今天的数据缺一条」⇒ 服务端 /calendar 的 from/to **默认只给本月**,
而列表视图要显示 -7 ~ +60 天 ⇒ 端上显式传范围
(loadModule 新增 pathOverride 参数);
· 「点击了某一天之后并不明显,应该把这一天的颜色变深,而今天用一个较浅的颜色」
⇒ 照 SyncCalendar:选中 = 深色实底白字、今天 = 浅色底。
真机实测:17 号(选中)bg=#FFB0763A,18 号(今天)bg=#FFF5F0EA。
另:FbKeyStore 补上一个真机抓到的 HUKS 坑 —— isKeyItemExist 在密钥不存在时
**会抛 12000011**(不是返回 false),首次运行必被误判成"本机不支持 HUKS"
⇒ 永久明文降级。改成把"不存在"当正常分支,且降级态每次启动重试一次
(把同一把 DEK 重新用 KEK 包起来,不换密钥以免重下全部附件)。
验证:hvigor BUILD SUCCESSFUL;_fb_arkts_guard.js 328/0;
_fb_v59ui_falsify.py 19 项注入 23/0;_fb_home_api.py 60/0;
六条已在真机逐条取像素/布局树确认。
390 lines
16 KiB
Plaintext
390 lines
16 KiB
Plaintext
/**
|
||
* 家书 FamilyBook · 本地数据密钥(鸿蒙端,v58)
|
||
*
|
||
* 🔴 这里解决的是用户 2026-10-17 定的 L3 那一句:
|
||
* 「本地加密(HarmonyOS 密钥库 HUKS,踢设备即吊销密钥):数据在但读不出,
|
||
* 重登后恢复密钥即可用。」
|
||
*
|
||
* 做法(两层密钥):
|
||
* · **KEK**(密钥加密密钥):HUKS 里生成,别名 `fb_kek_v1`。**不可导出**,
|
||
* 跟着「应用 + 本机」绑定 —— 拷走文件也拿不到它。
|
||
* · **DEK**(数据密钥):32 字节随机数,真正用来 AES-GCM 加密附件内容。
|
||
* 它**用 KEK 包裹**后存在 preferences 里(`dek_wrap`)。
|
||
* ⇒ 删掉 HUKS 里的 KEK("踢设备"/退出登录)→ 包裹体永远解不开 → DEK 没了
|
||
* → 附件全成乱码。这就是"数据在但读不出"。
|
||
* ⇒ 重新登录后:**生成一把新 DEK、重新下载附件**。
|
||
* 为什么不做"用密码从服务端取回原 DEK":那等于让服务端持有一把能解开
|
||
* 你手机本地数据的密钥 —— 为了省一次重下,反而把风险搬到了服务端。
|
||
* 本地库/附件本来就只是服务端数据的缓存,重下是正确且便宜的恢复路径。
|
||
*
|
||
* ⚠️⚠️ **降级路径是刻意设计的,不是偷懒**:
|
||
* 如果这台机器上 HUKS 用不了(老系统、KeyStore 异常、被安全策略禁掉),
|
||
* 我们**不会**让附件功能瘫掉 —— 而是把 DEK **明文**存进 preferences,
|
||
* 并把 `weak` 置为 true。
|
||
* 代价:这台机器上"密钥可被拷走",加密只剩一层(RDB 那层还在)。
|
||
* 收益:用户还能正常看图。
|
||
* ⚠️ 这个 `weak` **必须在设置页的「诊断」里如实显示**,不能静默降级
|
||
* —— 静默降级等于骗用户"你的数据是加密的"。
|
||
*/
|
||
|
||
import { huks } from '@kit.UniversalKeystoreKit';
|
||
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
|
||
import { preferences } from '@kit.ArkData';
|
||
import { util } from '@kit.ArkTS';
|
||
import { common } from '@kit.AbilityKit';
|
||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||
import { BusinessError } from '@kit.BasicServicesKit';
|
||
|
||
const DOMAIN: number = 0x0000;
|
||
const TAG: string = 'FbKeyStore';
|
||
|
||
const STORE_NAME: string = 'fb_keys';
|
||
/** 包裹后的 DEK(base64(iv|ct|tag)) */
|
||
const KEY_WRAP: string = 'dek_wrap';
|
||
/** 降级时明文 DEK(base64)。正常路径下**不存在** */
|
||
const KEY_PLAIN: string = 'dek_plain';
|
||
/** HUKS 里 KEK 的别名(删它 = 吊销) */
|
||
const KEK_ALIAS: string = 'fb_kek_v1';
|
||
|
||
const DEK_LEN: number = 32;
|
||
const IV_LEN: number = 12;
|
||
const TAG_LEN: number = 16;
|
||
|
||
/**
|
||
* HUKS「条目不存在」错误码。
|
||
*
|
||
* 🔴🔴 2026-10-18 真机实测(harmonyos 真机,非模拟器):
|
||
* `huks.isKeyItemExist(alias, ...)` 在密钥**不存在**时**会抛异常**,
|
||
* 而不是文档说的"返回 false" —— 抛出的 code 正是 12000011
|
||
* (HUKS_ERR_CODE_ITEM_NOT_EXIST;HUKS 服务侧日志为
|
||
* `HksKeyExist: leave KeyExist, result = -13`,-13 = HKS_ERROR_NOT_EXIST)。
|
||
*
|
||
* 首次运行一定走到这条(KEK 还没生成)⇒ 如果不接住这个异常,
|
||
* 就会被当成"本机不支持 HUKS",直接**永久降级成明文密钥**
|
||
* (实测症状:设置页显示"已降级(明文保存)",而 HUKS 其实完全可用)。
|
||
*/
|
||
const HUKS_ERR_ITEM_NOT_EXIST: number = 12000011;
|
||
|
||
class FbKeyStoreImpl {
|
||
private store: preferences.Preferences | null = null;
|
||
private dekBytes: Uint8Array | null = null;
|
||
private weak: boolean = false;
|
||
private ready: boolean = false;
|
||
private lastError: string = '';
|
||
|
||
/**
|
||
* KEK 的**固定**属性(算法 / 长度 / 填充 / 分组模式)。
|
||
*
|
||
* ⚠️ 这三项 + `PURPOSE` 是 HUKS 生成密钥必须的一整套;而"加/解密会话"也
|
||
* 需要同一套里的算法与模式。**两处共用这一份,各自再补一个 `PURPOSE`** ——
|
||
* 不能像最初那样把 `kekGenProps()`(里面已经带了 `ENCRYPT|DECRYPT`)
|
||
* 直接拿去开会话再 push 一个 `PURPOSE`:**同一个 tag 出现两次**的行为
|
||
* 在 HUKS 里没有定义(不同版本可能取第一个,也可能直接报错)。
|
||
*/
|
||
private kekBaseProps(): huks.HuksParam[] {
|
||
return [
|
||
{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES },
|
||
{ tag: huks.HuksTag.HUKS_TAG_KEY_SIZE, value: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256 },
|
||
{ tag: huks.HuksTag.HUKS_TAG_PADDING, value: huks.HuksKeyPadding.HUKS_PADDING_NONE },
|
||
{ tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE, value: huks.HuksCipherMode.HUKS_MODE_GCM }
|
||
];
|
||
}
|
||
|
||
/** 生成 KEK 用(固定属性 + 加解密两个用途) */
|
||
private kekGenProps(): huks.HuksParam[] {
|
||
const props: huks.HuksParam[] = this.kekBaseProps();
|
||
props.push({
|
||
tag: huks.HuksTag.HUKS_TAG_PURPOSE,
|
||
value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT
|
||
| huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT
|
||
});
|
||
return props;
|
||
}
|
||
|
||
/** 开会话用(固定属性 + **单个**用途,绝不让 `PURPOSE` 出现两次) */
|
||
private kekSessionProps(purpose: number): huks.HuksParam[] {
|
||
const props: huks.HuksParam[] = this.kekBaseProps();
|
||
props.push({ tag: huks.HuksTag.HUKS_TAG_PURPOSE, value: purpose });
|
||
return props;
|
||
}
|
||
|
||
/**
|
||
* 预加载 + 拿到可用的 DEK(启动时调一次即可)。
|
||
*
|
||
* @returns DEK 是否可用(true = 附件可以正常读写)
|
||
*/
|
||
async init(ctx: common.Context): Promise<boolean> {
|
||
if (this.ready) {
|
||
return this.dekBytes !== null;
|
||
}
|
||
try {
|
||
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
||
} catch (err) {
|
||
hilog.warn(DOMAIN, TAG, '密钥库预加载失败:%{public}s', JSON.stringify(err));
|
||
this.ready = true;
|
||
return false;
|
||
}
|
||
|
||
// ① 上次降级过(明文 DEK 在):先用它把数据救活,但**不要就此认命** ——
|
||
// HUKS 可能是被临时性原因挡住的(系统刚升级、密钥库服务还没起来…),
|
||
// 下次启动也许就好了。所以这里试着把同一把 DEK **重新用 KEK 包起来**;
|
||
// 只有真包不上才继续留在降级态。
|
||
// 🔴 2026-10-18 为什么这样改:原来只要见过一次 KEY_PLAIN 就永远走降级
|
||
// ("上次已降级,别再试一遍 HUKS 白等")⇒ 一次误判 ⇒ **终身明文**。
|
||
// 真机实测正是如此:`isKeyItemExist` 抛的一个"不存在"被误当成"不支持",
|
||
// 之后每次启动都直接沿用明文,HUKS 明明可用也升不回来。
|
||
const plainV: preferences.ValueType = await this.store.get(KEY_PLAIN, '');
|
||
if (typeof plainV === 'string' && plainV !== '') {
|
||
const plainDek: Uint8Array | null = this.b64Decode(plainV);
|
||
this.dekBytes = plainDek;
|
||
this.weak = true;
|
||
this.ready = true;
|
||
hilog.info(DOMAIN, TAG, '读到了上次的降级密钥,尝试重新用 HUKS 保护');
|
||
if (plainDek !== null) {
|
||
await this.tryUpgradeFromPlain(plainDek);
|
||
}
|
||
return this.dekBytes !== null;
|
||
}
|
||
|
||
// ② 正常路径:解包
|
||
await this.loadOrCreate(ctx);
|
||
this.ready = true;
|
||
return this.dekBytes !== null;
|
||
}
|
||
|
||
/**
|
||
* 把"已降级"的那把明文 DEK **重新包进 HUKS**(成功则升回正常态)。
|
||
*
|
||
* ⚠️ 只重新包裹、**不换密钥**:附件密文是用这把 DEK 加的,
|
||
* 换一把会让已落盘的附件全部解不开(白下几百个文件)。
|
||
* 等价的错误做法是"直接调 loadOrCreate 生成新 DEK" —— 那等于每次
|
||
* 从降级态恢复都要重下全部附件。
|
||
*/
|
||
private async tryUpgradeFromPlain(dek: Uint8Array): Promise<void> {
|
||
if (this.store === null) {
|
||
return;
|
||
}
|
||
const wrapped: string | null = await this.wrap(dek);
|
||
if (wrapped === null) {
|
||
hilog.info(DOMAIN, TAG, 'HUKS 仍不可用,继续使用明文密钥');
|
||
return;
|
||
}
|
||
this.weak = false;
|
||
this.lastError = '';
|
||
await this.store.put(KEY_WRAP, wrapped);
|
||
await this.store.delete(KEY_PLAIN);
|
||
await this.store.flush();
|
||
hilog.info(DOMAIN, TAG, '已把降级的密钥重新交给 HUKS 保护(不再明文保存)');
|
||
}
|
||
|
||
/** 读包裹体 → 解包;没有包裹体就新生成一把 */
|
||
private async loadOrCreate(ctx: common.Context): Promise<void> {
|
||
if (this.store === null) {
|
||
return;
|
||
}
|
||
const wrapV: preferences.ValueType = await this.store.get(KEY_WRAP, '');
|
||
const wrap: string = typeof wrapV === 'string' ? wrapV : '';
|
||
|
||
if (wrap !== '') {
|
||
const dek: Uint8Array | null = await this.unwrap(wrap);
|
||
if (dek !== null) {
|
||
this.dekBytes = dek;
|
||
this.weak = false;
|
||
hilog.info(DOMAIN, TAG, '本地数据密钥已解包(HUKS 保护)');
|
||
return;
|
||
}
|
||
// ⚠️ 解包失败 = KEK 已被删(被踢/清过数据)⇒ 旧附件**永久读不出**。
|
||
// 这里不报错,直接换新 DEK:旧缓存会在队列里被判成"无效"并重下。
|
||
hilog.warn(DOMAIN, TAG, '包裹体解不开(密钥已被吊销),换一把新的数据密钥');
|
||
}
|
||
|
||
// 新 DEK
|
||
const fresh: Uint8Array | null = this.randomBytes(DEK_LEN);
|
||
if (fresh === null) {
|
||
this.lastError = '随机数生成失败';
|
||
return;
|
||
}
|
||
const wrapped: string | null = await this.wrap(fresh);
|
||
if (wrapped === null) {
|
||
// ③ 降级:明文存
|
||
this.weak = true;
|
||
this.dekBytes = fresh;
|
||
this.lastError = 'HUKS 不可用,密钥以明文保存(已降级)';
|
||
await this.store.put(KEY_PLAIN, this.b64Encode(fresh));
|
||
await this.store.delete(KEY_WRAP);
|
||
await this.store.flush();
|
||
hilog.warn(DOMAIN, TAG, '本地数据密钥降级为明文保存:%{public}s', this.lastError);
|
||
return;
|
||
}
|
||
this.weak = false;
|
||
this.dekBytes = fresh;
|
||
await this.store.put(KEY_WRAP, wrapped);
|
||
await this.store.delete(KEY_PLAIN);
|
||
await this.store.flush();
|
||
hilog.info(DOMAIN, TAG, '新数据密钥已生成并用 HUKS 包裹');
|
||
}
|
||
|
||
/**
|
||
* KEK 是否已存在。
|
||
*
|
||
* ⚠️ **不能用** `huks.isKeyItemExist()` 的返回值直接判 —— 它在"不存在"时
|
||
* 会抛 `12000011`(见 `HUKS_ERR_ITEM_NOT_EXIST` 的说明)。
|
||
* ⇒ 这里把"不存在"(异常码 / 返回 false)统一成 false,
|
||
* 真正的不支持(其它错误码)才往上抛,由调用方决定是否降级。
|
||
*/
|
||
private async kekExists(): Promise<boolean> {
|
||
try {
|
||
return await huks.isKeyItemExist(KEK_ALIAS, { properties: [] });
|
||
} catch (err) {
|
||
const e: BusinessError = err as BusinessError;
|
||
if (e.code === HUKS_ERR_ITEM_NOT_EXIST) {
|
||
return false; // 正常情况:还没生成过 KEK
|
||
}
|
||
// ⚠️ ArkTS 的 `throw` **不接受任意类型**(arkts-limited-throw)——
|
||
// catch 到的 `err` 是 unknown,`throw err` 编译不过,必须包成 Error。
|
||
throw new Error('判断 KEK 是否存在失败:code='
|
||
+ (e.code !== undefined ? e.code : -1)
|
||
+ ' msg=' + (e.message !== undefined ? e.message : ''));
|
||
}
|
||
}
|
||
|
||
/** 用 HUKS 的 KEK 加密 DEK,返回 base64(iv|ct|tag);失败返回 null(→ 降级) */
|
||
private async wrap(dek: Uint8Array): Promise<string | null> {
|
||
try {
|
||
const exists: boolean = await this.kekExists();
|
||
if (!exists) {
|
||
await huks.generateKeyItem(KEK_ALIAS, { properties: this.kekGenProps() });
|
||
hilog.info(DOMAIN, TAG, 'HUKS 里已生成 KEK(不可导出)');
|
||
}
|
||
const iv: Uint8Array | null = this.randomBytes(IV_LEN);
|
||
if (iv === null) {
|
||
return null;
|
||
}
|
||
const props: huks.HuksParam[] =
|
||
this.kekSessionProps(huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT);
|
||
props.push({ tag: huks.HuksTag.HUKS_TAG_NONCE, value: iv });
|
||
const opt: huks.HuksOptions = { properties: props, inData: dek };
|
||
const handle: huks.HuksSessionHandle = await huks.initSession(KEK_ALIAS, opt);
|
||
const ret: huks.HuksReturnResult = await huks.finishSession(handle.handle, opt);
|
||
const out: Uint8Array | undefined = ret.outData;
|
||
if (out === undefined || out.length === 0) {
|
||
return null;
|
||
}
|
||
// 落盘格式:iv(12) | 密文+tag
|
||
const blob: Uint8Array = new Uint8Array(IV_LEN + out.length);
|
||
blob.set(iv, 0);
|
||
blob.set(out, IV_LEN);
|
||
return this.b64Encode(blob);
|
||
} catch (err) {
|
||
const e: BusinessError = err as BusinessError;
|
||
hilog.warn(DOMAIN, TAG, 'HUKS 包裹失败:code=%{public}d msg=%{public}s',
|
||
e.code !== undefined ? e.code : -1,
|
||
e.message !== undefined ? e.message : JSON.stringify(err));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 解包;失败返回 null(= 密钥已吊销) */
|
||
private async unwrap(b64: string): Promise<Uint8Array | null> {
|
||
try {
|
||
const blob: Uint8Array | null = this.b64Decode(b64);
|
||
if (blob === null || blob.length <= IV_LEN + TAG_LEN) {
|
||
return null;
|
||
}
|
||
const iv: Uint8Array = blob.slice(0, IV_LEN);
|
||
const body: Uint8Array = blob.slice(IV_LEN);
|
||
const ct: Uint8Array = body.slice(0, body.length - TAG_LEN);
|
||
const tag: Uint8Array = body.slice(body.length - TAG_LEN);
|
||
|
||
const props: huks.HuksParam[] =
|
||
this.kekSessionProps(huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT);
|
||
props.push({ tag: huks.HuksTag.HUKS_TAG_NONCE, value: iv });
|
||
props.push({ tag: huks.HuksTag.HUKS_TAG_AE_TAG, value: tag });
|
||
const opt: huks.HuksOptions = { properties: props, inData: ct };
|
||
const handle: huks.HuksSessionHandle = await huks.initSession(KEK_ALIAS, opt);
|
||
const ret: huks.HuksReturnResult = await huks.finishSession(handle.handle, opt);
|
||
const out: Uint8Array | undefined = ret.outData;
|
||
return out !== undefined && out.length === DEK_LEN ? out : null;
|
||
} catch (err) {
|
||
hilog.warn(DOMAIN, TAG, 'HUKS 解包失败(密钥多半已被吊销):%{public}s', JSON.stringify(err));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 当前 DEK(null = 还没有 / 已被吊销) */
|
||
dek(): Uint8Array | null {
|
||
return this.dekBytes;
|
||
}
|
||
|
||
/** 是否处于"降级"状态(密钥明文保存)—— 设置页诊断要显示 */
|
||
isWeak(): boolean {
|
||
return this.weak;
|
||
}
|
||
|
||
/** 诊断用:最近一次失败原因 */
|
||
errorText(): string {
|
||
return this.lastError;
|
||
}
|
||
|
||
/**
|
||
* 🔴 **吊销密钥**(被踢下线 / 彻底退出登录时调)。
|
||
*
|
||
* 删 HUKS 里的 KEK + 删包裹体 + 清内存 → 已经落盘的附件密文**再也解不开**
|
||
* (不是"暂时不给看",是数学上解不开)。这是用户要的"踢设备即吊销密钥"。
|
||
*
|
||
* ⚠️ 与"清库"的先后顺序不重要,但**两件都要做**:
|
||
* 只吊销密钥不清库 = 库里还有明文 JSON 缓存;只清库不吊销 = 附件密文还在。
|
||
*/
|
||
async revoke(ctx: common.Context): Promise<void> {
|
||
this.dekBytes = null;
|
||
this.weak = false;
|
||
try {
|
||
await huks.deleteKeyItem(KEK_ALIAS, { properties: [] });
|
||
hilog.info(DOMAIN, TAG, 'HUKS 里的 KEK 已删除');
|
||
} catch (err) {
|
||
// 本来就不存在也会走到这里 —— 不算错误
|
||
hilog.info(DOMAIN, TAG, '删除 KEK 时返回(可能本来就不存在):%{public}s', JSON.stringify(err));
|
||
}
|
||
try {
|
||
if (this.store === null) {
|
||
this.store = await preferences.getPreferences(ctx, STORE_NAME);
|
||
}
|
||
await this.store.delete(KEY_WRAP);
|
||
await this.store.delete(KEY_PLAIN);
|
||
await this.store.flush();
|
||
} catch (err) {
|
||
hilog.warn(DOMAIN, TAG, '清除包裹体失败:%{public}s', JSON.stringify(err));
|
||
}
|
||
// 吊销之后重新初始化:下一次用附件时会生成一把全新的 DEK(旧缓存作废)
|
||
this.ready = false;
|
||
}
|
||
|
||
/* ---- 小工具 ---- */
|
||
|
||
private randomBytes(n: number): Uint8Array | null {
|
||
try {
|
||
const rnd: cryptoFramework.Random = cryptoFramework.createRandom();
|
||
const blob: cryptoFramework.DataBlob = rnd.generateRandomSync(n);
|
||
return blob.data;
|
||
} catch (err) {
|
||
hilog.error(DOMAIN, TAG, '随机数失败:%{public}s', JSON.stringify(err));
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private b64Encode(b: Uint8Array): string {
|
||
return new util.Base64Helper().encodeToStringSync(b);
|
||
}
|
||
|
||
private b64Decode(s: string): Uint8Array | null {
|
||
try {
|
||
return new util.Base64Helper().decodeSync(s);
|
||
} catch (err) {
|
||
return null;
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 全局单例 */
|
||
export const FB_KEY_STORE: FbKeyStoreImpl = new FbKeyStoreImpl();
|