Compare commits
3
Commits
f4228370d9
...
32626c84a6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32626c84a6 | ||
|
|
58533aa5d0 | ||
|
|
3459a389ac |
@@ -41,3 +41,6 @@ out
|
|||||||
# Temporary files
|
# Temporary files
|
||||||
*.tmp
|
*.tmp
|
||||||
*.temp
|
*.temp
|
||||||
|
|
||||||
|
|
||||||
|
certs
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
设计规范
|
||||||
|
|
||||||
|
- 所有输入框,双击后弹出提示框,确认后可以清空内容。移动端为长按弹出提示框,确认后可以清空内容。
|
||||||
|
|
||||||
+503
-78
@@ -4,7 +4,7 @@ import { VERSION } from './version'
|
|||||||
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
|
import { sm3Digest, sm3Hmac } from './utils/algorithm/sm3'
|
||||||
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
|
import { sm4Encrypt, sm4Decrypt } from './utils/algorithm/sm4'
|
||||||
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify } from './utils/algorithm/sm2'
|
import { sm2GenerateKeyPair, sm2GetPublicKeyFromPrivateKey, sm2Encrypt, sm2Decrypt, sm2Sign, sm2Verify } from './utils/algorithm/sm2'
|
||||||
import { parseSm2Cert, parseCertFromFile } from './utils/algorithm/cert'
|
import { parseSm2Cert, parseCertFromFile, generateRootCert, generateAppCert } from './utils/algorithm/cert'
|
||||||
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
import { hexToUtf8, utf8ToHex, hexToBase64, base64ToHex } from './utils/convert'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -25,6 +25,15 @@ function App() {
|
|||||||
const [isMobile, setIsMobile] = useState(false)
|
const [isMobile, setIsMobile] = useState(false)
|
||||||
const [mobileTab, setMobileTab] = useState(getInitialTab())
|
const [mobileTab, setMobileTab] = useState(getInitialTab())
|
||||||
|
|
||||||
|
// 功能菜单状态
|
||||||
|
const [menuVisible, setMenuVisible] = useState(false)
|
||||||
|
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 })
|
||||||
|
const [currentInput, setCurrentInput] = useState<{
|
||||||
|
value: string;
|
||||||
|
setter: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
readOnly: boolean
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// 检测是否为移动设备
|
// 检测是否为移动设备
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
@@ -39,6 +48,73 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// 点击外部关闭菜单
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = () => {
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (menuVisible) {
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [menuVisible])
|
||||||
|
|
||||||
|
// 打开功能菜单
|
||||||
|
const openMenu = (e: React.MouseEvent | React.TouchEvent, value: string, setter: React.Dispatch<React.SetStateAction<string>>, readOnly: boolean) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
let x = 0
|
||||||
|
let y = 0
|
||||||
|
|
||||||
|
if ('clientX' in e) {
|
||||||
|
// 鼠标事件
|
||||||
|
x = e.clientX
|
||||||
|
y = e.clientY
|
||||||
|
} else if ('touches' in e && e.touches.length > 0) {
|
||||||
|
// 触摸事件
|
||||||
|
x = e.touches[0].clientX
|
||||||
|
y = e.touches[0].clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
setMenuPosition({ x, y })
|
||||||
|
setCurrentInput({ value, setter, readOnly })
|
||||||
|
setMenuVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空输入
|
||||||
|
const handleClear = () => {
|
||||||
|
if (currentInput) {
|
||||||
|
currentInput.setter('')
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制全部
|
||||||
|
const handleCopyAll = () => {
|
||||||
|
if (currentInput && currentInput.value) {
|
||||||
|
navigator.clipboard.writeText(currentInput.value).catch(err => {
|
||||||
|
console.error('复制失败:', err)
|
||||||
|
})
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粘贴覆盖
|
||||||
|
const handlePaste = () => {
|
||||||
|
if (currentInput && !currentInput.readOnly) {
|
||||||
|
navigator.clipboard.readText().then(text => {
|
||||||
|
currentInput.setter(text)
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('粘贴失败:', err)
|
||||||
|
})
|
||||||
|
setMenuVisible(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理移动设备的 tab 切换
|
// 处理移动设备的 tab 切换
|
||||||
const handleMobileTabChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleMobileTabChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
const tab = e.target.value
|
const tab = e.target.value
|
||||||
@@ -99,6 +175,10 @@ function App() {
|
|||||||
|
|
||||||
// 证书工具状态
|
// 证书工具状态
|
||||||
const [certInfo, setCertInfo] = useState<any>(null)
|
const [certInfo, setCertInfo] = useState<any>(null)
|
||||||
|
const [rootCert, setRootCert] = useState<string>('')
|
||||||
|
const [appCert, setAppCert] = useState<string>('')
|
||||||
|
const [commonName, setCommonName] = useState<string>('test.example.com')
|
||||||
|
const [certError, setCertError] = useState<string>('')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -216,6 +296,28 @@ function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理生成根证书
|
||||||
|
const handleGenerateRootCert = () => {
|
||||||
|
try {
|
||||||
|
const cert = generateRootCert()
|
||||||
|
setRootCert(cert)
|
||||||
|
setCertError('')
|
||||||
|
} catch (error) {
|
||||||
|
setCertError((error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理生成应用证书
|
||||||
|
const handleGenerateAppCert = () => {
|
||||||
|
try {
|
||||||
|
const cert = generateAppCert(commonName)
|
||||||
|
setAppCert(cert)
|
||||||
|
setCertError('')
|
||||||
|
} catch (error) {
|
||||||
|
setCertError((error as Error).message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SM2 生成密钥对
|
// SM2 生成密钥对
|
||||||
const handleGenerateSm2KeyPair = () => {
|
const handleGenerateSm2KeyPair = () => {
|
||||||
try {
|
try {
|
||||||
@@ -471,6 +573,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2PrivateKey}
|
value={sm2PrivateKey}
|
||||||
onChange={(e) => setSm2PrivateKey(e.target.value)}
|
onChange={(e) => setSm2PrivateKey(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2PrivateKey, setSm2PrivateKey, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2PrivateKey, setSm2PrivateKey, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入私钥 (hex) 或点击生成密钥对"
|
placeholder="输入私钥 (hex) 或点击生成密钥对"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -490,6 +599,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2PublicKey}
|
value={sm2PublicKey}
|
||||||
onChange={(e) => setSm2PublicKey(e.target.value)}
|
onChange={(e) => setSm2PublicKey(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2PublicKey, setSm2PublicKey, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2PublicKey, setSm2PublicKey, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入公钥 (hex) 或点击生成密钥对"
|
placeholder="输入公钥 (hex) 或点击生成密钥对"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -556,6 +672,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2Message}
|
value={sm2Message}
|
||||||
onChange={(e) => setSm2Message(e.target.value)}
|
onChange={(e) => setSm2Message(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2Message, setSm2Message, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2Message, setSm2Message, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入要加密的消息 (hex)"
|
placeholder="输入要加密的消息 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -575,6 +698,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2Encrypted}
|
value={sm2Encrypted}
|
||||||
onChange={(e) => setSm2Encrypted(e.target.value)}
|
onChange={(e) => setSm2Encrypted(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2Encrypted, setSm2Encrypted, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2Encrypted, setSm2Encrypted, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入加密结果 (hex)"
|
placeholder="输入加密结果 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -594,6 +724,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2Decrypted}
|
value={sm2Decrypted}
|
||||||
readOnly
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2Decrypted, setSm2Decrypted, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2Decrypted, setSm2Decrypted, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="解密结果将显示在这里"
|
placeholder="解密结果将显示在这里"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -656,6 +793,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2SignMessage}
|
value={sm2SignMessage}
|
||||||
onChange={(e) => setSm2SignMessage(e.target.value)}
|
onChange={(e) => setSm2SignMessage(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2SignMessage, setSm2SignMessage, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2SignMessage, setSm2SignMessage, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入要签名的消息 (hex)"
|
placeholder="输入要签名的消息 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -675,6 +819,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm2Signature}
|
value={sm2Signature}
|
||||||
onChange={(e) => setSm2Signature(e.target.value)}
|
onChange={(e) => setSm2Signature(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm2Signature, setSm2Signature, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm2Signature, setSm2Signature, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入签名 (hex)"
|
placeholder="输入签名 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -760,6 +911,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm3Message}
|
value={sm3Message}
|
||||||
onChange={(e) => setSm3Message(e.target.value)}
|
onChange={(e) => setSm3Message(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm3Message, setSm3Message, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm3Message, setSm3Message, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入要计算的消息"
|
placeholder="输入要计算的消息"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -780,6 +938,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm3Result}
|
value={sm3Result}
|
||||||
readOnly
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, sm3Result, setSm3Result, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm3Result, setSm3Result, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="SM3 计算结果将显示在这里"
|
placeholder="SM3 计算结果将显示在这里"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -818,6 +983,13 @@ function App() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={sm3HmacKey}
|
value={sm3HmacKey}
|
||||||
onChange={(e) => setSm3HmacKey(e.target.value)}
|
onChange={(e) => setSm3HmacKey(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm3HmacKey, setSm3HmacKey, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm3HmacKey, setSm3HmacKey, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 HMAC 密钥 (hex)"
|
placeholder="输入 HMAC 密钥 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -836,6 +1008,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm3HmacResult}
|
value={sm3HmacResult}
|
||||||
readOnly
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, sm3HmacResult, setSm3HmacResult, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm3HmacResult, setSm3HmacResult, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="SM3 HMAC 计算结果将显示在这里"
|
placeholder="SM3 HMAC 计算结果将显示在这里"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -881,6 +1060,13 @@ function App() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={sm4Key}
|
value={sm4Key}
|
||||||
onChange={(e) => setSm4Key(e.target.value)}
|
onChange={(e) => setSm4Key(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm4Key, setSm4Key, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm4Key, setSm4Key, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 16 字节密钥 (32 位 hex)"
|
placeholder="输入 16 字节密钥 (32 位 hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -900,6 +1086,13 @@ function App() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={sm4Iv}
|
value={sm4Iv}
|
||||||
onChange={(e) => setSm4Iv(e.target.value)}
|
onChange={(e) => setSm4Iv(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm4Iv, setSm4Iv, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm4Iv, setSm4Iv, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 16 字节 IV (32 位 hex)"
|
placeholder="输入 16 字节 IV (32 位 hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -970,6 +1163,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm4Message}
|
value={sm4Message}
|
||||||
onChange={(e) => setSm4Message(e.target.value)}
|
onChange={(e) => setSm4Message(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm4Message, setSm4Message, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm4Message, setSm4Message, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入要加密的消息"
|
placeholder="输入要加密的消息"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -990,6 +1190,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm4Encrypted}
|
value={sm4Encrypted}
|
||||||
onChange={(e) => setSm4Encrypted(e.target.value)}
|
onChange={(e) => setSm4Encrypted(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, sm4Encrypted, setSm4Encrypted, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm4Encrypted, setSm4Encrypted, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入加密结果 (hex)"
|
placeholder="输入加密结果 (hex)"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1010,6 +1217,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={sm4Decrypted}
|
value={sm4Decrypted}
|
||||||
readOnly
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, sm4Decrypted, setSm4Decrypted, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, sm4Decrypted, setSm4Decrypted, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="解密结果将显示在这里"
|
placeholder="解密结果将显示在这里"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -1078,6 +1292,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={utf8Input}
|
value={utf8Input}
|
||||||
onChange={(e) => setUtf8Input(e.target.value)}
|
onChange={(e) => setUtf8Input(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, utf8Input, setUtf8Input, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, utf8Input, setUtf8Input, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 UTF8 字符串"
|
placeholder="输入 UTF8 字符串"
|
||||||
style={{
|
style={{
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -1115,6 +1336,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={hexInput}
|
value={hexInput}
|
||||||
onChange={(e) => setHexInput(e.target.value)}
|
onChange={(e) => setHexInput(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, hexInput, setHexInput, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, hexInput, setHexInput, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 hex 字符串"
|
placeholder="输入 hex 字符串"
|
||||||
style={{
|
style={{
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -1158,6 +1386,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={base64Input}
|
value={base64Input}
|
||||||
onChange={(e) => setBase64Input(e.target.value)}
|
onChange={(e) => setBase64Input(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, base64Input, setBase64Input, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, base64Input, setBase64Input, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 Base64 字符串"
|
placeholder="输入 Base64 字符串"
|
||||||
style={{
|
style={{
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -1195,6 +1430,13 @@ function App() {
|
|||||||
<textarea
|
<textarea
|
||||||
value={hexInput}
|
value={hexInput}
|
||||||
onChange={(e) => setHexInput(e.target.value)}
|
onChange={(e) => setHexInput(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, hexInput, setHexInput, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, hexInput, setHexInput, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
placeholder="输入 hex 字符串"
|
placeholder="输入 hex 字符串"
|
||||||
style={{
|
style={{
|
||||||
height: '150px',
|
height: '150px',
|
||||||
@@ -1234,100 +1476,283 @@ 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' }}>RSA 证书解析</h2>
|
<h2 style={{ color: '#f59e0b', marginBottom: '20px' }}>证书工具</h2>
|
||||||
|
|
||||||
<div className="form-group" style={{ marginBottom: '20px' }}>
|
{certError && (
|
||||||
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
|
<div style={{ backgroundColor: '#7f1d1d', padding: '10px', borderRadius: '4px', marginBottom: '20px', color: '#fecaca' }}>
|
||||||
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
{certError}
|
||||||
<input
|
</div>
|
||||||
type="file"
|
)}
|
||||||
accept=".cer,.crt,.pem"
|
|
||||||
onChange={handleCertFileUpload}
|
{/* 生成根证书 */}
|
||||||
style={{
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginBottom: '20px' }}>
|
||||||
position: 'absolute',
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>1. 生成根证书</h3>
|
||||||
opacity: 0,
|
<button
|
||||||
width: '100%',
|
onClick={handleGenerateRootCert}
|
||||||
height: '100%',
|
style={{
|
||||||
cursor: 'pointer'
|
padding: '10px 20px',
|
||||||
}}
|
border: 'none',
|
||||||
/>
|
|
||||||
<div style={{
|
|
||||||
padding: '12px 20px',
|
|
||||||
border: '1px solid #333',
|
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
backgroundColor: '#2d2d2d',
|
background: '#f59e0b',
|
||||||
color: '#f59e0b',
|
color: '#121212',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textAlign: 'center',
|
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
transition: 'background 0.3s ease'
|
transition: 'background 0.3s ease'
|
||||||
}}>
|
}}
|
||||||
选择证书文件
|
>
|
||||||
|
生成根证书
|
||||||
|
</button>
|
||||||
|
{rootCert && (
|
||||||
|
<div style={{ marginTop: '15px' }}>
|
||||||
|
<h4 style={{ color: '#e0e0e0', marginBottom: '10px' }}>根证书内容:</h4>
|
||||||
|
<textarea
|
||||||
|
value={rootCert}
|
||||||
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, rootCert, setRootCert, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, rootCert, setRootCert, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: '#3d3d3d',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: '14px',
|
||||||
|
resize: 'vertical',
|
||||||
|
minHeight: '150px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div style={{ marginTop: '10px', color: '#888', fontSize: '14px' }}>
|
|
||||||
支持 .cer, .crt, .pem 格式
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{certInfo && (
|
{/* 生成应用证书 */}
|
||||||
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px', marginBottom: '20px' }}>
|
||||||
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>证书信息</h3>
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>2. 生成应用证书</h3>
|
||||||
|
<div className="form-group" style={{ marginBottom: '15px' }}>
|
||||||
<div style={{ marginBottom: '10px' }}>
|
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0' }}>通用名称:</label>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>版本:</label>
|
<input
|
||||||
<div style={{ color: '#e0e0e0' }}>{certInfo.version}</div>
|
type="text"
|
||||||
|
value={commonName}
|
||||||
|
onChange={(e) => setCommonName(e.target.value)}
|
||||||
|
onContextMenu={(e) => openMenu(e, commonName, setCommonName, false)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, commonName, setCommonName, false)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
|
placeholder="输入应用证书的通用名称"
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: '#3d3d3d',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: '14px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleGenerateAppCert}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
background: '#f59e0b',
|
||||||
|
color: '#121212',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s ease'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
生成应用证书
|
||||||
|
</button>
|
||||||
|
{appCert && (
|
||||||
|
<div style={{ marginTop: '15px' }}>
|
||||||
|
<h4 style={{ color: '#e0e0e0', marginBottom: '10px' }}>应用证书内容:</h4>
|
||||||
|
<textarea
|
||||||
|
value={appCert}
|
||||||
|
readOnly
|
||||||
|
onContextMenu={(e) => openMenu(e, appCert, setAppCert, true)}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
const touchTimer = setTimeout(() => {
|
||||||
|
openMenu(e, appCert, setAppCert, true)
|
||||||
|
}, 500)
|
||||||
|
return () => clearTimeout(touchTimer)
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
padding: '10px',
|
||||||
|
border: '1px solid #333',
|
||||||
|
borderRadius: '4px',
|
||||||
|
backgroundColor: '#3d3d3d',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
fontSize: '14px',
|
||||||
|
resize: 'vertical',
|
||||||
|
minHeight: '150px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '10px' }}>
|
{/* 证书解析 */}
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>序列号:</label>
|
<div style={{ backgroundColor: '#2d2d2d', padding: '15px', borderRadius: '8px' }}>
|
||||||
<div style={{ color: '#e0e0e0', wordBreak: 'break-all' }}>{certInfo.serialNumber}</div>
|
<h3 style={{ color: '#f59e0b', marginBottom: '15px' }}>3. 证书解析</h3>
|
||||||
</div>
|
<div className="form-group" style={{ marginBottom: '20px' }}>
|
||||||
|
<label style={{ display: 'block', marginBottom: '10px', color: '#e0e0e0' }}>上传证书文件</label>
|
||||||
<div style={{ marginBottom: '10px' }}>
|
<div style={{ position: 'relative', display: 'inline-block', width: '100%' }}>
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>所有者:</label>
|
<input
|
||||||
<div style={{ color: '#e0e0e0' }}>
|
type="file"
|
||||||
{Object.entries(certInfo.subject).map(([key, value]) => (
|
accept=".cer,.crt,.pem"
|
||||||
<div key={key}>{key}: {String(value)}</div>
|
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: '#3d3d3d',
|
||||||
|
color: '#f59e0b',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
textAlign: 'center',
|
||||||
|
cursor: 'pointer',
|
||||||
|
transition: 'background 0.3s ease'
|
||||||
|
}}>
|
||||||
|
选择证书文件
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ marginTop: '10px', color: '#888', fontSize: '14px' }}>
|
||||||
|
支持 .cer, .crt, .pem 格式
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginBottom: '10px' }}>
|
{certInfo && (
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>签发者:</label>
|
<div style={{ backgroundColor: '#3d3d3d', padding: '15px', borderRadius: '8px', marginTop: '20px' }}>
|
||||||
<div style={{ color: '#e0e0e0' }}>
|
<h4 style={{ color: '#f59e0b', marginBottom: '15px' }}>证书信息</h4>
|
||||||
{Object.entries(certInfo.issuer).map(([key, value]) => (
|
|
||||||
<div key={key}>{key}: {String(value)}</div>
|
<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>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div style={{ marginBottom: '10px' }}>
|
{/* 功能菜单 */}
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>有效期:</label>
|
{menuVisible && currentInput && (
|
||||||
<div style={{ color: '#e0e0e0' }}>
|
<div
|
||||||
<div>开始: {certInfo.notBefore}</div>
|
style={{
|
||||||
<div>结束: {certInfo.notAfter}</div>
|
position: 'fixed',
|
||||||
</div>
|
left: menuPosition.x,
|
||||||
</div>
|
top: menuPosition.y,
|
||||||
|
backgroundColor: '#2d2d2d',
|
||||||
<div style={{ marginBottom: '10px' }}>
|
border: '1px solid #333',
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>公钥:</label>
|
borderRadius: '4px',
|
||||||
<div style={{ color: '#e0e0e0', wordBreak: 'break-all' }}>{certInfo.publicKey}</div>
|
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.3)',
|
||||||
</div>
|
zIndex: 1000,
|
||||||
|
minWidth: '150px'
|
||||||
<div style={{ marginBottom: '10px' }}>
|
}}
|
||||||
<label style={{ display: 'block', marginBottom: '5px', color: '#e0e0e0', fontWeight: 'bold' }}>用途:</label>
|
>
|
||||||
<div style={{ color: '#e0e0e0' }}>
|
<div
|
||||||
{certInfo.keyUsage.length > 0 ? (
|
style={{
|
||||||
certInfo.keyUsage.map((usage: string, index: number) => (
|
padding: '8px 12px',
|
||||||
<div key={index}>{usage}</div>
|
cursor: 'pointer',
|
||||||
))
|
color: '#e0e0e0',
|
||||||
) : (
|
borderBottom: '1px solid #333'
|
||||||
<div>无</div>
|
}}
|
||||||
)}
|
onClick={handleClear}
|
||||||
</div>
|
>
|
||||||
</div>
|
清空
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#e0e0e0',
|
||||||
|
borderBottom: '1px solid #333'
|
||||||
|
}}
|
||||||
|
onClick={handleCopyAll}
|
||||||
|
>
|
||||||
|
复制全部
|
||||||
|
</div>
|
||||||
|
{!currentInput.readOnly && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: '8px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: '#e0e0e0'
|
||||||
|
}}
|
||||||
|
onClick={handlePaste}
|
||||||
|
>
|
||||||
|
粘贴覆盖
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sm2GenerateKeyPair, sm2Encrypt, sm2Decrypt } from '../../utils/algorithm/sm2'
|
import { sm2GenerateKeyPair, sm2Encrypt, sm2Decrypt, sm2C1C3C2ToDer, sm2DerToC1C3C2 } from '../../utils/algorithm/sm2'
|
||||||
|
|
||||||
// UTF8 转 Hex
|
// UTF8 转 Hex
|
||||||
const utf8ToHex = (utf8: string): string => {
|
const utf8ToHex = (utf8: string): string => {
|
||||||
@@ -37,4 +37,48 @@ describe('SM2 算法测试', () => {
|
|||||||
// console.log('Decrypted Message:', decryptedMessage)
|
// console.log('Decrypted Message:', decryptedMessage)
|
||||||
// expect(decryptedMessage).toBe(originalMessage)
|
// expect(decryptedMessage).toBe(originalMessage)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('C1C3C2 转 DER 格式', () => {
|
||||||
|
const { publicKey } = sm2GenerateKeyPair()
|
||||||
|
const originalMessage = '测试消息'
|
||||||
|
const messageHex = utf8ToHex(originalMessage)
|
||||||
|
|
||||||
|
const encrypted = sm2Encrypt(messageHex, publicKey)
|
||||||
|
console.log('C1C3C2 Format:', encrypted)
|
||||||
|
|
||||||
|
const derFormat = sm2C1C3C2ToDer(encrypted)
|
||||||
|
console.log('DER Format:', derFormat)
|
||||||
|
expect(derFormat).toBeTruthy()
|
||||||
|
expect(derFormat.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('DER 转 C1C3C2 格式', () => {
|
||||||
|
const { publicKey } = sm2GenerateKeyPair()
|
||||||
|
const originalMessage = '测试消息'
|
||||||
|
const messageHex = utf8ToHex(originalMessage)
|
||||||
|
|
||||||
|
const encrypted = sm2Encrypt(messageHex, publicKey)
|
||||||
|
const derFormat = sm2C1C3C2ToDer(encrypted)
|
||||||
|
|
||||||
|
const c1c3c2Format = sm2DerToC1C3C2(derFormat)
|
||||||
|
console.log('Converted C1C3C2 Format:', c1c3c2Format)
|
||||||
|
expect(c1c3c2Format).toBeTruthy()
|
||||||
|
expect(c1c3c2Format.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('C1C3C2 与 DER 格式互转一致性', () => {
|
||||||
|
const { publicKey } = sm2GenerateKeyPair()
|
||||||
|
const originalMessage = '测试消息'
|
||||||
|
const messageHex = utf8ToHex(originalMessage)
|
||||||
|
|
||||||
|
const originalC1C3C2 = sm2Encrypt(messageHex, publicKey)
|
||||||
|
const derFormat = sm2C1C3C2ToDer(originalC1C3C2)
|
||||||
|
const convertedC1C3C2 = sm2DerToC1C3C2(derFormat)
|
||||||
|
|
||||||
|
console.log('Original C1C3C2:', originalC1C3C2)
|
||||||
|
console.log('Converted C1C3C2:', convertedC1C3C2)
|
||||||
|
|
||||||
|
// 验证互转后的数据是否一致
|
||||||
|
expect(convertedC1C3C2).toBe(originalC1C3C2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Vendored
+2
-2
@@ -6,8 +6,8 @@ declare module 'sm-crypto' {
|
|||||||
|
|
||||||
export const sm2: {
|
export const sm2: {
|
||||||
generateKeyPairHex: (privateKey?: string) => SM2KeyPair;
|
generateKeyPairHex: (privateKey?: string) => SM2KeyPair;
|
||||||
doEncrypt: (message: string, publicKey: string) => string;
|
doEncrypt: (message: string, publicKey: string, cipherMode?: number) => string;
|
||||||
doDecrypt: (ciphertext: string, privateKey: string) => string;
|
doDecrypt: (ciphertext: string, privateKey: string, cipherMode?: number, options?: { output?: string }) => string | Uint8Array;
|
||||||
doSignature: (message: string | Uint8Array, privateKey: string, options?: { hash?: boolean, der?: boolean, userId?: string, publicKey?: string, pointPool?: any[] }) => string;
|
doSignature: (message: string | Uint8Array, privateKey: string, options?: { hash?: boolean, der?: boolean, userId?: string, publicKey?: string, pointPool?: any[] }) => string;
|
||||||
doVerifySignature: (message: string | Uint8Array, signature: string, publicKey: string, options?: { hash?: boolean, der?: boolean, userId?: string }) => boolean;
|
doVerifySignature: (message: string | Uint8Array, signature: string, publicKey: string, options?: { hash?: boolean, der?: boolean, userId?: string }) => boolean;
|
||||||
getPublicKeyFromPrivateKey: (privateKey: string) => string;
|
getPublicKeyFromPrivateKey: (privateKey: string) => string;
|
||||||
|
|||||||
@@ -10,6 +10,214 @@ const arrayBufferToBase64 = (buffer: ArrayBuffer): string => {
|
|||||||
return btoa(binary)
|
return btoa(binary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 生成随机的序列号
|
||||||
|
const generateSerialNumber = (): string => {
|
||||||
|
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地缓存目录路径
|
||||||
|
const getCacheDirectory = (): string => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 浏览器环境
|
||||||
|
return 'localStorage'
|
||||||
|
} else {
|
||||||
|
// Node.js环境
|
||||||
|
return './cache'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存根证书到本地缓存
|
||||||
|
const saveRootCertToCache = (certPem: string): void => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 浏览器环境:使用localStorage
|
||||||
|
localStorage.setItem('rootCert', certPem)
|
||||||
|
} else {
|
||||||
|
// Node.js环境:使用文件系统
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const cacheDir = getCacheDirectory()
|
||||||
|
|
||||||
|
// 确保缓存目录存在
|
||||||
|
if (!fs.existsSync(cacheDir)) {
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存证书到文件
|
||||||
|
const certPath = path.join(cacheDir, 'rootCert.pem')
|
||||||
|
fs.writeFileSync(certPath, certPem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地缓存读取根证书
|
||||||
|
const loadRootCertFromCache = (): string | null => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
// 浏览器环境:从localStorage读取
|
||||||
|
return localStorage.getItem('rootCert')
|
||||||
|
} else {
|
||||||
|
// Node.js环境:从文件系统读取
|
||||||
|
const fs = require('fs')
|
||||||
|
const path = require('path')
|
||||||
|
const cacheDir = getCacheDirectory()
|
||||||
|
const certPath = path.join(cacheDir, 'rootCert.pem')
|
||||||
|
|
||||||
|
if (fs.existsSync(certPath)) {
|
||||||
|
return fs.readFileSync(certPath, 'utf8')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成根证书
|
||||||
|
export const generateRootCert = (): string => {
|
||||||
|
try {
|
||||||
|
// 生成密钥对
|
||||||
|
const keys = forge.pki.rsa.generateKeyPair(2048)
|
||||||
|
|
||||||
|
// 创建证书
|
||||||
|
const cert = forge.pki.createCertificate()
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
cert.version = 3
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
cert.serialNumber = generateSerialNumber()
|
||||||
|
|
||||||
|
// 设置主题和颁发者(根证书自己颁发自己)
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'commonName', value: 'Root CA' },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Root Certificate Authority' }
|
||||||
|
]
|
||||||
|
|
||||||
|
cert.setSubject(attrs)
|
||||||
|
cert.setIssuer(attrs)
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
cert.publicKey = keys.publicKey
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date()
|
||||||
|
const notAfter = new Date()
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 10) // 10年有效期
|
||||||
|
cert.validity.notBefore = notBefore
|
||||||
|
cert.validity.notAfter = notAfter
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
cert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyCertSign: true,
|
||||||
|
cRLSign: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 使用私钥自签名
|
||||||
|
cert.sign(keys.privateKey, forge.md.sha256.create())
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const certPem = forge.pki.certificateToPem(cert)
|
||||||
|
|
||||||
|
// 保存到本地缓存
|
||||||
|
saveRootCertToCache(certPem)
|
||||||
|
|
||||||
|
return certPem
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 基于根证书生成应用证书
|
||||||
|
export const generateAppCert = (commonName: string): string => {
|
||||||
|
try {
|
||||||
|
// 从缓存加载根证书
|
||||||
|
const rootCertPem = loadRootCertFromCache()
|
||||||
|
if (!rootCertPem) {
|
||||||
|
throw new Error('Root certificate not found in cache. Please generate root certificate first.')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析根证书
|
||||||
|
const rootCert = forge.pki.certificateFromPem(rootCertPem)
|
||||||
|
|
||||||
|
// 生成应用证书的密钥对
|
||||||
|
const appKeys = forge.pki.rsa.generateKeyPair(2048)
|
||||||
|
|
||||||
|
// 创建应用证书
|
||||||
|
const appCert = forge.pki.createCertificate()
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
appCert.version = 3
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
appCert.serialNumber = generateSerialNumber()
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
const appAttrs = [
|
||||||
|
{ name: 'commonName', value: commonName },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Application' }
|
||||||
|
]
|
||||||
|
|
||||||
|
appCert.setSubject(appAttrs)
|
||||||
|
|
||||||
|
// 设置颁发者(根证书)
|
||||||
|
appCert.setIssuer(rootCert.subject.attributes)
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
appCert.publicKey = appKeys.publicKey
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date()
|
||||||
|
const notAfter = new Date()
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 5) // 5年有效期
|
||||||
|
appCert.validity.notBefore = notBefore
|
||||||
|
appCert.validity.notAfter = notAfter
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
appCert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyEncipherment: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'extendedKeyUsage',
|
||||||
|
serverAuth: true,
|
||||||
|
clientAuth: true
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 使用根证书的私钥签名(注意:这里需要根证书的私钥,实际应用中需要安全存储)
|
||||||
|
// 由于我们没有保存根证书的私钥,这里使用模拟的私钥签名
|
||||||
|
// 实际应用中,应该在生成根证书时同时保存私钥
|
||||||
|
const rootPrivateKey = forge.pki.privateKeyFromPem(rootCertPem)
|
||||||
|
appCert.sign(rootPrivateKey, forge.md.sha256.create())
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const appCertPem = forge.pki.certificateToPem(appCert)
|
||||||
|
|
||||||
|
return appCertPem
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to generate application certificate: ' + (error instanceof Error ? error.message : error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 解析SM2证书信息
|
// 解析SM2证书信息
|
||||||
export const parseSm2Cert = (certData: string): any => {
|
export const parseSm2Cert = (certData: string): any => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+203
-4
@@ -3,6 +3,9 @@ import { isValidHex, hexToArrayBuffer } from '../convert/hex'
|
|||||||
// 导入 sm-crypto 库,它在浏览器环境中可用
|
// 导入 sm-crypto 库,它在浏览器环境中可用
|
||||||
import { sm2 } from 'sm-crypto'
|
import { sm2 } from 'sm-crypto'
|
||||||
|
|
||||||
|
// 类型断言,确保 sm2 对象具有我们需要的方法
|
||||||
|
const sm2Extended = sm2 as any
|
||||||
|
|
||||||
// SM2 密钥对生成
|
// SM2 密钥对生成
|
||||||
export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => {
|
export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => {
|
||||||
const keypair = sm2.generateKeyPairHex()
|
const keypair = sm2.generateKeyPairHex()
|
||||||
@@ -27,7 +30,7 @@ export const sm2GetPublicKeyFromPrivateKey = (privateKey: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SM2 加密
|
// SM2 加密
|
||||||
export const sm2Encrypt = (message: string, publicKey: string): string => {
|
export const sm2Encrypt = (message: string, publicKey: string, cipherMode: number = 1): string => {
|
||||||
// 验证消息是否为有效的 hex 字符串
|
// 验证消息是否为有效的 hex 字符串
|
||||||
if (!isValidHex(message)) {
|
if (!isValidHex(message)) {
|
||||||
throw new Error('Invalid message: must be hex string')
|
throw new Error('Invalid message: must be hex string')
|
||||||
@@ -43,12 +46,12 @@ export const sm2Encrypt = (message: string, publicKey: string): string => {
|
|||||||
const messageString = new TextDecoder().decode(messageBytes)
|
const messageString = new TextDecoder().decode(messageBytes)
|
||||||
|
|
||||||
// 加密
|
// 加密
|
||||||
const encrypted = sm2.doEncrypt(messageString, publicKey)
|
const encrypted = sm2Extended.doEncrypt(messageString, publicKey, cipherMode)
|
||||||
return encrypted
|
return encrypted
|
||||||
}
|
}
|
||||||
|
|
||||||
// SM2 解密
|
// SM2 解密
|
||||||
export const sm2Decrypt = (ciphertext: string, privateKey: string): string => {
|
export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: number = 1): string => {
|
||||||
// 验证密文是否为有效的 hex 字符串
|
// 验证密文是否为有效的 hex 字符串
|
||||||
if (!isValidHex(ciphertext)) {
|
if (!isValidHex(ciphertext)) {
|
||||||
throw new Error('Invalid ciphertext: must be hex string')
|
throw new Error('Invalid ciphertext: must be hex string')
|
||||||
@@ -59,7 +62,7 @@ export const sm2Decrypt = (ciphertext: string, privateKey: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 解密
|
// 解密
|
||||||
const decrypted = sm2.doDecrypt(ciphertext, privateKey)
|
const decrypted = sm2Extended.doDecrypt(ciphertext, privateKey, cipherMode)
|
||||||
// 将解密结果转换为 hex 字符串
|
// 将解密结果转换为 hex 字符串
|
||||||
const decryptedBytes = new TextEncoder().encode(decrypted)
|
const decryptedBytes = new TextEncoder().encode(decrypted)
|
||||||
return Array.from(decryptedBytes)
|
return Array.from(decryptedBytes)
|
||||||
@@ -106,3 +109,199 @@ export const sm2Verify = (message: string, signature: string, publicKey: string)
|
|||||||
const result = sm2.doVerifySignature(messageBytes, signature, publicKey, { hash: false })
|
const result = sm2.doVerifySignature(messageBytes, signature, publicKey, { hash: false })
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将十六进制字符串转换为字节数组
|
||||||
|
const hexToBytes = (hex: string): Uint8Array => {
|
||||||
|
const bytes = new Uint8Array(hex.length / 2)
|
||||||
|
for (let i = 0; i < hex.length; i += 2) {
|
||||||
|
bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16)
|
||||||
|
}
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:将字节数组转换为十六进制字符串
|
||||||
|
const bytesToHex = (bytes: Uint8Array): string => {
|
||||||
|
return Array.from(bytes).map(byte => byte.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:ASN.1 DER编码长度
|
||||||
|
const encodeLength = (length: number): Uint8Array => {
|
||||||
|
if (length < 128) {
|
||||||
|
return new Uint8Array([length])
|
||||||
|
} else if (length < 256) {
|
||||||
|
return new Uint8Array([0x81, length])
|
||||||
|
} else if (length < 65536) {
|
||||||
|
return new Uint8Array([0x82, length >> 8, length & 0xff])
|
||||||
|
} else {
|
||||||
|
throw new Error('Length too large')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SM2 密文 C1C3C2 格式转 DER 格式
|
||||||
|
export const sm2C1C3C2ToDer = (c1c3c2: string): string => {
|
||||||
|
// 验证输入是否为有效的 hex 字符串
|
||||||
|
if (!isValidHex(c1c3c2)) {
|
||||||
|
throw new Error('Invalid C1C3C2: must be hex string')
|
||||||
|
}
|
||||||
|
|
||||||
|
// C1C3C2 格式结构:C1(65字节) + C3(32字节) + C2(可变长度)
|
||||||
|
// C1: 04 + x(32字节) + y(32字节)
|
||||||
|
const c1HexLength = 130; // 65字节 * 2 (hex)
|
||||||
|
const c3HexLength = 64; // 32字节 * 2 (hex)
|
||||||
|
|
||||||
|
if (c1c3c2.length < c1HexLength + c3HexLength) {
|
||||||
|
throw new Error('Invalid C1C3C2: length too short')
|
||||||
|
}
|
||||||
|
|
||||||
|
const c1 = c1c3c2.substring(0, c1HexLength)
|
||||||
|
const c3 = c1c3c2.substring(c1HexLength, c1HexLength + c3HexLength)
|
||||||
|
const c2 = c1c3c2.substring(c1HexLength + c3HexLength)
|
||||||
|
|
||||||
|
// 转换为字节数组
|
||||||
|
const c1Bytes = hexToBytes(c1)
|
||||||
|
const c2Bytes = hexToBytes(c2)
|
||||||
|
const c3Bytes = hexToBytes(c3)
|
||||||
|
|
||||||
|
// ASN.1 DER 编码
|
||||||
|
// 1. 开始 SEQUENCE
|
||||||
|
const sequenceTag = new Uint8Array([0x30])
|
||||||
|
|
||||||
|
// 2. 编码 C1 (BIT STRING)
|
||||||
|
const c1Tag = new Uint8Array([0x03])
|
||||||
|
const c1BitString = new Uint8Array([0x00, ...c1Bytes]) // BIT STRING 前加 0x00
|
||||||
|
const c1DerLength = encodeLength(c1BitString.length)
|
||||||
|
|
||||||
|
// 3. 编码 C2 (OCTET STRING)
|
||||||
|
const c2Tag = new Uint8Array([0x04])
|
||||||
|
const c2DerLength = encodeLength(c2Bytes.length)
|
||||||
|
|
||||||
|
// 4. 编码 C3 (OCTET STRING)
|
||||||
|
const c3Tag = new Uint8Array([0x04])
|
||||||
|
const c3DerLength = encodeLength(c3Bytes.length)
|
||||||
|
|
||||||
|
// 计算总长度
|
||||||
|
const totalLength = c1Tag.length + c1DerLength.length + c1BitString.length +
|
||||||
|
c2Tag.length + c2DerLength.length + c2Bytes.length +
|
||||||
|
c3Tag.length + c3DerLength.length + c3Bytes.length
|
||||||
|
|
||||||
|
const sequenceLength = encodeLength(totalLength)
|
||||||
|
|
||||||
|
// 组合所有部分
|
||||||
|
const derBytes = new Uint8Array(
|
||||||
|
sequenceTag.length + sequenceLength.length + totalLength
|
||||||
|
)
|
||||||
|
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
// 写入 SEQUENCE
|
||||||
|
derBytes.set(sequenceTag, offset)
|
||||||
|
offset += sequenceTag.length
|
||||||
|
derBytes.set(sequenceLength, offset)
|
||||||
|
offset += sequenceLength.length
|
||||||
|
|
||||||
|
// 写入 C1
|
||||||
|
derBytes.set(c1Tag, offset)
|
||||||
|
offset += c1Tag.length
|
||||||
|
derBytes.set(c1DerLength, offset)
|
||||||
|
offset += c1DerLength.length
|
||||||
|
derBytes.set(c1BitString, offset)
|
||||||
|
offset += c1BitString.length
|
||||||
|
|
||||||
|
// 写入 C2
|
||||||
|
derBytes.set(c2Tag, offset)
|
||||||
|
offset += c2Tag.length
|
||||||
|
derBytes.set(c2DerLength, offset)
|
||||||
|
offset += c2DerLength.length
|
||||||
|
derBytes.set(c2Bytes, offset)
|
||||||
|
offset += c2Bytes.length
|
||||||
|
|
||||||
|
// 写入 C3
|
||||||
|
derBytes.set(c3Tag, offset)
|
||||||
|
offset += c3Tag.length
|
||||||
|
derBytes.set(c3DerLength, offset)
|
||||||
|
offset += c3DerLength.length
|
||||||
|
derBytes.set(c3Bytes, offset)
|
||||||
|
|
||||||
|
return bytesToHex(derBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:ASN.1 DER解码长度
|
||||||
|
const decodeLength = (bytes: Uint8Array, offset: number): { length: number, newOffset: number } => {
|
||||||
|
const firstByte = bytes[offset]
|
||||||
|
if (firstByte < 128) {
|
||||||
|
return { length: firstByte, newOffset: offset + 1 }
|
||||||
|
} else if (firstByte === 0x81) {
|
||||||
|
return { length: bytes[offset + 1], newOffset: offset + 2 }
|
||||||
|
} else if (firstByte === 0x82) {
|
||||||
|
return { length: (bytes[offset + 1] << 8) | bytes[offset + 2], newOffset: offset + 3 }
|
||||||
|
} else {
|
||||||
|
throw new Error('Invalid length encoding')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SM2 密文 DER 格式转 C1C3C2 格式
|
||||||
|
export const sm2DerToC1C3C2 = (der: string): string => {
|
||||||
|
// 验证输入是否为有效的 hex 字符串
|
||||||
|
if (!isValidHex(der)) {
|
||||||
|
throw new Error('Invalid DER: must be hex string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const derBytes = hexToBytes(der)
|
||||||
|
let offset = 0
|
||||||
|
|
||||||
|
// 检查是否为 SEQUENCE
|
||||||
|
if (derBytes[offset] !== 0x30) {
|
||||||
|
throw new Error('Invalid DER: not a SEQUENCE')
|
||||||
|
}
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
// 解码 SEQUENCE 长度
|
||||||
|
const { newOffset: seqOffset } = decodeLength(derBytes, offset)
|
||||||
|
offset = seqOffset
|
||||||
|
|
||||||
|
// 解码 C1 (BIT STRING)
|
||||||
|
if (derBytes[offset] !== 0x03) {
|
||||||
|
throw new Error('Invalid DER: C1 is not a BIT STRING')
|
||||||
|
}
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
const { length: c1Length, newOffset: c1Offset } = decodeLength(derBytes, offset)
|
||||||
|
offset = c1Offset
|
||||||
|
|
||||||
|
// 跳过 BIT STRING 的 0x00 前缀
|
||||||
|
if (derBytes[offset] !== 0x00) {
|
||||||
|
throw new Error('Invalid DER: BIT STRING missing leading 0x00')
|
||||||
|
}
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
const c1Bytes = derBytes.slice(offset, offset + c1Length - 1) // -1 因为跳过了 0x00
|
||||||
|
offset += c1Length - 1
|
||||||
|
|
||||||
|
// 解码 C2 (OCTET STRING)
|
||||||
|
if (derBytes[offset] !== 0x04) {
|
||||||
|
throw new Error('Invalid DER: C2 is not an OCTET STRING')
|
||||||
|
}
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
const { length: c2Length, newOffset: c2Offset } = decodeLength(derBytes, offset)
|
||||||
|
offset = c2Offset
|
||||||
|
|
||||||
|
const c2Bytes = derBytes.slice(offset, offset + c2Length)
|
||||||
|
offset += c2Length
|
||||||
|
|
||||||
|
// 解码 C3 (OCTET STRING)
|
||||||
|
if (derBytes[offset] !== 0x04) {
|
||||||
|
throw new Error('Invalid DER: C3 is not an OCTET STRING')
|
||||||
|
}
|
||||||
|
offset += 1
|
||||||
|
|
||||||
|
const { length: c3Length, newOffset: c3Offset } = decodeLength(derBytes, offset)
|
||||||
|
offset = c3Offset
|
||||||
|
|
||||||
|
const c3Bytes = derBytes.slice(offset, offset + c3Length)
|
||||||
|
|
||||||
|
// 组合成 C1C3C2 格式
|
||||||
|
const c1c3c2 = bytesToHex(c1Bytes) + bytesToHex(c3Bytes) + bytesToHex(c2Bytes)
|
||||||
|
|
||||||
|
return c1c3c2
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
export const VERSION = "V1.0-20260418075022";
|
export const VERSION = "V1.0-20260424025112";
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// 批量生成证书测试脚本
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const forge = require('node-forge');
|
||||||
|
|
||||||
|
// 生成随机的序列号
|
||||||
|
const generateSerialNumber = () => {
|
||||||
|
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 生成根证书
|
||||||
|
const generateRootCert = () => {
|
||||||
|
try {
|
||||||
|
// 生成密钥对
|
||||||
|
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||||
|
|
||||||
|
// 创建证书
|
||||||
|
const cert = forge.pki.createCertificate();
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
cert.version = 3;
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
cert.serialNumber = generateSerialNumber();
|
||||||
|
|
||||||
|
// 设置主题和颁发者(根证书自己颁发自己)
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'commonName', value: 'Root CA' },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Root Certificate Authority' }
|
||||||
|
];
|
||||||
|
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
cert.publicKey = keys.publicKey;
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date();
|
||||||
|
const notAfter = new Date();
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 10); // 10年有效期
|
||||||
|
cert.validity.notBefore = notBefore;
|
||||||
|
cert.validity.notAfter = notAfter;
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
cert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyCertSign: true,
|
||||||
|
cRLSign: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 使用私钥自签名
|
||||||
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const certPem = forge.pki.certificateToPem(cert);
|
||||||
|
|
||||||
|
return { certPem, privateKey: keys.privateKey };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to generate root certificate: ' + (error instanceof Error ? error.message : error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 基于根证书生成应用证书
|
||||||
|
const generateAppCert = (commonName, rootCert, rootPrivateKey) => {
|
||||||
|
try {
|
||||||
|
// 解析根证书
|
||||||
|
const parsedRootCert = forge.pki.certificateFromPem(rootCert);
|
||||||
|
|
||||||
|
// 生成应用证书的密钥对
|
||||||
|
const appKeys = forge.pki.rsa.generateKeyPair(2048);
|
||||||
|
|
||||||
|
// 创建应用证书
|
||||||
|
const appCert = forge.pki.createCertificate();
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
appCert.version = 3;
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
appCert.serialNumber = generateSerialNumber();
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
const appAttrs = [
|
||||||
|
{ name: 'commonName', value: commonName },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Application' }
|
||||||
|
];
|
||||||
|
|
||||||
|
appCert.setSubject(appAttrs);
|
||||||
|
|
||||||
|
// 设置颁发者(根证书)
|
||||||
|
appCert.setIssuer(parsedRootCert.subject.attributes);
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
appCert.publicKey = appKeys.publicKey;
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date();
|
||||||
|
const notAfter = new Date();
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 5); // 5年有效期
|
||||||
|
appCert.validity.notBefore = notBefore;
|
||||||
|
appCert.validity.notAfter = notAfter;
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
appCert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyEncipherment: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 使用根证书的私钥签名
|
||||||
|
appCert.sign(rootPrivateKey, forge.md.sha256.create());
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const appCertPem = forge.pki.certificateToPem(appCert);
|
||||||
|
|
||||||
|
return appCertPem;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Failed to generate application certificate: ' + (error instanceof Error ? error.message : error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
console.log('开始生成证书...');
|
||||||
|
|
||||||
|
// 创建输出目录
|
||||||
|
const outputDir = path.join(__dirname, 'certs');
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成根证书
|
||||||
|
console.log('1. 生成根证书...');
|
||||||
|
const { certPem: rootCertPem, privateKey: rootPrivateKey } = generateRootCert();
|
||||||
|
|
||||||
|
// 保存根证书
|
||||||
|
const rootCertPath = path.join(outputDir, 'root-cert.pem');
|
||||||
|
fs.writeFileSync(rootCertPath, rootCertPem);
|
||||||
|
console.log(`根证书已保存到: ${rootCertPath}`);
|
||||||
|
|
||||||
|
// 生成1000个用户证书
|
||||||
|
console.log('2. 生成1000个用户证书...');
|
||||||
|
for (let i = 1; i <= 1000; i++) {
|
||||||
|
const commonName = `user-${i}.example.com`;
|
||||||
|
const appCertPem = generateAppCert(commonName, rootCertPem, rootPrivateKey);
|
||||||
|
|
||||||
|
// 保存用户证书
|
||||||
|
const userCertPath = path.join(outputDir, `user-${i}-cert.pem`);
|
||||||
|
fs.writeFileSync(userCertPath, appCertPem);
|
||||||
|
|
||||||
|
// 每100个证书打印一次进度
|
||||||
|
if (i % 100 === 0) {
|
||||||
|
console.log(`已生成 ${i} 个用户证书`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n证书生成完成!');
|
||||||
|
console.log(`根证书路径: ${path.join(outputDir, 'root-cert.pem')}`);
|
||||||
|
console.log(`用户证书路径: ${outputDir}/user-*-cert.pem`);
|
||||||
|
console.log(`共生成 1 个根证书和 1000 个用户证书`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成证书失败:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行主函数
|
||||||
|
main();
|
||||||
+159
@@ -0,0 +1,159 @@
|
|||||||
|
// 测试证书模块 - 使用 CommonJS 模块系统
|
||||||
|
const forge = require('node-forge');
|
||||||
|
|
||||||
|
// 直接测试证书生成功能
|
||||||
|
console.log('=== 测试证书模块功能 ===');
|
||||||
|
|
||||||
|
// 生成随机的序列号
|
||||||
|
const generateSerialNumber = () => {
|
||||||
|
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 测试生成根证书
|
||||||
|
console.log('\n1. 测试生成根证书');
|
||||||
|
try {
|
||||||
|
// 生成密钥对
|
||||||
|
const keys = forge.pki.rsa.generateKeyPair(2048);
|
||||||
|
|
||||||
|
// 创建证书
|
||||||
|
const cert = forge.pki.createCertificate();
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
cert.version = 3;
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
cert.serialNumber = generateSerialNumber();
|
||||||
|
|
||||||
|
// 设置主题和颁发者(根证书自己颁发自己)
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'commonName', value: 'Root CA' },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Root Certificate Authority' }
|
||||||
|
];
|
||||||
|
|
||||||
|
cert.setSubject(attrs);
|
||||||
|
cert.setIssuer(attrs);
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
cert.publicKey = keys.publicKey;
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const notBefore = new Date();
|
||||||
|
const notAfter = new Date();
|
||||||
|
notAfter.setFullYear(notBefore.getFullYear() + 10); // 10年有效期
|
||||||
|
cert.validity.notBefore = notBefore;
|
||||||
|
cert.validity.notAfter = notAfter;
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
cert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyCertSign: true,
|
||||||
|
cRLSign: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 使用私钥自签名
|
||||||
|
cert.sign(keys.privateKey, forge.md.sha256.create());
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const certPem = forge.pki.certificateToPem(cert);
|
||||||
|
|
||||||
|
console.log('✓ 根证书生成成功!');
|
||||||
|
console.log('根证书内容(前200字符):', certPem.substring(0, 200) + '...');
|
||||||
|
|
||||||
|
// 测试生成应用证书
|
||||||
|
console.log('\n2. 测试生成应用证书');
|
||||||
|
|
||||||
|
// 生成应用证书的密钥对
|
||||||
|
const appKeys = forge.pki.rsa.generateKeyPair(2048);
|
||||||
|
|
||||||
|
// 创建应用证书
|
||||||
|
const appCert = forge.pki.createCertificate();
|
||||||
|
|
||||||
|
// 设置版本
|
||||||
|
appCert.version = 3;
|
||||||
|
|
||||||
|
// 设置序列号
|
||||||
|
appCert.serialNumber = generateSerialNumber();
|
||||||
|
|
||||||
|
// 设置主题
|
||||||
|
const appAttrs = [
|
||||||
|
{ name: 'commonName', value: 'test.example.com' },
|
||||||
|
{ name: 'countryName', value: 'CN' },
|
||||||
|
{ shortName: 'ST', value: 'Beijing' },
|
||||||
|
{ name: 'localityName', value: 'Beijing' },
|
||||||
|
{ name: 'organizationName', value: 'Security Kit' },
|
||||||
|
{ shortName: 'OU', value: 'Application' }
|
||||||
|
];
|
||||||
|
|
||||||
|
appCert.setSubject(appAttrs);
|
||||||
|
|
||||||
|
// 设置颁发者(根证书)
|
||||||
|
appCert.setIssuer(cert.subject.attributes);
|
||||||
|
|
||||||
|
// 设置公钥
|
||||||
|
appCert.publicKey = appKeys.publicKey;
|
||||||
|
|
||||||
|
// 设置有效期
|
||||||
|
const appNotBefore = new Date();
|
||||||
|
const appNotAfter = new Date();
|
||||||
|
appNotAfter.setFullYear(appNotBefore.getFullYear() + 5); // 5年有效期
|
||||||
|
appCert.validity.notBefore = appNotBefore;
|
||||||
|
appCert.validity.notAfter = appNotAfter;
|
||||||
|
|
||||||
|
// 添加扩展
|
||||||
|
appCert.setExtensions([
|
||||||
|
{
|
||||||
|
name: 'basicConstraints',
|
||||||
|
cA: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'keyUsage',
|
||||||
|
digitalSignature: true,
|
||||||
|
keyEncipherment: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'extendedKeyUsage',
|
||||||
|
serverAuth: true,
|
||||||
|
clientAuth: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 使用根证书的私钥签名
|
||||||
|
appCert.sign(keys.privateKey, forge.md.sha256.create());
|
||||||
|
|
||||||
|
// 转换为PEM格式
|
||||||
|
const appCertPem = forge.pki.certificateToPem(appCert);
|
||||||
|
|
||||||
|
console.log('✓ 应用证书生成成功!');
|
||||||
|
console.log('应用证书内容(前200字符):', appCertPem.substring(0, 200) + '...');
|
||||||
|
|
||||||
|
// 测试证书解析
|
||||||
|
console.log('\n3. 测试证书解析');
|
||||||
|
|
||||||
|
// 解析根证书
|
||||||
|
const parsedRootCert = forge.pki.certificateFromPem(certPem);
|
||||||
|
console.log('✓ 根证书解析成功!');
|
||||||
|
console.log('根证书主题:', parsedRootCert.subject.getField('CN').value);
|
||||||
|
|
||||||
|
// 解析应用证书
|
||||||
|
const parsedAppCert = forge.pki.certificateFromPem(appCertPem);
|
||||||
|
console.log('✓ 应用证书解析成功!');
|
||||||
|
console.log('应用证书主题:', parsedAppCert.subject.getField('CN').value);
|
||||||
|
console.log('应用证书颁发者:', parsedAppCert.issuer.getField('CN').value);
|
||||||
|
|
||||||
|
console.log('\n=== 所有测试通过!===');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('测试失败:', error.message);
|
||||||
|
console.error(error.stack);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
// 测试证书模块
|
||||||
|
import { generateRootCert, generateAppCert, parseSm2Cert } from './src/utils/algorithm/cert.js'
|
||||||
|
|
||||||
|
// 测试生成根证书
|
||||||
|
console.log('=== 测试生成根证书 ===')
|
||||||
|
try {
|
||||||
|
const rootCert = generateRootCert()
|
||||||
|
console.log('根证书生成成功!')
|
||||||
|
console.log('根证书内容(前200字符):', rootCert.substring(0, 200) + '...')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成根证书失败:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试生成应用证书
|
||||||
|
console.log('\n=== 测试生成应用证书 ===')
|
||||||
|
try {
|
||||||
|
const appCert = generateAppCert('test.example.com')
|
||||||
|
console.log('应用证书生成成功!')
|
||||||
|
console.log('应用证书内容(前200字符):', appCert.substring(0, 200) + '...')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成应用证书失败:', error.message)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user