一款高度可配置且性能卓越的工具,用于混淆嵌入在应用程序代码中的 Swift 字面量,您应该保护这些字面量免受静态代码分析的影响,从而提高应用程序抵抗逆向工程的能力。
只需将该工具与您的 Swift 包或 Xcode 项目集成,配置您自己的混淆算法以及秘密字面量列表,然后构建项目 🚀
Swift Confidential 可以为您节省大量时间,特别是如果您正在开发 iOS 应用程序并寻求满足 OWASP MASVS-RESILIENCE 要求。
几乎每个应用程序都至少有少量字面量嵌入在代码中,包括:URL、各种客户端标识符(例如 API 密钥或 API 令牌)、pinning 数据(例如 PEM 证书或 SPKI 摘要)、Keychain 项目标识符、RASP 相关字面量(例如可疑 dylib 列表或用于越狱检测的可疑文件路径列表)以及许多其他特定于上下文的字面量。虽然列出的代码字面量示例可能看起来无害,但在许多情况下,不对它们进行混淆可能被视为向潜在的威胁行为者伸出了橄榄枝。在安全敏感型应用程序中尤其如此,例如移动银行应用程序、2FA 身份验证器应用程序和密码管理器。作为一名负责任的软件工程师,您应该意识到,从应用程序包中提取源代码字面量通常非常容易,即使经验不足的恶意用户也可以轻松完成。
在示例 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 目标一起使用,具体取决于您的需求。请参阅下面的相关部分,了解详细的安装说明。
要将 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
列表中,以显式地将此文件从目标资源中排除。
要将 Swift Confidential 直接与您的 Xcode 目标集成
swift-confidential
和 swift-confidential-plugin
包添加到您的 Xcode 项目。请参阅 官方文档,了解有关如何添加包依赖项的分步说明。当要求选择要添加到您的目标的 swift-confidential
包产品时,请确保选择 ConfidentialKit
库。Build Phases
面板,在 Run Build Tool Plug-ins
部分中,单击 +
按钮,选择 Confidential
插件,然后单击 Add
按钮。为了方便起见,您还可以将 confidential.yml
配置文件添加到您的 Xcode 项目,但 请务必不要将其添加到任何 Xcode 目标。
设置完成后,构建您的目标,Confidential 插件将自动生成一个带有混淆秘密字面量的 Swift 源文件。此外,每当插件检测到 confidential.yml
配置文件发生更改或您清理构建项目时,它都会重新生成混淆的秘密字面量。
重要提示
请确保为 swift-confidential
和 swift-confidential-plugin
包使用相同的版本要求。有关 API 稳定性的更多信息,请参阅 版本控制 部分。
Swift Confidential 支持多种配置选项,所有选项都存储在单个 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 属性的访问级别修饰符,该属性包含混淆的秘密字面量数据。支持的值为 internal 和 public 。如果未指定,则使用顶层 defaultAccessModifier 值。有关使用详情,请参阅 访问控制 部分。 |
name | 字符串 | 生成的 Swift 属性的名称,该属性包含混淆的秘密字面量数据。此值按原样使用,不进行有效性检查。因此,请确保使用有效的属性名称。 必需。 |
namespace | 字符串 | 用于封闭生成的秘密字面量声明的命名空间。有关使用详情,请参阅 命名空间 部分。 |
value | 字符串或字符串列表 | 要混淆的秘密字面量的明文值。YAML 数据类型分别映射到 Swift 中的 String 和 Array<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 插件 期望配置文件名为 confidential.yml
或 confidential.yaml
,并且每个 SwiftPM 目标/Xcode 项目假定只有一个配置文件。如果您将插件与 SwiftPM 目标一起使用,并在不同的子目录中定义了多个配置文件,则插件将使用它找到的第一个配置文件,但具体是哪个文件是不确定的。而如果您将插件应用于 Xcode 项目的目标,则配置文件应位于项目的顶层目录中(所有其他配置文件都将被忽略)。
本项目遵循 语义版本控制。虽然仍处于主版本 0
,但源稳定性仅在次版本(例如 0.3.0
和 0.3.1
之间)内得到保证。如果您想防止潜在的源破坏性包更新,您可以使用源代码控制要求来指定您的包依赖项(例如 .upToNextMinor(from: "0.3.0")
)。
此工具和代码在 Apache License v2.0 和 Runtime Library Exception 下发布。请参阅 LICENSE 了解更多信息。