Files
plates-server/src/common/encryption.service.ts
2025-08-13 15:17:33 +08:00

105 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Injectable, Logger } from '@nestjs/common';
import * as crypto from 'crypto';
@Injectable()
export class EncryptionService {
private readonly logger = new Logger(EncryptionService.name);
private readonly algorithm = 'aes-256-cbc';
private readonly keyLength = 32; // 256 bits for AES
private readonly hmacKeyLength = 32; // 256 bits for HMAC
private readonly ivLength = 16; // 128 bits for CBC
private readonly hmacLength = 32; // 256 bits for SHA256
// 从环境变量获取密钥,或使用默认密钥(生产环境必须使用环境变量)
private readonly encryptionKey: Buffer;
private readonly hmacKey: Buffer;
constructor() {
const encryptionKeyString = process.env.ENCRYPTION_KEY || 'your-default-32-char-aes-secret-key!';
const hmacKeyString = process.env.HMAC_KEY || 'your-default-32-char-hmac-secret-key';
this.logger.log(`EncryptionService constructor encryptionKeyString: ${encryptionKeyString}`);
this.logger.log(`EncryptionService constructor hmacKeyString: ${hmacKeyString}`);
// 两个独立的密钥
this.encryptionKey = Buffer.from(encryptionKeyString.slice(0, 32).padEnd(32, '0'), 'utf8');
this.hmacKey = Buffer.from(hmacKeyString.slice(0, 32).padEnd(32, '0'), 'utf8');
}
/**
* 加密数据
* @param plaintext 要加密的明文
* @returns 加密后的base64字符串格式iv.ciphertext.hmac
*/
encrypt(plaintext: string): string {
try {
const iv = crypto.randomBytes(this.ivLength);
const cipher = crypto.createCipheriv(this.algorithm, this.encryptionKey, iv);
let ciphertext = cipher.update(plaintext, 'utf8');
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
// 计算 HMAC (对 iv + ciphertext 进行HMAC)
const dataToHmac = Buffer.concat([iv, ciphertext]);
const hmac = crypto.createHmac('sha256', this.hmacKey);
hmac.update(dataToHmac);
const hmacDigest = hmac.digest();
// 组合 iv + ciphertext + hmac 并编码为base64
const combined = Buffer.concat([iv, ciphertext, hmacDigest]);
return combined.toString('base64');
} catch (error) {
throw new Error(`加密失败: ${error.message}`);
}
}
/**
* 解密数据
* @param encryptedData 加密的base64字符串
* @returns 解密后的明文
*/
decrypt(encryptedData: string): string {
try {
const combined = Buffer.from(encryptedData, 'base64');
// 检查数据长度是否足够
if (combined.length < this.ivLength + this.hmacLength) {
throw new Error('加密数据格式无效');
}
// 提取 iv, ciphertext, hmac
const iv = combined.slice(0, this.ivLength);
const hmacDigest = combined.slice(-this.hmacLength);
const ciphertext = combined.slice(this.ivLength, -this.hmacLength);
// 验证 HMAC
const dataToHmac = Buffer.concat([iv, ciphertext]);
const hmac = crypto.createHmac('sha256', this.hmacKey);
hmac.update(dataToHmac);
const expectedHmac = hmac.digest();
if (!crypto.timingSafeEqual(hmacDigest, expectedHmac)) {
throw new Error('HMAC验证失败数据可能被篡改');
}
// 解密
const decipher = crypto.createDecipheriv(this.algorithm, this.encryptionKey, iv);
let plaintext = decipher.update(ciphertext, undefined, 'utf8');
plaintext += decipher.final('utf8');
return plaintext;
} catch (error) {
throw new Error(`解密失败: ${error.message}`);
}
}
/**
* 生成新的加密密钥(用于初始化)
* 返回两个独立的密钥
*/
generateKey(): { encryptionKey: string; hmacKey: string } {
return {
encryptionKey: crypto.randomBytes(this.keyLength).toString('base64'),
hmacKey: crypto.randomBytes(this.hmacKeyLength).toString('base64')
};
}
}