一、为什么Swift代码里的敏感信息硬编码会出大问题

做iOS开发的人,多多少少都干过这么一件事:把接口密钥、数据库密码、第三方服务的AppKey这类敏感信息,直接写在Swift代码里。比如测试接口的时候嫌麻烦,把接口地址和密钥直接拼在代码里,测完忘了改;或者为了快速迭代,把敏感信息硬编码在某个工具类里,后面就一直留着了。

这么做的风险真的很大。现在iOS应用的安全检测越来越严,不管是苹果的App Store审核,还是企业内部的安全扫描,只要发现代码里有硬编码的敏感信息,直接就给你打回来。更严重的是,要是有人把应用的IPA包反编译,能直接看到这些敏感信息,要是被恶意利用,损失根本没法预估。比如之前有个电商应用,因为硬编码了支付接口的密钥,被黑客拿到后刷了几十万的订单,最后整个开发团队都背了锅。

可能有人会说:“我把敏感信息写在私有类里,别人看不到吧?”别太天真,现在的反编译工具能把Swift代码的类、方法、变量名还原得差不多,哪怕你起个怪名字,只要是硬编码的字符串,一搜就能搜出来。还有人会把敏感信息做个简单的转码,比如把密钥转成Base64再写进去,这也没用,反编译的时候直接把转码后的字符串再转回来,还是能拿到原始内容。

所以,解决Swift代码里的敏感信息硬编码问题,不能靠藏,得靠主动检测和预防。

二、用正则表达式快速扫描硬编码的敏感信息

首先,我们得知道怎么快速找到代码里的硬编码敏感信息,最方便的工具就是正则表达式。正则就像一个搜索模板,我们可以给它定好规则,让它帮我们把符合规则的内容找出来。

2.1 正则扫描的具体步骤

第一步,得先明确我们要找的敏感信息类型,比如:

  • 接口密钥:一般是一串随机的字母数字,长度在16位以上
  • 数据库密码:可能是一串字母数字加特殊字符的组合
  • 第三方服务的AppKey:比如微信、支付宝的AppKey,有固定的格式

第二步,写对应的正则表达式。比如找16位以上的随机字符串,正则可以这么写:"[A-Za-z0-9]{16,}";找带特殊字符的密码,可以写成"[A-Za-z0-9!@#$%^&*()]{8,}"

第三步,用Xcode自带的搜索功能扫描整个项目。Xcode的搜索框支持正则搜索,只要把写好的正则输进去,就能把项目里所有符合规则的字符串都找出来,然后我们再逐个排查,哪些是真的敏感信息,哪些是正常的内容(比如版本号、测试用的字符串)。

2.2 正则扫描的优缺点

优点很明显:速度快,能快速定位可疑内容;不需要额外的工具,用Xcode自带的功能就能做;规则可以自己定制,想找什么就写什么规则。

缺点也有:容易出现误报。比如项目里的测试用例里有个16位的测试字符串,或者某个工具类里的固定参数,也会被搜出来,需要人工逐个排查;复杂的敏感信息格式(比如带时间戳的动态密钥)很难用正则精准匹配,容易漏扫。

三、用编译器警告提前阻断硬编码的敏感信息

正则扫描是事后找问题,最好的办法是事前预防,也就是写代码的时候,只要有人硬编码敏感信息,编译器就直接给警告,不让你提交代码。这就要用到Swift的编译器特性了。

3.1 具体实现步骤

我们可以写一个自定义的属性包装器(Property Wrapper),这个包装器的作用是:只要有人把敏感信息直接赋值给它,编译器就会触发警告。

首先,我们要用到Swift的@available属性,这个属性本来是用来标记某个API在某个版本之前不能用的,我们可以用它来模拟警告。然后,我们再写一个自定义的属性包装器,把敏感信息的存储和警告逻辑结合起来。

下面是完整的代码示例,技术栈统一为Swift 5.0+:

// 技术栈:Swift 5.0+
// 自定义属性包装器,用于标记敏感信息,防止硬编码
@propertyWrapper
struct SensitiveInfo<T> {
    // 私有存储,不允许直接从外部访问
    private var storage: T
    
    // 包装器的初始化方法,这里会触发编译器警告
    @available(*, deprecated, message: "禁止硬编码敏感信息!请从配置文件或环境变量读取")
    init(wrappedValue: T) {
        self.storage = wrappedValue
    }
    
    // 提供访问存储值的方法,只有在允许的情况下才能读取
    var wrappedValue: T {
        get { storage }
        set { storage = newValue }
    }
}

// 下面是使用示例
class AppConfig {
    // 敏感信息:接口密钥,用自定义包装器标记
    @SensitiveInfo var apiKey: String = "abcdefghijklmnop1234567890" // 这里会触发编译器警告
    
    // 敏感信息:数据库密码
    @SensitiveInfo var dbPassword: String = "P@ssw0rd123456789" // 这里也会触发编译器警告
    
    // 正确的使用方式:从配置文件读取敏感信息
    init() {
        // 假设这里是从项目的配置文件(比如Info.plist)读取apiKey
        if let key = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String {
            self.apiKey = key
        }
        // 假设这里是从环境变量读取dbPassword
        if let password = ProcessInfo.processInfo.environment["DB_PASSWORD"] {
            self.dbPassword = password
        }
    }
}

3.2 实现的原理

上面的代码里,SensitiveInfo这个属性包装器的初始化方法加了@available属性,把它标记为“已废弃”,并且给出了警告信息。只要我们把敏感信息直接赋值给@SensitiveInfo标记的变量,编译器就会认为我们用了一个废弃的API,从而触发警告。

而正确的使用方式是从配置文件或者环境变量读取敏感信息,这时候赋值是在初始化方法里,不会触发初始化方法的警告,因为@available标记的是初始化方法本身,只有直接调用初始化方法(也就是直接给变量赋值)的时候才会触发警告。

3.3 编译器警告的优缺点

优点:事前预防,写代码的时候就能发现问题,不会等到提交代码或者安全扫描的时候才发现;规则固定,只要是硬编码的敏感信息,都会触发警告,不会出现漏扫;不需要人工排查,编译器会自动提示。

缺点:只能检测硬编码的情况,不能检测其他的安全问题(比如敏感信息传输的时候没有加密);需要提前定义好所有的敏感信息变量,要是漏定义了某个敏感信息,还是会出现硬编码的问题。

四、两种方法的结合使用

正则扫描和编译器警告这两种方法,单独用都有缺点,结合起来用效果最好。

比如,我们可以先在项目里定义好所有的敏感信息变量,用@SensitiveInfo标记,这样写代码的时候只要硬编码就会触发警告。然后,我们再定期用正则扫描整个项目,排查有没有漏定义的敏感信息变量,或者有没有人偷偷绕过编译器警告(比如把敏感信息拆成多个字符串拼接起来,这样编译器不会触发警告)。

比如有人这么写代码:

// 技术栈:Swift 5.0+
// 绕过编译器警告的写法:把敏感信息拆成多个字符串拼接
class HackConfig {
    var apiKey: String = "abcdef" + "ghijkl" + "mnop123" + "4567890"
}

这种写法编译器不会触发警告,但是用正则扫描就能搜出来这个拼接后的16位以上的字符串,然后我们再人工排查,就能发现问题。

五、应用场景、注意事项和总结

5.1 应用场景

这两种方法适合所有的iOS开发项目,不管是个人项目还是企业项目。尤其是企业项目,因为企业项目的安全要求更高,必须要保证代码里没有硬编码的敏感信息。另外,这两种方法也适合团队开发,能规范团队成员的代码习惯,避免因为个人疏忽导致的安全问题。

5.2 注意事项

  • 正则扫描的规则要定期更新,比如项目里新增了一种敏感信息类型,就要及时更新正则表达式。
  • 自定义属性包装器的@available属性的警告信息要写清楚,让团队成员一看就知道为什么会触发警告,以及正确的做法是什么。
  • 敏感信息的存储要安全,不能只靠编译器警告,还要保证配置文件和环境变量里的敏感信息不会被泄露。比如配置文件要加密存储,环境变量要设置为只允许项目访问。
  • 要定期对项目进行安全扫描,除了检测硬编码的敏感信息,还要检测其他的安全问题,比如代码注入、跨站脚本攻击等。

5.3 总结

Swift代码里的敏感信息硬编码是一个很常见的安全问题,但是只要用对方法,就能很好地解决。正则扫描能快速定位可疑内容,编译器警告能事前预防硬编码的敏感信息,把这两种方法结合起来,就能形成一个完整的检测和预防体系,保证项目的安全。

最后,安全开发是一个长期的过程,不能只靠工具,还要养成良好的代码习惯,比如不要硬编码敏感信息,定期对代码进行安全检查,及时更新安全规则等。只有这样,才能保证我们的应用不会因为敏感信息泄露而出现安全问题。