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
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=C1C3C20=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 STRING42是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 STRING20表示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(`系统安全异常,请联系管理员`)
}
}

View File

@@ -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()

View File

@@ -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