diff --git a/packages/common/src/config.ts b/packages/common/src/config.ts index fde7546..4f9c3b9 100644 --- a/packages/common/src/config.ts +++ b/packages/common/src/config.ts @@ -18,6 +18,8 @@ export interface XuqmConfig { // 崩溃采集服务(rn-bugcollect 使用) bugCollectApiUrl: string bugCollectEnabled: boolean + // 请求签名密钥(从配置文件读取) + signingKey?: string } export interface XuqmUserInfo { @@ -54,6 +56,7 @@ export interface XuqmRemoteConfig { export function initConfigFromRemote( options: XuqmInitOptions, remote: XuqmRemoteConfig, + signingKey?: string, ): void { const apiUrl = remote.imApiUrl ?? remote.apiUrl ?? '' _config = { @@ -68,6 +71,7 @@ export function initConfigFromRemote( licenseEnabled: remote.licenseEnabled ?? false, bugCollectApiUrl: remote.bugCollectApiUrl ?? '', bugCollectEnabled: remote.bugCollectEnabled ?? false, + signingKey, } } diff --git a/packages/common/src/configCrypto.ts b/packages/common/src/configCrypto.ts index 3e9c394..fc258a5 100644 --- a/packages/common/src/configCrypto.ts +++ b/packages/common/src/configCrypto.ts @@ -24,6 +24,7 @@ export interface DecryptedConfig { companyName?: string baseUrl?: string serverUrl?: string + signingKey?: string issuedAt?: string expiresAt?: string } diff --git a/packages/common/src/http.ts b/packages/common/src/http.ts index 7381ca1..a978eca 100644 --- a/packages/common/src/http.ts +++ b/packages/common/src/http.ts @@ -1,5 +1,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { DEFAULT_TENANT_PLATFORM_URL } from './constants' +import { getConfig } from './config' +import { getSigningKey } from './sdk' const TOKEN_KEY = '@xuqm:token' let _baseUrl = DEFAULT_TENANT_PLATFORM_URL @@ -26,6 +28,52 @@ export async function _clearToken(): Promise { return AsyncStorage.removeItem(TOKEN_KEY) } +function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0 + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16) + }) +} + +async function hmacSha256(secret: string, data: string): Promise { + // 使用 react-native-quick-crypto 或降级到纯 JS 实现 + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const qc = require('react-native-quick-crypto') as { default?: { createHmac?: Function }; createHmac?: Function } + const createHmac = qc.createHmac ?? qc.default?.createHmac + if (createHmac) { + const hmac = createHmac('sha256', secret) + hmac.update(data) + return hmac.digest('base64url') + } + } catch { + // 降级到纯 JS 实现 + } + // 纯 JS HMAC-SHA256 实现(用于没有 react-native-quick-crypto 的环境) + // 注意:这是一个简化实现,生产环境建议使用 react-native-quick-crypto + const encoder = new TextEncoder() + const keyData = encoder.encode(secret) + const dataBytes = encoder.encode(data) + + // 使用 SubtleCrypto API(如果可用) + if (typeof globalThis.crypto?.subtle !== 'undefined') { + const key = await globalThis.crypto.subtle.importKey( + 'raw', + keyData, + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'] + ) + const signature = await globalThis.crypto.subtle.sign('HMAC', key, dataBytes) + return btoa(String.fromCharCode(...new Uint8Array(signature))) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') + } + + throw new Error('[XuqmSDK] No HMAC-SHA256 implementation available') +} + export async function apiRequest( path: string, options: { @@ -53,6 +101,29 @@ export async function apiRequest( if (token) headers['Authorization'] = `Bearer ${token}` } + // 注入签名头 + try { + const signingKey = getSigningKey() + if (signingKey) { + const config = getConfig() + const timestamp = Math.floor(Date.now() / 1000).toString() + const nonce = generateUUID() + const userId = config.appKey // 使用 appKey 作为 userId 的默认值 + const payload = `${config.appKey}\n${userId}\n${timestamp}\n${nonce}` + const signature = await hmacSha256(signingKey, payload) + headers['X-App-Key'] = config.appKey + headers['X-Timestamp'] = timestamp + headers['X-Nonce'] = nonce + headers['X-Signature'] = signature + headers['X-User-Id'] = userId + } + } catch (e) { + // 签名失败不应阻止请求 + if (_debugHttp) { + console.debug('[XuqmSDK][HTTP] signature injection failed:', e) + } + } + if (_debugHttp) { console.debug('[XuqmSDK][HTTP] request', { method: options.method ?? 'GET', diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 6a1f008..547f04c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,7 +1,7 @@ // 自动初始化(对齐 Android ContentProvider 模式) import './autoInit' -export { XuqmSDK } from './sdk' +export { XuqmSDK, getSigningKey } from './sdk' export type { XuqmInitOptions, XuqmConfig, XuqmUserInfo } from './config' export { getConfig, diff --git a/packages/common/src/sdk.ts b/packages/common/src/sdk.ts index 2ff569c..2b8c808 100644 --- a/packages/common/src/sdk.ts +++ b/packages/common/src/sdk.ts @@ -6,6 +6,7 @@ import { decryptConfigFile } from './configCrypto' let _initPromise: Promise | null = null let _initResolve: (() => void) | null = null let _initReject: ((e: unknown) => void) | null = null +let _signingKey: string | undefined = undefined function ensureInitPromise(): Promise { if (!_initPromise) { @@ -65,7 +66,7 @@ export const XuqmSDK = { licenseEnabled: remote.licenseEnabled as boolean | undefined, bugCollectApiUrl: remote.bugCollectApiUrl as string | undefined, bugCollectEnabled: remote.bugCollectEnabled as boolean | undefined, - }) + }, _signingKey) configureHttp({ baseUrl: (remote.apiUrl as string | undefined) ?? baseUrl, debug: options.debug, @@ -90,6 +91,10 @@ export const XuqmSDK = { const file = await decryptConfigFile(encryptedContent) // 配置文件解密后包含 appKey 和 platformUrl(字段名 serverUrl 或 baseUrl) const platformUrl = file.serverUrl ?? file.baseUrl ?? undefined + // 保存 signingKey 供后续请求签名使用 + if (file.signingKey) { + _signingKey = file.signingKey + } await this.initialize({ appKey: file.appKey, platformUrl, debug: options?.debug }) }, @@ -122,3 +127,7 @@ export async function awaitInitialization(): Promise { if (isInitialized()) return await ensureInitPromise() } + +export function getSigningKey(): string | undefined { + return _signingKey +} diff --git a/packages/update/src/UpdateSDK.ts b/packages/update/src/UpdateSDK.ts index 5b44d12..acd3c93 100644 --- a/packages/update/src/UpdateSDK.ts +++ b/packages/update/src/UpdateSDK.ts @@ -1,6 +1,6 @@ import AsyncStorage from '@react-native-async-storage/async-storage' import { Linking, Platform } from 'react-native' -import { apiRequest, getConfig, getUserId, _registerUserInfoHandler } from '@xuqm/rn-common' +import { apiRequest, getConfig, getUserId, getDeviceInfo, _registerUserInfoHandler } from '@xuqm/rn-common' import { getAppVersionCode, getAppVersionName, _devSetAppVersion } from './NativeVersion' import { md5 } from './md5' @@ -228,10 +228,16 @@ export const UpdateSDK = { } const config = getConfig() + const deviceInfo = await getDeviceInfo() const params: Record = { appKey: config.appKey, platform: Platform.OS === 'android' ? 'ANDROID' : 'IOS', currentVersionCode: String(getAppVersionCode()), + currentVersionName: getAppVersionName() ?? '', + deviceId: deviceInfo.deviceId, + model: deviceInfo.model, + osVersion: deviceInfo.osVersion, + vendor: deviceInfo.pushVendor, } const userId = getUserId()?.trim() if (userId) params.userId = userId