关于 Base64 的一次调试经历

2019-08-07 10:18:57 +08:00
 shawndev

下班前同事突然叫住我,「晨晓,这里有个问题你帮忙看一下」。

著名佚名人士曾说过——最好的下班时间是六点,其次是现在。但我,六点没有下班,现在也没有下班。

简要复述一下问题,开发一个包含加解密报文的 SDK,在 SDK 中测试数据可以正常加解密,而集成了 SDK 的应用手动输入数据加解密却总是解密失败。

我叮嘱同事先检查报文各个部分的长度是否和设计文档一致,确定数据的有效性;然后对每个步骤独立执行,确定密钥的正确性,同事检查后反馈这两项没有问题。

首先查看函数的调用,检查传入参数。

// 加密
NSString *plaintext = @"test";
NSData *encrypted = [SDKCryptor encrypt:plaintext];
...
// 解密
NSData *decrypted = [SDKCryptor decrypt:encrypted];
NSString *message = [[NSString alloc] initWithData:decrypted
                                          encoding:NSUTF8StringEncoding];
// 解密失败 decrypted 为空

确认传入参数没有问题后,检查 SDK 的实现,忽略掉无关逻辑后注意到这样一行代码。

@implementation SDKCryptor
+ (NSData *)encrypt:(NSString *)plain {
  NSData *pubKey = [Keychain pubKey];
  NSData *encoded = [[NSData alloc] initWithBase64EncodedString:plain options:0];
  NSData *encrypted = [RSAUtil encrypt:encoded withPubKey:pubKey];
  return encrypted;
}
@end

这里 initWithBase64EncodedString:options: 的用法引起了我的注意,入参原本应该是 base64EncodedString,即经过 base64 编码的字符串,而入参"test"显然没有经过 base64 编码。

SDK 和测试代码在同一工程下,修改代码可以立即生效,但集成应用需要每次将 SDK 工程重新打包后才能够测试(私有项目所以没有采用 Carthage 管理 framework )。因此没有急于同时修改 SDK 和测试应用的代码观察结果。

同事显然不能信服这么低级的方案,坚持再次运行了 SDK 的测试代码,居然真的解密出来了"test"。

只好继续排查。通过对 SDKCryptor 的 encrypt:方法断点,在 SDK 的测试代码中 data 确实返回了值。b5eb2d 看到这里我确定,问题就出在这里。

我向同事解释,base64 后的字节长度一定大于原始信息,且至少是原始数据的 4/3 倍长度。这是由 base64 编码方式决定的,以 ascii 编码为例,单个字节 0x07 就是发出声音,属于不可打印字符,base64 编码将任意三个 Byte 即 24bit 按照每 6bit 一组分成四份,再将分组后的 6bit 映射到 A-Z, a-z, 0-9, +, / 等共计 64(2^6)个字符。

通过命令行可以验证"test"的 base64 编码后字符串。

$ echo "test" | tr -d \\n | base64
dGVzdA==

tr -d \n 作用为去掉 echo 句末的换行符,可以看到结果为 8 个 ascii 字符,所以编码后的字节数应该为 8 字节而不是 b5eb2d 所示的 3 字节。4 字节补全为最接近的 3 的整数倍,即 6 字节,通过 base64 编码后长度变为 4/3 即 8 字节。

b5eb2d 这一结果从何而来?同事的测试代码又为什么能通过呢?

>>> from base64 import b64encode, b64decode
>>> b64encode("test".encode("utf-8"))
b'dGVzdA=='
>>> [hex(i) for i in b64decode(b"test")]
['0xb5', '0xeb', '0x2d']

使用 Python 验证 base64 编码,首先明确的是上述的 API 确实存在误用。

这时回想我前面提到的 base64 编码长度关系,恍然大悟,尽管存在 API 的误用,但由于"test"长度恰好是 4 的整倍数,每个字符又都是合法的 base64 字符,因此刚好可以解码出,解密时通过 base64 编码又还原回原字符串"test"。 在集成应用中使用时,输入的内容不是合法的 base64 编码字符串,加密时 base64 解码得到空的 data,解密后自然没有数据。

验证我的猜想有两种方式,第一种对加密过程的 base64 解码 log 输出或符号断点。第二种则是修改测试数据,模拟用户输入的情况。

果然,在将"test"替换为中文输入后,SDK 的测试代码也出现了解密失败。

回顾这次排查的过程,有以下几点值得注意:

  1. SDK 和应用放在同一项目下可以更方便的断点调试,怕麻烦会很容易错失修复 bug 的机会

  2. 熟悉 API 和编程基础(这里指 base64 编码)可以加速发现代码中的错误

  3. 测试时务必保证上下文和「案发现场」一致,这里测试数据"test"和用户手动输入的数据不同始终没有被重视

  4. "test"作为测试阶段经常出现的字符串,用于测试 base64 的相关操作时是一个很特殊的字符串,既可以作为编码输入也可以作为解码输入,即使两者用反也可得到正确的结果

更让人哭笑不得的是,工程中另一个部分的序列号同样是 12 位的数字字母字符串,由于长度和字符的特殊性,同样以错误的方式正确运行至今。

很多程序员和项目经理对单元测试的态度是浪费时间,通过这个例子不难看出单元测试可以用有限的案例还两只程序猿一个准时的下班。


如果喜欢这篇文章,欢迎关注我的公众号「晨晓」获得及时的更新。

账号:chenxiaopost

6107 次点击
所在节点    iDev
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/589720

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX