feat(sdk): 将许可证文件替换为初始化配置文件
- 将 license.xuqm 文件替换为 config.xuqm 配置文件 - 实现 ConfigFileReader 来读取和解密配置文件 - 添加 ConfigFileCrypto 用于配置文件加密解密 - 更新 autoInitialize 方法以从配置文件自动初始化 - 移除对 sdk-license 的反射依赖 - 在 HarmonySDK 中实现配置端点动态配置 - 更新 iOS SDK 中的配置文件读取逻辑 - 统一各平台配置文件格式和处理方式
这个提交包含在:
父节点
3ca918206b
当前提交
dbabf813ea
@ -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,
|
||||
|
||||
@ -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<void> {
|
||||
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<void> {
|
||||
SDKContext.init(config)
|
||||
await SDKContext.initPreferences(context)
|
||||
|
||||
@ -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.<salt>.<iv>.<ciphertext>).
|
||||
* Algorithm: AES-256-GCM with PBKDF2-HMAC-SHA256 key derivation (120,000 iterations).
|
||||
*/
|
||||
export async function decryptConfigFile(content: string): Promise<string> {
|
||||
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<cryptoFramework.SymKey> {
|
||||
// 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'
|
||||
@ -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<ConfigFile | null> {
|
||||
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<string | null> {
|
||||
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<ConfigFile | null> {
|
||||
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'
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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<T>(
|
||||
@ -12,7 +12,7 @@ export class HttpClient {
|
||||
): Promise<T> {
|
||||
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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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<T> {
|
||||
|
||||
@ -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<Object> {
|
||||
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<string, string> = {}
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户