Swift Confidential

CI codecov Swift Platforms

一款高度可配置且性能卓越的工具,用于混淆嵌入在应用程序代码中的 Swift 字面量,您应该保护这些字面量免受静态代码分析的影响,从而提高应用程序抵抗逆向工程的能力。

只需将该工具与您的 Swift 包或 Xcode 项目集成,配置您自己的混淆算法以及秘密字面量列表,然后构建项目 🚀

Swift Confidential 可以为您节省大量时间,特别是如果您正在开发 iOS 应用程序并寻求满足 OWASP MASVS-RESILIENCE 要求。

动机

几乎每个应用程序都至少有少量字面量嵌入在代码中,包括:URL、各种客户端标识符(例如 API 密钥或 API 令牌)、pinning 数据(例如 PEM 证书或 SPKI 摘要)、Keychain 项目标识符、RASP 相关字面量(例如可疑 dylib 列表或用于越狱检测的可疑文件路径列表)以及许多其他特定于上下文的字面量。虽然列出的代码字面量示例可能看起来无害,但在许多情况下,不对它们进行混淆可能被视为向潜在的威胁行为者伸出了橄榄枝。在安全敏感型应用程序中尤其如此,例如移动银行应用程序、2FA 身份验证器应用程序和密码管理器。作为一名负责任的软件工程师,您应该意识到,从应用程序包中提取源代码字面量通常非常容易,即使经验不足的恶意用户也可以轻松完成。

Mach-O C String Literals 在示例 Mach-O 文件中,对 __TEXT.__cstring 区段的快速浏览揭示了关于应用程序的许多有趣信息。

该工具旨在通过引入可组合的混淆技术来为上述问题提供优雅且可维护的解决方案,这些技术可以自由组合以形成用于混淆选定的 Swift 字面量的算法。

注意

虽然 Swift Confidential 肯定会使代码的静态分析更具挑战性,但 它绝不是您应该采用的唯一代码加固技术,以保护您的应用程序免受逆向工程和篡改。为了达到一定的安全级别,我们强烈建议您使用 运行时应用程序自我保护 (RASP) 检查 以及 Swift 代码混淆 来补充此工具的安全措施。话虽如此,没有任何安全措施可以保证绝对安全。任何有动机且足够熟练的攻击者最终都会绕过所有安全保护。因此,始终保持您的威胁模型处于最新状态

入门指南

首先在您的 SwiftPM 目标的源目录或 Xcode 项目的根目录中创建一个 confidential.yml YAML 配置文件(取决于首选的安装方法)。配置至少必须包含混淆算法和一个或多个秘密定义。

例如,假设的 RASP 模块的配置文件可能如下所示

algorithm:
  - encrypt using aes-192-gcm
  - shuffle
defaultNamespace: create ObfuscatedLiterals
secrets:
  - name: suspiciousDynamicLibraries
    value:
      - Substrate
      - Substitute
      - FridaGadget
      # ... other suspicious dylibs
  - name: suspiciousFilePaths
    value:
      - /.installed_unc0ver
      - /usr/sbin/frida-server
      - /private/var/lib/cydia
      # ... other suspicious file paths

警告

以上配置中的算法仅作为示例,请勿在您的生产代码中使用此特定算法。相反,从下面描述的 混淆技术 中组合您自己的算法,并且 不要与任何人分享您的算法。此外,遵循 安全 SDLC 最佳实践,请考虑不要将生产算法提交到您的存储库中,而是配置您的 CI/CD 管道以运行自定义脚本(理想情况下在构建步骤之前),该脚本将通过从秘密库中检索到的值替换算法值来修改配置文件。

创建配置文件后,您可以使用 Confidential 构建工具插件(请参阅下面的 安装部分)来生成带有混淆秘密字面量的 Swift 代码。

在幕后,Confidential 插件通过发出以下命令来调用 swift-confidential CLI 工具

swift-confidential obfuscate --configuration "path/to/confidential.yml" --output "${PLUGIN_WORK_DIRECTORY}/ObfuscatedSources/Confidential.generated.swift"

成功执行命令后,生成的 Confidential.generated.swift 文件将包含类似于以下内容的代码

import ConfidentialKit
import Foundation

internal enum ObfuscatedLiterals {

    @ConfidentialKit.Obfuscated<Swift.Array<Swift.String>>(deobfuscateData)
    internal static var suspiciousDynamicLibraries: ConfidentialKit.Obfuscation.Secret = .init(data: [0x14, 0x4b, 0xe5, 0x48, 0xd2, 0xc4, 0xb1, 0xba, 0xac, 0xa8, 0x65, 0x8e, 0x15, 0x34, 0x12, 0x87, 0x35, 0x49, 0xfb, 0xa4, 0xc8, 0x10, 0x5f, 0x4a, 0xe0, 0xf3, 0x69, 0x4a, 0x53, 0xa1, 0xdf, 0x58, 0x9d, 0x45, 0xa3, 0xf3, 0x00, 0xa2, 0x0f, 0x9c, 0x7d, 0x93, 0x14, 0x20, 0x04, 0xb2, 0xe8, 0x97, 0x26, 0x04, 0x5b, 0x00, 0x9e, 0x06, 0x30, 0x23, 0xaa, 0xa2, 0xc4, 0xfc, 0xba, 0x22, 0x97, 0x2b, 0x2d, 0x6e, 0x5f, 0x1d, 0xd5, 0xab, 0x9a, 0xe0, 0xf3, 0x1f, 0x17, 0x58, 0xab, 0xda, 0x49, 0x0a, 0xc2, 0x0a, 0xa2, 0x9a, 0xcc, 0x6d, 0x8c, 0x5e, 0xc0, 0x73, 0x77, 0x76, 0x6c, 0x2f, 0x2c, 0x2b, 0x2a, 0x65, 0x48, 0x04, 0x01, 0x07, 0x0b, 0x78, 0x1c, 0x52, 0x6a, 0x6f, 0x0e, 0x01, 0x6e, 0x63, 0x08, 0x5b, 0x62, 0x5f, 0x59, 0x72, 0x5a, 0x5c, 0x68, 0x1f, 0x1a, 0x64, 0x12, 0x13, 0x19, 0x55, 0x53, 0x4f, 0x06, 0x4e, 0x46, 0x7e, 0x10, 0x60, 0x40, 0x7d, 0x48, 0x76, 0x77, 0x4a, 0x7f, 0x1d, 0x71, 0x51, 0x03, 0x7a, 0x47, 0x09, 0x56, 0x11, 0x6c, 0x49, 0x0a, 0x04, 0x5e, 0x0f, 0x61, 0x65, 0x41, 0x75, 0x73, 0x4b, 0x57, 0x0d, 0x42, 0x02, 0x4c, 0x1e, 0x18, 0x1b, 0x45, 0x69, 0x66, 0x00, 0x7b, 0x6b, 0x70, 0x6d, 0x50, 0x0c, 0x5d, 0x54, 0x4d, 0x79, 0x74, 0x58, 0x44, 0x05, 0x43, 0x7c, 0x67], nonce: 13452749969377545032)

    @ConfidentialKit.Obfuscated<Swift.Array<Swift.String>>(deobfuscateData)
    internal static var suspiciousFilePaths: ConfidentialKit.Obfuscation.Secret = .init(data: [0x04, 0xdf, 0x99, 0x61, 0x39, 0xca, 0x19, 0x3d, 0xcd, 0xa9, 0xd0, 0xf3, 0x31, 0xc9, 0x8a, 0x2a, 0x00, 0x76, 0x51, 0xab, 0xae, 0xc1, 0xf8, 0x31, 0x00, 0x14, 0x40, 0x78, 0x5e, 0x8e, 0x14, 0x98, 0xc4, 0xbb, 0x26, 0xb4, 0x48, 0x6c, 0x56, 0xd8, 0x99, 0x31, 0x19, 0x96, 0xce, 0x8a, 0x97, 0x00, 0xde, 0xa4, 0x83, 0xe0, 0xcc, 0x1a, 0x3b, 0x2a, 0x55, 0xb7, 0x72, 0x36, 0xa1, 0xd2, 0x70, 0x0c, 0x8d, 0xe6, 0xe6, 0x78, 0x41, 0xa9, 0xdb, 0x45, 0x38, 0x5b, 0x97, 0x22, 0xb4, 0x8a, 0x4d, 0xd6, 0x59, 0xaa, 0x4e, 0xf7, 0x36, 0xba, 0xda, 0x0c, 0xb2, 0x82, 0x9e, 0x64, 0xd4, 0x41, 0xd7, 0x48, 0x0b, 0x04, 0xa4, 0x77, 0xfa, 0xcf, 0x07, 0xd2, 0x3b, 0x4d, 0xc7, 0x3d, 0x65, 0xb2, 0xfa, 0x1c, 0x77, 0x7f, 0xd4, 0x24, 0xf3, 0x99, 0xbd, 0xad, 0x1e, 0x17, 0x8e, 0x5a, 0xc2, 0xae, 0x9d, 0xb5, 0xa1, 0x3d, 0x1a, 0x70, 0xcd, 0x80, 0x8e, 0x9a, 0xb1, 0x75, 0xf3, 0x8c, 0xc7, 0x01, 0x94, 0x9e, 0xaf, 0x98, 0xb8, 0xf9, 0xd0, 0xbd, 0xbe, 0xca, 0xe5, 0xcc, 0xfa, 0xc6, 0xa3, 0xec, 0xae, 0x8a, 0xb9, 0xd6, 0xbb, 0x01, 0xc7, 0x8b, 0xc1, 0xac, 0xc9, 0xd8, 0x86, 0xf5, 0xe7, 0xb3, 0xc8, 0xfd, 0x99, 0xdc, 0xc4, 0x81, 0xad, 0xd4, 0xe0, 0x9f, 0xa6, 0x05, 0x8d, 0xea, 0x96, 0xa9, 0xe8, 0x92, 0xf6, 0x90, 0x8f, 0xb5, 0xb1, 0xb7, 0xc0, 0xdd, 0xce, 0xfb, 0xab, 0xe9, 0xe4, 0xf8, 0xe6, 0xc3, 0xba, 0xa7, 0xdb, 0xf4, 0xcb, 0xfe, 0xc5, 0xde, 0xd7, 0xcd, 0xf3, 0xd2, 0xe2, 0x88, 0xa8, 0xcf, 0x95, 0x93, 0x9a, 0xa1, 0xe1, 0xfc, 0xb4, 0x82, 0xb0, 0xd3, 0xf0, 0x97, 0xd5, 0xf7, 0x87, 0x03, 0xef, 0xdf, 0xbf, 0xee, 0x9c, 0x8e, 0x02, 0xb2, 0x91, 0xa4, 0x89, 0xeb, 0xa0, 0xd9, 0xf1, 0xc2, 0xff, 0xe3, 0xb6, 0xaa, 0x00, 0xa5, 0xed, 0xda, 0xbc, 0xd1, 0x9d, 0x80, 0x9b, 0x8c, 0xa2, 0x84, 0x85, 0x83, 0xf2], nonce: 4402772458530791297)

    @inline(__always)
    private static func deobfuscateData(_ data: Foundation.Data, nonce: Swift.UInt64) throws -> Foundation.Data {
        try ConfidentialKit.Obfuscation.Encryption.DataCrypter(algorithm: .aes192GCM)
            .deobfuscate(
                try ConfidentialKit.Obfuscation.Randomization.DataShuffler()
                    .deobfuscate(data, nonce: nonce),
                nonce: nonce
            )
    }
}

然后,例如,您可以使用生成的 suspiciousDynamicLibraries 属性的 projected value 在您自己的代码中迭代反混淆的可疑动态库数组

let suspiciousLibraries = ObfuscatedLiterals.$suspiciousDynamicLibraries
    .map { $0.lowercased() }
let checkPassed = loadedLibraries
    .allSatisfy { !suspiciousLibraries.contains(where: $0.lowercased().contains) }

安装

Swift Confidential 可以与 SwiftPM 和 Xcode 目标一起使用,具体取决于您的需求。请参阅下面的相关部分,了解详细的安装说明。

SwiftPM

要将 Swift Confidential 与您的 SwiftPM 目标一起使用,请将 ConfidentialKit 库以及 Confidential 插件添加到包的依赖项中,然后分别添加到目标的依赖项和插件中

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    // name, platforms, products, etc.
    dependencies: [
        // other dependencies
        .package(url: "https://github.com/securevale/swift-confidential.git", .upToNextMinor(from: "0.3.0")),
        .package(url: "https://github.com/securevale/swift-confidential-plugin.git", .upToNextMinor(from: "0.3.0"))
    ],
    targets: [
        .target(
            name: "MyLibrary",
            dependencies: [
                // other dependencies
                .product(name: "ConfidentialKit", package: "swift-confidential")
            ],
            exclude: ["confidential.yml"],
            plugins: [
                // other plugins
                .plugin(name: "Confidential", package: "swift-confidential-plugin")
            ]
        )
    ]
)

请确保将 confidential.yml 配置文件的路径添加到目标的 exclude 列表中,以显式地将此文件从目标资源中排除。

Xcode

要将 Swift Confidential 直接与您的 Xcode 目标集成

为了方便起见,您还可以将 confidential.yml 配置文件添加到您的 Xcode 项目,但 请务必不要将其添加到任何 Xcode 目标

设置完成后,构建您的目标,Confidential 插件将自动生成一个带有混淆秘密字面量的 Swift 源文件。此外,每当插件检测到 confidential.yml 配置文件发生更改或您清理构建项目时,它都会重新生成混淆的秘密字面量。

重要提示

请确保为 swift-confidentialswift-confidential-plugin 包使用相同的版本要求。有关 API 稳定性的更多信息,请参阅 版本控制 部分。

配置

Swift Confidential 支持多种配置选项,所有选项都存储在单个 YAML 配置文件中。

YAML 配置键

下表列出了要包含在配置文件中的键以及要包含在每个键中的信息类型。CLI 工具会忽略配置文件中的任何其他键。

值类型 描述
algorithm 字符串列表 表示构成混淆算法的各个步骤的混淆技术列表。有关使用详情,请参阅 混淆技术 部分。
必需。
defaultAccessModifier 字符串 应用于每个生成的秘密字面量的默认访问级别修饰符,除非秘密定义另有说明。默认值为 internal。有关使用详情,请参阅 访问控制 部分。
defaultNamespace 字符串 用于封闭所有未显式分配命名空间的生成秘密字面量的默认命名空间。默认值为 extend Obfuscation.Secret from ConfidentialKit。有关使用详情,请参阅 命名空间 部分。
implementationOnlyImport 布尔值 指定是否生成仅限实现的 ConfidentialKit 导入。默认值为 false。有关使用详情,请参阅 为分发构建库 部分。
secrets 对象列表 定义要混淆的秘密字面量的对象列表。有关使用详情,请参阅 秘密 部分。
必需。
配置示例
algorithm:
  - encrypt using aes-192-gcm
  - shuffle
defaultNamespace: create Secrets
secrets:
  - name: apiKey
    value: 214C1E2E-A87E-4460-8205-4562FDF54D1C
  - name: trustedSPKIDigests
    value:
      - 7a6820614ee600bbaed493522c221c0d9095f3b4d7839415ffab16cbf61767ad
      - cf84a70a41072a42d0f25580b5cb54d6a9de45db824bbb7ba85d541b099fd49f
      - c1a5d45809269301993d028313a5c4a5d8b2f56de9725d4d1af9da1ccf186f30
    namespace: extend Pinning from Crypto

警告
以上配置中的算法仅作为示例,请勿在您的生产代码中使用此特定算法

混淆技术

混淆技术是可组合的构建块,您可以从中创建自己的混淆算法。您可以按任何顺序组合它们,这样除了您之外,没有人知道秘密字面量是如何混淆的。

压缩

此技术涉及使用您选择的算法进行数据压缩。通常,压缩技术是非多态的,这意味着给定相同的输入数据,每次运行都会产生相同的输出数据。但是,Swift Confidential 应用了额外的多态混淆例程来掩盖识别所用压缩算法的字节。

语法

compress using <algorithm>

下表显示了支持的算法

算法 描述
lzfse LZFSE 压缩算法。
lz4 LZ4 压缩算法。
lzma LZMA 压缩算法。
zlib zlib 压缩算法。

加密

此技术涉及使用您选择的算法进行数据加密。加密技术是多态的,这意味着给定相同的输入数据,每次运行都会产生不同的输出数据。

语法

encrypt using <algorithm>

下表显示了支持的算法

算法 描述
aes-128-gcm 高级加密标准 (AES) 算法,采用伽罗瓦/计数器模式 (GCM) 和 128 位密钥。
aes-192-gcm 高级加密标准 (AES) 算法,采用伽罗瓦/计数器模式 (GCM) 和 192 位密钥。
aes-256-gcm 高级加密标准 (AES) 算法,采用伽罗瓦/计数器模式 (GCM) 和 256 位密钥。
chacha20-poly ChaCha20-Poly1305 算法。

随机化

此技术涉及数据随机化。随机化技术是多态的,这意味着给定相同的输入数据,每次运行都会产生不同的输出数据。

注意

随机化技术最适合大小不超过 256 字节的秘密。对于较大的秘密,混淆数据的大小将从 2N 增长到 3N,其中 N 是输入数据大小(以字节为单位),如果输入数据的大小大于 65536 字节,则甚至增长到 5N(32 位平台)或 9N(64 位平台)。因此,此技术的内部实现将在后续版本中进行更改。

语法

shuffle

秘密

配置文件使用 YAML 对象来描述要混淆的秘密字面量。下表列出了用于定义秘密字面量的键以及要包含在每个键中的信息类型。

值类型 描述
accessModifier 字符串 生成的 Swift 属性的访问级别修饰符,该属性包含混淆的秘密字面量数据。支持的值为 internalpublic。如果未指定,则使用顶层 defaultAccessModifier 值。有关使用详情,请参阅 访问控制 部分。
name 字符串 生成的 Swift 属性的名称,该属性包含混淆的秘密字面量数据。此值按原样使用,不进行有效性检查。因此,请确保使用有效的属性名称。
必需。
namespace 字符串 用于封闭生成的秘密字面量声明的命名空间。有关使用详情,请参阅 命名空间 部分。
value 字符串或字符串列表 要混淆的秘密字面量的明文值。YAML 数据类型分别映射到 Swift 中的 StringArray<String>
必需。
秘密定义示例

假设您想混淆用于引用存储在 Keychain 或 Secure Enclave 中的私钥的标签

name: secretVaultKeyTag
value: com.example.app.keys.secret_vault_private_key
accessModifier: internal
namespace: extend KeychainAccess.Key from Crypto

上面的 YAML 秘密定义将导致生成以下 Swift 代码

import Crypto
// ... other imports

extension Crypto.KeychainAccess.Key {

    @ConfidentialKit.Obfuscated<Swift.String>(deobfuscateData)
    internal static var secretVaultKeyTag: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */], nonce: /* cryptographically secure random number */)

    // ... other secret declarations
}

您可能还需要混淆相关值的列表,例如要进行 pinning 的可信 SPKI 摘要列表

name: trustedSPKIDigests
value:
  - 7a6820614ee600bbaed493522c221c0d9095f3b4d7839415ffab16cbf61767ad
  - cf84a70a41072a42d0f25580b5cb54d6a9de45db824bbb7ba85d541b099fd49f
  - c1a5d45809269301993d028313a5c4a5d8b2f56de9725d4d1af9da1ccf186f30
accessModifier: public
namespace: extend Pinning from Crypto

使用上面的 YAML 秘密定义,将生成以下 Swift 代码

import Crypto
// ... other imports

extension Crypto.Pinning {

    @ConfidentialKit.Obfuscated<Swift.Array<Swift.String>>(deobfuscateData)
    public static var trustedSPKIDigests: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */], nonce: /* cryptographically secure random number */)

    // ... other secret declarations
}

命名空间

根据 Swift 编程最佳实践,Swift Confidential 将生成的秘密字面量声明封装在命名空间(即无 case 枚举)中。命名空间语法允许您创建新命名空间或扩展现有命名空间。

注意

目前不支持创建嵌套命名空间。

语法

create <namespace> # creates new namespace

extend <namespace> [from <module>] # extends existing namespace, optionally specifying 
                                   # the module to which this namespace belongs
用法示例

假设您想将生成的秘密字面量声明保留在名为 Secrets 的新命名空间中,请使用以下 YAML 代码

create Secrets

上面的命名空间定义将导致生成以下 Swift 代码

internal enum Secrets {

    // Encapsulated declarations ...
}

但是,如果您希望将生成的秘密字面量声明保留在名为 Pinning 的现有命名空间中,并从 Crypto 模块导入,请改用以下 YAML 代码

extend Pinning from Crypto

使用上面的命名空间定义,将生成以下 Swift 代码

import Crypto
// ... other imports

extension Crypto.Pinning {

    // Encapsulated declarations ...
}

访问控制

您可以为生成的 Swift 代码指定访问级别修饰符,包括全局级别和每个秘密级别。但是,一般建议是使用默认的 internal 访问级别,以便保持代码的良好封装性。

语法

<access_modifier>

下表显示了支持的访问级别修饰符

访问修饰符 描述
internal 生成的声明仅在其定义模块内可访问。
public 生成的声明在其定义模块以及任何导入定义模块的模块中均可访问。
用法示例

假设您想将所有秘密字面量保存在其他项目模块/目标使用的单个共享 Swift 模块中,您可以使用类似于以下内容的配置

algorithm:
  - encrypt using aes-192-gcm
  - shuffle
defaultNamespace: create Secrets
defaultAccessModifier: public
secrets:
  - name: apiKey
    value: 214C1E2E-A87E-4460-8205-4562FDF54D1C
  - name: trustedSPKIDigests
    value:
      - 7a6820614ee600bbaed493522c221c0d9095f3b4d7839415ffab16cbf61767ad
      - cf84a70a41072a42d0f25580b5cb54d6a9de45db824bbb7ba85d541b099fd49f
      - c1a5d45809269301993d028313a5c4a5d8b2f56de9725d4d1af9da1ccf186f30

警告
以上配置中的算法仅作为示例,请勿在您的生产代码中使用此特定算法

defaultAccessModifier 设置为 public 后,所有基于 secrets 列表生成的 Swift 属性都可以在其定义模块外部访问

import ConfidentialKit
import Foundation

public enum Secrets {

    @ConfidentialKit.Obfuscated<Swift.String>(deobfuscateData)
    public static var apiKey: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */], nonce: /* cryptographically secure random number */)

    @ConfidentialKit.Obfuscated<Swift.Array<Swift.String>>(deobfuscateData)
    public static var trustedSPKIDigests: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */], nonce: /* cryptographically secure random number */)

    // ...
}

此外,如果您需要更细粒度的控制,您可以通过在秘密定义中指定访问级别修饰符来覆盖 defaultAccessModifier,如 秘密 部分所述。

为分发构建库

默认情况下,Swift Confidential 不会使用 @_implementationOnly 属性注释生成的 ConfidentialKit 导入。但是,在某些情况下,例如 创建 XCFramework 包 时,您应该使用仅限实现的导入,以避免向您的库使用者公开内部符号。要启用仅限实现的 ConfidentialKit 导入,请将 implementationOnlyImport 配置选项设置为 true

重要提示

仅限实现的导入适用于仅在内部使用的类型,因此,如果任何秘密的访问级别设置为 public,则启用 implementationOnlyImport 是错误的。另请注意,将 implementationOnlyImport 选项设置为 true 并不意味着对扩展的命名空间进行仅限实现的导入。

Confidential Swift Package 插件的其他注意事项

Confidential 插件 期望配置文件名为 confidential.ymlconfidential.yaml,并且每个 SwiftPM 目标/Xcode 项目假定只有一个配置文件。如果您将插件与 SwiftPM 目标一起使用,并在不同的子目录中定义了多个配置文件,则插件将使用它找到的第一个配置文件,但具体是哪个文件是不确定的。而如果您将插件应用于 Xcode 项目的目标,则配置文件应位于项目的顶层目录中(所有其他配置文件都将被忽略)。

版本控制

本项目遵循 语义版本控制。虽然仍处于主版本 0,但源稳定性仅在次版本(例如 0.3.00.3.1 之间)内得到保证。如果您想防止潜在的源破坏性包更新,您可以使用源代码控制要求来指定您的包依赖项(例如 .upToNextMinor(from: "0.3.0"))。

许可证

此工具和代码在 Apache License v2.0 和 Runtime Library Exception 下发布。请参阅 LICENSE 了解更多信息。