V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iOS 开发实用技术导航
NSHipster 中文版
http://nshipster.cn/
cocos2d 开源 2D 游戏引擎
http://www.cocos2d-iphone.org/
CocoaPods
http://cocoapods.org/
Google Analytics for Mobile 统计解决方案
http://code.google.com/mobile/analytics/
WWDC
https://developer.apple.com/wwdc/
Design Guides and Resources
https://developer.apple.com/design/
Transcripts of WWDC sessions
http://asciiwwdc.com
Cocoa with Love
http://cocoawithlove.com/
Cocoa Dev Central
http://cocoadevcentral.com/
NSHipster
http://nshipster.com/
Style Guides
Google Objective-C Style Guide
NYTimes Objective-C Style Guide
Useful Tools and Services
Charles Web Debugging Proxy
Smore
shawndev
V2EX  ›  iDev

关于 Base64 的一次调试经历

  •  
  •   shawndev · 2019-08-07 10:18:57 +08:00 · 6195 次点击
    这是一个创建于 1936 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    简要复述一下问题,开发一个包含加解密报文的 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

    qrcode_for_gh_aed32f0f4077_430.jpg

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2652 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 05:46 · PVG 13:46 · LAX 21:46 · JFK 00:46
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.