- 在 SDKConfig 中新增 signingKey 配置项 - 在 ConfigFile 中添加 signingKey 序列化支持 - 实现 API 客户端请求签名拦截器,支持 HMAC-SHA256 签名 - 在 XuqmSDK 中管理 signingKey 的存储和读取 - 扩展 UpdateApi 接口增加 currentVersionName 参数 - 在 UpdateSDK 中集成设备信息和版本号上报功能 - 为 RN SDK 添加签名密钥管理和 HTTP 请求签名注入 - 实现跨平台的 HMAC-SHA256 签名算法支持
87 行
3.2 KiB
TypeScript
87 行
3.2 KiB
TypeScript
/**
|
||
* 内置配置文件解密。
|
||
*
|
||
* 支持格式:
|
||
* XUQM-LICENSE-V1.{salt}.{iv}.{ciphertext}
|
||
* XUQM-CONFIG-V1.{salt}.{iv}.{ciphertext}
|
||
*
|
||
* 使用 react-native-quick-crypto 的 SubtleCrypto(peer dependency)。
|
||
*/
|
||
|
||
const MAGIC_LICENSE = 'XUQM-LICENSE-V1'
|
||
const MAGIC_CONFIG = 'XUQM-CONFIG-V1'
|
||
const PASSPHRASE_CONFIG = 'xuqm-config-file-v1.2026.internal'
|
||
const PASSPHRASE_LICENSE = 'xuqm-license-file-v1.2026.internal'
|
||
const PBKDF2_ITERATIONS = 120_000
|
||
|
||
function getPassphrase(magic: string): string {
|
||
return magic === MAGIC_CONFIG ? PASSPHRASE_CONFIG : PASSPHRASE_LICENSE
|
||
}
|
||
|
||
export interface DecryptedConfig {
|
||
appKey: string
|
||
appName?: string
|
||
companyName?: string
|
||
baseUrl?: string
|
||
serverUrl?: string
|
||
signingKey?: string
|
||
issuedAt?: string
|
||
expiresAt?: string
|
||
}
|
||
|
||
interface SubtleCryptoLike {
|
||
importKey(format: string, keyData: BufferSource, algorithm: string | Record<string, unknown>, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>
|
||
deriveKey(algorithm: string | Record<string, unknown>, baseKey: CryptoKey, derivedKeyAlgorithm: string | Record<string, unknown>, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>
|
||
decrypt(algorithm: string | Record<string, unknown>, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>
|
||
}
|
||
|
||
function getSubtle(): SubtleCryptoLike {
|
||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||
const qc = require('react-native-quick-crypto') as { subtle?: SubtleCryptoLike; default?: { subtle?: SubtleCryptoLike } }
|
||
const subtle = qc.subtle ?? qc.default?.subtle
|
||
if (!subtle) throw new Error('[XuqmSDK] react-native-quick-crypto not available')
|
||
return subtle
|
||
}
|
||
|
||
function base64UrlDecode(s: string): Uint8Array {
|
||
const padded = s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4)
|
||
const binary = atob(padded)
|
||
return Uint8Array.from({ length: binary.length }, (_, i) => binary.charCodeAt(i))
|
||
}
|
||
|
||
async function deriveKey(salt: Uint8Array, passphrase: string): Promise<CryptoKey> {
|
||
const subtle = getSubtle()
|
||
const passphraseKey = await subtle.importKey(
|
||
'raw',
|
||
new TextEncoder().encode(passphrase),
|
||
{ name: 'PBKDF2' },
|
||
false,
|
||
['deriveKey'],
|
||
)
|
||
return subtle.deriveKey(
|
||
{ name: 'PBKDF2', salt: salt as unknown as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
|
||
passphraseKey,
|
||
{ name: 'AES-GCM', length: 256 },
|
||
false,
|
||
['decrypt'],
|
||
)
|
||
}
|
||
|
||
export async function decryptConfigFile(content: string): Promise<DecryptedConfig> {
|
||
const parts = content.trim().split('.')
|
||
const magic = parts[0]
|
||
if (parts.length !== 4 || (magic !== MAGIC_CONFIG && magic !== MAGIC_LICENSE)) {
|
||
throw new Error('[XuqmSDK] Invalid config/license file format')
|
||
}
|
||
const salt = base64UrlDecode(parts[1])
|
||
const iv = base64UrlDecode(parts[2])
|
||
const ciphertext = base64UrlDecode(parts[3])
|
||
|
||
const passphrase = getPassphrase(magic)
|
||
const key = await deriveKey(salt, passphrase)
|
||
const subtle = getSubtle()
|
||
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv: iv as unknown as BufferSource }, key, ciphertext as unknown as BufferSource)
|
||
const json = new TextDecoder().decode(plainBuffer)
|
||
return JSON.parse(json) as DecryptedConfig
|
||
}
|