sm2-加密

This commit is contained in:
wu.jifen
2025-08-25 16:57:10 +08:00
parent 8b3a60f22d
commit e97eac836e
4 changed files with 214 additions and 84 deletions

18
src/api/safety/index.js Normal file
View 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
})
}

View File

@@ -1,16 +1,31 @@
// sm2-utils.js // sm2-utils.js
import { sm2 } from 'sm-crypto' import { sm2 } from 'sm-crypto'
import { getPrivateKeyHex, getPublicKeyHex } from '@/api/safety'
// DER 编码的 SM2 公钥hex // DER 编码的 SM2 公钥hex(未解析的公钥)
const derPublicKeyHex = let derPublicKeyHex = sessionStorage.getItem('derPublicKeyHex')
'3059301306072a8648ce3d020106082a811ccf5501822d0342000403e352a001b6fb4de360ce710745e1fbac40d7f87e1c1d0e1655c4c045c06d3739a55d455e589b82a0030bf9b29a6b0bb466369e92278a105714354430af5512'
// 私钥64位 HEX // 私钥64位 HEX(未解析的私钥)
export const privateKeyHex = let privateKeyHex = sessionStorage.getItem('privateKeyHex')
'7607e5f3c7a64105ad29dad2b23d5e154219b2173da0bbb1be6fd4e902b36667'
// 缓存提取后的原始公钥 // 不加密的URL列表
let cachedPublicKeyHex = null 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 开头) * 从 DER 编码的 SM2 公钥hex中提取原始未压缩公钥04 开头)
@@ -18,10 +33,9 @@ let cachedPublicKeyHex = null
* @returns {string} 原始公钥 hex 字符串04 开头130位 * @returns {string} 原始公钥 hex 字符串04 开头130位
*/ */
function extractSm2RawPublicKey(derHex) { function extractSm2RawPublicKey(derHex) {
if (cachedPublicKeyHex) { if (cachedRawPublicKey) {
return cachedPublicKeyHex return cachedRawPublicKey
} }
let derBuffer let derBuffer
try { try {
derBuffer = derBuffer =
@@ -29,9 +43,9 @@ function extractSm2RawPublicKey(derHex) {
? Buffer.from(derHex, 'hex') ? Buffer.from(derHex, 'hex')
: new Uint8Array(derHex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16))) : new Uint8Array(derHex.match(/[\da-f]{2}/gi).map(h => parseInt(h, 16)))
} catch (error) { } catch (error) {
throw new Error('公钥 hex 格式无效') console.error('公钥 hex 格式无效', error)
throw new Error('系统安全异常,请联系管理员')
} }
// 查找 03 42 00 04 模式 // 查找 03 42 00 04 模式
const pattern = [0x03, 0x42, 0x00, 0x04] const pattern = [0x03, 0x42, 0x00, 0x04]
let offset = -1 let offset = -1
@@ -49,7 +63,7 @@ function extractSm2RawPublicKey(derHex) {
} }
if (offset === -1) { if (offset === -1) {
throw new Error('无法在 DER 中找到 SM2 公钥数据') throw new Error('系统安全异常,请联系管理员')
} }
// 提取 65 字节04 + X + Y // 提取 65 字节04 + X + Y
@@ -60,9 +74,7 @@ function extractSm2RawPublicKey(derHex) {
? publicKeyRaw.toString('hex') ? publicKeyRaw.toString('hex')
: Array.from(publicKeyRaw) : Array.from(publicKeyRaw)
.map(b => b.toString(16).padStart(2, '0')) .map(b => b.toString(16).padStart(2, '0'))
.join('') .join()
cachedPublicKeyHex = hex
return hex return hex
} }
@@ -72,32 +84,26 @@ function extractSm2RawPublicKey(derHex) {
* @param {string} url - 请求 URL用于判断是否加密 * @param {string} url - 请求 URL用于判断是否加密
* @returns {any|string} 加密后的 hex 字符串(带 04 前缀),或原数据 * @returns {any|string} 加密后的 hex 字符串(带 04 前缀),或原数据
*/ */
export function encrypt(data, url) { export function encrypt(data) {
// 白名单:/bpic/ta/* 路径不加密
if (/^\/bpic\/ta\//.test(url)) {
return data
}
try { try {
const plaintext = typeof data === 'string' ? data : JSON.stringify(data) const plaintext = typeof data === 'string' ? data : JSON.stringify(data)
var publicKeyHex = extractSm2RawPublicKey(derPublicKeyHex) var publicKeyHex = parseSM2PublicKey(derPublicKeyHex)
publicKeyHex = '04' + publicKeyHex var a = sessionStorage.getItem('derPublicKeyHex')
publicKeyHex = publicKeyHex = publicKeyHex.startsWith('04')
'0403e352a001b6fb4de360ce710745e1fbac40d7f87e1c1d0e1655c4c045c06d3739a55d455e589b82a0030bf9b29a6b0bb466369e92278a105714354430af5512' ? publicKeyHex
: '04' + publicKeyHex
// 执行标准 SM2 加密C1C3C2 模式) // 执行标准 SM2 加密C1C3C2 模式)
const ciphertext = sm2.doEncrypt(plaintext, publicKeyHex, 1) const ciphertext = sm2.doEncrypt(plaintext, publicKeyHex, 1)
// 🔥 必须加 '04' 前缀(适配后端) // 🔥 必须加 '04' 前缀(适配后端)
const finalCiphertext = '04' + ciphertext const finalCiphertext = ciphertext.startsWith('04')
? ciphertext
console.log('[SM2加密] 明文:', plaintext) : '04' + ciphertext
console.log('[SM2加密] 密文:', finalCiphertext)
return finalCiphertext return finalCiphertext
} catch (error) { } catch (error) {
console.error('[SM2加密失败]', error) throw new Error(`系统安全异常,请联系管理员`)
throw new Error(`SM2加密失败: ${error.message}`)
} }
} }
@@ -108,13 +114,12 @@ export function encrypt(data, url) {
* @param {number} mode - 加密模式1=C1C3C20=C1C2C3 * @param {number} mode - 加密模式1=C1C3C20=C1C2C3
* @returns {string|null} 明文 或 null失败 * @returns {string|null} 明文 或 null失败
*/ */
export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) { export function decryptWithPrivateKey(cipherHex) {
try { try {
var privateKey = parseSM2PrivateKey(privateKeyHex)
// 校验私钥 // 校验私钥
if (!/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) { if (!/^[0-9a-fA-F]{64}$/.test(privateKey)) {
throw new Error( throw new Error('系统安全异常,请联系管理员')
'私钥格式错误:必须是 64 位十六进制字符串' + privateKeyHex.length
)
} }
let cleanCipher = cipherHex.trim() let cleanCipher = cipherHex.trim()
@@ -124,21 +129,19 @@ export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) {
// 判断是否是人为添加的 04标准 C1C3C2 密文长度约 512~520加 04 后变 514+ // 判断是否是人为添加的 04标准 C1C3C2 密文长度约 512~520加 04 后变 514+
const expectedMinLen = 512 const expectedMinLen = 512
if (cleanCipher.length > expectedMinLen) { if (cleanCipher.length > expectedMinLen) {
console.warn('[SM2解密] 检测到 04 前缀,自动去除')
cleanCipher = cleanCipher.substring(2) cleanCipher = cleanCipher.substring(2)
} }
} }
// 执行解密 // 执行解密
const plaintext = sm2.doDecrypt(cleanCipher, privateKeyHex, mode) const plaintext = sm2.doDecrypt(cleanCipher, privateKey, 1)
if (plaintext === false || plaintext === undefined) { if (plaintext === false || plaintext === undefined) {
throw new Error('解密失败:返回 false可能是密钥/模式/格式不匹配') throw new Error('系统安全异常,请联系管理员')
} }
return plaintext return plaintext
} catch (error) { } catch (error) {
console.error('[SM2 解密错误]', error.message)
return null return null
} }
} }
@@ -147,49 +150,125 @@ export function decryptWithPrivateKey(cipherHex, privateKeyHex, mode = 1) {
* 获取原始公钥(用于调试) * 获取原始公钥(用于调试)
*/ */
export function getPublicKey() { 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) export function parseSM2PublicKey(publicKeyHex) {
if (result) {
console.log('🎉 解密成功!明文:', result)
// 尝试解析为 JSON
try { try {
const json = JSON.parse(result) const cleanKey = publicKeyHex.replace(/\s/g, '')
console.log('📦 JSON 解析成功:', json)
} catch (e) { // 查找公钥数据开始位置, 公钥数据在 "03420004" 之后03是BIT STRING42是66字节长度04是未压缩格式标识
console.log(' 明文不是 JSON 格式') 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通配符模式转换为正则表达式 // PKCS#8私钥结构通常包含版本号、算法标识和私钥数据
const excludePatterns = ENCRYPT_EXCLUDE_URLS.map(pattern => { // 直接提取(适用于标准格式) 私钥通常在"0420"之后04表示OCTET STRING20表示32字节长度
// 将 * 转换为 .* 用于正则匹配 const privateKeyIndex = cleanKey.indexOf('0420')
const regexPattern = pattern.replace(/\*/g, '.*') if (privateKeyIndex !== -1) {
return new RegExp('^' + regexPattern + '$') const start = privateKeyIndex + 4
}) const privateKeyHex = cleanKey.substring(start, start + 64)
// 检查是否需要加密 if (/^[0-9a-fA-F]{64}$/.test(privateKeyHex)) {
export function shouldEncrypt(config) { return privateKeyHex.toLowerCase()
// 获取url eg:/sysUserEx/baseLogin }
const url = config.url.substring(config.url.lastIndexOf(':')).substring(6) }
// 排除不加密的url
const isExcluded = excludePatterns.some(pattern => pattern.test(url)) // 从末尾提取64字符
return !isExcluded 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(`系统安全异常,请联系管理员`)
}
} }

View File

@@ -4,10 +4,41 @@ import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style import 'nprogress/nprogress.css' // progress bar style
import { getToken, removeToken } from '@/assets/js/utils/auth' // get token from cookie import { getToken, removeToken } from '@/assets/js/utils/auth' // get token from cookie
import { Message } from 'element-ui' 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 NProgress.configure({ showSpinner: false }) // NProgress Configuration
const whiteList = ['/login', '/authentication', '/404'] // no redirect whitelist const whiteList = ['/login', '/authentication', '/404'] // no redirect whitelist
router.beforeEach((to, from, next) => { 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() NProgress.start()
if (getToken()) { if (getToken()) {
/* has token*/ /* has token*/
@@ -45,7 +76,7 @@ router.beforeEach((to, from, next) => {
NProgress.done() NProgress.done()
} }
} }
}) }
router.afterEach(() => { router.afterEach(() => {
// finish progress bar // finish progress bar
NProgress.done() NProgress.done()

View File

@@ -8,8 +8,7 @@ import { logger } from 'runjs/lib/common'
import { import {
decryptWithPrivateKey, decryptWithPrivateKey,
encrypt, encrypt,
shouldEncrypt, shouldEncrypt
privateKeyHex
} from '@/assets/js/utils/encrypt' } from '@/assets/js/utils/encrypt'
// create an axios instance // create an axios instance
@@ -57,12 +56,13 @@ service.interceptors.request.use(
return true return true
} }
// 跳过自定义标记不加密的请求 // // 跳过自定义标记不加密的请求 (在encrypt.js中已写这部分逻辑)
return !shouldEncrypt(config) const url = config.url.substring(config.url.lastIndexOf(':')).substring(6)
return !shouldEncrypt(url)
} }
// 4. 执行加密逻辑 // 4. 执行加密逻辑
if (!shouldSkipEncryption() && shouldEncrypt(config)) { if (!shouldSkipEncryption()) {
// 添加加密标识(便于调试) // 添加加密标识(便于调试)
config.headers['X-Encrypted'] = 'true' config.headers['X-Encrypted'] = 'true'
@@ -92,12 +92,14 @@ service.interceptors.response.use(
// 解密处理 todo 用config.headers['X-Encrypted'] === 'true'判断是否需要解密不一定准确 // 解密处理 todo 用config.headers['X-Encrypted'] === 'true'判断是否需要解密不一定准确
if (response.config.headers['X-Encrypted'] === 'true') { if (response.config.headers['X-Encrypted'] === 'true') {
try { try {
res = decryptWithPrivateKey(res, privateKeyHex, 1) res = decryptWithPrivateKey(res)
} catch (e) { } catch (e) {
logger.error('解密响应失败', e) logger.error('解密响应失败', e)
} }
} }
res = JSON.parse(res) if (typeof res === 'string') {
res = JSON.parse(res)
}
endLoading() endLoading()
if (response.config.back) { if (response.config.back) {
return res return res