下班前同事突然叫住我,「晨晓,这里有个问题你帮忙看一下」。 著名佚名人士曾说过——最好的下班时间是六点,其次是现在。但 我,六点没有下班, 现在也没有下班。 简要复述一下问题,开发一个包含加解密报文的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的测试代码,居然真的解密出来了"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字符,因此刚好可以解码出<b5eb2d>,解密时通过base64编码又还原回原字符串"test"。
在集成应用中使用时,输入的内容不是合法的base64编码字符串,加密时base64解码得到空的data,解密后自然没有数据。
验证我的猜想有两种方式,第一种对加密过程的base64解码log输出或符号断点。第二种则是修改测试数据,模拟用户输入的情况。
果然,在将"test"替换为中文输入后,SDK的测试代码也出现了解密失败。
回顾这次排查的过程,有以下几点值得注意:
-
SDK和应用放在同一项目下可以更方便的断点调试,怕麻烦会很容易错失修复bug的机会
-
熟悉API和编程基础(这里指base64编码)可以加速发现代码中的错误
-
测试时务必保证上下文和「案发现场」一致,这里测试数据"test"和用户手动输入的数据不同始终没有被重视
-
"test"作为测试阶段经常出现的字符串,用于测试base64的相关操作时是一个很特殊的字符串,既可以作为编码输入也可以作为解码输入,即使两者用反也可得到正确的结果