Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398f0bd097 | ||
|
|
7e450fca0d | ||
|
|
42f7e55c72 |
No files matched your search
@@ -120,6 +120,11 @@ export default defineManifestConfig({
|
||||
/* 小程序特有相关 */
|
||||
'mp-weixin': {
|
||||
appid: VITE_WX_APPID,
|
||||
// 仅供开发者工具选择调试基础库版本;线上客户端的最低基础库门槛需在
|
||||
// 微信公众平台「开发管理 → 基础库最低版本设置」中配置(代码里改不动)。
|
||||
// 功能硬性要求:wx.getRandomValues 需基础库 >= 2.17.3,低于该版本的
|
||||
// 线上客户端登录时将收到 fail-closed 报错
|
||||
libVersion: '2.19.4',
|
||||
setting: {
|
||||
urlCheck: false,
|
||||
// 是否启用 ES6 转 ES5
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"pinia": "2.0.36",
|
||||
"pinia-plugin-persistedstate": "3.2.1",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"sm-crypto": "0.5.8",
|
||||
"vue": "^3.4.21",
|
||||
"wot-design-uni": "^1.9.1",
|
||||
"z-paging": "2.8.7"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
diff --git a/src/sm2/rng.js b/src/sm2/rng.js
|
||||
index c6572c1756579fb100c0d7bdf0cec323971ec211..cdb1f1fcc7d58c52138f6f9fd76240bae5dde5d4 100644
|
||||
--- a/src/sm2/rng.js
|
||||
+++ b/src/sm2/rng.js
|
||||
@@ -54,8 +54,14 @@ function rngSeedTime() {
|
||||
rngSeedInt(new Date().getTime())
|
||||
}
|
||||
|
||||
-// Initialize the pool with junk if needed.
|
||||
-if (rngPool == null) {
|
||||
+// [xiaozhi patch] 熵池初始化从模块加载时推迟到首次取随机字节时(见 rngGetByte)。
|
||||
+// 原因:微信小程序等环境没有同步的 crypto.getRandomValues,模块加载即取熵会直接抛异常;
|
||||
+// 推迟后,调用方可在首次使用前异步准备好真随机源(见 manager-mobile/src/utils/secure-random.ts)。
|
||||
+// fail-closed 语义保持不变:首次取熵时仍要求存在可用的 crypto.getRandomValues,否则抛出异常。
|
||||
+function ensureRngPool() {
|
||||
+ if (rngPool != null)
|
||||
+ return
|
||||
+
|
||||
rngPool = []
|
||||
rngPptr = 0
|
||||
let t
|
||||
@@ -82,6 +88,7 @@ if (rngPool == null) {
|
||||
|
||||
function rngGetByte() {
|
||||
if (rngState == null) {
|
||||
+ ensureRngPool()
|
||||
rngSeedTime()
|
||||
rngState = new Arcfour()
|
||||
rngState.init(rngPool)
|
||||
Generated
+8
-5
@@ -11,6 +11,9 @@ patchedDependencies:
|
||||
'@dcloudio/uni-h5':
|
||||
hash: 5763725268e9a493075be3c82d91b590b9ae30e997a04a418494dd7d6d327b9d
|
||||
path: patches/@dcloudio__uni-h5.patch
|
||||
sm-crypto@0.5.8:
|
||||
hash: 62930665cf9612107916df2f12c9deab4ad907f6ac9080520b84bffe016c3352
|
||||
path: patches/sm-crypto@0.5.8.patch
|
||||
|
||||
importers:
|
||||
|
||||
@@ -92,8 +95,8 @@ importers:
|
||||
specifier: 3.2.1
|
||||
version: 3.2.1(pinia@2.0.36(typescript@5.7.2)(vue@3.5.15(typescript@5.7.2)))
|
||||
sm-crypto:
|
||||
specifier: ^0.3.13
|
||||
version: 0.3.14
|
||||
specifier: 0.5.8
|
||||
version: 0.5.8(patch_hash=62930665cf9612107916df2f12c9deab4ad907f6ac9080520b84bffe016c3352)
|
||||
vue:
|
||||
specifier: ^3.4.21
|
||||
version: 3.5.15(typescript@5.7.2)
|
||||
@@ -6026,8 +6029,8 @@ packages:
|
||||
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
resolution: {integrity: sha512-sR7NuGAJH93Om8keAF2/rj1EqFzEiUey4aKGyDo3nQ2BiVqhgq8UkiIoIp5qhV9w4pG7qArhfqyewcG8AbhXbg==}
|
||||
sm-crypto@0.5.8:
|
||||
resolution: {integrity: sha512-bpz1/PthgZHcLHyIl28mbYZHmeuTDrdWpwHZ7BGzFjQuB7OOhjlbcPEMt44aqUfZ2S5YJXM8v20xYO1j9CC45Q==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
@@ -14156,7 +14159,7 @@ snapshots:
|
||||
ansi-styles: 6.2.1
|
||||
is-fullwidth-code-point: 5.0.0
|
||||
|
||||
sm-crypto@0.3.14:
|
||||
sm-crypto@0.5.8(patch_hash=62930665cf9612107916df2f12c9deab4ad907f6ac9080520b84bffe016c3352):
|
||||
dependencies:
|
||||
jsbn: 1.1.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
packages:
|
||||
- '**'
|
||||
- '!node_modules'
|
||||
|
||||
patchedDependencies:
|
||||
'@dcloudio/uni-h5': patches/@dcloudio__uni-h5.patch
|
||||
sm-crypto@0.5.8: patches/sm-crypto@0.5.8.patch
|
||||
@@ -0,0 +1,126 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
// #ifdef APP-PLUS
|
||||
import { ensureSecureRandomReady, registerRandomBridge } from '@/utils/secure-random'
|
||||
// #endif
|
||||
|
||||
interface BridgeItem {
|
||||
length: number
|
||||
resolve: (bytes: number[]) => void
|
||||
reject: (err: Error) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* App 端专用(页面中 #ifdef APP-PLUS 条件挂载):
|
||||
* 逻辑层无 Web Crypto,经 renderjs 在视图层 WebView 取
|
||||
* crypto.getRandomValues 后 callMethod 回传,注册为 secure-random 的随机源。
|
||||
*/
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return {
|
||||
/** 变化后触发 renderjs 取随机(每次新对象引用确保 prop 变更被感知) */
|
||||
bridgeRequest: null as { id: number, length: number } | null,
|
||||
// 仅类型检查用:renderjs 模块绑定由 uni-app 编译期处理,运行时不读取
|
||||
secureRandomRenderer: { onPropChange: () => {} } as { onPropChange: (...args: any[]) => void },
|
||||
seq: 0,
|
||||
queue: [] as BridgeItem[],
|
||||
sending: false,
|
||||
currentId: 0,
|
||||
timeoutTimer: undefined as ReturnType<typeof setTimeout> | undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// #ifdef APP-PLUS
|
||||
registerRandomBridge((length: number) =>
|
||||
this.requestBytes(length).then(bytes => new Uint8Array(bytes)),
|
||||
)
|
||||
ensureSecureRandomReady().catch((e: Error) => {
|
||||
console.warn('[secure-random] App 端随机源预热失败', e)
|
||||
})
|
||||
// #endif
|
||||
},
|
||||
beforeUnmount() {
|
||||
clearTimeout(this.timeoutTimer)
|
||||
},
|
||||
methods: {
|
||||
requestBytes(length: number): Promise<number[]> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push({ length, resolve, reject })
|
||||
this.pump()
|
||||
})
|
||||
},
|
||||
/** 串行发送:prop 变更过快时视图层可能丢失中间状态,故一次只发一个请求 */
|
||||
pump() {
|
||||
if (this.sending || this.queue.length === 0)
|
||||
return
|
||||
this.sending = true
|
||||
const item = this.queue[0]
|
||||
this.currentId = ++this.seq
|
||||
this.bridgeRequest = { id: this.currentId, length: item.length }
|
||||
// 超时保护:视图层无响应时按 fail-closed 报错,避免登录流程永久挂起
|
||||
clearTimeout(this.timeoutTimer)
|
||||
this.timeoutTimer = setTimeout(() => {
|
||||
this.onRandomResult({ id: this.currentId, error: 'renderjs 随机数请求超时' })
|
||||
}, 5000)
|
||||
},
|
||||
/** renderjs 通过 callMethod 回传结果(或错误) */
|
||||
onRandomResult(res: { id: number, bytes?: number[], error?: string }) {
|
||||
if (res.id !== this.currentId)
|
||||
return
|
||||
clearTimeout(this.timeoutTimer)
|
||||
const item = this.queue.shift()
|
||||
this.sending = false
|
||||
if (item) {
|
||||
if (res.error)
|
||||
item.reject(new Error(res.error))
|
||||
else
|
||||
item.resolve(res.bytes || [])
|
||||
}
|
||||
this.pump()
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script module="secureRandomRenderer" lang="renderjs">
|
||||
// 视图层(WebView):可访问 crypto.getRandomValues
|
||||
export default {
|
||||
methods: {
|
||||
onPropChange(newVal, oldVal, ownerInstance, instance) {
|
||||
if (!newVal || !newVal.id || !newVal.length)
|
||||
return
|
||||
try {
|
||||
const cryptoObj = (typeof crypto !== 'undefined' && crypto)
|
||||
|| (typeof window !== 'undefined' && window.crypto)
|
||||
if (!cryptoObj || typeof cryptoObj.getRandomValues !== 'function')
|
||||
throw new Error('视图层缺少 crypto.getRandomValues')
|
||||
const bytes = new Uint8Array(newVal.length)
|
||||
cryptoObj.getRandomValues(bytes)
|
||||
ownerInstance.callMethod('onRandomResult', { id: newVal.id, bytes: Array.from(bytes) })
|
||||
}
|
||||
catch (e) {
|
||||
ownerInstance.callMethod('onRandomResult', {
|
||||
id: newVal.id,
|
||||
error: String((e && e.message) || e),
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="secure-random-bridge"
|
||||
:prop="bridgeRequest"
|
||||
:change:prop="secureRandomRenderer.onPropChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.secure-random-bridge {
|
||||
width: 0;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -92,6 +92,7 @@
|
||||
"minified": true
|
||||
},
|
||||
"usingComponents": true,
|
||||
"libVersion": "2.19.4",
|
||||
"optimization": {
|
||||
"subPackages": true
|
||||
},
|
||||
|
||||
@@ -13,6 +13,10 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
// 导入API接口
|
||||
import { retrievePassword, sendSmsCode } from '@/api/auth'
|
||||
// #ifdef APP-PLUS
|
||||
// App 端随机源桥接:为 SM2 加密提供安全随机数(无视觉展示)
|
||||
import SecureRandomBridge from '@/components/secure-random-bridge/index.vue'
|
||||
// #endif
|
||||
// 导入国际化相关功能
|
||||
import { initI18n, t } from '@/i18n'
|
||||
import { useConfigStore } from '@/store'
|
||||
@@ -235,7 +239,7 @@ async function handleResetPassword() {
|
||||
try {
|
||||
// 拼接图形验证码和新密码进行加密
|
||||
const captchaAndPassword = formData.value.captcha + formData.value.newPassword
|
||||
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
|
||||
encryptedPassword = await sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('密码加密失败:', error)
|
||||
@@ -305,6 +309,9 @@ onMounted(async () => {
|
||||
class="app-container box-border h-screen w-full"
|
||||
:style="{ paddingTop: `${safeAreaInsets?.top}px` }"
|
||||
>
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<SecureRandomBridge />
|
||||
<!-- #endif -->
|
||||
<view class="header">
|
||||
<view class="back-button" @click="goBack">
|
||||
<wd-icon name="arrow-left" custom-class="back-icon" />
|
||||
|
||||
@@ -13,6 +13,10 @@ import type { LoginData } from '@/api/auth'
|
||||
import type { Language } from '@/store/lang'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { login } from '@/api/auth'
|
||||
// #ifdef APP-PLUS
|
||||
// App 端随机源桥接:为 SM2 加密提供安全随机数(无视觉展示)
|
||||
import SecureRandomBridge from '@/components/secure-random-bridge/index.vue'
|
||||
// #endif
|
||||
// 导入国际化相关功能
|
||||
import { changeLanguage, getCurrentLanguage, getSupportedLanguages, initI18n, t } from '@/i18n'
|
||||
import { useConfigStore, useUserStore } from '@/store'
|
||||
@@ -202,7 +206,7 @@ async function handleLogin() {
|
||||
try {
|
||||
// 拼接验证码和密码
|
||||
const captchaAndPassword = formData.value.captcha + formData.value.password
|
||||
encryptedPassword = sm2Encrypt(sm2PublicKey, captchaAndPassword)
|
||||
encryptedPassword = await sm2Encrypt(sm2PublicKey, captchaAndPassword)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('密码加密失败:', error)
|
||||
@@ -281,6 +285,9 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<view class="app-container box-border h-screen w-full" :style="{ paddingTop: `${safeAreaInsets?.top}px` }">
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<SecureRandomBridge />
|
||||
<!-- #endif -->
|
||||
<view class="header">
|
||||
<view class="logo-section">
|
||||
<wd-img :width="80" :height="80" round src="/static/logo.png" class="logo" />
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { register, sendSmsCode } from '@/api/auth'
|
||||
// #ifdef APP-PLUS
|
||||
// App 端随机源桥接:为 SM2 加密提供安全随机数(无视觉展示)
|
||||
import SecureRandomBridge from '@/components/secure-random-bridge/index.vue'
|
||||
// #endif
|
||||
// 导入国际化相关功能
|
||||
import { initI18n, t } from '@/i18n'
|
||||
import { useConfigStore } from '@/store'
|
||||
@@ -251,7 +255,7 @@ async function handleRegister() {
|
||||
try {
|
||||
// 拼接验证码和密码
|
||||
const captchaAndPassword = formData.value.captcha + formData.value.password
|
||||
encryptedPassword = sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
|
||||
encryptedPassword = await sm2Encrypt(sm2PublicKey.value, captchaAndPassword)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('密码加密失败:', error)
|
||||
@@ -325,6 +329,9 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<view class="app-container box-border h-screen w-full" :style="{ paddingTop: `${safeAreaInsets?.top}px` }">
|
||||
<!-- #ifdef APP-PLUS -->
|
||||
<SecureRandomBridge />
|
||||
<!-- #endif -->
|
||||
<view class="header">
|
||||
<view class="back-button" @click="goBack">
|
||||
<wd-icon name="arrow-left" custom-class="back-icon" />
|
||||
|
||||
@@ -2,6 +2,21 @@ import smCrypto from 'sm-crypto'
|
||||
import { pages, subPackages } from '@/pages.json'
|
||||
|
||||
import { isMpWeixin } from './platform'
|
||||
import { ensureSecureRandomReady } from './secure-random'
|
||||
|
||||
/**
|
||||
* 获取 sm2 模块(确保密码学安全随机源已就绪)。
|
||||
*
|
||||
* sm-crypto >= 0.5.0(CVE-2026-73567 修复)强制要求 crypto.getRandomValues。
|
||||
* 小程序等环境没有该 API,项目中通过 pnpm patch(patches/sm-crypto@0.5.8.patch)
|
||||
* 将 sm-crypto 的熵池初始化推迟到首次取随机字节时,并由 secure-random.ts 提供
|
||||
* 垫片。因此小程序端首次调用 sm2 前必须 await 本方法,用 wx.getRandomValues
|
||||
* 预热真随机缓冲池;浏览器 / Node 等原生支持 globalThis.crypto 的环境立即就绪。
|
||||
*/
|
||||
async function getSm2(): Promise<typeof smCrypto.sm2> {
|
||||
await ensureSecureRandomReady()
|
||||
return smCrypto.sm2
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时服务端地址覆盖存储键
|
||||
@@ -201,11 +216,11 @@ export function getEnvBaseUploadUrl() {
|
||||
|
||||
/**
|
||||
* 生成SM2密钥对(十六进制格式)
|
||||
* @returns {object} 包含公钥和私钥的对象
|
||||
* @returns {Promise<object>} 包含公钥和私钥的对象
|
||||
*/
|
||||
export function generateSm2KeyPairHex() {
|
||||
export async function generateSm2KeyPairHex() {
|
||||
// 使用sm-crypto库生成SM2密钥对
|
||||
const sm2 = smCrypto.sm2
|
||||
const sm2 = await getSm2()
|
||||
const keypair = sm2.generateKeyPairHex()
|
||||
|
||||
return {
|
||||
@@ -220,9 +235,9 @@ export function generateSm2KeyPairHex() {
|
||||
* SM2公钥加密
|
||||
* @param {string} publicKey 公钥(十六进制格式)
|
||||
* @param {string} plainText 明文
|
||||
* @returns {string} 加密后的密文(十六进制格式)
|
||||
* @returns {Promise<string>} 加密后的密文(十六进制格式)
|
||||
*/
|
||||
export function sm2Encrypt(publicKey: string, plainText: string): string {
|
||||
export async function sm2Encrypt(publicKey: string, plainText: string): Promise<string> {
|
||||
if (!publicKey) {
|
||||
throw new Error('公钥不能为null或undefined')
|
||||
}
|
||||
@@ -231,7 +246,7 @@ export function sm2Encrypt(publicKey: string, plainText: string): string {
|
||||
throw new Error('明文不能为空')
|
||||
}
|
||||
|
||||
const sm2 = smCrypto.sm2
|
||||
const sm2 = await getSm2()
|
||||
// SM2加密,添加04前缀表示未压缩公钥
|
||||
const encrypted = sm2.doEncrypt(plainText, publicKey, 1)
|
||||
// 转换为十六进制格式(与后端保持一致,添加04前缀)
|
||||
@@ -244,10 +259,10 @@ export function sm2Encrypt(publicKey: string, plainText: string): string {
|
||||
* SM2私钥解密
|
||||
* @param {string} privateKey 私钥(十六进制格式)
|
||||
* @param {string} cipherText 密文(十六进制格式)
|
||||
* @returns {string} 解密后的明文
|
||||
* @returns {Promise<string>} 解密后的明文(当前项目内暂无调用点,保留备用)
|
||||
*/
|
||||
export function sm2Decrypt(privateKey: string, cipherText: string): string {
|
||||
const sm2 = smCrypto.sm2
|
||||
export async function sm2Decrypt(privateKey: string, cipherText: string): Promise<string> {
|
||||
const sm2 = await getSm2()
|
||||
// 移除04前缀(与后端保持一致)
|
||||
const dataWithoutPrefix = cipherText.startsWith('04') ? cipherText.substring(2) : cipherText
|
||||
// SM2解密
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 为 sm-crypto >= 0.5.0 提供 crypto.getRandomValues 兼容层
|
||||
* (0.5.0 起强制要求 CSPRNG,不支持时模块加载即抛异常,见 CVE-2026-73567)。
|
||||
*
|
||||
* 策略:
|
||||
* 1. 经平台异步 CSPRNG API 分块并行取真随机字节,填入同步缓冲池。
|
||||
* 探测顺序 wx → uni → my → tt → swan → ks → qq → jd → lark → xhs;
|
||||
* App 端逻辑层无 CSPRNG,
|
||||
* 由 secure-random-bridge 组件经 renderjs 回传(registerRandomBridge 兜底)。
|
||||
* 2. 垫片同步从缓冲池取字节,池不足即抛错,不回退弱随机(fail-closed)。
|
||||
* 3. sm-crypto 熵池初始化已通过 pnpm patch 推迟到首次取随机
|
||||
* (patches/sm-crypto@0.5.8.patch),首次 sm2 调用前 await
|
||||
* ensureSecureRandomReady() 即可。
|
||||
*
|
||||
* 无任何 CSPRNG 的平台(如支付宝/百度小程序)会 reject,属预期行为。
|
||||
*/
|
||||
const CHUNK_SIZE = 64
|
||||
const CHUNK_COUNT = 8
|
||||
const LOW_WATER_MARK = 128
|
||||
|
||||
// 标记垫片自身,探测时与原生实现区分,避免失败重试被短路成假成功
|
||||
const SHIM_MARKER = Symbol('secureRandomShim')
|
||||
|
||||
// App 端桥接通道,由 secure-random-bridge 组件挂载时注册
|
||||
let bridgeProvider: ((length: number) => Promise<Uint8Array>) | null = null
|
||||
|
||||
export function registerRandomBridge(provider: (length: number) => Promise<Uint8Array>) {
|
||||
bridgeProvider = provider
|
||||
}
|
||||
|
||||
let pool: number[] = []
|
||||
let readyPromise: Promise<void> | null = null
|
||||
let refillInFlight = false
|
||||
|
||||
function getGlobalObj(): Record<string, any> | null {
|
||||
// sm-crypto 读取的是 globalThis.crypto,垫片必须装在同一对象上
|
||||
if (typeof globalThis !== 'undefined')
|
||||
return globalThis as unknown as Record<string, any>
|
||||
return null
|
||||
}
|
||||
|
||||
function hasNativeGetRandomValues(): boolean {
|
||||
try {
|
||||
// 探测 globalThis.crypto:Node、h5:ssr 等环境有原生 CSPRNG 但无 window
|
||||
const fn = getGlobalObj()?.crypto?.getRandomValues
|
||||
// 排除垫片自身,否则失败重试会被短路成假成功
|
||||
return typeof fn === 'function' && !(fn as any)[SHIM_MARKER]
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按优先级探测提供异步 CSPRNG 的平台全局对象。
|
||||
* uni.getRandomValues 仅有声明无实现,故排在 wx 之后兜底;
|
||||
* 其余平台对象未在 TS 类型中声明,经 globalThis 探测,
|
||||
* 对应平台未来实现同名 API 时自动生效。
|
||||
*/
|
||||
function findRandomApi(): { name: string, api: any } | null {
|
||||
const globalObj = getGlobalObj()
|
||||
const candidates: Array<[string, any]> = [
|
||||
['wx', typeof wx !== 'undefined' ? wx : null],
|
||||
['uni', typeof uni !== 'undefined' ? uni : null],
|
||||
['my', globalObj ? globalObj.my : null],
|
||||
['tt', globalObj ? globalObj.tt : null],
|
||||
['swan', globalObj ? globalObj.swan : null],
|
||||
['ks', globalObj ? globalObj.ks : null],
|
||||
['qq', globalObj ? globalObj.qq : null],
|
||||
['jd', globalObj ? globalObj.jd : null],
|
||||
['lark', globalObj ? globalObj.lark : null],
|
||||
['xhs', globalObj ? globalObj.xhs : null],
|
||||
]
|
||||
for (const [name, api] of candidates) {
|
||||
if (api && typeof api.getRandomValues === 'function')
|
||||
return { name, api }
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function fetchRandomChunk(length: number): Promise<Uint8Array> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const found = findRandomApi()
|
||||
if (!found && !bridgeProvider) {
|
||||
reject(new Error(
|
||||
'当前平台不支持获取密码学安全随机数(getRandomValues),无法安全执行 SM2 加密。'
|
||||
+ '当前已适配:微信小程序(wx.getRandomValues)、浏览器、Node 环境、'
|
||||
+ 'App 端(renderjs 桥接,需页面挂载 secure-random-bridge 组件);'
|
||||
+ '其余小程序平台因缺少 CSPRNG API 被按 fail-closed 原则拒绝,'
|
||||
+ '如需支持请扩展 secure-random.ts 的 findRandomApi 适配链。',
|
||||
))
|
||||
return
|
||||
}
|
||||
if (found) {
|
||||
found.api.getRandomValues({
|
||||
length,
|
||||
success: (res: { randomValues: ArrayBuffer | Uint8Array | number[] }) => {
|
||||
resolve(new Uint8Array(res.randomValues))
|
||||
},
|
||||
fail: (err: unknown) => {
|
||||
reject(err instanceof Error ? err : new Error(`getRandomValues 调用失败: ${JSON.stringify(err)}`))
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
bridgeProvider!(length).then(resolve, (err: unknown) => {
|
||||
reject(err instanceof Error ? err : new Error(`renderjs 桥接取随机失败: ${String(err)}`))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function refillPool(totalLength: number): Promise<void> {
|
||||
const chunks = await Promise.all(
|
||||
Array.from({ length: Math.ceil(totalLength / CHUNK_SIZE) }, () => fetchRandomChunk(CHUNK_SIZE)),
|
||||
)
|
||||
for (const chunk of chunks) {
|
||||
for (let i = 0; i < chunk.length; i++)
|
||||
pool.push(chunk[i])
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleRefill() {
|
||||
if (refillInFlight)
|
||||
return
|
||||
refillInFlight = true
|
||||
refillPool(256)
|
||||
.catch((err) => {
|
||||
// 补充失败仅告警:池仍够用则无感,耗尽时由 getRandomValues 抛错兜底
|
||||
console.warn('[secure-random] 补充随机数缓冲池失败', err)
|
||||
})
|
||||
.finally(() => {
|
||||
refillInFlight = false
|
||||
})
|
||||
}
|
||||
|
||||
function createGetRandomValues<T extends ArrayBufferView>(array: T): T {
|
||||
// 按字节拷贝,兼容 Uint16Array/Uint32Array 等视图
|
||||
const view = array as unknown as ArrayBufferView
|
||||
const bytes = new Uint8Array(view.buffer, view.byteOffset, view.byteLength)
|
||||
if (pool.length < bytes.length) {
|
||||
scheduleRefill()
|
||||
throw new Error('安全随机数缓冲池未就绪或已耗尽,请先等待 ensureSecureRandomReady() 完成')
|
||||
}
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
bytes[i] = pool[i]
|
||||
pool = pool.slice(bytes.length)
|
||||
if (pool.length < LOW_WATER_MARK)
|
||||
scheduleRefill()
|
||||
return array
|
||||
}
|
||||
|
||||
;(createGetRandomValues as any)[SHIM_MARKER] = true
|
||||
|
||||
function installShim() {
|
||||
const globalObj = getGlobalObj()
|
||||
if (!globalObj)
|
||||
throw new Error('无法获取全局对象,无法安装 crypto.getRandomValues 垫片')
|
||||
|
||||
if (globalObj.crypto && typeof globalObj.crypto.getRandomValues === 'function')
|
||||
return
|
||||
|
||||
if (globalObj.crypto) {
|
||||
// 只补缺的方法,保留宿主 crypto 其他成员;defineProperty 规避只读访问器赋值失败
|
||||
Object.defineProperty(globalObj.crypto, 'getRandomValues', {
|
||||
value: createGetRandomValues,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
else {
|
||||
globalObj.crypto = { getRandomValues: createGetRandomValues }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保密码学安全随机源可用(首次 sm2 调用前必须 await)。
|
||||
* 原生支持的环境直接就绪;缺失环境安装垫片并预热缓冲池;
|
||||
* 无 CSPRNG 的环境 reject,由调用方 fail-closed 处理。
|
||||
*/
|
||||
export function ensureSecureRandomReady(): Promise<void> {
|
||||
if (hasNativeGetRandomValues())
|
||||
return Promise.resolve()
|
||||
if (!readyPromise) {
|
||||
readyPromise = (async () => {
|
||||
installShim()
|
||||
await refillPool(CHUNK_SIZE * CHUNK_COUNT)
|
||||
})().catch((err) => {
|
||||
// 失败后重置,允许后续重试
|
||||
readyPromise = null
|
||||
throw err
|
||||
})
|
||||
}
|
||||
return readyPromise
|
||||
}
|
||||
Generated
+4
-4
@@ -17,7 +17,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"opus-decoder": "^0.7.7",
|
||||
"opus-recorder": "^8.0.5",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"sm-crypto": "^0.5.8",
|
||||
"v-code-diff": "^1.16.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
@@ -12485,9 +12485,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sm-crypto": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.3.13.tgz",
|
||||
"integrity": "sha512-ztNF+pZq6viCPMA1A6KKu3bgpkmYti5avykRHbcFIdSipFdkVmfUw2CnpM2kBJyppIalqvczLNM3wR8OQ0pT5w==",
|
||||
"version": "0.5.8",
|
||||
"resolved": "https://registry.npmmirror.com/sm-crypto/-/sm-crypto-0.5.8.tgz",
|
||||
"integrity": "sha512-bpz1/PthgZHcLHyIl28mbYZHmeuTDrdWpwHZ7BGzFjQuB7OOhjlbcPEMt44aqUfZ2S5YJXM8v20xYO1j9CC45Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jsbn": "^1.1.0"
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"normalize.css": "^8.0.1",
|
||||
"opus-decoder": "^0.7.7",
|
||||
"opus-recorder": "^8.0.5",
|
||||
"sm-crypto": "^0.3.13",
|
||||
"sm-crypto": "^0.5.8",
|
||||
"v-code-diff": "^1.16.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-axios": "^3.5.2",
|
||||
|
||||
Reference in new issue
Block a user