鸿蒙开发历险记 · 第2期
你给鸿蒙App加了AGC云同步功能,测试机上一切完美。直到有一天你换了一台手机登录同一个华为账号——云端数据像是从来没存在过。更诡异的是,换回旧手机又能用了。这不是网络问题,这是六个坑在连环等你。
这不是一次简单的bug修复,是一场从权限模型到密码学的完整排查。六个坑分两条线层层递进:
架构线(坑1-3):权限模型绑错了身份→权限放宽导致密钥明文可读→加密方案本身有密码学缺陷——每一层修复都暴露下一层更深的问题。
实现线(坑4-6):加密算法性能炸了主线程→schema校验规则反直觉→版本号不同步——都是真机落地才暴露的实现细节坑。
六个坑排查下来,最大的教训是:SDK源码是最好的文档,错误信息原文就是搜索关键词。

同一个华为账号,在设备A上同步的数据,换到设备B(或设备A卸载重装)登录同一账号后——读不到、也没法在原有记录基础上继续写。但同一设备不重装时完全正常,很容易被误判成"网络问题"或"偶发bug"。
对照初始化代码:
signInAnonymously()会在每次应用启动时产生一个新的匿名登录身份(UID),这个身份跟设备/安装绑定,跟华为账号完全无关。
AGC Cloud DB的权限规则(Creator/Authenticated/World/Administrator)判断"你是谁",只认AGC Auth产生的UID,不认业务层面的用户账号。项目原来的权限配置是Creator: Read/Upsert/Delete(仅创建者可读写),"创建者"指的就是这个匿名UID——换设备后匿名UID变了,等于变成了另一个人,自然读不到也写不了旧数据。
· 用AGC云函数HTTP触发器做服务端身份校验——已实际部署尝试过,被HDA-SYSTEM请求签名机制卡住,触发器要求签名、纯Bearer token会被拒绝为404,放弃。
· 让华为账号登录直接关联为AGC Auth身份——对照项目实际安装的@hw-agconnect/cloud@1.0.2客户端SDK源码(Auth.ts)验证过,这个方法不存在。SDK的signIn()只支持手机号/邮箱两种凭证类型,没有第三方OAuth/idToken凭证类型。
坑1的"正确方案"(AGC Auth关联华为账号)在当前SDK版本走不通,只能接受"权限放宽+App层过滤"的次优解。这不是懒,是SDK能力边界限制下的务实取舍——如果未来SDK支持了华为账号关联登录,应该优先升级到数据库原生隔离。
匿名登录保留(它是访问Cloud DB的门槛要求),但"谁能看到哪些数据"从AGC的Creator权限移交给App代码用真实的华为账号UnionID过滤:
AGC控制台权限规则从"仅创建者可读写"改成"已登录用户可读写"(Authenticated行的query/upsert/delete都勾上)。
坑1的方案上线后,权限从"仅创建者"放宽为"任意已登录用户"——这意味着任何能匿名登录的客户端,理论上都能读到所有用户的记录。而记录里的TOTP密钥(secret字段)当时是明文存储的。
设计一开始只考虑了"能不能同步成功",没考虑权限放宽这一步的连带影响:数据库层面的隔离降级了,但没有用加密去补上这个缺口。这不是权限放宽本身的错,是没有同步评估它对已有明文字段的影响。
权限放宽是一整套连带影响,不是改一个配置就完事。 放宽之前,先检查明文敏感字段是否需要加密、全局主键是否需要按用户维度做成复合主键。
上传前加密、下载后解密,加解密只发生在云端序列化的边界(buildRecord/recordToToken),App内其余逻辑拿到的始终是明文:
同时,secret原来是Zone内的全局唯一主键,不同账号之间会因为主键冲突互相覆盖对方记录。改成(userId, secret密文)复合主键,从根上防止跨账号覆盖:
坑2的加密方案上线后,代码review指出:encryptFieldForCloud对同一账号的所有记录都用同一个固定密钥做异或——即密文1 ⊕ 密文2 = 明文1 ⊕ 明文2。TOTP密钥是Base32字符集,account/brand又是明文存在旁边,给crib-dragging攻击留了空间。
密钥派生只用了unionId + 固定字符串,没有引入任何"每条记录/每个字段都不同"的变量。这是密码学上的经典错误——多次一密(two-time pad):同一个密钥流被复用到多条不同数据上,任何两条密文异或就能泄露明文的异或值。
"多次一密"是密码学课本第一章就会警告的错误,但在业务迭代中很容易被忽略——因为"每条记录用一个不同密钥"这件事,在第一版加密方案里根本没有被当作设计约束来考虑。加密不是"加上就行",salt里要包含能区分每条数据的唯一标识。
把salt派生从只用固定字符串,改成固定字符串 + recordId + fieldName三者拼接:
recordId(明文字段,加解密时都能拿到)和fieldName(字面量)参与派生,让每条记录的每个字段都有独立密钥流。同时保持确定性——同一账号对同一(recordId, fieldName)每次加密结果一致,供本地/云端按secret去重比对使用。
真机测试时,加一个口令后App卡住十几秒,提示"云端备份失败",然后App直接退出。
真机hilog里能直接看到系统看门狗的记录:
坑3的加密方案给secret/account/brand三个字段各跑一次PBKDF2(10000次迭代的纯JS HMAC-SHA1实现,没有硬件加速)。cloudBackup循环本地全部口令,每个口令3次密钥派生,同步跑在主线程上——JS单线程模型下async/await不会让纯CPU计算让出主线程,多个口令连续跑下来足以堵住主线程数秒,触发HarmonyOS的看门狗强杀进程。
PBKDF2从10000降到10,看似"不安全",实际是安全参数与使用场景不匹配。 10000次迭代的合理场景是"防止低熵、用户手输的密码被离线暴力破解"。这里派生密钥用的是账号UnionID,不是用户随手起的密码——真正需要的安全属性只是"确定性+每条记录每个字段唯一"(已由坑3的salt保证),迭代次数对这个目标没有实质帮助,只有CPU成本。而这个CPU成本在真机上高到会让看门狗直接杀掉App。
把迭代次数从10000降到10:
改完之后同样的操作耗时从卡死6+秒变成毫秒级,问题消失。
曾考虑换成3DES分组加密替代PBKDF2+XOR,评估后放弃:3DES已被NIST列为不建议新项目使用的过时算法,64位分组长度有已知弱点,换了性能和安全性都没有提升。真要升级,应该用鸿蒙自带的@ohos.security.cryptoFramework(原生AES,有硬件加速),但那是一次更大的独立改造,不适合当作应急修复。
坑2加了复合主键之后,initCloudDB()本地直接报错:
后来在别的字段上调整时,又反过来报了另一个错:
这两条都是SDK本地校验抛出的(不经过网络),对照SDK源码逐字核实:
两条规则合起来是:主键字段禁止设置默认值;非主键的notNull: true字段必须设置默认值(哪怕值是空字符串)——正好相反。改动主键结构时,容易只改了主键那个字段的默认值配置,忘了检查其他notNull字段是不是也要补默认值。

对照上面两条规则逐字段核对:
这类本地校验错误信息本身已经把规则讲得很清楚了,直接去SDK源码里搜错误信息原文定位校验函数,比对着控制台截图去猜字段该不该有默认值快得多、准得多。
坑5修完、本地校验通过之后,实际写入数据时报了一个来自服务端的错误:
卡了十几秒才返回,不是立刻报错。
对照SDK源码,"object type is not found"对应错误码1006001(INVALID_SCHEMA_NAME),是服务端返回的错误。进一步查schemaVersion的流转路径,发现它会被塞进请求上下文参与服务端的schema版本匹配。
这个项目里,对象类型的复合主键是在AGC控制台里手工删除重建过一次的。控制台删除重建之后,服务端记录的对象类型版本状态发生了变化,但客户端代码里schemaVersion还写死在旧的数字上——版本号对不上,服务端直接认为"没有这个schema"。
排查走了一点弯路:先把schemaVersion改成一个新数字后,报错变成了object type field mismatched(方向对了,但字段还有出入);然后对着控制台截图猜字段属性,来回改了两轮都没对上。最终决定不再对着截图猜,而是把控制台对象类型整个删除重建,逐字段严格按客户端代码现有声明重新配置一遍,重建后控制台会显示一个新的版本号,把这个数字同步更新到代码里,问题才彻底解决。
客户端schema声明和控制台对象类型定义,是两份需要手动保持一致的独立状态,schemaVersion是它们之间唯一的匹配锚点。每次在控制台改了对象类型(尤其是删除重建),都要把控制台显示的新版本号同步更新到代码里。 如果报出"object type"相关的模糊错误、且已经改了两轮字段属性还没修对,别继续对着截图猜,直接控制台删除重建、逐字段严格对照代码配置一遍,比反复猜快得多。
initCloudDB()返回Promise<boolean>,失败时返回false——但调用方一直没检查这个返回值:
排查坑6的过程中,这个误导性日志一度让人以为失败发生在下游,而不是初始化本身。修复很直接:
跟上一篇同样的@Watch根治方案:

☐ 换设备/重装后数据读写失效 → 先确认Cloud DB权限规则判定的"创建者"是不是绑定在设备本地的匿名登录UID上,而不是业务账号
☐ 放宽了Cloud DB权限规则 → 同步检查明文敏感字段是否需要加密、全局主键是否需要按用户维度做成复合主键——权限放宽是一整套连带影响
☐ 自己实现加密派生密钥 → 确认同一账号/同一密钥是否被复用到多条不同数据上(多次一密),salt里要包含能区分每条数据的唯一标识
☐ App操作耗时长甚至被系统杀掉 → 检查是否在主线程做了同步的重计算(哈希循环、加解密),先明确到底需要什么级别的安全强度,别无脑套用"密码存储"场景的高强度参数
☐ initCloudDB()/schema初始化本地报错(不经过网络)→ 去SDK源码里搜错误信息原文定位校验规则,别对着控制台猜;已验证的两条规则:主键字段禁止默认值,非主键notNull字段必须有默认值
☐ 云端写入/查询报"object type"相关服务端错误 → 检查schemaVersion是否跟控制台当前版本一致,尤其是控制台手工改过对象类型(特别是删除重建过)之后;两轮猜字段属性都没修对,就直接控制台删除重建、逐字段对照代码严格配置
① 报错信息(尤其是本地校验抛出来的)往往已经把规则讲清楚了,先去SDK源码里搜错误文本原文定位校验函数,不要凭经验猜
② 服务端返回的模糊错误("object type not found/mismatched")没法只靠客户端代码判断,该去控制台核实真实配置状态时就去核实,别在代码这一侧空转
③ 对着截图判断"某个输入框是空的"≠"没有配置这个属性",这类视觉信息本身就有歧义,能找到权威源码/文档验证的,优先用源码验证
④ 同一类"某处忘了做某件事"的症状在2处以上复现,说明是设计问题不是漏洞补丁问题,换成响应式/集中式写法,而不是继续挨个排查调用点
⑤ 性能问题(主线程卡死、App被杀)跟功能性bug要分开看,先用真机日志/看门狗记录确认是不是真的卡在计算上,再决定是降低算法强度还是换执行方式
上一篇我们聊了IAP恢复购买的三个暗坑,这一篇的六个坑更深层——涉及权限模型和密码学。你对接入AGC云数据库最大的顾虑是什么?留言聊聊。
上一篇:明明买了会员却恢复不了?鸿蒙IAP三个暗坑实录 → 关注系列不迷路

往期推荐
News Watch
明明买了会员却恢复不了?鸿蒙IAP三个暗坑排查实录
2026-07-03

我只说了一句话,AI就"猜"中了我的梦想书房——谷歌这步棋,比想象中更狠
2026-07-01

我用AI+下班时间开发的鸿蒙App,正式上架了:但这套隐藏流程,比写代码更让人崩溃
2026-06-26

一个@符号,正在悄悄颠覆职场——你还在假装看不见?
2026-06-24
