XuqmGroup-RNSDK/packages/common/src/configCrypto.ts
XuqmGroup 0cf264cdf6 feat(core): 添加请求签名功能并扩展更新接口
- 在 SDKConfig 中新增 signingKey 配置项
- 在 ConfigFile 中添加 signingKey 序列化支持
- 实现 API 客户端请求签名拦截器,支持 HMAC-SHA256 签名
- 在 XuqmSDK 中管理 signingKey 的存储和读取
- 扩展 UpdateApi 接口增加 currentVersionName 参数
- 在 UpdateSDK 中集成设备信息和版本号上报功能
- 为 RN SDK 添加签名密钥管理和 HTTP 请求签名注入
- 实现跨平台的 HMAC-SHA256 签名算法支持
2026-06-19 01:27:56 +08:00

87 行
3.2 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

/**
* 内置配置文件解密。
*
* 支持格式:
* XUQM-LICENSE-V1.{salt}.{iv}.{ciphertext}
* XUQM-CONFIG-V1.{salt}.{iv}.{ciphertext}
*
* 使用 react-native-quick-crypto 的 SubtleCryptopeer 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
}