This commit is contained in:
parent
88acc24e86
commit
f4228370d9
320
src/App.tsx
320
src/App.tsx
@ -8,9 +8,22 @@ import { parseSm2Cert, parseCertFromFile } from './utils/algorithm/cert'
|
||||
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
||||
|
||||
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 [mobileTab, setMobileTab] = useState('sm3')
|
||||
const [mobileTab, setMobileTab] = useState(getInitialTab())
|
||||
|
||||
// 检测是否为移动设备
|
||||
useEffect(() => {
|
||||
@ -28,13 +41,27 @@ function App() {
|
||||
|
||||
// 处理移动设备的 tab 切换
|
||||
const handleMobileTabChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
setMobileTab(e.target.value)
|
||||
setActiveTab(e.target.value)
|
||||
const tab = e.target.value
|
||||
setMobileTab(tab)
|
||||
setActiveTab(tab)
|
||||
// 保存到localStorage
|
||||
try {
|
||||
localStorage.setItem('activeTab', tab)
|
||||
} catch (error) {
|
||||
console.error('Failed to save to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理桌面端的 tab 切换
|
||||
const handleTabChange = (tab: string) => {
|
||||
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()
|
||||
reader.onload = (event) => {
|
||||
const result = event.target?.result as ArrayBuffer
|
||||
const result = event.target?.result
|
||||
if (result) {
|
||||
const base64Cert = parseCertFromFile(result)
|
||||
handleParseCert(base64Cert)
|
||||
try {
|
||||
const base64Cert = parseCertFromFile(result as ArrayBuffer)
|
||||
handleParseCert(base64Cert)
|
||||
} catch (error) {
|
||||
alert('证书读取失败: ' + (error instanceof Error ? error.message : String(error)))
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.onerror = () => {
|
||||
alert('证书文件读取失败')
|
||||
}
|
||||
reader.readAsArrayBuffer(file)
|
||||
}
|
||||
|
||||
@ -1033,68 +1067,34 @@ function App() {
|
||||
{/* 转换工具面板 */}
|
||||
{activeTab === 'convert' && (
|
||||
<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' }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>UTF8 输入</label>
|
||||
<textarea
|
||||
value={utf8Input}
|
||||
onChange={(e) => setUtf8Input(e.target.value)}
|
||||
placeholder="输入 UTF8 字符串"
|
||||
style={{
|
||||
height: '200px',
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
color: '#e0e0e0',
|
||||
fontSize: '14px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
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
|
||||
onClick={handleHexToUtf8}
|
||||
{/* Hex / UTF8 互转卡片 */}
|
||||
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginBottom: '20px' }}>
|
||||
<h3 style={{ color: '#e0e0e0', marginBottom: '15px' }}>Hex / UTF8 互转</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' }}>UTF8 输入</label>
|
||||
<textarea
|
||||
value={utf8Input}
|
||||
onChange={(e) => setUtf8Input(e.target.value)}
|
||||
placeholder="输入 UTF8 字符串"
|
||||
style={{
|
||||
height: '150px',
|
||||
width: '100%',
|
||||
padding: '10px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#1e1e1e',
|
||||
color: '#e0e0e0',
|
||||
fontSize: '14px',
|
||||
resize: 'vertical'
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleUtf8ToHex}
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@ -1103,15 +1103,78 @@ function App() {
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.3s ease',
|
||||
flex: 1
|
||||
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: '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
|
||||
</button>
|
||||
<button
|
||||
onClick={handleHexToBase64}
|
||||
</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
|
||||
onClick={handleBase64ToHex}
|
||||
style={{
|
||||
marginTop: '0.5rem',
|
||||
padding: '8px 16px',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@ -1120,51 +1183,50 @@ function App() {
|
||||
fontSize: '14px',
|
||||
fontWeight: 'bold',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.3s ease',
|
||||
flex: 1
|
||||
transition: 'background 0.3s ease'
|
||||
}}
|
||||
>
|
||||
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
|
||||
</button>
|
||||
</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>
|
||||
)}
|
||||
@ -1172,23 +1234,41 @@ function App() {
|
||||
{/* 证书工具面板 */}
|
||||
{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>
|
||||
<h2 style={{ color: '#f59e0b', marginBottom: '20px' }}>RSA 证书解析</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',
|
||||
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".cer,.crt,.pem"
|
||||
onChange={handleCertFileUpload}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
opacity: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
<div style={{
|
||||
padding: '12px 20px',
|
||||
border: '1px solid #333',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: '#2d2d2d',
|
||||
color: '#e0e0e0',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
/>
|
||||
color: '#f59e0b',
|
||||
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>
|
||||
|
||||
{certInfo && (
|
||||
|
||||
@ -1,14 +1,28 @@
|
||||
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证书信息
|
||||
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'))
|
||||
// 解析DER编码的证书(浏览器兼容)
|
||||
const binary = atob(cleanCert)
|
||||
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)
|
||||
|
||||
// 提取证书信息
|
||||
@ -27,16 +41,20 @@ export const parseSm2Cert = (certData: string): any => {
|
||||
})
|
||||
|
||||
// 提取公钥信息
|
||||
const publicKey = cert.publicKey
|
||||
let publicKeyHex = ''
|
||||
|
||||
// 尝试获取公钥的十六进制表示
|
||||
try {
|
||||
if (typeof publicKey === 'object' && publicKey !== null) {
|
||||
if (cert.publicKey) {
|
||||
// 尝试将公钥转换为PEM格式,然后提取十六进制
|
||||
const pem = forge.pki.publicKeyToPem(publicKey)
|
||||
// 从PEM中提取十六进制
|
||||
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '')
|
||||
try {
|
||||
const pem = forge.pki.publicKeyToPem(cert.publicKey)
|
||||
// 从PEM中提取十六进制
|
||||
publicKeyHex = pem.replace(/-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s/g, '')
|
||||
} catch (e) {
|
||||
// 如果无法转换为PEM,使用空字符串
|
||||
publicKeyHex = ''
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果转换失败,使用空字符串
|
||||
@ -69,6 +87,5 @@ export const parseSm2Cert = (certData: string): any => {
|
||||
|
||||
// 从文件内容解析证书
|
||||
export const parseCertFromFile = (fileContent: ArrayBuffer): string => {
|
||||
const buffer = Buffer.from(fileContent)
|
||||
return buffer.toString('base64')
|
||||
return arrayBufferToBase64(fileContent)
|
||||
}
|
||||
|
||||
@ -1 +1 @@
|
||||
export const VERSION = "V1.0-20260418071243";
|
||||
export const VERSION = "V1.0-20260418075022";
|
||||
Loading…
Reference in New Issue
Block a user