mirror of
http://112.124.100.131/ebiz-ai/ebiz-ai-knowledge-manage.git
synced 2025-12-09 02:46:50 +08:00
sm2-加密
This commit is contained in:
18
src/api/safety/index.js
Normal file
18
src/api/safety/index.js
Normal file
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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(`系统安全异常,请联系管理员`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user