From 3459a389aca1aa0b94c1ed5609a58c9a3557f2df Mon Sep 17 00:00:00 2001 From: cheney Date: Thu, 23 Apr 2026 14:18:39 +0800 Subject: [PATCH] =?UTF-8?q?der=20c1c3c2=20=E8=BD=AC=E6=8D=A2=E5=B7=A5?= =?UTF-8?q?=E5=85=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/design.md | 4 + src/__tests__/algorithm/sm2.test.ts | 46 ++++++- src/types/sm-crypto.d.ts | 4 +- src/utils/algorithm/sm2.ts | 207 +++++++++++++++++++++++++++- src/version.js | 2 +- 5 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 docs/design.md diff --git a/docs/design.md b/docs/design.md new file mode 100644 index 0000000..6e044ac --- /dev/null +++ b/docs/design.md @@ -0,0 +1,4 @@ +设计规范 + +- 所有输入框,双击后弹出提示框,确认后可以清空内容。移动端为长按弹出提示框,确认后可以清空内容。 + diff --git a/src/__tests__/algorithm/sm2.test.ts b/src/__tests__/algorithm/sm2.test.ts index 1a6ffd5..bc0955a 100644 --- a/src/__tests__/algorithm/sm2.test.ts +++ b/src/__tests__/algorithm/sm2.test.ts @@ -1,4 +1,4 @@ -import { sm2GenerateKeyPair, sm2Encrypt, sm2Decrypt } from '../../utils/algorithm/sm2' +import { sm2GenerateKeyPair, sm2Encrypt, sm2Decrypt, sm2C1C3C2ToDer, sm2DerToC1C3C2 } from '../../utils/algorithm/sm2' // UTF8 转 Hex const utf8ToHex = (utf8: string): string => { @@ -37,4 +37,48 @@ describe('SM2 算法测试', () => { // console.log('Decrypted Message:', decryptedMessage) // 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) + }) }) diff --git a/src/types/sm-crypto.d.ts b/src/types/sm-crypto.d.ts index e0acce8..751a8a7 100644 --- a/src/types/sm-crypto.d.ts +++ b/src/types/sm-crypto.d.ts @@ -6,8 +6,8 @@ declare module 'sm-crypto' { export const sm2: { generateKeyPairHex: (privateKey?: string) => SM2KeyPair; - doEncrypt: (message: string, publicKey: string) => string; - doDecrypt: (ciphertext: string, privateKey: string) => string; + doEncrypt: (message: string, publicKey: string, cipherMode?: number) => 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; doVerifySignature: (message: string | Uint8Array, signature: string, publicKey: string, options?: { hash?: boolean, der?: boolean, userId?: string }) => boolean; getPublicKeyFromPrivateKey: (privateKey: string) => string; diff --git a/src/utils/algorithm/sm2.ts b/src/utils/algorithm/sm2.ts index e32bbc1..8480de4 100644 --- a/src/utils/algorithm/sm2.ts +++ b/src/utils/algorithm/sm2.ts @@ -3,6 +3,9 @@ import { isValidHex, hexToArrayBuffer } from '../convert/hex' // 导入 sm-crypto 库,它在浏览器环境中可用 import { sm2 } from 'sm-crypto' +// 类型断言,确保 sm2 对象具有我们需要的方法 +const sm2Extended = sm2 as any + // SM2 密钥对生成 export const sm2GenerateKeyPair = (): { privateKey: string; publicKey: string } => { const keypair = sm2.generateKeyPairHex() @@ -27,7 +30,7 @@ export const sm2GetPublicKeyFromPrivateKey = (privateKey: string): string => { } // SM2 加密 -export const sm2Encrypt = (message: string, publicKey: string): string => { +export const sm2Encrypt = (message: string, publicKey: string, cipherMode: number = 1): string => { // 验证消息是否为有效的 hex 字符串 if (!isValidHex(message)) { 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 encrypted = sm2.doEncrypt(messageString, publicKey) + const encrypted = sm2Extended.doEncrypt(messageString, publicKey, cipherMode) return encrypted } // SM2 解密 -export const sm2Decrypt = (ciphertext: string, privateKey: string): string => { +export const sm2Decrypt = (ciphertext: string, privateKey: string, cipherMode: number = 1): string => { // 验证密文是否为有效的 hex 字符串 if (!isValidHex(ciphertext)) { 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 字符串 const decryptedBytes = new TextEncoder().encode(decrypted) 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 }) 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 +} diff --git a/src/version.js b/src/version.js index df9d45e..8f8b3bd 100644 --- a/src/version.js +++ b/src/version.js @@ -1 +1 @@ -export const VERSION = "V1.0-20260418075022"; \ No newline at end of file +export const VERSION = "V1.0-20260423021405"; \ No newline at end of file