From 616abd236412f50ab19f9dc8043a8f76849215cd Mon Sep 17 00:00:00 2001 From: bvbej Date: Sat, 7 Feb 2026 15:38:01 +0800 Subject: [PATCH] =?UTF-8?q?[=F0=9F=9A=80]=20mysql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/crypto/client/axios/encryptedAxios.ts | 202 ++++++++++++++++++++++ pkg/crypto/client/crypto/aes.ts | 90 ++++++++++ pkg/crypto/client/crypto/hmac.ts | 54 ++++++ pkg/crypto/client/crypto/interface.ts | 50 ++++++ pkg/crypto/client/crypto/rsa.ts | 125 +++++++++++++ pkg/crypto/client/index.ts | 7 + pkg/crypto/client/utils/base64.ts | 38 ++++ pkg/crypto/client/utils/uuid.ts | 10 ++ 8 files changed, 576 insertions(+) create mode 100644 pkg/crypto/client/axios/encryptedAxios.ts create mode 100644 pkg/crypto/client/crypto/aes.ts create mode 100644 pkg/crypto/client/crypto/hmac.ts create mode 100644 pkg/crypto/client/crypto/interface.ts create mode 100644 pkg/crypto/client/crypto/rsa.ts create mode 100644 pkg/crypto/client/index.ts create mode 100644 pkg/crypto/client/utils/base64.ts create mode 100644 pkg/crypto/client/utils/uuid.ts diff --git a/pkg/crypto/client/axios/encryptedAxios.ts b/pkg/crypto/client/axios/encryptedAxios.ts new file mode 100644 index 0000000..ab0c922 --- /dev/null +++ b/pkg/crypto/client/axios/encryptedAxios.ts @@ -0,0 +1,202 @@ +import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; +import { IEncryptor, ISigner, CryptoConfig, EncryptedRequest, EncryptedResponse } from '../crypto/interface'; +import { uint8ArrayToBase64, base64ToUint8Array, stringToUint8Array, uint8ArrayToString } from '../utils/base64'; +import { generateUUID } from '../utils/uuid'; + +/** + * 加密Axios实例 + */ +export class EncryptedAxios { + private axiosInstance: AxiosInstance; + private encryptor: IEncryptor | null = null; + private signer: ISigner | null = null; + private config: CryptoConfig; + + constructor( + encryptor?: IEncryptor, + signer?: ISigner, + config: CryptoConfig = {}, + axiosConfig?: AxiosRequestConfig + ) { + this.encryptor = encryptor || null; + this.signer = signer || null; + this.config = { + timestampWindow: 5 * 60 * 1000, // 默认5分钟 + enableTimestamp: true, + enableSignature: true, + ...config, + }; + + // 创建axios实例 + this.axiosInstance = axios.create(axiosConfig); + + // 添加请求拦截器 + this.axiosInstance.interceptors.request.use( + this.encryptRequestInterceptor.bind(this), + (error) => Promise.reject(error) + ); + + // 添加响应拦截器 + this.axiosInstance.interceptors.response.use( + this.decryptResponseInterceptor.bind(this), + (error) => Promise.reject(error) + ); + } + + /** + * 请求拦截器 - 加密请求数据 + */ + private async encryptRequestInterceptor( + config: InternalAxiosRequestConfig + ): Promise { + // 放行GET和OPTIONS请求 + if (config.method?.toUpperCase() === 'GET' || config.method?.toUpperCase() === 'OPTIONS') { + return config; + } + + // 如果没有配置加密器,直接返回 + if (!this.encryptor) { + return config; + } + + try { + // 将请求数据转换为JSON字符串 + const plaintext = JSON.stringify(config.data || {}); + const plaintextBytes = stringToUint8Array(plaintext); + + // 加密数据 + const ciphertext = await this.encryptor.encrypt(plaintextBytes); + const encryptedData = uint8ArrayToBase64(ciphertext); + + // 构建加密请求体 + const encryptedRequest: EncryptedRequest = { + data: encryptedData, + timestamp: Date.now(), + request_id: generateUUID(), + algorithm: this.encryptor.name(), + }; + + // 生成签名 + if (this.config.enableSignature && this.signer) { + const signature = await this.signer.sign(plaintextBytes); + encryptedRequest.signature = uint8ArrayToBase64(signature); + } + + // 替换请求数据 + config.data = encryptedRequest; + config.headers['Content-Type'] = 'application/json'; + + // 保存request_id供响应使用 + config.headers['X-Request-ID'] = encryptedRequest.request_id; + + } catch (error) { + console.error('加密请求失败:', error); + throw error; + } + + return config; + } + + /** + * 响应拦截器 - 解密响应数据 + */ + private async decryptResponseInterceptor( + response: AxiosResponse + ): Promise { + // 如果没有配置加密器或响应不是加密格式,直接返回 + if (!this.encryptor || !response.data || typeof response.data !== 'object') { + return response; + } + + // 检查是否是加密响应 + const encryptedResponse = response.data as EncryptedResponse; + if (!encryptedResponse.data || !encryptedResponse.request_id) { + // 不是加密响应,直接返回 + return response; + } + + try { + // 验证时间戳 + if (this.config.enableTimestamp) { + this.verifyTimestamp(encryptedResponse.timestamp); + } + + // 解密数据 + const ciphertext = base64ToUint8Array(encryptedResponse.data); + const plaintext = await this.encryptor.decrypt(ciphertext); + + // 验证签名 + if (this.config.enableSignature && this.signer && encryptedResponse.signature) { + const signature = base64ToUint8Array(encryptedResponse.signature); + const isValid = await this.signer.verify(plaintext, signature); + if (!isValid) { + throw new Error('签名验证失败'); + } + } + + // 将解密后的数据转换为JSON对象 + const decryptedData = uint8ArrayToString(plaintext); + response.data = JSON.parse(decryptedData); + + } catch (error) { + console.error('解密响应失败:', error); + throw error; + } + + return response; + } + + /** + * 验证时间戳 + */ + private verifyTimestamp(timestamp: number): void { + const now = Date.now(); + const diff = Math.abs(now - timestamp); + + if (diff > (this.config.timestampWindow || 5 * 60 * 1000)) { + throw new Error('请求超时'); + } + } + + /** + * 获取axios实例 + */ + getInstance(): AxiosInstance { + return this.axiosInstance; + } + + /** + * GET 请求 + */ + get(url: string, config?: AxiosRequestConfig): Promise> { + return this.axiosInstance.get(url, config); + } + + /** + * POST 请求 + */ + post(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + return this.axiosInstance.post(url, data, config); + } + + /** + * PUT 请求 + */ + put(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + return this.axiosInstance.put(url, data, config); + } + + /** + * DELETE 请求 + */ + delete(url: string, config?: AxiosRequestConfig): Promise> { + return this.axiosInstance.delete(url, config); + } + + /** + * PATCH 请求 + */ + patch(url: string, data?: any, config?: AxiosRequestConfig): Promise> { + return this.axiosInstance.patch(url, data, config); + } +} diff --git a/pkg/crypto/client/crypto/aes.ts b/pkg/crypto/client/crypto/aes.ts new file mode 100644 index 0000000..5fad134 --- /dev/null +++ b/pkg/crypto/client/crypto/aes.ts @@ -0,0 +1,90 @@ +import { IEncryptor } from './interface'; + +/** + * AES-GCM加密器 + */ +export class AESEncryptor implements IEncryptor { + private key: CryptoKey | null = null; + + constructor(keyString: string) { + this.importKey(keyString); + } + + /** + * 导入密钥 + */ + private async importKey(keyString: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(keyString.padEnd(32, '0').substring(0, 32)); + + this.key = await crypto.subtle.importKey( + 'raw', + keyData, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'] + ); + } + + /** + * 加密数据 + */ + async encrypt(plaintext: Uint8Array): Promise { + if (!this.key) { + throw new Error('密钥未设置'); + } + + // 生成随机IV + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv, + }, + this.key, + plaintext + ); + + // 将IV和密文拼接在一起 + const result = new Uint8Array(iv.length + encrypted.byteLength); + result.set(iv, 0); + result.set(new Uint8Array(encrypted), iv.length); + + return result; + } + + /** + * 解密数据 + */ + async decrypt(ciphertext: Uint8Array): Promise { + if (!this.key) { + throw new Error('密钥未设置'); + } + + // 提取IV + const iv = ciphertext.slice(0, 12); + const data = ciphertext.slice(12); + + const decrypted = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: iv, + }, + this.key, + data + ); + + return new Uint8Array(decrypted); + } + + /** + * 返回算法名称 + */ + name(): string { + return 'AES-GCM-256'; + } +} diff --git a/pkg/crypto/client/crypto/hmac.ts b/pkg/crypto/client/crypto/hmac.ts new file mode 100644 index 0000000..ab1b8aa --- /dev/null +++ b/pkg/crypto/client/crypto/hmac.ts @@ -0,0 +1,54 @@ +import { ISigner } from './interface'; + +/** + * HMAC签名器 + */ +export class HMACSigner implements ISigner { + private key: CryptoKey | null = null; + + constructor(keyString: string) { + this.importKey(keyString); + } + + /** + * 导入密钥 + */ + private async importKey(keyString: string): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(keyString); + + this.key = await crypto.subtle.importKey( + 'raw', + keyData, + { + name: 'HMAC', + hash: 'SHA-256', + }, + false, + ['sign', 'verify'] + ); + } + + /** + * 生成签名 + */ + async sign(data: Uint8Array): Promise { + if (!this.key) { + throw new Error('密钥未设置'); + } + + const signature = await crypto.subtle.sign('HMAC', this.key, data); + return new Uint8Array(signature); + } + + /** + * 验证签名 + */ + async verify(data: Uint8Array, signature: Uint8Array): Promise { + if (!this.key) { + throw new Error('密钥未设置'); + } + + return await crypto.subtle.verify('HMAC', this.key, signature, data); + } +} diff --git a/pkg/crypto/client/crypto/interface.ts b/pkg/crypto/client/crypto/interface.ts new file mode 100644 index 0000000..5ae1cff --- /dev/null +++ b/pkg/crypto/client/crypto/interface.ts @@ -0,0 +1,50 @@ +/** + * 加密器接口 + */ +export interface IEncryptor { + encrypt(plaintext: Uint8Array): Promise; + decrypt(ciphertext: Uint8Array): Promise; + name(): string; +} + +/** + * 签名器接口 + */ +export interface ISigner { + sign(data: Uint8Array): Promise; + verify(data: Uint8Array, signature: Uint8Array): Promise; +} + +/** + * 配置选项 + */ +export interface CryptoConfig { + secretKey?: string; // 对称加密密钥 + signKey?: string; // 签名密钥 + publicKey?: string; // RSA公钥(PEM格式) + privateKey?: string; // RSA私钥(PEM格式) + timestampWindow?: number; // 时间戳窗口(毫秒) + enableTimestamp?: boolean; // 是否启用时间戳验证 + enableSignature?: boolean; // 是否启用签名 +} + +/** + * 加密请求体 + */ +export interface EncryptedRequest { + data: string; // Base64编码的加密数据 + signature?: string; // Base64编码的签名 + timestamp: number; // 时间戳 + request_id: string; // 请求ID + algorithm: string; // 加密算法名称 +} + +/** + * 加密响应体 + */ +export interface EncryptedResponse { + data: string; // Base64编码的加密数据 + signature?: string; // Base64编码的签名 + timestamp: number; // 时间戳 + request_id: string; // 请求ID +} diff --git a/pkg/crypto/client/crypto/rsa.ts b/pkg/crypto/client/crypto/rsa.ts new file mode 100644 index 0000000..80c6d42 --- /dev/null +++ b/pkg/crypto/client/crypto/rsa.ts @@ -0,0 +1,125 @@ +import { IEncryptor } from './interface'; + +/** + * RSA加密器(使用Web Crypto API) + */ +export class RSAEncryptor implements IEncryptor { + private publicKey: CryptoKey | null = null; + private privateKey: CryptoKey | null = null; + + constructor(publicKeyPEM?: string, privateKeyPEM?: string) { + if (publicKeyPEM) { + this.importPublicKey(publicKeyPEM); + } + if (privateKeyPEM) { + this.importPrivateKey(privateKeyPEM); + } + } + + /** + * 导入公钥(PEM格式) + */ + private async importPublicKey(pem: string): Promise { + const pemHeader = '-----BEGIN PUBLIC KEY-----'; + const pemFooter = '-----END PUBLIC KEY-----'; + const pemContents = pem + .replace(pemHeader, '') + .replace(pemFooter, '') + .replace(/\s/g, ''); + + const binaryDer = this.base64ToArrayBuffer(pemContents); + + this.publicKey = await crypto.subtle.importKey( + 'spki', + binaryDer, + { + name: 'RSA-OAEP', + hash: 'SHA-256', + }, + true, + ['encrypt'] + ); + } + + /** + * 导入私钥(PEM格式) + */ + private async importPrivateKey(pem: string): Promise { + const pemHeader = '-----BEGIN PRIVATE KEY-----'; + const pemFooter = '-----END PRIVATE KEY-----'; + const pemContents = pem + .replace(pemHeader, '') + .replace(pemFooter, '') + .replace(/\s/g, ''); + + const binaryDer = this.base64ToArrayBuffer(pemContents); + + this.privateKey = await crypto.subtle.importKey( + 'pkcs8', + binaryDer, + { + name: 'RSA-OAEP', + hash: 'SHA-256', + }, + true, + ['decrypt'] + ); + } + + /** + * Base64转ArrayBuffer + */ + private base64ToArrayBuffer(base64: string): ArrayBuffer { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } + + /** + * 加密数据 + */ + async encrypt(plaintext: Uint8Array): Promise { + if (!this.publicKey) { + throw new Error('公钥未设置'); + } + + const encrypted = await crypto.subtle.encrypt( + { + name: 'RSA-OAEP', + }, + this.publicKey, + plaintext + ); + + return new Uint8Array(encrypted); + } + + /** + * 解密数据 + */ + async decrypt(ciphertext: Uint8Array): Promise { + if (!this.privateKey) { + throw new Error('私钥未设置'); + } + + const decrypted = await crypto.subtle.decrypt( + { + name: 'RSA-OAEP', + }, + this.privateKey, + ciphertext + ); + + return new Uint8Array(decrypted); + } + + /** + * 返回算法名称 + */ + name(): string { + return 'RSA-OAEP-SHA256'; + } +} diff --git a/pkg/crypto/client/index.ts b/pkg/crypto/client/index.ts new file mode 100644 index 0000000..d70ce1f --- /dev/null +++ b/pkg/crypto/client/index.ts @@ -0,0 +1,7 @@ +export * from './crypto/interface'; +export * from './crypto/rsa'; +export * from './crypto/hmac'; +export * from './crypto/aes'; +export * from './axios/encryptedAxios'; +export * from './utils/base64'; +export * from './utils/uuid'; diff --git a/pkg/crypto/client/utils/base64.ts b/pkg/crypto/client/utils/base64.ts new file mode 100644 index 0000000..2e9108d --- /dev/null +++ b/pkg/crypto/client/utils/base64.ts @@ -0,0 +1,38 @@ +/** + * Uint8Array 转 Base64 + */ +export function uint8ArrayToBase64(bytes: Uint8Array): string { + let binary = ''; + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); +} + +/** + * Base64 转 Uint8Array + */ +export function base64ToUint8Array(base64: string): Uint8Array { + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} + +/** + * 字符串转 Uint8Array + */ +export function stringToUint8Array(str: string): Uint8Array { + const encoder = new TextEncoder(); + return encoder.encode(str); +} + +/** + * Uint8Array 转字符串 + */ +export function uint8ArrayToString(bytes: Uint8Array): string { + const decoder = new TextDecoder(); + return decoder.decode(bytes); +} diff --git a/pkg/crypto/client/utils/uuid.ts b/pkg/crypto/client/utils/uuid.ts new file mode 100644 index 0000000..fff1fa6 --- /dev/null +++ b/pkg/crypto/client/utils/uuid.ts @@ -0,0 +1,10 @@ +/** + * 生成UUID v4 + */ +export function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}