This commit is contained in:
parent
b757597db9
commit
88acc24e86
22
package-lock.json
generated
22
package-lock.json
generated
@ -9,12 +9,14 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@peculiar/webcrypto": "^1.5.0",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"gm-crypto": "*",
|
||||
"gmsm-sm2js": "^0.7.1",
|
||||
"gmsm-sm3js": "^0.2.0",
|
||||
"gmsm-sm4js": "^0.7.0",
|
||||
"node-forge": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sjcl-with-all": "^1.0.8",
|
||||
@ -2417,12 +2419,20 @@
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-forge": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node-forge/-/node-forge-1.3.14.tgz",
|
||||
"integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
@ -6386,6 +6396,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-forge": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.4.0.tgz",
|
||||
"integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==",
|
||||
"license": "(BSD-3-Clause OR GPL-2.0)",
|
||||
"engines": {
|
||||
"node": ">= 6.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@ -7792,7 +7811,6 @@
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
|
||||
@ -12,12 +12,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@peculiar/webcrypto": "^1.5.0",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"buffer": "^6.0.3",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"gm-crypto": "*",
|
||||
"gmsm-sm2js": "^0.7.1",
|
||||
"gmsm-sm3js": "^0.2.0",
|
||||
"gmsm-sm4js": "^0.7.0",
|
||||
"node-forge": "^1.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sjcl-with-all": "^1.0.8",
|
||||
|
||||
134
src/App.tsx
134
src/App.tsx
@ -4,6 +4,7 @@ import { VERSION } from './version'
|
||||
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
|
||||
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
|
||||
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify } from './utils/algorithm/sm2'
|
||||
import { parseSm2Cert, parseCertFromFile } from './utils/algorithm/cert'
|
||||
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
||||
|
||||
function App() {
|
||||
@ -69,6 +70,9 @@ function App() {
|
||||
const [utf8Input, setUtf8Input] = useState('')
|
||||
const [base64Input, setBase64Input] = useState('')
|
||||
|
||||
// 证书工具状态
|
||||
const [certInfo, setCertInfo] = useState<any>(null)
|
||||
|
||||
|
||||
|
||||
// SM3 计算
|
||||
@ -151,6 +155,33 @@ function App() {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理证书文件上传
|
||||
const handleCertFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result as ArrayBuffer
|
||||
if (result) {
|
||||
const base64Cert = parseCertFromFile(result)
|
||||
handleParseCert(base64Cert)
|
||||
}
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
|
||||
// 处理证书解析
|
||||
const handleParseCert = (certContent: string) => {
|
||||
try {
|
||||
const info = parseSm2Cert(certContent)
|
||||
setCertInfo(info)
|
||||
} catch (error) {
|
||||
alert('证书解析失败: ' + (error instanceof Error ? error.message : error))
|
||||
setCertInfo(null)
|
||||
}
|
||||
}
|
||||
|
||||
// SM2 生成密钥对
|
||||
const handleGenerateSm2KeyPair = () => {
|
||||
try {
|
||||
@ -307,6 +338,24 @@ function App() {
|
||||
>
|
||||
转换工具
|
||||
</button>
|
||||
<button
|
||||
className={`tab ${activeTab === 'cert' ? 'active' : ''}`}
|
||||
onClick={() => handleTabChange('cert')}
|
||||
style={{
|
||||
padding: '12px 24px',
|
||||
border: 'none',
|
||||
background: activeTab === 'cert' ? '#333' : '#1e1e1e',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
fontWeight: activeTab === 'cert' ? 'bold' : 'normal',
|
||||
color: activeTab === 'cert' ? '#f59e0b' : '#e0e0e0',
|
||||
outline: 'none',
|
||||
transition: 'all 0.3s ease',
|
||||
borderBottom: activeTab === 'cert' ? '3px solid #f59e0b' : 'none'
|
||||
}}
|
||||
>
|
||||
证书工具
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -334,6 +383,7 @@ function App() {
|
||||
<option value="sm3" style={{ background: '#1e1e1e', color: '#e0e0e0' }}>SM3</option>
|
||||
<option value="sm4" style={{ background: '#1e1e1e', color: '#e0e0e0' }}>SM4</option>
|
||||
<option value="convert" style={{ background: '#1e1e1e', color: '#e0e0e0' }}>转换工具</option>
|
||||
<option value="cert" style={{ background: '#1e1e1e', color: '#e0e0e0' }}>证书工具</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
@ -1119,6 +1169,90 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 证书工具面板 */}
|
||||
{activeTab === 'cert' && (
|
||||
<div className="panel" style={{ backgroundColor: '#1e1e1e', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.3)' }}>
|
||||
<h2 style={{ color: '#f59e0b', marginBottom: '20px' }}>SM2 证书信息解析</h2>
|
||||
|
||||
<div className="form-group" style={{ marginBottom: '20px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
|
||||
<input
|
||||
type="file"
|
||||
accept=".cer,.crt,.pem"
|
||||
onChange={handleCertFileUpload}
|
||||
style={{
|
||||
padding: '10px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
color: '#e0e0e0',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{certInfo && (
|
||||
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
|
||||
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>证书信息</h3>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>版本:</label>
|
||||
<div style={{ color: '#e0e0e0' }}>{certInfo.version}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>序列号:</label>
|
||||
<div style={{ color: '#e0e0e0', wordBreak: 'break-all' }}>{certInfo.serialNumber}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>所有者:</label>
|
||||
<div style={{ color: '#e0e0e0' }}>
|
||||
{Object.entries(certInfo.subject).map(([key, value]) => (
|
||||
<div key={key}>{key}: {String(value)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>签发者:</label>
|
||||
<div style={{ color: '#e0e0e0' }}>
|
||||
{Object.entries(certInfo.issuer).map(([key, value]) => (
|
||||
<div key={key}>{key}: {String(value)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>有效期:</label>
|
||||
<div style={{ color: '#e0e0e0' }}>
|
||||
<div>开始: {certInfo.notBefore}</div>
|
||||
<div>结束: {certInfo.notAfter}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>公钥:</label>
|
||||
<div style={{ color: '#e0e0e0', wordBreak: 'break-all' }}>{certInfo.publicKey}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>用途:</label>
|
||||
<div style={{ color: '#e0e0e0' }}>
|
||||
{certInfo.keyUsage.length > 0 ? (
|
||||
certInfo.keyUsage.map((usage: string, index: number) => (
|
||||
<div key={index}>{usage}</div>
|
||||
))
|
||||
) : (
|
||||
<div>无</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer style={{ marginTop: '2rem', padding: '1rem', borderTop: '1px solid #333', textAlign: 'center', color: '#e0e0e0' }}>
|
||||
<p>© 2026 安全算法计算工具</p>
|
||||
|
||||
BIN
src/__tests__/certs/oca11sm2.cer
Normal file
BIN
src/__tests__/certs/oca11sm2.cer
Normal file
Binary file not shown.
BIN
src/__tests__/certs/sm2.cer
Normal file
BIN
src/__tests__/certs/sm2.cer
Normal file
Binary file not shown.
74
src/utils/algorithm/cert.ts
Normal file
74
src/utils/algorithm/cert.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import forge from 'node-forge'
|
||||
|
||||
// 解析SM2证书信息
|
||||
export const parseSm2Cert = (certData: string): any => {
|
||||
try {
|
||||
// 移除证书中的换行符和空格
|
||||
const cleanCert = certData.replace(/\s/g, '')
|
||||
|
||||
// 解析DER编码的证书
|
||||
const derBuffer = Buffer.from(cleanCert, 'base64')
|
||||
const asn1 = forge.asn1.fromDer(derBuffer.toString('binary'))
|
||||
const cert = forge.pki.certificateFromAsn1(asn1)
|
||||
|
||||
// 提取证书信息
|
||||
const subject: any = {}
|
||||
cert.subject.attributes.forEach((attr: any) => {
|
||||
if (attr.name) {
|
||||
subject[attr.name] = attr.value
|
||||
}
|
||||
})
|
||||
|
||||
const issuer: any = {}
|
||||
cert.issuer.attributes.forEach((attr: any) => {
|
||||
if (attr.name) {
|
||||
issuer[attr.name] = attr.value
|
||||
}
|
||||
})
|
||||
|
||||
// 提取公钥信息
|
||||
const publicKey = cert.publicKey
|
||||
let publicKeyHex = ''
|
||||
|
||||
// 尝试获取公钥的十六进制表示
|
||||
try {
|
||||
if (typeof publicKey === 'object' && publicKey !== null) {
|
||||
// 尝试将公钥转换为PEM格式,然后提取十六进制
|
||||
const pem = forge.pki.publicKeyToPem(publicKey)
|
||||
// 从PEM中提取十六进制
|
||||
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '')
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果转换失败,使用空字符串
|
||||
publicKeyHex = ''
|
||||
}
|
||||
|
||||
// 提取证书用途
|
||||
let keyUsage: string[] = []
|
||||
if (cert.extensions) {
|
||||
const keyUsageExt = cert.extensions.find((ext: any) => ext.id === 'keyUsage')
|
||||
if (keyUsageExt && keyUsageExt.value) {
|
||||
keyUsage = keyUsageExt.value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: cert.version,
|
||||
serialNumber: cert.serialNumber.toString(),
|
||||
subject: subject,
|
||||
issuer: issuer,
|
||||
notBefore: cert.validity.notBefore,
|
||||
notAfter: cert.validity.notAfter,
|
||||
publicKey: publicKeyHex,
|
||||
keyUsage: keyUsage
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error('Failed to parse certificate: ' + (error instanceof Error ? error.message : error))
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件内容解析证书
|
||||
export const parseCertFromFile = (fileContent: ArrayBuffer): string => {
|
||||
const buffer = Buffer.from(fileContent)
|
||||
return buffer.toString('base64')
|
||||
}
|
||||
@ -1 +1 @@
|
||||
export const VERSION = "V1.0-20260418070227";
|
||||
export const VERSION = "V1.0-20260418071243";
|
||||
Loading…
Reference in New Issue
Block a user