251 lines
8.3 KiB
TypeScript
251 lines
8.3 KiB
TypeScript
import * as sjcl from 'sjcl-with-all'
|
||
import { bindSM4 } from 'gmsm-sm4js'
|
||
import { SM4 } from 'gm-crypto'
|
||
|
||
// 绑定 SM4 到 sjcl
|
||
bindSM4(sjcl)
|
||
|
||
// 初始化 sjcl 的加密模式(这些模式在 sjcl.beware 中定义)
|
||
if (sjcl.beware) {
|
||
// 触发模式的初始化
|
||
Object.keys(sjcl.beware).forEach(key => {
|
||
if (typeof sjcl.beware[key] === 'function') {
|
||
sjcl.beware[key]()
|
||
}
|
||
})
|
||
}
|
||
|
||
// 验证是否为有效的 hex 字符串
|
||
const isValidHex = (hex: string): boolean => {
|
||
return /^[0-9a-fA-F]*$/.test(hex) && hex.length % 2 === 0
|
||
}
|
||
|
||
// Hex 字符串转字节数组
|
||
const hexToBytes = (hex: string): number[] => {
|
||
const bytes: number[] = []
|
||
for (let i = 0; i < hex.length; i += 2) {
|
||
bytes.push(parseInt(hex.substr(i, 2), 16))
|
||
}
|
||
return bytes
|
||
}
|
||
|
||
// 字节数组转 hex 字符串
|
||
const bytesToHex = (bytes: number[]): string => {
|
||
return bytes.map(byte => byte.toString(16).padStart(2, '0')).join('')
|
||
}
|
||
|
||
// PKCS7 填充
|
||
const pkcs7Pad = (data: number[], blockSize: number): number[] => {
|
||
const padLength = blockSize - (data.length % blockSize)
|
||
const padded = [...data]
|
||
for (let i = 0; i < padLength; i++) {
|
||
padded.push(padLength)
|
||
}
|
||
return padded
|
||
}
|
||
|
||
// PKCS7 去填充
|
||
const pkcs7Unpad = (data: number[]): number[] => {
|
||
if (data.length === 0) {
|
||
return data
|
||
}
|
||
const padLength = data[data.length - 1]
|
||
if (padLength > data.length) {
|
||
return data
|
||
}
|
||
// 验证填充是否正确
|
||
for (let i = data.length - padLength; i < data.length; i++) {
|
||
if (data[i] !== padLength) {
|
||
return data
|
||
}
|
||
}
|
||
return data.slice(0, data.length - padLength)
|
||
}
|
||
|
||
// SM4 加密
|
||
export const sm4Encrypt = (message: string, key: string, mode: string, iv?: string, paddingMode: 'pkcs7' | 'none' = 'pkcs7'): string => {
|
||
// 验证消息是否为有效的 hex 字符串
|
||
if (!isValidHex(message)) {
|
||
throw new Error('Invalid message: must be hex string')
|
||
}
|
||
// 验证密钥是否为有效的 hex 字符串
|
||
if (!isValidHex(key)) {
|
||
throw new Error('Invalid key: must be hex string')
|
||
}
|
||
// 验证 IV 是否为有效的 hex 字符串(如果提供)
|
||
if (iv && !isValidHex(iv)) {
|
||
throw new Error('Invalid IV: must be hex string')
|
||
}
|
||
|
||
// 根据模式选择加密方式
|
||
const modeLower = mode.toLowerCase()
|
||
if (modeLower === 'ecb') {
|
||
// ECB 模式使用 gm-crypto
|
||
return SM4.encrypt(message, key, {
|
||
mode: 'ecb',
|
||
inputEncoding: 'hex',
|
||
outputEncoding: 'hex',
|
||
padding: paddingMode === 'pkcs7' ? 'pkcs7' : 'none'
|
||
})
|
||
} else if (modeLower === 'cbc') {
|
||
// CBC 模式使用 gmsm-sm4js
|
||
if (!iv) {
|
||
throw new Error('IV is required for CBC mode')
|
||
}
|
||
// 转换为字节数组
|
||
const messageBytes = hexToBytes(message)
|
||
const keyBytes = hexToBytes(key)
|
||
|
||
// 处理填充
|
||
let dataToEncrypt = messageBytes
|
||
if (paddingMode === 'pkcs7') {
|
||
dataToEncrypt = pkcs7Pad(messageBytes, 16)
|
||
}
|
||
|
||
// 创建 SM4 密钥
|
||
const sm4Key = new sjcl.cipher.sm4(sjcl.codec.bytes.toBits(keyBytes))
|
||
|
||
const ivBytes = hexToBytes(iv)
|
||
const ivBits = sjcl.codec.bytes.toBits(ivBytes)
|
||
const dataBits = sjcl.codec.bytes.toBits(dataToEncrypt)
|
||
const encryptedBits = sjcl.mode.cbc.encrypt(sm4Key, dataBits, ivBits)
|
||
const encryptedBytes = sjcl.codec.bytes.fromBits(encryptedBits)
|
||
return bytesToHex(encryptedBytes)
|
||
} else if (modeLower === 'gcm') {
|
||
// GCM 模式使用 gmsm-sm4js
|
||
if (!iv) {
|
||
throw new Error('IV is required for GCM mode')
|
||
}
|
||
// 转换为字节数组
|
||
const messageBytes = hexToBytes(message)
|
||
const keyBytes = hexToBytes(key)
|
||
|
||
// 处理填充
|
||
let dataToEncrypt = messageBytes
|
||
if (paddingMode === 'pkcs7') {
|
||
dataToEncrypt = pkcs7Pad(messageBytes, 16)
|
||
}
|
||
|
||
// 创建 SM4 密钥
|
||
const sm4Key = new sjcl.cipher.sm4(sjcl.codec.bytes.toBits(keyBytes))
|
||
|
||
const ivBytes = hexToBytes(iv)
|
||
const ivBits = sjcl.codec.bytes.toBits(ivBytes)
|
||
const dataBits = sjcl.codec.bytes.toBits(dataToEncrypt)
|
||
// 传递标签长度参数(128 位)
|
||
const encryptedResult = sjcl.mode.gcm.encrypt(sm4Key, dataBits, ivBits, sjcl.codec.bytes.toBits([]), 128)
|
||
// GCM 模式返回的数据包含 tag
|
||
if (!encryptedResult) {
|
||
throw new Error('GCM encryption failed: result is undefined')
|
||
}
|
||
// 检查 encryptedResult 的类型
|
||
if (Array.isArray(encryptedResult)) {
|
||
// 如果返回的是数组,直接使用(包含了 tag)
|
||
const encryptedBytes = sjcl.codec.bytes.fromBits(encryptedResult)
|
||
return bytesToHex(encryptedBytes)
|
||
} else if (encryptedResult.data) {
|
||
// 如果返回的是对象,使用 data 属性(可能已经包含了 tag)
|
||
const encryptedBits = encryptedResult.data
|
||
const encryptedBytes = sjcl.codec.bytes.fromBits(encryptedBits)
|
||
return bytesToHex(encryptedBytes)
|
||
} else {
|
||
throw new Error('GCM encryption failed: unexpected result format')
|
||
}
|
||
} else {
|
||
throw new Error(`Unsupported mode: ${mode}`)
|
||
}
|
||
}
|
||
|
||
// SM4 解密
|
||
export const sm4Decrypt = (encrypted: string, key: string, mode: string, iv?: string, paddingMode: 'pkcs7' | 'none' = 'pkcs7'): string => {
|
||
// 验证加密数据是否为有效的 hex 字符串
|
||
if (!isValidHex(encrypted)) {
|
||
throw new Error('Invalid encrypted data: must be hex string')
|
||
}
|
||
// 验证密钥是否为有效的 hex 字符串
|
||
if (!isValidHex(key)) {
|
||
throw new Error('Invalid key: must be hex string')
|
||
}
|
||
// 验证 IV 是否为有效的 hex 字符串(如果提供)
|
||
if (iv && !isValidHex(iv)) {
|
||
throw new Error('Invalid IV: must be hex string')
|
||
}
|
||
|
||
// 根据模式选择解密方式
|
||
const modeLower = mode.toLowerCase()
|
||
if (modeLower === 'ecb') {
|
||
// ECB 模式使用 gm-crypto
|
||
return SM4.decrypt(encrypted, key, {
|
||
mode: 'ecb',
|
||
inputEncoding: 'hex',
|
||
outputEncoding: 'hex',
|
||
padding: paddingMode === 'pkcs7' ? 'pkcs7' : 'none'
|
||
})
|
||
} else if (modeLower === 'cbc') {
|
||
// CBC 模式使用 gmsm-sm4js
|
||
if (!iv) {
|
||
throw new Error('IV is required for CBC mode')
|
||
}
|
||
// 转换为字节数组
|
||
const encryptedBytes = hexToBytes(encrypted)
|
||
const keyBytes = hexToBytes(key)
|
||
|
||
// 创建 SM4 密钥
|
||
const sm4Key = new sjcl.cipher.sm4(sjcl.codec.bytes.toBits(keyBytes))
|
||
|
||
const ivBytes = hexToBytes(iv)
|
||
const ivBits = sjcl.codec.bytes.toBits(ivBytes)
|
||
const encryptedBits = sjcl.codec.bytes.toBits(encryptedBytes)
|
||
const decryptedBits = sjcl.mode.cbc.decrypt(sm4Key, encryptedBits, ivBits)
|
||
const decryptedBytes = sjcl.codec.bytes.fromBits(decryptedBits)
|
||
// 处理去填充
|
||
if (paddingMode === 'pkcs7') {
|
||
return bytesToHex(pkcs7Unpad(decryptedBytes))
|
||
}
|
||
return bytesToHex(decryptedBytes)
|
||
} else if (modeLower === 'gcm') {
|
||
// GCM 模式使用 gmsm-sm4js
|
||
if (!iv) {
|
||
throw new Error('IV is required for GCM mode')
|
||
}
|
||
// 转换为字节数组
|
||
const encryptedBytes = hexToBytes(encrypted)
|
||
const keyBytes = hexToBytes(key)
|
||
|
||
// 创建 SM4 密钥
|
||
const sm4Key = new sjcl.cipher.sm4(sjcl.codec.bytes.toBits(keyBytes))
|
||
|
||
const ivBytes = hexToBytes(iv)
|
||
const ivBits = sjcl.codec.bytes.toBits(ivBytes)
|
||
const encryptedBits = sjcl.codec.bytes.toBits(encryptedBytes)
|
||
// 传递标签长度参数(128 位)
|
||
const decryptedResult = sjcl.mode.gcm.decrypt(sm4Key, encryptedBits, ivBits, sjcl.codec.bytes.toBits([]), 128)
|
||
// 检查 decryptedResult 的类型
|
||
if (!decryptedResult) {
|
||
throw new Error('GCM decryption failed: result is undefined')
|
||
}
|
||
if (Array.isArray(decryptedResult)) {
|
||
// 如果返回的是数组,直接使用
|
||
const decryptedBytes = sjcl.codec.bytes.fromBits(decryptedResult)
|
||
// 处理去填充
|
||
if (paddingMode === 'pkcs7') {
|
||
return bytesToHex(pkcs7Unpad(decryptedBytes))
|
||
}
|
||
return bytesToHex(decryptedBytes)
|
||
} else if (decryptedResult.data) {
|
||
// 如果返回的是对象,使用 data 属性
|
||
const decryptedBits = decryptedResult.data
|
||
const decryptedBytes = sjcl.codec.bytes.fromBits(decryptedBits)
|
||
// 处理去填充
|
||
if (paddingMode === 'pkcs7') {
|
||
return bytesToHex(pkcs7Unpad(decryptedBytes))
|
||
}
|
||
return bytesToHex(decryptedBytes)
|
||
} else {
|
||
throw new Error('GCM decryption failed: unexpected result format')
|
||
}
|
||
} else {
|
||
throw new Error(`Unsupported mode: ${mode}`)
|
||
}
|
||
}
|