/** * 内置配置文件解密。 * * 支持格式: * 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 issuedAt?: string expiresAt?: string } interface SubtleCryptoLike { importKey(format: string, keyData: BufferSource, algorithm: string | Record, extractable: boolean, keyUsages: string[]): Promise deriveKey(algorithm: string | Record, baseKey: CryptoKey, derivedKeyAlgorithm: string | Record, extractable: boolean, keyUsages: string[]): Promise decrypt(algorithm: string | Record, key: CryptoKey, data: BufferSource): Promise } 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 { 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 { 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 }