105 lines
3.8 KiB
TypeScript
105 lines
3.8 KiB
TypeScript
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')
|
||
};
|
||
}
|
||
}
|