From dbabf813ea4e3c7f848d175c4db4dac868bbc1de Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 2 Jun 2026 17:15:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(sdk):=20=E5=B0=86=E8=AE=B8=E5=8F=AF?= =?UTF-8?q?=E8=AF=81=E6=96=87=E4=BB=B6=E6=9B=BF=E6=8D=A2=E4=B8=BA=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 license.xuqm 文件替换为 config.xuqm 配置文件 - 实现 ConfigFileReader 来读取和解密配置文件 - 添加 ConfigFileCrypto 用于配置文件加密解密 - 更新 autoInitialize 方法以从配置文件自动初始化 - 移除对 sdk-license 的反射依赖 - 在 HarmonySDK 中实现配置端点动态配置 - 更新 iOS SDK 中的配置文件读取逻辑 - 统一各平台配置文件格式和处理方式 --- xuqm-sdk/Index.ets | 1 + xuqm-sdk/src/main/ets/XuqmSDK.ets | 27 +++++++ .../src/main/ets/core/ConfigFileCrypto.ets | 81 +++++++++++++++++++ .../src/main/ets/core/ConfigFileReader.ets | 57 +++++++++++++ xuqm-sdk/src/main/ets/core/Endpoints.ets | 18 +++++ xuqm-sdk/src/main/ets/core/HttpClient.ets | 4 +- xuqm-sdk/src/main/ets/core/SDKContext.ets | 4 + xuqm-sdk/src/main/ets/core/Types.ets | 14 ++++ xuqm-sdk/src/main/ets/im/ImClient.ets | 6 +- 9 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 xuqm-sdk/src/main/ets/core/ConfigFileCrypto.ets create mode 100644 xuqm-sdk/src/main/ets/core/ConfigFileReader.ets diff --git a/xuqm-sdk/Index.ets b/xuqm-sdk/Index.ets index fdaeb50..4e790b0 100644 --- a/xuqm-sdk/Index.ets +++ b/xuqm-sdk/Index.ets @@ -5,6 +5,7 @@ export { UpdateSDK } from './src/main/ets/update/UpdateSDK' export { SDKContext } from './src/main/ets/core/SDKContext' export { HttpClient } from './src/main/ets/core/HttpClient' export type { + ConfigFile, SDKConfig, LoginSession, ImMessage, diff --git a/xuqm-sdk/src/main/ets/XuqmSDK.ets b/xuqm-sdk/src/main/ets/XuqmSDK.ets index 0188625..218d146 100644 --- a/xuqm-sdk/src/main/ets/XuqmSDK.ets +++ b/xuqm-sdk/src/main/ets/XuqmSDK.ets @@ -1,5 +1,6 @@ import common from '@ohos.app.ability.common' import type { LoginSession, SDKConfig } from './core/Types' +import { ConfigFileReader } from './core/ConfigFileReader' import { SDKContext } from './core/SDKContext' import { ImClient } from './im/ImClient' import { PushSDK } from './push/PushSDK' @@ -9,6 +10,32 @@ export class XuqmSDK { private static _imClient: ImClient | null = null private static _pushClient: PushSDK = new PushSDK() + /** + * Auto-initialize from the embedded config file (resources/rawfile/xuqm/config.xuqm). + * Reads and decrypts the config file directly — no hardcoded appKey needed. + */ + static async autoInitialize(context: common.UIAbilityContext): Promise { + const configFile = await ConfigFileReader.read(context) + if (!configFile) { + throw new Error( + 'No config file found in resources/rawfile/xuqm/. ' + + 'Download config.xuqm from the tenant platform and place it in resources/rawfile/xuqm/.' + ) + } + if (configFile.packageName && configFile.packageName !== context.bundleName) { + throw new Error( + `Config package name mismatch: config=${configFile.packageName}, local=${context.bundleName}. ` + + 'Please download the correct config file for this app.' + ) + } + const config: SDKConfig = { + appKey: configFile.appKey, + debug: false, + serverUrl: configFile.serverUrl, + } + await this.init(context, config) + } + static async init(context: common.UIAbilityContext, config: SDKConfig): Promise { SDKContext.init(config) await SDKContext.initPreferences(context) diff --git a/xuqm-sdk/src/main/ets/core/ConfigFileCrypto.ets b/xuqm-sdk/src/main/ets/core/ConfigFileCrypto.ets new file mode 100644 index 0000000..f6c22fd --- /dev/null +++ b/xuqm-sdk/src/main/ets/core/ConfigFileCrypto.ets @@ -0,0 +1,81 @@ +import cryptoFramework from '@ohos.security.cryptoFramework' + +const MAGIC = 'XUQM-CONFIG-V1' +const PASSPHRASE = 'xuqm-config-file-v1.2026.internal' +const KEY_BITS = 256 +const ITERATIONS = 120_000 +const GCM_TAG_BITS = 128 + +/** + * Decrypts the init config file (format: XUQM-CONFIG-V1...). + * Algorithm: AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation (120,000 iterations). + */ +export async function decryptConfigFile(content: string): Promise { + const parts = content.trim().split('.') + if (parts.length !== 4 || parts[0] !== MAGIC) { + throw new Error('Invalid config file format') + } + + const salt = base64UrlDecode(parts[1]) + const iv = base64UrlDecode(parts[2]) + const cipherText = base64UrlDecode(parts[3]) + + const key = await deriveKey(salt) + + const cipher = cryptoFramework.createCipher('AES256|GCM|NoPadding') + const gcmParams: cryptoFramework.GcmParamsSpec = { + iv: { data: iv }, + aad: { data: new Uint8Array(0) }, + authTag: { data: new Uint8Array(GCM_TAG_BITS / 8) }, + algId: 'AES256|GCM|NoPadding', + } + + await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, key, gcmParams) + const decrypted = await cipher.doFinal({ data: cipherText }) + return arrayBufferToString(decrypted.data) +} + +async function deriveKey(salt: Uint8Array): Promise { + // PBKDF2 key derivation + const kdf = cryptoFramework.createKdf('PBKDF2|SHA256') + const kdfParams: cryptoFramework.KdfParamsSpec = { + algName: 'PBKDF2', + salt: { data: salt }, + iterations: ITERATIONS, + keyLen: KEY_BITS / 8, + } + + const passphraseData = stringToArrayBuffer(PASSPHRASE) + const derivedKey = await kdf.deriveKey(passphraseData, kdfParams) + return derivedKey +} + +function base64UrlDecode(value: string): Uint8Array { + let s = value.replace(/-/g, '+').replace(/_/g, '/') + const rem = s.length % 4 + if (rem > 0) { + s += '='.repeat(4 - rem) + } + return base64ToUint8Array(s) +} + +function base64ToUint8Array(base64: string): Uint8Array { + const binary = atob(base64) + const bytes = new Uint8Array(binary.length) + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i) + } + return bytes +} + +function arrayBufferToString(buffer: ArrayBuffer): string { + const decoder = new util.TextDecoder('utf-8') + return decoder.decode(new Uint8Array(buffer)) +} + +function stringToArrayBuffer(str: string): Uint8Array { + const encoder = new util.TextEncoder() + return encoder.encodeInto(str) +} + +import util from '@ohos.util' diff --git a/xuqm-sdk/src/main/ets/core/ConfigFileReader.ets b/xuqm-sdk/src/main/ets/core/ConfigFileReader.ets new file mode 100644 index 0000000..d38c9e6 --- /dev/null +++ b/xuqm-sdk/src/main/ets/core/ConfigFileReader.ets @@ -0,0 +1,57 @@ +import common from '@ohos.app.ability.common' +import { decryptConfigFile } from './ConfigFileCrypto' +import type { ConfigFile } from './Types' + +/** + * Reads and decrypts the init config file from resources/rawfile/xuqm/. + * Looks for config.xuqm first, then falls back to any *.xuqmconfig file. + * This is used by XuqmSDK.autoInitialize() and does NOT depend on sdk-license. + */ +export class ConfigFileReader { + static async read(context: common.UIAbilityContext): Promise { + try { + const rawFileManager = context.resourceManager + const encrypted = await this.readRawFile(rawFileManager, 'xuqm/config.xuqm') + if (encrypted) { + return await this.parse(encrypted) + } + // Fallback: try any *.xuqmconfig file + const files = await rawFileManager.getRawFileList('xuqm') + const fallback = files.find(f => f.toLowerCase().endsWith('.xuqmconfig')) + if (fallback) { + const fallbackContent = await this.readRawFile(rawFileManager, `xuqm/${fallback}`) + if (fallbackContent) { + return await this.parse(fallbackContent) + } + } + return null + } catch (_: Error) { + return null + } + } + + private static async readRawFile( + rawFileManager: resourceManager.ResourceManager, + path: string + ): Promise { + try { + const buffer = await rawFileManager.getRawFileContent(path) + const decoder = new util.TextDecoder('utf-8') + return decoder.decode(buffer) + } catch (_: Error) { + return null + } + } + + private static async parse(encrypted: string): Promise { + try { + const json = await decryptConfigFile(encrypted) + return JSON.parse(json) as ConfigFile + } catch (_: Error) { + return null + } + } +} + +import resourceManager from '@ohos.resourceManager' +import util from '@ohos.util' diff --git a/xuqm-sdk/src/main/ets/core/Endpoints.ets b/xuqm-sdk/src/main/ets/core/Endpoints.ets index 988b562..ea045c5 100644 --- a/xuqm-sdk/src/main/ets/core/Endpoints.ets +++ b/xuqm-sdk/src/main/ets/core/Endpoints.ets @@ -1,2 +1,20 @@ export const DEFAULT_API_BASE_URL = 'https://dev.xuqinmin.com' export const DEFAULT_IM_WS_URL = 'wss://dev.xuqinmin.com/ws/im' + +let _apiBaseUrl = DEFAULT_API_BASE_URL +let _imWsUrl = DEFAULT_IM_WS_URL + +export function configureEndpoints(serverUrl: string): void { + const base = serverUrl.endsWith('/') ? serverUrl : serverUrl + '/' + const wsBase = serverUrl.replace(/\/$/, '').replace('https://', 'wss://').replace('http://', 'ws://') + _apiBaseUrl = base + _imWsUrl = `${wsBase}/ws/im` +} + +export function getApiBaseUrl(): string { + return _apiBaseUrl +} + +export function getImWsUrl(): string { + return _imWsUrl +} diff --git a/xuqm-sdk/src/main/ets/core/HttpClient.ets b/xuqm-sdk/src/main/ets/core/HttpClient.ets index bf0e9f5..aedc517 100644 --- a/xuqm-sdk/src/main/ets/core/HttpClient.ets +++ b/xuqm-sdk/src/main/ets/core/HttpClient.ets @@ -1,7 +1,7 @@ import http from '@ohos.net.http' import type { ApiResponse, HttpHeaders } from './Types' import { SDKContext } from './SDKContext' -import { DEFAULT_API_BASE_URL } from './Endpoints' +import { getApiBaseUrl } from './Endpoints' export class HttpClient { static async request( @@ -12,7 +12,7 @@ export class HttpClient { ): Promise { const config = SDKContext.getConfig() const token = SDKContext.getToken() - const url = DEFAULT_API_BASE_URL.replace(/\/$/, '') + path + (query ? '?' + query : '') + const url = getApiBaseUrl().replace(/\/$/, '') + path + (query ? '?' + query : '') const client = http.createHttp() try { diff --git a/xuqm-sdk/src/main/ets/core/SDKContext.ets b/xuqm-sdk/src/main/ets/core/SDKContext.ets index dd400fc..76c6e4a 100644 --- a/xuqm-sdk/src/main/ets/core/SDKContext.ets +++ b/xuqm-sdk/src/main/ets/core/SDKContext.ets @@ -1,5 +1,6 @@ import preferences from '@ohos.data.preferences' import type { InstalledRnBundleInfo, LoginSession, SDKConfig } from './Types' +import { configureEndpoints } from './Endpoints' const TOKEN_KEY = 'xuqm_token' const USER_ID_KEY = 'xuqm_user_id' @@ -14,6 +15,9 @@ export class SDKContext { static init(config: SDKConfig): void { SDKContext._config = config + if (config.serverUrl) { + configureEndpoints(config.serverUrl) + } if (config.debug) { console.log('[XuqmSDK] init appKey=' + config.appKey) } diff --git a/xuqm-sdk/src/main/ets/core/Types.ets b/xuqm-sdk/src/main/ets/core/Types.ets index dee1fa5..4f2253b 100644 --- a/xuqm-sdk/src/main/ets/core/Types.ets +++ b/xuqm-sdk/src/main/ets/core/Types.ets @@ -1,6 +1,20 @@ export interface SDKConfig { appKey: string debug: boolean + serverUrl?: string +} + +export interface ConfigFile { + appKey: string + appName?: string + companyName?: string + packageName?: string + iosBundleId?: string + harmonyBundleName?: string + baseUrl?: string + serverUrl?: string + issuedAt?: string + expiresAt?: string } export interface ApiResponse { diff --git a/xuqm-sdk/src/main/ets/im/ImClient.ets b/xuqm-sdk/src/main/ets/im/ImClient.ets index 05a332e..59ccfe9 100644 --- a/xuqm-sdk/src/main/ets/im/ImClient.ets +++ b/xuqm-sdk/src/main/ets/im/ImClient.ets @@ -2,7 +2,7 @@ import webSocket from '@ohos.net.webSocket' import http from '@ohos.net.http' import { HttpClient } from '../core/HttpClient' import { SDKContext } from '../core/SDKContext' -import { DEFAULT_API_BASE_URL, DEFAULT_IM_WS_URL } from '../core/Endpoints' +import { getApiBaseUrl, getImWsUrl } from '../core/Endpoints' import type { ChatType, ConversationData, @@ -132,7 +132,7 @@ export class ImClient { if (this.destroyed) return const config = SDKContext.getConfig() const token = SDKContext.getToken() ?? '' - const url = DEFAULT_IM_WS_URL.replace(/\/$/, '') + '?token=' + encodeURIComponent(token) + const url = getImWsUrl().replace(/\/$/, '') + '?token=' + encodeURIComponent(token) this.ws = webSocket.createWebSocket() @@ -751,7 +751,7 @@ export class ImClient { private async uploadFile(filePath: string): Promise { const token = SDKContext.getToken() - const url = DEFAULT_API_BASE_URL.replace(/\/$/, '') + '/api/file/upload' + const url = getApiBaseUrl().replace(/\/$/, '') + '/api/file/upload' const client = http.createHttp() try { const header: Record = {}