feat(sdk): 将许可证文件替换为初始化配置文件

- 将 license.xuqm 文件替换为 config.xuqm 配置文件
- 实现 ConfigFileReader 来读取和解密配置文件
- 添加 ConfigFileCrypto 用于配置文件加密解密
- 更新 autoInitialize 方法以从配置文件自动初始化
- 移除对 sdk-license 的反射依赖
- 在 HarmonySDK 中实现配置端点动态配置
- 更新 iOS SDK 中的配置文件读取逻辑
- 统一各平台配置文件格式和处理方式
这个提交包含在:
XuqmGroup 2026-06-02 17:15:49 +08:00
父节点 f9f5c3c6d7
当前提交 25b80ce9e3
共有 5 个文件被更改,包括 124 次插入35 次删除

查看文件

@ -0,0 +1,63 @@
import Foundation
import CryptoKit
import CommonCrypto
/// Decrypts the init config file (format: XUQM-CONFIG-V1.<salt>.<iv>.<ciphertext>).
/// Algorithm: AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation (120,000 iterations).
enum ConfigFileCrypto {
private static let magic = "XUQM-CONFIG-V1"
private static let passphrase = "xuqm-config-file-v1.2026.internal"
private static let keyByteCount = 32
private static let pbkdf2Iterations: UInt32 = 120_000
static func decrypt(_ content: String) throws -> String {
let parts = content.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: ".")
guard parts.count == 4, parts[0] == magic else {
throw ConfigFileError.invalidFormat
}
guard let salt = base64UrlDecode(parts[1]),
let iv = base64UrlDecode(parts[2]),
let ciphertext = base64UrlDecode(parts[3]) else {
throw ConfigFileError.invalidFormat
}
let key = try deriveKey(salt: salt)
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 ConfigFileError.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 ConfigFileError.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)
}
}
enum ConfigFileError: Error {
case invalidFormat
case keyDerivationFailed
case decryptionFailed
}

查看文件

@ -0,0 +1,38 @@
import Foundation
/// Reads and decrypts the init config file from the app bundle.
/// Looks for xuqm/config.xuqm first, then falls back to any *.xuqmconfig file.
/// This is used by XuqmSDK.autoInitialize() and does NOT depend on XuqmLicenseSDK.
public enum ConfigFileReader {
public static func read() -> ConfigFileData? {
guard let url = Bundle.main.url(forResource: "config", withExtension: "xuqm", subdirectory: "xuqm"),
let encrypted = try? String(contentsOf: url, encoding: .utf8) else {
// Fallback: try any *.xuqmconfig file
if let fallbackURL = findConfigFallback(),
let encrypted = try? String(contentsOf: fallbackURL, encoding: .utf8) {
return parse(encrypted)
}
return nil
}
return parse(encrypted)
}
static func parse(_ encrypted: String) -> ConfigFileData? {
guard let json = try? ConfigFileCrypto.decrypt(encrypted),
let data = json.data(using: .utf8),
let file = try? JSONDecoder().decode(ConfigFileData.self, from: data) else {
return nil
}
return file
}
private static func findConfigFallback() -> URL? {
guard let resourceURL = Bundle.main.resourceURL else { return nil }
let xuqmDir = resourceURL.appendingPathComponent("xuqm")
guard let contents = try? FileManager.default.contentsOfDirectory(at: xuqmDir, includingPropertiesForKeys: nil) else {
return nil
}
return contents.first { $0.pathExtension.lowercased() == "xuqmconfig" }
}
}

查看文件

@ -1,6 +1,9 @@
import Foundation
public struct LicenseFileData: Sendable {
/// Decrypted content of the init config file (xuqm/config.xuqm).
/// Contains the appKey and optional server URL needed to bootstrap the SDK.
/// This is separate from XuqmLicenseSDK's LicenseFile (device activation).
public struct ConfigFileData: Codable, Sendable {
public let appKey: String
public let appName: String?
public let companyName: String?
@ -57,23 +60,20 @@ public final class XuqmSDK: NSObject {
public var onLogin: (@MainActor @Sendable (String, String) async throws -> Void)?
public var onLogout: (@MainActor @Sendable (String) async throws -> Void)?
public var onDeviceToken: (@MainActor @Sendable (String, String) async throws -> Void)?
public var licenseReader: (@Sendable () throws -> LicenseFileData?)?
private override init() {
super.init()
}
/// Auto-initialize from the embedded license file using the registered licenseReader hook.
/// Auto-initialize from the embedded config file (xuqm/config.xuqm).
/// Reads and decrypts the config file directly no external hooks needed.
public func autoInitialize(debug: Bool = false) throws {
guard let reader = licenseReader else {
throw XuqmSDKError.noLicenseFile
}
guard let file = try reader() else {
throw XuqmSDKError.noLicenseFile
guard let file = ConfigFileReader.read() else {
throw XuqmSDKError.noConfigFile
}
let bundleId = Bundle.main.bundleIdentifier ?? ""
if let licenseBundle = file.iosBundleId, !licenseBundle.isEmpty, licenseBundle != bundleId {
throw XuqmSDKError.packageMismatch(license: licenseBundle, local: bundleId)
if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId {
throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId)
}
let cfg = SDKConfig(appKey: file.appKey, debug: debug)
initialize(config: cfg)
@ -161,6 +161,7 @@ public final class XuqmSDK: NSObject {
}
public enum XuqmSDKError: Error {
case noConfigFile
case noLicenseFile
case packageMismatch(license: String, local: String)
}

查看文件

@ -18,8 +18,8 @@ public final class LicenseSDK: @unchecked Sendable {
private init() {}
/// Manual initialization. Most apps only need to place the license file at
/// assets/xuqm/license.xuqm (bundle) and call checkLicense() auto-init handles the rest.
/// Manual initialization. Typically not needed call checkLicense() directly
/// after XuqmSDK is initialized (via config file or manual init).
public func initialize(appKey: String, baseUrl: String? = nil, deviceName: String? = nil) {
let url = normalize(baseUrl ?? Self.defaultBaseUrl)
if store.appKey != nil && store.appKey != appKey { store.clear() }
@ -32,13 +32,14 @@ public final class LicenseSDK: @unchecked Sendable {
/// Check license validity. Returns cached result within 10-minute window.
/// On network error, falls back to last cached OK status.
/// Automatically uses appKey from XuqmSDK if not manually initialized.
public func checkLicense(userInfo: LicenseUserInfo? = nil) async -> LicenseResult {
if !initialized {
await XuqmSDK.shared.awaitInitialization()
tryAutoInitialize()
ensureInitializedFromSDK()
}
guard initialized, let appKey else {
return .error("LicenseSDK not initialized")
return .error("LicenseSDK not initialized. Ensure XuqmSDK is initialized first.")
}
let cachedStatus = store.status
@ -86,7 +87,7 @@ public final class LicenseSDK: @unchecked Sendable {
public func getStatus() async -> LicenseStatus {
if !initialized {
await XuqmSDK.shared.awaitInitialization()
tryAutoInitialize()
ensureInitializedFromSDK()
}
switch store.status {
case Self.statusOk: return .ok
@ -98,7 +99,7 @@ public final class LicenseSDK: @unchecked Sendable {
public func getDeviceId() async -> String? {
if !initialized {
await XuqmSDK.shared.awaitInitialization()
tryAutoInitialize()
ensureInitializedFromSDK()
}
if !initialized { return nil }
return store.deviceId
@ -106,10 +107,11 @@ public final class LicenseSDK: @unchecked Sendable {
public func clear() { store.clear() }
/// Initialize from XuqmSDK's config. Called when LicenseSDK is used before manual init.
@discardableResult
private func tryAutoInitialize() -> Bool {
guard let file = LicenseFileReader.read(), !file.appKey.isEmpty else { return false }
initialize(appKey: file.appKey, baseUrl: file.baseUrl)
private func ensureInitializedFromSDK() -> Bool {
guard XuqmSDK.shared.initialized, let config = XuqmSDK.shared.config else { return false }
initialize(appKey: config.appKey)
return true
}

查看文件

@ -13,22 +13,7 @@ extension XuqmSDK {
/// Register all module hooks. Called automatically when any module is first used.
public func registerModuleHooks() {
// License reader hook
licenseReader = {
guard let file = LicenseFileReader.read() else { return nil }
return LicenseFileData(
appKey: file.appKey,
appName: file.appName,
companyName: file.companyName,
packageName: file.packageName,
iosBundleId: file.iosBundleId,
harmonyBundleName: file.harmonyBundleName,
baseUrl: file.baseUrl,
serverUrl: file.serverUrl,
issuedAt: file.issuedAt,
expiresAt: file.expiresAt
)
}
// Note: licenseReader hook removed autoInitialize now reads config.xuqm directly via ConfigFileReader.
// Initialize hook auto-start push if configured
onInitialize = { @MainActor config in