0.1.99 的故事:钥匙串从来没写成功过,而我们今天才知道

今天 James 在 0.1.98 上登录:码是对的,服务器也确认建立了会话,但 App 界面像什么都没发生,再点一次还报「码用过了」。

查下来是一个让人后背发凉的事实:正式发布包的钥匙串写入,从第一天起就没有成功过。

三层防御是怎么一层层漏掉的

第一层:我们调钥匙串时带了一个较新的存储标志(Data Protection keychain)。它要求应用签名里有一项特定授权,而 Developer ID 方式分发的 Mac 应用默认没有这项授权——于是每次写入都被系统拒绝。开发调试用的另一种签名恰好也没有,所以任何环境都没成功过,只是没人看到报错。

第二层:为什么没人看到?因为 0.1.97 有一份 UserDefaults 明文副本做兜底。钥匙串写失败,副本还在,登录看起来一切正常。兜底机制把主路径的失败完全掩盖了——它太好用了,好用到主路径死了两个版本都没人发现。

第三层:0.1.98 删掉明文副本。这个决定本身是对的(任何本机进程一条命令就能读走凭证,我们自己当场演示过)——但删兜底之前,没有人验证过主路径是活的。删完,登录态彻底无处可存。

第四层,也是最不该漏的一层:0.1.98 发布前的真机验证没有覆盖「全新登录」这条路。当时机器上已经有登录态(靠旧副本活着),看起来一切正常。

修法,和比修法更重要的教训

修法本身很小:放弃那个需要特殊授权的存储方式,改用系统经典钥匙串——对 Developer ID 应用完全可用,落盘加密,其他程序想读会触发系统授权框。顺手修了「回车 + 点按钮重复提交,第二次的报错顶掉第一次的真实结果」。

教训比修法值钱:

一、兜底机制必须报告它在兜底。 静默的兜底不是可靠性,是遮眼布。如果 0.1.97 在「从副本恢复」时打一行显眼的日志、甚至在界面上提示,这个问题两个版本前就暴露了。

二、删兜底之前,先证明主路径活着。 「删掉备胎」这个动作,隐含的前提是「主胎没漏气」。这个前提要验证,不能推断。

三、发布前的真机验证要包含「从零开始」。 带着旧状态的机器验不出初始化路径的问题。每个和存储、登录、首启相关的版本,都要有一次干净状态下的完整流程。

这三条已经写进仓库的可靠性文档,下次砍任何兜底机制之前,先过这张单子。