整理
All checks were successful
Build and Deploy / build (push) Successful in 1m4s

This commit is contained in:
cheney 2026-04-18 15:53:54 +08:00
parent 88acc24e86
commit f4228370d9
3 changed files with 227 additions and 130 deletions

View File

@ -8,9 +8,22 @@ import { parseSm2Cert, parseCertFromFile } from './utils/algorithm/cert'
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert' import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
function App() { function App() {
const [activeTab, setActiveTab] = useState('sm3') // 从localStorage读取当前分类默认sm3
const getInitialTab = () => {
try {
const savedTab = localStorage.getItem('activeTab')
if (['sm2', 'sm3', 'cert'].includes(savedTab || '')) {
return savedTab || 'sm3'
}
} catch (error) {
console.error('Failed to read from localStorage:', error)
}
return 'sm3'
}
const [activeTab, setActiveTab] = useState(getInitialTab())
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [mobileTab, setMobileTab] = useState('sm3') const [mobileTab, setMobileTab] = useState(getInitialTab())
// 检测是否为移动设备 // 检测是否为移动设备
useEffect(() => { useEffect(() => {
@ -28,13 +41,27 @@ function App() {
// 处理移动设备的 tab 切换 // 处理移动设备的 tab 切换
const handleMobileTabChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handleMobileTabChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setMobileTab(e.target.value) const tab = e.target.value
setActiveTab(e.target.value) setMobileTab(tab)
setActiveTab(tab)
// 保存到localStorage
try {
localStorage.setItem('activeTab', tab)
} catch (error) {
console.error('Failed to save to localStorage:', error)
}
} }
// 处理桌面端的 tab 切换 // 处理桌面端的 tab 切换
const handleTabChange = (tab: string) => { const handleTabChange = (tab: string) => {
setActiveTab(tab) setActiveTab(tab)
setMobileTab(tab)
// 保存到localStorage
try {
localStorage.setItem('activeTab', tab)
} catch (error) {
console.error('Failed to save to localStorage:', error)
}
} }
@ -162,12 +189,19 @@ function App() {
const reader = new FileReader() const reader = new FileReader()
reader.onload = (event) => { reader.onload = (event) => {
const result = event.target?.result as ArrayBuffer const result = event.target?.result
if (result) { if (result) {
const base64Cert = parseCertFromFile(result) try {
handleParseCert(base64Cert) const base64Cert = parseCertFromFile(result as ArrayBuffer)
handleParseCert(base64Cert)
} catch (error) {
alert('证书读取失败: ' + (error instanceof Error ? error.message : String(error)))
}
} }
} }
reader.onerror = () => {
alert('证书文件读取失败')
}
reader.readAsArrayBuffer(file) reader.readAsArrayBuffer(file)
} }
@ -1033,68 +1067,34 @@ function App() {
{/* 转换工具面板 */} {/* 转换工具面板 */}
{activeTab === 'convert' && ( {activeTab === 'convert' && (
<div className="panel" style={{ backgroundColor: '#1e1e1e', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.3)' }}> <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' }}>Hex / UTF8 / Base64 </h2>
<div style={{ display: 'flex', gap: '1rem', marginBottom: '1rem', flexDirection: isMobile ? 'column' : 'row' }}> {/* Hex / UTF8 互转卡片 */}
<div className="form-group" style={{ flex: 1 }}> <div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>UTF8 </label> <h3 style={{ color: '#e0e0e0', marginBottom: '15px' }}>Hex / UTF8 </h3>
<textarea <div style={{ display: 'flex', gap: '1rem', flexDirection: isMobile ? 'column' : 'row' }}>
value={utf8Input} <div className="form-group" style={{ flex: 1 }}>
onChange={(e) => setUtf8Input(e.target.value)} <label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>UTF8 </label>
placeholder="输入 UTF8 字符串" <textarea
style={{ value={utf8Input}
height: '200px', onChange={(e) => setUtf8Input(e.target.value)}
width: '100%', placeholder="输入 UTF8 字符串"
padding: '10px', style={{
border: '1px solid #333', height: '150px',
borderRadius: '4px', width: '100%',
backgroundColor: '#2d2d2d', padding: '10px',
color: '#e0e0e0', border: '1px solid #333',
fontSize: '14px', borderRadius: '4px',
resize: 'vertical' backgroundColor: '#1e1e1e',
}} color: '#e0e0e0',
/> fontSize: '14px',
<button resize: 'vertical'
onClick={handleUtf8ToHex} }}
style={{ />
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
UTF8 Hex
</button>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>Hex </label>
<textarea
value={hexInput}
onChange={(e) => setHexInput(e.target.value)}
placeholder="输入 hex 字符串"
style={{
height: '200px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#2d2d2d',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<div style={{ display: 'flex', gap: '10px', marginTop: '0.5rem' }}>
<button <button
onClick={handleHexToUtf8} onClick={handleUtf8ToHex}
style={{ style={{
marginTop: '0.5rem',
padding: '8px 16px', padding: '8px 16px',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
@ -1103,15 +1103,78 @@ function App() {
fontSize: '14px', fontSize: '14px',
fontWeight: 'bold', fontWeight: 'bold',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.3s ease', transition: 'background 0.3s ease'
flex: 1 }}
>
UTF8 Hex
</button>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>Hex </label>
<textarea
value={hexInput}
onChange={(e) => setHexInput(e.target.value)}
placeholder="输入 hex 字符串"
style={{
height: '150px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#1e1e1e',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button
onClick={handleHexToUtf8}
style={{
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}} }}
> >
UTF8 Hex UTF8 Hex
</button> </button>
</div>
</div>
</div>
{/* Hex / Base64 互转卡片 */}
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px' }}>
<h3 style={{ color: '#e0e0e0', marginBottom: '15px' }}>Hex / Base64 </h3>
<div style={{ display: 'flex', gap: '1rem', flexDirection: isMobile ? 'column' : 'row' }}>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>Base64 </label>
<textarea
value={base64Input}
onChange={(e) => setBase64Input(e.target.value)}
placeholder="输入 Base64 字符串"
style={{
height: '150px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#1e1e1e',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button <button
onClick={handleHexToBase64} onClick={handleBase64ToHex}
style={{ style={{
marginTop: '0.5rem',
padding: '8px 16px', padding: '8px 16px',
border: 'none', border: 'none',
borderRadius: '4px', borderRadius: '4px',
@ -1120,51 +1183,50 @@ function App() {
fontSize: '14px', fontSize: '14px',
fontWeight: 'bold', fontWeight: 'bold',
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.3s ease', transition: 'background 0.3s ease'
flex: 1 }}
>
Hex Base64
</button>
</div>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>Hex </label>
<textarea
value={hexInput}
onChange={(e) => setHexInput(e.target.value)}
placeholder="输入 hex 字符串"
style={{
height: '150px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#1e1e1e',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button
onClick={handleHexToBase64}
style={{
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}} }}
> >
Base64 Hex Base64 Hex
</button> </button>
</div> </div>
</div> </div>
<div className="form-group" style={{ flex: 1 }}>
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>Base64 </label>
<textarea
value={base64Input}
onChange={(e) => setBase64Input(e.target.value)}
placeholder="输入 Base64 字符串"
style={{
height: '200px',
width: '100%',
padding: '10px',
border: '1px solid #333',
borderRadius: '4px',
backgroundColor: '#2d2d2d',
color: '#e0e0e0',
fontSize: '14px',
resize: 'vertical'
}}
/>
<button
onClick={handleBase64ToHex}
style={{
marginTop: '0.5rem',
padding: '8px 16px',
border: 'none',
borderRadius: '4px',
background: '#f59e0b',
color: '#121212',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}
>
Hex Base64
</button>
</div>
</div> </div>
</div> </div>
)} )}
@ -1172,23 +1234,41 @@ function App() {
{/* 证书工具面板 */} {/* 证书工具面板 */}
{activeTab === 'cert' && ( {activeTab === 'cert' && (
<div className="panel" style={{ backgroundColor: '#1e1e1e', padding: '20px', borderRadius: '8px', boxShadow: '0 2px 4px rgba(0, 0, 0, 0.3)' }}> <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> <h2 style={{ color: '#f59e0b', marginBottom: '20px' }}>RSA </h2>
<div className="form-group" style={{ marginBottom: '20px' }}> <div className="form-group" style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}></label> <label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}></label>
<input <div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
type="file" <input
accept=".cer,.crt,.pem" type="file"
onChange={handleCertFileUpload} accept=".cer,.crt,.pem"
style={{ onChange={handleCertFileUpload}
padding: '10px', style={{
position: 'absolute',
opacity: 0,
width: '100%',
height: '100%',
cursor: 'pointer'
}}
/>
<div style={{
padding: '12px 20px',
border: '1px solid #333', border: '1px solid #333',
borderRadius: '4px', borderRadius: '4px',
backgroundColor: '#2d2d2d', backgroundColor: '#2d2d2d',
color: '#e0e0e0', color: '#f59e0b',
cursor: 'pointer' fontSize: '16px',
}} fontWeight: 'bold',
/> textAlign: 'center',
cursor: 'pointer',
transition: 'background 0.3s ease'
}}>
</div>
</div>
<div style={{ marginTop: '10px', color: '#888', fontSize: '14px' }}>
.cer, .crt, .pem
</div>
</div> </div>
{certInfo && ( {certInfo && (

View File

@ -1,14 +1,28 @@
import forge from 'node-forge' import forge from 'node-forge'
// 将ArrayBuffer转换为Base64字符串浏览器兼容
const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
}
// 解析SM2证书信息 // 解析SM2证书信息
export const parseSm2Cert = (certData: string): any => { export const parseSm2Cert = (certData: string): any => {
try { try {
// 移除证书中的换行符和空格 // 移除证书中的换行符和空格
const cleanCert = certData.replace(/\s/g, '') const cleanCert = certData.replace(/\s/g, '')
// 解析DER编码的证书 // 解析DER编码的证书浏览器兼容
const derBuffer = Buffer.from(cleanCert, 'base64') const binary = atob(cleanCert)
const asn1 = forge.asn1.fromDer(derBuffer.toString('binary')) const derBuffer = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
derBuffer[i] = binary.charCodeAt(i)
}
const asn1 = forge.asn1.fromDer(String.fromCharCode(...derBuffer))
const cert = forge.pki.certificateFromAsn1(asn1) const cert = forge.pki.certificateFromAsn1(asn1)
// 提取证书信息 // 提取证书信息
@ -27,16 +41,20 @@ export const parseSm2Cert = (certData: string): any => {
}) })
// 提取公钥信息 // 提取公钥信息
const publicKey = cert.publicKey
let publicKeyHex = '' let publicKeyHex = ''
// 尝试获取公钥的十六进制表示 // 尝试获取公钥的十六进制表示
try { try {
if (typeof publicKey === 'object' && publicKey !== null) { if (cert.publicKey) {
// 尝试将公钥转换为PEM格式然后提取十六进制 // 尝试将公钥转换为PEM格式然后提取十六进制
const pem = forge.pki.publicKeyToPem(publicKey) try {
// 从PEM中提取十六进制 const pem = forge.pki.publicKeyToPem(cert.publicKey)
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '') // 从PEM中提取十六进制
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '')
} catch (e) {
// 如果无法转换为PEM使用空字符串
publicKeyHex = ''
}
} }
} catch (e) { } catch (e) {
// 如果转换失败,使用空字符串 // 如果转换失败,使用空字符串
@ -69,6 +87,5 @@ export const parseSm2Cert = (certData: string): any => {
// 从文件内容解析证书 // 从文件内容解析证书
export const parseCertFromFile = (fileContent: ArrayBuffer): string => { export const parseCertFromFile = (fileContent: ArrayBuffer): string => {
const buffer = Buffer.from(fileContent) return arrayBufferToBase64(fileContent)
return buffer.toString('base64')
} }

View File

@ -1 +1 @@
export const VERSION = "V1.0-20260418071243"; export const VERSION = "V1.0-20260418075022";