XuqmGroup-iOSSDK/Sources/XuqmLicenseSDK/LicenseFileCrypto.swift
XuqmGroup e016adbb71 chore(ci): 更新 Jenkins 配置以支持多模块版本管理
- 为 Android、Flutter、iOS 和 RN SDK 的 Jenkinsfile 添加模块化版本控制
- 引入版本升级策略选择(major/minor/patch)和自定义版本号功能
- 实现多模块独立版本管理和选择性构建发布
- 更新 iOS SDK Package.swift 以支持独立模块化库
- 修改 iOS SDK podspec 文件以适应新的标签命名约定
- 优化 Jenkins 构建流程以支持按需选择特定模块进行构建和发布
- 修复 iOS 测试中的可选类型转换问题以提高代码健壮性
2026-05-23 01:20:57 +08:00

68 行
2.7 KiB
Swift

import Foundation
import XuqmCoreSDK
import CryptoKit
import CommonCrypto
enum LicenseFileCryptoError: Error {
case invalidFormat
case keyDerivationFailed
case decryptionFailed
}
enum LicenseFileCrypto {
private static let magic = "XUQM-LICENSE-V1"
private static let passphrase = "xuqm-license-file-v1.2026.internal"
private static let keyByteCount = 32
private static let pbkdf2Iterations: UInt32 = 120_000
// Format: MAGIC.base64UrlSalt.base64UrlIV.base64UrlCiphertext
// Base64: URL-safe, no padding, no wrap
// Algorithm: AES/GCM/NoPadding, PBKDF2WithHmacSHA256, 256-bit key, 128-bit tag
static func decrypt(_ content: String) throws -> String {
let parts = content.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ".")
guard parts.count == 4, parts[0] == magic else {
throw LicenseFileCryptoError.invalidFormat
}
guard let salt = base64UrlDecode(parts[1]),
let iv = base64UrlDecode(parts[2]),
let ciphertext = base64UrlDecode(parts[3]) else {
throw LicenseFileCryptoError.invalidFormat
}
let key = try deriveKey(salt: salt)
// CryptoKit SealedBox(combined:) expects nonce(12) || ciphertext || tag(16)
// Android JCE appends the 16-byte GCM tag at end of ciphertext matches this format exactly
let combined = iv + ciphertext
let sealedBox = try AES.GCM.SealedBox(combined: combined)
let decrypted = try AES.GCM.open(sealedBox, using: key)
guard let plaintext = String(data: decrypted, encoding: .utf8) else {
throw LicenseFileCryptoError.decryptionFailed
}
return plaintext
}
private static func deriveKey(salt: Data) throws -> SymmetricKey {
let passphraseBytes = Array(passphrase.utf8)
let saltBytes = Array(salt)
var derivedKey = [UInt8](repeating: 0, count: keyByteCount)
let status = CCKeyDerivationPBKDF(
CCPBKDFAlgorithm(kCCPBKDF2),
passphrase, passphraseBytes.count,
saltBytes, saltBytes.count,
CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256),
pbkdf2Iterations,
&derivedKey, keyByteCount
)
guard status == kCCSuccess else {
throw LicenseFileCryptoError.keyDerivationFailed
}
return SymmetricKey(data: Data(derivedKey))
}
private static func base64UrlDecode(_ value: String) -> Data? {
var s = value.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
let rem = s.count % 4
if rem > 0 { s += String(repeating: "=", count: 4 - rem) }
return Data(base64Encoded: s)
}
}