import config from "@/config"; import forge, { primeForgeRandomBytes } from "@/utils/transportForge"; import { ensureTransportCryptoPolicyLoaded, getTransportCryptoPolicy, getTransportRequestPath, shouldEncryptQuery, shouldEncryptRequest, shouldEncryptResponse, } from "@/utils/transportCryptoPolicy"; const TRANSPORT_BASE_URL = config.baseUrl; const TRANSPORT_ENABLE_HEADER = "X-Transport-Encrypt"; const TRANSPORT_KEY_ID_HEADER = "X-Key-Id"; const ENCRYPTED_RESPONSE_HEADER = "x-body-encrypted"; const DEFAULT_TRANSPORT_ENVELOPE_VERSION = "1"; const KEY_REFRESH_BUFFER_MIN_SECONDS = 30; const KEY_REFRESH_BUFFER_MAX_SECONDS = 300; const TRANSPORT_KEY_META_CACHE_KEY = "transportCryptoKeyMeta"; const TRANSPORT_RETRYABLE_ERROR_MESSAGES = new Set([ "Decryption failed", "密钥版本不存在", ]); const AES_GCM_TAG_LENGTH_BYTES = 16; const FORGE_RANDOM_POOL_BYTES = 4096; let cachedKeyMeta = null; let inflightKeyMetaPromise = null; /** * 获取当前运行时的全局对象引用。 * * @returns {Object|undefined} 全局对象 */ function getRuntimeGlobal() { if (typeof globalThis !== "undefined") { return globalThis; } if (typeof self !== "undefined") { return self; } if (typeof window !== "undefined") { return window; } if (typeof global !== "undefined") { return global; } return undefined; } /** * 为 Forge 补齐运行时依赖的全局对象别名。 * * @returns {void} */ function ensureForgeRuntimeGlobal() { const runtimeGlobal = getRuntimeGlobal(); if (!runtimeGlobal) { return; } if (typeof runtimeGlobal.self === "undefined") { runtimeGlobal.self = runtimeGlobal; } if (typeof runtimeGlobal.window === "undefined") { runtimeGlobal.window = runtimeGlobal; } if (typeof runtimeGlobal.global === "undefined") { runtimeGlobal.global = runtimeGlobal; } } /** * 获取已完成运行时适配的 Forge 实例。 * * @returns {Promise} Forge 实例 */ async function getForge() { ensureForgeRuntimeGlobal(); return forge; } /** * 从请求头对象中读取指定字段。 * * @param {Object} headers 请求头对象 * @param {string} name 请求头名称 * @returns {*} 请求头值 */ function getHeaderValue(headers, name) { if (!headers) { return undefined; } return headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()]; } /** * 为请求头对象设置指定字段。 * * @param {Object} headers 请求头对象 * @param {string} name 请求头名称 * @param {*} value 请求头值 * @returns {void} */ function setHeaderValue(headers, name, value) { if (!headers) { return; } headers[name] = value; } /** * 获取 Uni 响应对象中的响应头映射,兼容 H5 与其它平台字段差异。 * * @param {Object} response Uni 响应对象 * @returns {Object|undefined} 响应头对象 */ function getResponseHeaders(response) { return response?.header || response?.headers; } /** * 将文本编码为 UTF-8 二进制字符串。 * * @param {string} text 原始文本 * @returns {string} UTF-8 二进制字符串 */ function encodeUtf8(text) { if (typeof TextEncoder !== "undefined") { return uint8ArrayToBytes(new TextEncoder().encode(String(text || ""))); } return unescape(encodeURIComponent(String(text || ""))); } /** * 将 UTF-8 二进制字符串解码为文本。 * * @param {string} bytes UTF-8 二进制字符串 * @returns {string} 解码后的文本 */ function decodeUtf8(bytes) { if (typeof TextDecoder !== "undefined") { return new TextDecoder().decode(bytesToUint8Array(bytes)); } return decodeURIComponent(escape(bytes)); } /** * 将 Uint8Array 转为 Forge 兼容的二进制字符串。 * * @param {Uint8Array} uint8Array 字节数组 * @returns {string} 二进制字符串 */ function uint8ArrayToBytes(uint8Array) { return Array.from(uint8Array, (item) => String.fromCharCode(item)).join(""); } /** * 将二进制字符串还原为 Uint8Array。 * * @param {string} bytes 二进制字符串 * @returns {Uint8Array} 字节数组 */ function bytesToUint8Array(bytes) { return Uint8Array.from(String(bytes || ""), (item) => item.charCodeAt(0)); } /** * 获取对象的内部类型标签。 * * @param {*} value 待检测值 * @returns {string} 内部类型标签 */ function getObjectTag(value) { return Object.prototype.toString.call(value); } /** * 判断对象是否为 ArrayBuffer 视图。 * * @param {*} value 待检测值 * @returns {boolean} 是否为视图对象 */ function isArrayBufferView(value) { return typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView(value); } /** * 判断对象是否表现为 ArrayBuffer。 * * @param {*} value 待检测值 * @returns {boolean} 是否可按 ArrayBuffer 处理 */ function isArrayBufferLike(value) { return ( value instanceof ArrayBuffer || getObjectTag(value) === "[object ArrayBuffer]" || (value && typeof value === "object" && typeof value.byteLength === "number" && typeof value.slice === "function" && !("length" in value)) ); } /** * 判断对象是否为数组风格的字节容器。 * * @param {*} value 待检测值 * @returns {boolean} 是否可按字节数组处理 */ function isByteArrayLikeObject(value) { return ( value && typeof value === "object" && typeof value.length === "number" && value.length >= 0 ); } /** * 尝试通过平台 Base64 API 归一化原生 ArrayBuffer 对象。 * * @param {*} randomValues 平台返回的随机数字节对象 * @returns {string|null} 归一化后的字节串,失败时返回 null */ function tryNormalizeArrayBufferLikeResult(randomValues) { const arrayBufferToBase64Api = (typeof uni !== "undefined" && uni.arrayBufferToBase64) || (typeof wx !== "undefined" && wx.arrayBufferToBase64); const base64ToArrayBufferApi = (typeof uni !== "undefined" && uni.base64ToArrayBuffer) || (typeof wx !== "undefined" && wx.base64ToArrayBuffer); if (!arrayBufferToBase64Api || !base64ToArrayBufferApi) { return null; } try { const base64Text = arrayBufferToBase64Api(randomValues); const arrayBuffer = base64ToArrayBufferApi(base64Text); return uint8ArrayToBytes(new Uint8Array(arrayBuffer)); } catch (error) { return null; } } /** * 将平台返回的随机数字节结果统一转换为二进制字符串。 * * @param {*} randomValues 平台返回结果 * @param {number} expectedLength 期望字节长度 * @returns {string} 归一化后的二进制字符串 */ function normalizeRandomBytesResult(randomValues, expectedLength) { if (randomValues instanceof Uint8Array) { return uint8ArrayToBytes(randomValues); } if (isArrayBufferView(randomValues)) { return uint8ArrayToBytes( new Uint8Array( randomValues.buffer, randomValues.byteOffset || 0, randomValues.byteLength, ), ); } if (randomValues instanceof ArrayBuffer) { return uint8ArrayToBytes(new Uint8Array(randomValues)); } if (Array.isArray(randomValues)) { return uint8ArrayToBytes(Uint8Array.from(randomValues)); } if (randomValues && typeof randomValues === "object") { if (randomValues.randomValues) { return normalizeRandomBytesResult( randomValues.randomValues, expectedLength, ); } if (randomValues.value) { return normalizeRandomBytesResult(randomValues.value, expectedLength); } if (randomValues.data) { return normalizeRandomBytesResult(randomValues.data, expectedLength); } if (randomValues.buffer instanceof ArrayBuffer) { return normalizeRandomBytesResult( new Uint8Array( randomValues.buffer, randomValues.byteOffset || 0, randomValues.byteLength || expectedLength, ), expectedLength, ); } if (isArrayBufferLike(randomValues)) { const normalizedBytes = tryNormalizeArrayBufferLikeResult(randomValues); if (normalizedBytes !== null) { return normalizedBytes; } } if (isByteArrayLikeObject(randomValues)) { return uint8ArrayToBytes(Uint8Array.from(randomValues)); } } if (typeof randomValues === "string" && randomValues.length === expectedLength) { return randomValues; } throw new Error( `平台随机数返回结果格式不受支持: tag=${getObjectTag(randomValues)}, keys=${Object.keys( randomValues || {}, ).join(",")}`, ); } /** * 请求平台随机数字节并统一为二进制字符串。 * * @param {Function} api 平台随机数 API * @param {number} length 需要的字节长度 * @returns {Promise} 随机数字节串 */ function requestPlatformRandomBytes(api, length) { return new Promise((resolve, reject) => { let settled = false; const resolveOnce = (result) => { if (settled) { return; } settled = true; resolve(normalizeRandomBytesResult(result, length)); }; const rejectOnce = (error) => { if (settled) { return; } settled = true; reject(error); }; try { const maybeResult = api({ length, success: resolveOnce, fail: rejectOnce, }); if (maybeResult && typeof maybeResult.then === "function") { maybeResult.then(resolveOnce).catch(rejectOnce); return; } if ( maybeResult && (maybeResult.randomValues || maybeResult.value || maybeResult.data) ) { resolveOnce(maybeResult); } } catch (error) { rejectOnce(error); } }); } /** * 在 app-plus iOS 端通过原生 NSUUID 获取随机字节兜底。 * * @param {number} length 需要的字节长度 * @returns {string|null} 随机数字节串,当前平台不支持时返回 null */ function getAppPlusIosRandomBytes(length) { // #ifdef APP-PLUS if (typeof plus === "undefined") { return null; } const osName = String(plus.os?.name || "").toLowerCase(); if (osName !== "ios" || typeof plus.ios?.importClass !== "function") { return null; } try { const uuidClass = plus.ios.importClass("NSUUID"); let randomBytes = ""; while (randomBytes.length < length) { const uuidObject = plus.ios.invoke(uuidClass, "UUID"); const uuidText = String(plus.ios.invoke(uuidObject, "UUIDString") || "") .replace(/-/g, "") .toLowerCase(); plus.ios.deleteObject(uuidObject); if (!uuidText) { return null; } for (let index = 0; index < uuidText.length && randomBytes.length < length; index += 2) { const byteValue = parseInt(uuidText.slice(index, index + 2), 16); if (isNaN(byteValue)) { return null; } randomBytes += String.fromCharCode(byteValue); } } return randomBytes; } catch (error) { console.warn("iOS NSUUID 初始化失败,继续尝试其它随机数能力", error); } // #endif return null; } /** * 在 app-plus Android 端通过原生 SecureRandom 获取随机字节。 * * @param {number} length 需要的字节长度 * @returns {string|null} 随机数字节串,当前平台不支持时返回 null */ function getAppPlusAndroidRandomBytes(length) { // #ifdef APP-PLUS if (typeof plus === "undefined") { return null; } const osName = String(plus.os?.name || "").toLowerCase(); if (osName !== "android" || typeof plus.android?.importClass !== "function") { return null; } try { plus.android.importClass("java.security.SecureRandom"); const secureRandom = typeof plus.android.newObject === "function" ? plus.android.newObject("java.security.SecureRandom") : null; if (!secureRandom) { return null; } plus.android.importClass(secureRandom); let randomBytes = ""; for (let index = 0; index < length; index += 1) { randomBytes += String.fromCharCode(secureRandom.nextInt(256)); } if (typeof plus.android.autoCollection === "function") { plus.android.autoCollection(secureRandom); } return randomBytes; } catch (error) { console.warn("Android SecureRandom 初始化失败,继续尝试其它随机数能力", error); } // #endif return null; } /** * 获取当前运行环境下的安全随机数字节。 * * @param {number} length 需要的字节长度 * @returns {Promise} 随机数字节串 */ async function getRandomBytes(length) { const runtimeGlobal = getRuntimeGlobal(); const runtimeCrypto = runtimeGlobal?.crypto; if (runtimeCrypto?.getRandomValues) { const bytes = new Uint8Array(length); runtimeCrypto.getRandomValues(bytes); return uint8ArrayToBytes(bytes); } if (typeof uni !== "undefined" && typeof uni.getRandomValues === "function") { return requestPlatformRandomBytes(uni.getRandomValues, length); } if (typeof wx !== "undefined" && typeof wx.getRandomValues === "function") { return requestPlatformRandomBytes(wx.getRandomValues, length); } const appPlusRandomBytes = getAppPlusAndroidRandomBytes(length) || getAppPlusIosRandomBytes(length); if (appPlusRandomBytes !== null) { return appPlusRandomBytes; } throw new Error("当前运行环境缺少安全随机数能力"); } /** * 预填充 Forge 需要的随机数字节池。 * * @param {number} length 预填充长度 * @returns {Promise} */ async function primeForgeRandomPool(length = FORGE_RANDOM_POOL_BYTES) { primeForgeRandomBytes(await getRandomBytes(length)); } /** * 将二进制字符串编码为 Base64URL 文本。 * * @param {string} bytes 二进制字符串 * @returns {string} Base64URL 文本 */ function toBase64Url(bytes) { const base64Text = uni.arrayBufferToBase64(bytesToUint8Array(bytes).buffer); return base64Text.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } /** * 将 Base64URL 文本还原为二进制字符串。 * * @param {string} text Base64URL 文本 * @returns {string} 二进制字符串 */ function fromBase64Url(text) { const normalizedText = String(text || "").replace(/-/g, "+").replace(/_/g, "/"); const paddingLength = (4 - (normalizedText.length % 4 || 4)) % 4; const arrayBuffer = uni.base64ToArrayBuffer( normalizedText + "=".repeat(paddingLength), ); return uint8ArrayToBytes(new Uint8Array(arrayBuffer)); } /** * 将查询信封编码为可放入 URL 的字符串。 * * @param {Object} envelope 查询信封 * @returns {string} 编码后的查询参数值 */ function encodeQueryEnvelope(envelope) { return toBase64Url(encodeUtf8(JSON.stringify(envelope))); } /** * 计算加密查询参数最终生成的 URL 长度。 * * @param {string} url 请求地址 * @param {Object} params 查询参数 * @returns {number} URL 长度 */ function buildQueryUrlLength(url = "", params = {}) { const queryText = Object.keys(params) .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join("&"); if (!queryText) { return String(url || "").length; } const normalizedUrl = String(url || ""); const separator = normalizedUrl.includes("?") ? "&" : "?"; return `${normalizedUrl}${separator}${queryText}`.length; } /** * 构建请求方向的 AAD 元数据。 * * @param {Object} requestConfig 请求配置 * @returns {Object} 请求 AAD */ function buildRequestAad(requestConfig) { return { method: (requestConfig.method || "get").toUpperCase(), path: getTransportRequestPath( requestConfig.url, requestConfig.baseUrl || TRANSPORT_BASE_URL, ), }; } /** * 构建响应方向的 AAD 元数据。 * * @param {Object} requestConfig 请求配置 * @returns {Object} 响应 AAD */ function buildResponseAad(requestConfig) { return { method: (requestConfig.method || "get").toUpperCase(), path: getTransportRequestPath( requestConfig.url, requestConfig.baseUrl || TRANSPORT_BASE_URL, ), direction: "response", }; } /** * 将空值载荷规范化为可序列化对象。 * * @param {*} payload 原始载荷 * @returns {*} 规范化后的载荷 */ function normalizePlainPayload(payload) { if (payload === undefined || payload === null) { return {}; } return payload; } /** * 将请求载荷序列化为 JSON 文本。 * * @param {*} payload 原始载荷 * @returns {string} JSON 文本 */ function stringifyPayload(payload) { return JSON.stringify(normalizePlainPayload(payload)); } /** * 克隆请求配置中的可变字段,便于失败重试恢复。 * * @param {*} value 待克隆值 * @returns {*} 克隆结果 */ function cloneRequestValue(value) { if (value === undefined || value === null) { return value; } const runtimeGlobal = getRuntimeGlobal(); if (typeof runtimeGlobal?.structuredClone === "function") { return runtimeGlobal.structuredClone(value); } if (typeof value === "object") { return JSON.parse(JSON.stringify(value)); } return value; } /** * 将信封字段转换为适合表单提交的字符串。 * * @param {*} value 字段值 * @returns {string} 序列化文本 */ function stringifyEnvelopeField(value) { if (value && typeof value === "object") { return JSON.stringify(value); } return String(value); } /** * 将加密信封编码为 x-www-form-urlencoded 文本。 * * @param {Object} envelope 信封对象 * @returns {string} 表单编码文本 */ function encodeFormEnvelope(envelope) { return Object.entries(envelope) .map( ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent( stringifyEnvelopeField(value), )}`, ) .join("&"); } /** * 将输入解析为 JSON 对象并校验结构。 * * @param {*} payload 原始数据 * @param {string} errorMessage 失败提示 * @returns {Object} 解析后的对象 */ function parseJsonObject(payload, errorMessage) { const parsedPayload = typeof payload === "string" ? JSON.parse(payload) : payload; if (!parsedPayload || typeof parsedPayload !== "object" || Array.isArray(parsedPayload)) { throw new Error(errorMessage); } return parsedPayload; } /** * 基于随机字节生成 UUID v4 字符串。 * * @returns {Promise} UUID 文本 */ async function createUuid() { const bytes = (await getRandomBytes(16)) .split("") .map((item) => item.charCodeAt(0)); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hexText = bytes .map((item) => item.toString(16).padStart(2, "0")) .join(""); return `${hexText.slice(0, 8)}-${hexText.slice(8, 12)}-${hexText.slice( 12, 16, )}-${hexText.slice(16, 20)}-${hexText.slice(20)}`; } /** * 校验公钥接口响应壳是否有效。 * * @param {Object} responsePayload 公钥接口原始响应 * @returns {void} */ function validateTransportPublicKeyResponse(responsePayload) { if ( responsePayload?.code !== 200 || !responsePayload?.data || typeof responsePayload.data !== "object" ) { throw new Error(responsePayload?.msg || "获取传输层公钥失败"); } } /** * 校验公钥业务载荷是否满足当前协议要求。 * * @param {Object} payload 公钥业务载荷 * @param {Object} transportPolicy 当前传输策略 * @returns {void} */ function validateTransportPublicKeyPayload(payload, transportPolicy) { if (!payload?.publicKey || !payload?.kid) { throw new Error("获取传输层公钥失败"); } if ( String(payload.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION) !== transportPolicy.envelopeVersion ) { throw new Error("传输层公钥协议版本不受支持"); } if (payload.alg !== transportPolicy.requestEnvelopeAlgorithm) { throw new Error("传输层公钥算法不受支持"); } } /** * 校验响应信封与当前请求上下文是否一致。 * * @param {Object} envelope 响应信封 * @param {Object} response 原始响应对象 * @param {Object} requestConfig 请求配置 * @param {Object} transportContext 请求加密上下文 * @param {Object} transportPolicy 当前传输策略 * @returns {void} */ function validateResponseEnvelope( envelope, response, requestConfig, transportContext, transportPolicy, ) { const expectedAad = buildResponseAad(requestConfig); const responseKid = getHeaderValue( getResponseHeaders(response), TRANSPORT_KEY_ID_HEADER, ); const aad = envelope.aad; if (String(envelope.v || "") !== transportPolicy.envelopeVersion) { throw new Error("传输层响应协议版本不受支持"); } if (String(envelope.alg || "") !== transportPolicy.responseEnvelopeAlgorithm) { throw new Error("传输层响应算法不受支持"); } if (String(envelope.kid || "") !== String(transportContext.kid)) { throw new Error("传输层响应密钥版本不匹配"); } if (responseKid && String(envelope.kid) !== String(responseKid)) { throw new Error("传输层响应头与响应体密钥版本不一致"); } if (!aad || typeof aad !== "object" || Array.isArray(aad)) { throw new Error("传输层响应AAD不合法"); } if ( String(aad.method || "").toUpperCase() !== expectedAad.method || String(aad.path || "") !== expectedAad.path ) { throw new Error("传输层响应的method/path与当前请求不匹配"); } if (String(aad.direction || "") !== expectedAad.direction) { throw new Error("传输层响应方向标识不合法"); } } /** * 记录原始请求快照,供密钥刷新后重试恢复。 * * @param {Object} requestConfig 请求配置 * @returns {void} */ function rememberOriginalRequestSnapshot(requestConfig) { if (requestConfig.__transportOriginalSnapshot) { return; } requestConfig.__transportOriginalSnapshot = { url: requestConfig.url, params: cloneRequestValue(requestConfig.params), data: cloneRequestValue(requestConfig.data), contentType: getHeaderValue(requestConfig.header, "Content-Type"), }; } /** * 获取当前 Unix 秒级时间戳。 * * @returns {number} 当前时间戳 */ function getNowTimestamp() { return Math.floor(Date.now() / 1000); } /** * 根据公钥有效期计算本地提前刷新时间。 * * @param {number} expireAt 公钥过期时间 * @param {number} fetchedAt 公钥获取时间 * @returns {number} 建议刷新时间 */ function buildKeyRefreshAt(expireAt, fetchedAt = getNowTimestamp()) { const normalizedExpireAt = Number(expireAt || 0); const normalizedFetchedAt = Number(fetchedAt || 0); const ttlSeconds = Math.max(normalizedExpireAt - normalizedFetchedAt, 0); if (!normalizedExpireAt || !ttlSeconds) { return 0; } const refreshBufferSeconds = Math.min( KEY_REFRESH_BUFFER_MAX_SECONDS, Math.max(KEY_REFRESH_BUFFER_MIN_SECONDS, Math.floor(ttlSeconds * 0.1)), ); return Math.max(normalizedFetchedAt, normalizedExpireAt - refreshBufferSeconds); } /** * 判断当前缓存的公钥元数据是否仍可使用。 * * @param {Object} keyMeta 公钥元数据 * @param {number} nowTimestamp 当前时间戳 * @returns {boolean} 是否仍可使用 */ function isUsableKeyMeta(keyMeta, nowTimestamp = getNowTimestamp()) { if (!keyMeta?.publicKeyPem || !keyMeta?.kid || !keyMeta?.expireAt) { return false; } const refreshAt = Number( keyMeta.refreshAt || buildKeyRefreshAt(keyMeta.expireAt, keyMeta.fetchedAt || nowTimestamp), ); return refreshAt > nowTimestamp; } /** * 请求后端公钥接口。 * 这里故意直接使用原始 uni.request,避免在获取公钥前再次进入统一 request * 包装器而形成“加密请求依赖公钥、公钥请求又依赖加密请求”的启动环路。 * * @param {string} publicKeyUrl 公钥接口地址 * @returns {Promise} Uni 请求响应 */ function requestPublicKey(publicKeyUrl) { return new Promise((resolve, reject) => { uni.request({ url: `${TRANSPORT_BASE_URL}${publicKeyUrl}`, method: "GET", timeout: 10000, success: resolve, fail: reject, }); }); } /** * 获取当前可用的后端公钥元信息。 * * @param {boolean} forceRefresh 是否强制刷新 * @returns {Promise} 公钥元信息 */ async function getTransportKeyMeta(forceRefresh = false) { const transportPolicy = await ensureTransportCryptoPolicyLoaded(); const nowTimestamp = getNowTimestamp(); if (!forceRefresh && !cachedKeyMeta) { const persistedKeyMeta = uni.getStorageSync(TRANSPORT_KEY_META_CACHE_KEY); if (isUsableKeyMeta(persistedKeyMeta, nowTimestamp)) { cachedKeyMeta = { kid: persistedKeyMeta.kid, alg: persistedKeyMeta.alg, envelopeVersion: persistedKeyMeta.envelopeVersion || transportPolicy.envelopeVersion, publicKeyPem: persistedKeyMeta.publicKeyPem, expireAt: persistedKeyMeta.expireAt, fetchedAt: persistedKeyMeta.fetchedAt || nowTimestamp, refreshAt: persistedKeyMeta.refreshAt || buildKeyRefreshAt( persistedKeyMeta.expireAt, persistedKeyMeta.fetchedAt || nowTimestamp, ), }; } } if (!forceRefresh && isUsableKeyMeta(cachedKeyMeta, nowTimestamp)) { return cachedKeyMeta; } if (inflightKeyMetaPromise) { return inflightKeyMetaPromise; } inflightKeyMetaPromise = requestPublicKey( transportPolicy.publicKeyUrl || "/transport/crypto/public-key", ) .then((response) => { const responsePayload = response.data || {}; const payload = responsePayload.data || {}; const fetchedAt = getNowTimestamp(); validateTransportPublicKeyResponse(responsePayload); validateTransportPublicKeyPayload(payload, transportPolicy); cachedKeyMeta = { kid: payload.kid, alg: payload.alg, envelopeVersion: String( payload.envelopeVersion || transportPolicy.envelopeVersion, ), publicKeyPem: payload.publicKey, expireAt: payload.expireAt, fetchedAt, refreshAt: buildKeyRefreshAt(payload.expireAt, fetchedAt), }; uni.setStorageSync(TRANSPORT_KEY_META_CACHE_KEY, cachedKeyMeta); inflightKeyMetaPromise = null; return cachedKeyMeta; }) .catch((error) => { inflightKeyMetaPromise = null; throw error; }); return inflightKeyMetaPromise; } /** * 使用 RSA-OAEP 加密当前请求的 AES 会话密钥。 * * @param {string} publicKeyPem PEM 格式公钥 * @param {string} aesKeyBytes AES 会话密钥字节串 * @returns {Promise} 加密后的会话密钥 */ async function rsaEncryptAesKey(publicKeyPem, aesKeyBytes) { const cryptoForge = await getForge(); await primeForgeRandomPool(); const publicKey = cryptoForge.pki.publicKeyFromPem(String(publicKeyPem || "")); return publicKey.encrypt(aesKeyBytes, "RSA-OAEP", { md: cryptoForge.md.sha256.create(), mgf1: { md: cryptoForge.md.sha256.create(), }, }); } /** * 为当前请求构建一次性的传输加密上下文。 * * @returns {Promise} 请求级传输上下文 */ async function buildTransportContext() { const keyMeta = await getTransportKeyMeta(); const aesKey = await getRandomBytes(32); const encryptedAesKey = await rsaEncryptAesKey(keyMeta.publicKeyPem, aesKey); return { kid: keyMeta.kid, alg: keyMeta.alg, envelopeVersion: keyMeta.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION, aesKey, ek: toBase64Url(encryptedAesKey), }; } /** * 使用 AES-GCM 对明文载荷执行信封加密。 * * @param {Object} context 请求级传输上下文 * @param {string} plainText 明文内容 * @param {Object} aad AAD 元数据 * @returns {Promise} 加密信封 */ async function encryptPayloadText(context, plainText, aad) { const cryptoForge = await getForge(); const iv = await getRandomBytes(12); const cipher = cryptoForge.cipher.createCipher("AES-GCM", context.aesKey); cipher.start({ iv, additionalData: encodeUtf8(JSON.stringify(aad)), tagLength: AES_GCM_TAG_LENGTH_BYTES * 8, }); cipher.update(cryptoForge.util.createBuffer(encodeUtf8(plainText))); if (!cipher.finish()) { throw new Error("Encryption failed"); } const ciphertext = cipher.output.getBytes() + cipher.mode.tag.getBytes(); return { v: context.envelopeVersion || DEFAULT_TRANSPORT_ENVELOPE_VERSION, kid: context.kid, alg: context.alg, ts: getNowTimestamp(), nonce: await createUuid(), ek: context.ek, aad, iv: toBase64Url(iv), ct: toBase64Url(ciphertext), }; } /** * 使用请求上下文中的 AES 密钥解密响应信封。 * * @param {Object} envelope 响应信封 * @param {Object} context 请求级传输上下文 * @returns {Promise} 解密后的明文 */ async function decryptEnvelope(envelope, context) { const cryptoForge = await getForge(); const encryptedPayload = fromBase64Url(envelope.ct); if (encryptedPayload.length <= AES_GCM_TAG_LENGTH_BYTES) { throw new Error("Decryption failed"); } const ciphertext = encryptedPayload.slice(0, -AES_GCM_TAG_LENGTH_BYTES); const tag = encryptedPayload.slice(-AES_GCM_TAG_LENGTH_BYTES); const decipher = cryptoForge.cipher.createDecipher("AES-GCM", context.aesKey); decipher.start({ iv: fromBase64Url(envelope.iv), additionalData: encodeUtf8(JSON.stringify(envelope.aad || {})), tagLength: AES_GCM_TAG_LENGTH_BYTES * 8, tag, }); decipher.update(cryptoForge.util.createBuffer(ciphertext)); if (!decipher.finish()) { throw new Error("Decryption failed"); } return decodeUtf8(decipher.output.getBytes()); } /** * 复用同一次请求内的传输上下文,避免重复生成密钥。 * * @param {Object} requestConfig 请求配置 * @returns {Promise} 请求级传输上下文 */ function getOrCreateTransportContext(requestConfig) { if (requestConfig.__transportCryptoContextPromise) { return requestConfig.__transportCryptoContextPromise; } requestConfig.__transportCryptoContextPromise = buildTransportContext(); return requestConfig.__transportCryptoContextPromise; } /** * 对 Uni 请求配置执行传输层加密封装。 * * @param {Object} requestConfig 请求配置 * @returns {Promise} 加密后的请求配置 */ export async function encryptTransportRequest(requestConfig) { const transportPolicy = await ensureTransportCryptoPolicyLoaded(); if (!shouldEncryptRequest(requestConfig, transportPolicy)) { requestConfig.__transportCryptoEnabledForRequest = false; return requestConfig; } rememberOriginalRequestSnapshot(requestConfig); const transportContext = await getOrCreateTransportContext(requestConfig); const contentType = String( getHeaderValue(requestConfig.header, "Content-Type") || "application/json", ).toLowerCase(); const method = (requestConfig.method || "get").toLowerCase(); const requestAad = buildRequestAad(requestConfig); if ( shouldEncryptQuery(requestConfig, transportPolicy) && (requestConfig.params || method === "get" || method === "delete") ) { const queryEnvelope = await encryptPayloadText( transportContext, JSON.stringify(normalizePlainPayload(requestConfig.params)), requestAad, ); requestConfig.params = { __enc: encodeQueryEnvelope(queryEnvelope) }; if ( buildQueryUrlLength(requestConfig.url, requestConfig.params) > Number(transportPolicy.maxEncryptedGetUrlLength || 4096) ) { throw new Error( "当前GET/DELETE请求参数加密后长度超限,请改用POST请求或精简查询条件", ); } } if (["post", "put", "patch", "delete"].includes(method)) { const plainText = stringifyPayload(requestConfig.data); const bodyEnvelope = await encryptPayloadText( transportContext, plainText, requestAad, ); if (contentType.includes("application/x-www-form-urlencoded")) { requestConfig.data = encodeFormEnvelope(bodyEnvelope); } else { requestConfig.data = bodyEnvelope; setHeaderValue( requestConfig.header, "Content-Type", "application/json;charset=utf-8", ); } } setHeaderValue(requestConfig.header, TRANSPORT_ENABLE_HEADER, "1"); setHeaderValue(requestConfig.header, TRANSPORT_KEY_ID_HEADER, transportContext.kid); requestConfig.__transportCryptoContext = transportContext; requestConfig.__transportCryptoEnabledForRequest = true; return requestConfig; } /** * 清空当前缓存的公钥元数据。 * * @returns {void} */ export function invalidateTransportKeyMeta() { cachedKeyMeta = null; inflightKeyMetaPromise = null; uni.removeStorageSync(TRANSPORT_KEY_META_CACHE_KEY); } /** * 将被加密改写过的请求恢复为原始形态。 * * @param {Object} requestConfig 请求配置 * @returns {Object} 恢复后的请求配置 */ export function resetTransportRequestConfig(requestConfig) { const originalSnapshot = requestConfig?.__transportOriginalSnapshot; if (!requestConfig || !originalSnapshot) { return requestConfig; } requestConfig.url = originalSnapshot.url; requestConfig.params = cloneRequestValue(originalSnapshot.params); requestConfig.data = cloneRequestValue(originalSnapshot.data); if (originalSnapshot.contentType) { setHeaderValue(requestConfig.header, "Content-Type", originalSnapshot.contentType); } delete requestConfig.__transportCryptoContext; delete requestConfig.__transportCryptoContextPromise; delete requestConfig.__transportCryptoEnabledForRequest; return requestConfig; } /** * 判断错误是否属于可通过刷新公钥进行重试的场景。 * * @param {Object} responseOrError 响应对象或错误对象 * @returns {boolean} 是否可刷新密钥重试 */ export function shouldRetryTransportWithFreshKey(responseOrError) { const responseMsg = responseOrError?.data?.msg || responseOrError?.response?.data?.msg; const errorMessage = responseOrError?.message; return ( TRANSPORT_RETRYABLE_ERROR_MESSAGES.has(responseMsg) || TRANSPORT_RETRYABLE_ERROR_MESSAGES.has(errorMessage) ); } /** * 解密成功响应中的传输层信封。 * * @param {Object} response 原始响应对象 * @param {Object} requestConfig 请求配置 * @returns {Promise} 解密后的响应对象 */ export async function decryptTransportResponse(response, requestConfig) { const encryptedResponseFlag = String( getHeaderValue(getResponseHeaders(response), ENCRYPTED_RESPONSE_HEADER) || "", ); if (encryptedResponseFlag !== "1") { return response; } if (!shouldEncryptResponse(requestConfig, getTransportCryptoPolicy())) { return response; } const transportPolicy = getTransportCryptoPolicy(); const transportContext = requestConfig.__transportCryptoContext; if (!transportContext) { throw new Error("缺少响应解密上下文"); } const envelope = parseJsonObject(response.data, "传输层响应信封格式不合法"); validateResponseEnvelope( envelope, response, requestConfig, transportContext, transportPolicy, ); const plaintext = await decryptEnvelope(envelope, transportContext); response.data = JSON.parse(plaintext); return response; } /** * 尝试解密异常响应中的传输层信封。 * * @param {Object} error 错误对象 * @param {Object} requestConfig 原始请求配置 * @returns {Promise} 原始或已解密的错误对象 */ export async function decryptTransportErrorResponse(error, requestConfig) { const response = error?.response || (error && (error.data !== undefined || error.header || error.headers || error.statusCode) ? error : null); const encryptedResponseFlag = String( getHeaderValue(getResponseHeaders(response), ENCRYPTED_RESPONSE_HEADER) || "", ); if (!response || encryptedResponseFlag !== "1") { return error; } if (!shouldEncryptResponse(requestConfig || {}, getTransportCryptoPolicy())) { return error; } const transportPolicy = getTransportCryptoPolicy(); const transportContext = requestConfig?.__transportCryptoContext; if (!transportContext) { return error; } try { const envelope = parseJsonObject(response.data, "传输层响应信封格式不合法"); validateResponseEnvelope( envelope, response, requestConfig, transportContext, transportPolicy, ); const plaintext = await decryptEnvelope(envelope, transportContext); response.data = JSON.parse(plaintext); if (!error.response) { error.response = response; } } catch (decryptError) { console.error(decryptError); } return error; }