Files
ShiErFeng/shierfeng-fastapi-app/src/utils/transportCrypto.js
T
2026-07-24 09:43:10 +08:00

1237 lines
36 KiB
JavaScript

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<Object>} 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<string>} 随机数字节串
*/
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<string>} 随机数字节串
*/
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<void>}
*/
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<string>} 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<Object>} 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<Object>} 公钥元信息
*/
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<string>} 加密后的会话密钥
*/
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<Object>} 请求级传输上下文
*/
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<Object>} 加密信封
*/
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<string>} 解密后的明文
*/
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<Object>} 请求级传输上下文
*/
function getOrCreateTransportContext(requestConfig) {
if (requestConfig.__transportCryptoContextPromise) {
return requestConfig.__transportCryptoContextPromise;
}
requestConfig.__transportCryptoContextPromise = buildTransportContext();
return requestConfig.__transportCryptoContextPromise;
}
/**
* 对 Uni 请求配置执行传输层加密封装。
*
* @param {Object} requestConfig 请求配置
* @returns {Promise<Object>} 加密后的请求配置
*/
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<Object>} 解密后的响应对象
*/
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<Object>} 原始或已解密的错误对象
*/
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;
}