feat(sdk): 将许可证文件替换为初始化配置文件
- 将 license.xuqm 文件替换为 config.xuqm 配置文件 - 实现 ConfigFileReader 来读取和解密配置文件 - 添加 ConfigFileCrypto 用于配置文件加密解密 - 更新 autoInitialize 方法以从配置文件自动初始化 - 移除对 sdk-license 的反射依赖 - 在 HarmonySDK 中实现配置端点动态配置 - 更新 iOS SDK 中的配置文件读取逻辑 - 统一各平台配置文件格式和处理方式
这个提交包含在:
父节点
f9f5c3c6d7
当前提交
25b80ce9e3
@ -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
|
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 appKey: String
|
||||||
public let appName: String?
|
public let appName: String?
|
||||||
public let companyName: 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 onLogin: (@MainActor @Sendable (String, String) async throws -> Void)?
|
||||||
public var onLogout: (@MainActor @Sendable (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 onDeviceToken: (@MainActor @Sendable (String, String) async throws -> Void)?
|
||||||
public var licenseReader: (@Sendable () throws -> LicenseFileData?)?
|
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.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 {
|
public func autoInitialize(debug: Bool = false) throws {
|
||||||
guard let reader = licenseReader else {
|
guard let file = ConfigFileReader.read() else {
|
||||||
throw XuqmSDKError.noLicenseFile
|
throw XuqmSDKError.noConfigFile
|
||||||
}
|
|
||||||
guard let file = try reader() else {
|
|
||||||
throw XuqmSDKError.noLicenseFile
|
|
||||||
}
|
}
|
||||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||||
if let licenseBundle = file.iosBundleId, !licenseBundle.isEmpty, licenseBundle != bundleId {
|
if let configBundle = file.iosBundleId, !configBundle.isEmpty, configBundle != bundleId {
|
||||||
throw XuqmSDKError.packageMismatch(license: licenseBundle, local: bundleId)
|
throw XuqmSDKError.packageMismatch(license: configBundle, local: bundleId)
|
||||||
}
|
}
|
||||||
let cfg = SDKConfig(appKey: file.appKey, debug: debug)
|
let cfg = SDKConfig(appKey: file.appKey, debug: debug)
|
||||||
initialize(config: cfg)
|
initialize(config: cfg)
|
||||||
@ -161,6 +161,7 @@ public final class XuqmSDK: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum XuqmSDKError: Error {
|
public enum XuqmSDKError: Error {
|
||||||
|
case noConfigFile
|
||||||
case noLicenseFile
|
case noLicenseFile
|
||||||
case packageMismatch(license: String, local: String)
|
case packageMismatch(license: String, local: String)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,8 +18,8 @@ public final class LicenseSDK: @unchecked Sendable {
|
|||||||
|
|
||||||
private init() {}
|
private init() {}
|
||||||
|
|
||||||
/// Manual initialization. Most apps only need to place the license file at
|
/// Manual initialization. Typically not needed — call checkLicense() directly
|
||||||
/// assets/xuqm/license.xuqm (bundle) and call checkLicense() — auto-init handles the rest.
|
/// after XuqmSDK is initialized (via config file or manual init).
|
||||||
public func initialize(appKey: String, baseUrl: String? = nil, deviceName: String? = nil) {
|
public func initialize(appKey: String, baseUrl: String? = nil, deviceName: String? = nil) {
|
||||||
let url = normalize(baseUrl ?? Self.defaultBaseUrl)
|
let url = normalize(baseUrl ?? Self.defaultBaseUrl)
|
||||||
if store.appKey != nil && store.appKey != appKey { store.clear() }
|
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.
|
/// Check license validity. Returns cached result within 10-minute window.
|
||||||
/// On network error, falls back to last cached OK status.
|
/// 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 {
|
public func checkLicense(userInfo: LicenseUserInfo? = nil) async -> LicenseResult {
|
||||||
if !initialized {
|
if !initialized {
|
||||||
await XuqmSDK.shared.awaitInitialization()
|
await XuqmSDK.shared.awaitInitialization()
|
||||||
tryAutoInitialize()
|
ensureInitializedFromSDK()
|
||||||
}
|
}
|
||||||
guard initialized, let appKey else {
|
guard initialized, let appKey else {
|
||||||
return .error("LicenseSDK not initialized")
|
return .error("LicenseSDK not initialized. Ensure XuqmSDK is initialized first.")
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedStatus = store.status
|
let cachedStatus = store.status
|
||||||
@ -86,7 +87,7 @@ public final class LicenseSDK: @unchecked Sendable {
|
|||||||
public func getStatus() async -> LicenseStatus {
|
public func getStatus() async -> LicenseStatus {
|
||||||
if !initialized {
|
if !initialized {
|
||||||
await XuqmSDK.shared.awaitInitialization()
|
await XuqmSDK.shared.awaitInitialization()
|
||||||
tryAutoInitialize()
|
ensureInitializedFromSDK()
|
||||||
}
|
}
|
||||||
switch store.status {
|
switch store.status {
|
||||||
case Self.statusOk: return .ok
|
case Self.statusOk: return .ok
|
||||||
@ -98,7 +99,7 @@ public final class LicenseSDK: @unchecked Sendable {
|
|||||||
public func getDeviceId() async -> String? {
|
public func getDeviceId() async -> String? {
|
||||||
if !initialized {
|
if !initialized {
|
||||||
await XuqmSDK.shared.awaitInitialization()
|
await XuqmSDK.shared.awaitInitialization()
|
||||||
tryAutoInitialize()
|
ensureInitializedFromSDK()
|
||||||
}
|
}
|
||||||
if !initialized { return nil }
|
if !initialized { return nil }
|
||||||
return store.deviceId
|
return store.deviceId
|
||||||
@ -106,10 +107,11 @@ public final class LicenseSDK: @unchecked Sendable {
|
|||||||
|
|
||||||
public func clear() { store.clear() }
|
public func clear() { store.clear() }
|
||||||
|
|
||||||
|
/// Initialize from XuqmSDK's config. Called when LicenseSDK is used before manual init.
|
||||||
@discardableResult
|
@discardableResult
|
||||||
private func tryAutoInitialize() -> Bool {
|
private func ensureInitializedFromSDK() -> Bool {
|
||||||
guard let file = LicenseFileReader.read(), !file.appKey.isEmpty else { return false }
|
guard XuqmSDK.shared.initialized, let config = XuqmSDK.shared.config else { return false }
|
||||||
initialize(appKey: file.appKey, baseUrl: file.baseUrl)
|
initialize(appKey: config.appKey)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,22 +13,7 @@ extension XuqmSDK {
|
|||||||
|
|
||||||
/// Register all module hooks. Called automatically when any module is first used.
|
/// Register all module hooks. Called automatically when any module is first used.
|
||||||
public func registerModuleHooks() {
|
public func registerModuleHooks() {
|
||||||
// License reader hook
|
// Note: licenseReader hook removed — autoInitialize now reads config.xuqm directly via ConfigFileReader.
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize hook — auto-start push if configured
|
// Initialize hook — auto-start push if configured
|
||||||
onInitialize = { @MainActor config in
|
onInitialize = { @MainActor config in
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户