diff --git a/src/api/safety/index.js b/src/api/safety/index.js new file mode 100644 index 0000000..bca5171 --- /dev/null +++ b/src/api/safety/index.js @@ -0,0 +1,18 @@ +import request from '@/assets/js/utils/request' +import getUrl from '@/assets/js/utils/get-url' + +export function getPublicKeyHex() { + return request({ + url: getUrl('/keyPair/getPublicKey'), + method: 'get', + noLoading: true + }) +} + +export function getPrivateKeyHex() { + return request({ + url: getUrl('/keyPair/getPrivateKey'), + method: 'get', + noLoading: true + }) +} diff --git a/src/assets/js/utils/encrypt.js b/src/assets/js/utils/encrypt.js index c5c5d6f..9245931 100644 --- a/src/assets/js/utils/encrypt.js +++ b/src/assets/js/utils/encrypt.js @@ -1,16 +1,31 @@ // sm2-utils.js import { sm2 } from 'sm-crypto' +import { getPrivateKeyHex, getPublicKeyHex } from '@/api/safety' -// DER 编码的 SM2 公钥(hex) -const derPublicKeyHex = - '3059301306072a8648ce3d020106082a811ccf5501822d0342000403e352a001b6fb4de360ce710745e1fbac40d7f87e1c1d0e1655c4c045c06d3739a55d455e589b82a0030bf9b29a6b0bb466369e92278a105714354430af5512' +// DER 编码的 SM2 公钥(hex)(未解析的公钥) +let derPublicKeyHex = sessionStorage.getItem('derPublicKeyHex') -// 私钥(64位 HEX) -export const privateKeyHex = - '7607e5f3c7a64105ad29dad2b23d5e154219b2173da0bbb1be6fd4e902b36667' +// 私钥(64位 HEX)(未解析的私钥) +let privateKeyHex = sessionStorage.getItem('privateKeyHex') -// 缓存提取后的原始公钥 -let cachedPublicKeyHex = null +// 不加密的URL列表 +const ENCRYPT_EXCLUDE_URLS = ['/bpic/ta/*', '/keyPair/*'] + +// 将ENCRYPT_EXCLUDE_URLS通配符模式转换为正则表达式 +const excludePatterns = ENCRYPT_EXCLUDE_URLS.map(pattern => { + // 将 * 转换为 .* 用于正则匹配 + const regexPattern = pattern.replace(/\*/g, '.*') + return new RegExp('^' + regexPattern + '$') +}) + +// 检查是否需要加密 +export function shouldEncrypt(url) { + // // 获取url eg:/sysUserEx/baseLogin + // const url = config.url.substring(config.url.lastIndexOf(':')).substring(6) + // 排除不加密的url + const isExcluded = excludePatterns.some(pattern => pattern.test(url)) + return !isExcluded +} /** * 从 DER 编码的 SM2 公钥(hex)中提取原始未压缩公钥(04 开头) @@ -18,10 +33,9 @@ let cachedPublicKeyHex = null * @returns {string} 原始公钥 hex 字符串(04 开头,130位) */ function extractSm2RawPublicKey(derHex) { - if (cachedPublicKeyHex) { - return cachedPublicKeyHex + if (cachedRawPublicKey) { + return cachedRawPublicKey } - let derBuffer try { derBuffer = @@ -29,9 +43,9 @@ function extractSm2RawPublicKey(derHex) { ? Buffer.from(derHex, 'hex') : new Uint8Array(derHex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))) } catch (error) { - throw new Error('公钥 hex 格式无效') + console.error('公钥 hex 格式无效', error) + throw new Error('系统安全异常,请联系管理员') } - // 查找 03 42 00 04 模式 const pattern = [0x03, 0x42, 0x00, 0x04] let offset = -1 @@ -49,7 +63,7 @@ function extractSm2RawPublicKey(derHex) { } if (offset === -1) { - throw new Error('无法在 DER 中找到 SM2 公钥数据') + throw new Error('系统安全异常,请联系管理员') } // 提取 65 字节(04 + X + Y) @@ -60,9 +74,7 @@ function extractSm2RawPublicKey(derHex) { ? publicKeyRaw.toString('hex') : Array.from(publicKeyRaw) .map(b => b.toString(16).padStart(2, '0')) - .join('') - - cachedPublicKeyHex = hex + .join() return hex } @@ -72,32 +84,26 @@ function extractSm2RawPublicKey(derHex) { * @param {string} url - 请求 URL,用于判断是否加密 * @returns {any|string} 加密后的 hex 字符串(带 04 前缀),或原数据 */ -export function encrypt(data, url) { - // 白名单:/bpic/ta/* 路径不加密 - if (/^\/bpic\/ta\//.test(url)) { - return data - } - +export function encrypt(data) { try { const plaintext = typeof data === 'string' ? data : JSON.stringify(data) - var publicKeyHex = extractSm2RawPublicKey(derPublicKeyHex) - publicKeyHex = '04' + publicKeyHex - publicKeyHex = - '0403e352a001b6fb4de360ce710745e1fbac40d7f87e1c1d0e1655c4c045c06d3739a55d455e589b82a0030bf9b29a6b0bb466369e92278a105714354430af5512' + var publicKeyHex = parseSM2PublicKey(derPublicKeyHex) + var a = sessionStorage.getItem('derPublicKeyHex') + publicKeyHex = publicKeyHex.startsWith('04') + ? publicKeyHex + : '04' + publicKeyHex // 执行标准 SM2 加密(C1C3C2 模式) const ciphertext = sm2.doEncrypt(plaintext, publicKeyHex, 1) // 🔥 必须加 '04' 前缀(适配后端) - const finalCiphertext = '04' + ciphertext - - console.log('[SM2加密] 明文:', plaintext) - console.log('[SM2加密] 密文:', finalCiphertext) + const finalCiphertext = ciphertext.startsWith('04') + ? ciphertext + : '04' + ciphertext return finalCiphertext } catch (error) { - console.error('[SM2加密失败]', error) - throw new Error(`SM2加密失败: ${error.message}`) + throw new Error(`系统安全异常,请联系管理员`) } } @@ -108,13 +114,12 @@ export function encrypt(data, url) { * @param {number} mode - 加密模式(1=C1C3C2,0=C1C2C3) * @returns {string|null} 明文 或 null(失败) */ -export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) { +export function decryptWithPrivateKey(cipherHex) { try { + var privateKey = parseSM2PrivateKey(privateKeyHex) // 校验私钥 - if (!/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) { - throw new Error( - '私钥格式错误:必须是 64 位十六进制字符串' + privateKeyHex.length - ) + if (!/^[0-9a-fA-F]{64}$/.test(privateKey)) { + throw new Error('系统安全异常,请联系管理员') } let cleanCipher = cipherHex.trim() @@ -124,21 +129,19 @@ export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) { // 判断是否是人为添加的 04(标准 C1C3C2 密文长度约 512~520,加 04 后变 514+) const expectedMinLen = 512 if (cleanCipher.length > expectedMinLen) { - console.warn('[SM2解密] 检测到 04 前缀,自动去除') cleanCipher = cleanCipher.substring(2) } } // 执行解密 - const plaintext = sm2.doDecrypt(cleanCipher, privateKeyHex, mode) + const plaintext = sm2.doDecrypt(cleanCipher, privateKey, 1) if (plaintext === false || plaintext === undefined) { - throw new Error('解密失败:返回 false,可能是密钥/模式/格式不匹配') + throw new Error('系统安全异常,请联系管理员') } return plaintext } catch (error) { - console.error('[SM2 解密错误]', error.message) return null } } @@ -147,49 +150,125 @@ export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) { * 获取原始公钥(用于调试) */ export function getPublicKey() { - return extractSm2RawPublicKey(derPublicKeyHex) + return derPublicKeyHex } -// ====================== -// 执行你的解密 -// ====================== +export function getPrivateKey() { + return privateKeyHex +} -const responseCipher = - '04185025c96dac6e2a098f718a6a08e9f3669a56b31a6ac3ce07aa3a89d7cdfe2ed551ab63c1f701a399bc06c38104cdb9453e8d945da6a4f307b70e4a05771ee0dd72fb665e7b8ab053aa3618aa33bd5c97fb99d5ce9b92eadf0704feef8833e92953d06dccedfdcfc560dab28afb0633f4c1f071c7ae6eb4e7cb7e0f0a516a811a5e282984aa8381e3b2d022fcdc70a976f0335f86ccbe76be8ae648cdfd8ec23dd6304a1b9c5caf8a8cf77ab28b7188e53b7f54531070081f68453912784def984d161df596810781b8b35dd2128a75e984b14011472ace0b4df64a2e9a88ceba3d8437ad67e6e38e06ba12c2f0da51901b9b018ff2f3d852dca8e60fce7f6a86001600df6d422068c60cfd3bfc0cca3199a5ea1477175249462781a3a305216a961ce75bfd1122a5324be8aad7586ed3df9ae8da0c5f52a84cbd6e9c0fcee182175b3d1f5105e589be28f3459a6f637d5301f6cfd01ca68f7aea20be3597a44b41d412fd276043704c33d63e89d7960c8503ced6e5785f23f11502ecac7a7615d302decef3feaeb63e84ed84a2d0212687be2ae6e26418889829499f47d43c2a5366d084770669d426159955f5503f59e47f6910c277cc16865f216bd26b08451886d2537b9e0163af350fa7a8907a00db8164855fdbcc1837425df6967c3c28f3f69d028d9d16403e7ffe9b4b3029caeffa6ac8fcdbac6f514c50505a87' +// 向后端接口获取未解析的公钥 +export function fetchPublicKey() { + getPublicKeyHex() + .then(res => { + if (res.success) { + sessionStorage.setItem('cachedRawPublicKey', res.content.content) + } + }) + .catch(err => { + console.log('系统安全异常', err) + }) +} -console.log('🔍 开始解密接口返回的报文...') +// 向后端接口获取未解析的私钥 +export function fetchPrivateKey() { + getPrivateKeyHex() + .then(res => { + if (res.success) { + sessionStorage.setItem('cachedPrivateKey', res.content.content) + } + }) + .catch(err => { + console.log('系统安全异常', err) + }) +} -const result = decryptWithPrivateKey(responseCipher, privateKeyHex, 1) - -if (result) { - console.log('🎉 解密成功!明文:', result) - - // 尝试解析为 JSON +export function parseSM2PublicKey(publicKeyHex) { try { - const json = JSON.parse(result) - console.log('📦 JSON 解析成功:', json) - } catch (e) { - console.log('ℹ️ 明文不是 JSON 格式') + const cleanKey = publicKeyHex.replace(/\s/g, '') + + // 查找公钥数据开始位置, 公钥数据在 "03420004" 之后(03是BIT STRING,42是66字节长度,04是未压缩格式标识) + const publicKeyDataIndex = cleanKey.indexOf('03420004') + if (publicKeyDataIndex !== -1) { + const publicKeyStart = publicKeyDataIndex + 8 // 跳过"03420004" + const publicKeyHex = cleanKey.substring(publicKeyStart) + + // SM2公钥应该是130字符(65字节,04 + 32字节X + 32字节Y) + if (publicKeyHex.length >= 130) { + const fullPublicKey = publicKeyHex.substring(0, 130) + if ( + fullPublicKey.startsWith('04') && + /^04[0-9a-fA-F]{128}$/.test(fullPublicKey) + ) { + return fullPublicKey.toLowerCase() + } + } + } + + // 如果找不到标准格式,尝试直接提取130字符 + if (cleanKey.length >= 130) { + // 查找04开头的130字符公钥 + for (let i = 0; i <= cleanKey.length - 130; i++) { + const potentialKey = cleanKey.substring(i, i + 130) + if ( + potentialKey.startsWith('04') && + /^04[0-9a-fA-F]{128}$/.test(potentialKey) + ) { + return potentialKey.toLowerCase() + } + } + } + throw new Error('系统安全异常,请联系管理员') + } catch (error) { + throw new Error(`系统安全异常,请联系管理员`) } -} else { - console.error('❌ 解密失败,请检查:私钥、密文完整性、模式') } -// 不加密的URL列表 -const ENCRYPT_EXCLUDE_URLS = ['/bpic/ta/*'] +/** + * 解析PKCS#8格式的SM2私钥 + * @param {string} pkcs8PrivateKeyHex - PKCS#8格式的私钥(16进制) + * @returns {string} - 可直接使用的SM2私钥(64字符16进制) + */ +export function parseSM2PrivateKey(pkcs8PrivateKeyHex) { + try { + // 移除可能的空格和换行 + const cleanKey = pkcs8PrivateKeyHex.replace(/\s/g, '') -// 将ENCRYPT_EXCLUDE_URLS通配符模式转换为正则表达式 -const excludePatterns = ENCRYPT_EXCLUDE_URLS.map(pattern => { - // 将 * 转换为 .* 用于正则匹配 - const regexPattern = pattern.replace(/\*/g, '.*') - return new RegExp('^' + regexPattern + '$') -}) + // PKCS#8私钥结构通常包含版本号、算法标识和私钥数据 + // 直接提取(适用于标准格式) 私钥通常在"0420"之后(04表示OCTET STRING,20表示32字节长度) + const privateKeyIndex = cleanKey.indexOf('0420') + if (privateKeyIndex !== -1) { + const start = privateKeyIndex + 4 + const privateKeyHex = cleanKey.substring(start, start + 64) -// 检查是否需要加密 -export function shouldEncrypt(config) { - // 获取url eg:/sysUserEx/baseLogin - const url = config.url.substring(config.url.lastIndexOf(':')).substring(6) - // 排除不加密的url - const isExcluded = excludePatterns.some(pattern => pattern.test(url)) - return !isExcluded + if (/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) { + return privateKeyHex.toLowerCase() + } + } + + // 从末尾提取64字符 + if (cleanKey.length >= 64) { + const privateKeyHex = cleanKey.slice(-64) + if (/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) { + return privateKeyHex.toLowerCase() + } + } + + // 手动解析ASN.1结构(更精确) + const asn1PrivateKeyIndex = cleanKey.indexOf('0420') + if (asn1PrivateKeyIndex !== -1) { + const privateKeyStart = asn1PrivateKeyIndex + 4 + const privateKeyHex = cleanKey.substring( + privateKeyStart, + privateKeyStart + 64 + ) + + if (/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) { + return privateKeyHex.toLowerCase() + } + } + throw new Error('系统安全异常,请联系管理员') + } catch (error) { + throw new Error(`系统安全异常,请联系管理员`) + } } diff --git a/src/assets/js/utils/permission.js b/src/assets/js/utils/permission.js index eb15e08..e4a1c71 100644 --- a/src/assets/js/utils/permission.js +++ b/src/assets/js/utils/permission.js @@ -4,10 +4,41 @@ import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css' // progress bar style import { getToken, removeToken } from '@/assets/js/utils/auth' // get token from cookie import { Message } from 'element-ui' +import { getPrivateKey, getPublicKey } from '@/assets/js/utils/encrypt' +import { getPrivateKeyHex, getPublicKeyHex } from '@/api/safety' NProgress.configure({ showSpinner: false }) // NProgress Configuration const whiteList = ['/login', '/authentication', '/404'] // no redirect whitelist router.beforeEach((to, from, next) => { + if (getPrivateKey() === '' || getPrivateKey() === null) { + // fetchPrivateKey() + getPrivateKeyHex() + .then(res => { + if (res.success) { + sessionStorage.setItem('privateKeyHex', res.content.content) + routerEach(to, from, next) + } + }) + .catch(err => { + console.log('系统安全异常,请联系管理员', err) + }) + } + if (getPublicKey() === '' || getPublicKey() === null) { + // fetchPublicKey() + getPublicKeyHex() + .then(res => { + if (res.success) { + sessionStorage.setItem('derPublicKeyHex', res.content.content) + routerEach(to, from, next) + } + }) + .catch(err => { + console.log('系统安全异常,请联系管理员', err) + }) + } + routerEach(to, from, next) +}) +function routerEach(to, from, next){ NProgress.start() if (getToken()) { /* has token*/ @@ -45,7 +76,7 @@ router.beforeEach((to, from, next) => { NProgress.done() } } -}) +} router.afterEach(() => { // finish progress bar NProgress.done() diff --git a/src/assets/js/utils/request.js b/src/assets/js/utils/request.js index 99f69a3..2c9df8f 100644 --- a/src/assets/js/utils/request.js +++ b/src/assets/js/utils/request.js @@ -8,8 +8,7 @@ import { logger } from 'runjs/lib/common' import { decryptWithPrivateKey, encrypt, - shouldEncrypt, - privateKeyHex + shouldEncrypt } from '@/assets/js/utils/encrypt' // create an axios instance @@ -57,12 +56,13 @@ service.interceptors.request.use( return true } - // 跳过自定义标记不加密的请求 - return !shouldEncrypt(config) + // // 跳过自定义标记不加密的请求 (在encrypt.js中已写这部分逻辑) + const url = config.url.substring(config.url.lastIndexOf(':')).substring(6) + return !shouldEncrypt(url) } // 4. 执行加密逻辑 - if (!shouldSkipEncryption() && shouldEncrypt(config)) { + if (!shouldSkipEncryption()) { // 添加加密标识(便于调试) config.headers['X-Encrypted'] = 'true' @@ -92,12 +92,14 @@ service.interceptors.response.use( // 解密处理 todo 用config.headers['X-Encrypted'] === 'true'判断是否需要解密不一定准确 if (response.config.headers['X-Encrypted'] === 'true') { try { - res = decryptWithPrivateKey(res, privateKeyHex, 1) + res = decryptWithPrivateKey(res) } catch (e) { logger.error('解密响应失败', e) } } - res = JSON.parse(res) + if (typeof res === 'string') { + res = JSON.parse(res) + } endLoading() if (response.config.back) { return res