2026-06-15 01:44:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 内置配置文件解密。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 支持格式:
|
|
|
|
|
|
* 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 14:48:47 +08:00
|
|
|
|
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 {
|
2026-06-15 01:44:20 +08:00
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
2026-06-16 14:48:47 +08:00
|
|
|
|
const qc = require('react-native-quick-crypto') as { subtle?: SubtleCryptoLike; default?: { subtle?: SubtleCryptoLike } }
|
2026-06-15 01:44:20 +08:00
|
|
|
|
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))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-16 14:48:47 +08:00
|
|
|
|
async function deriveKey(salt: Uint8Array, passphrase: string): Promise<CryptoKey> {
|
2026-06-15 01:44:20 +08:00
|
|
|
|
const subtle = getSubtle()
|
|
|
|
|
|
const passphraseKey = await subtle.importKey(
|
|
|
|
|
|
'raw',
|
|
|
|
|
|
new TextEncoder().encode(passphrase),
|
|
|
|
|
|
{ name: 'PBKDF2' },
|
|
|
|
|
|
false,
|
|
|
|
|
|
['deriveKey'],
|
|
|
|
|
|
)
|
|
|
|
|
|
return subtle.deriveKey(
|
2026-06-16 14:48:47 +08:00
|
|
|
|
{ name: 'PBKDF2', salt: salt as unknown as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
|
2026-06-15 01:44:20 +08:00
|
|
|
|
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()
|
2026-06-16 14:48:47 +08:00
|
|
|
|
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv: iv as unknown as BufferSource }, key, ciphertext as unknown as BufferSource)
|
2026-06-15 01:44:20 +08:00
|
|
|
|
const json = new TextDecoder().decode(plainBuffer)
|
|
|
|
|
|
return JSON.parse(json) as DecryptedConfig
|
|
|
|
|
|
}
|