feat: implement AES-256-GCM encryption for SMTP passwords with environment-based key support
Build and Push Multi-Platform Images / build-and-push (push) Successful in 27s
Build and Push Multi-Platform Images / build-and-push (push) Successful in 27s
This commit is contained in:
@@ -27,6 +27,7 @@ services:
|
||||
- SMTP_USER=${SMTP_USER:-}
|
||||
- SMTP_PASS=${SMTP_PASS:-}
|
||||
- SMTP_FROM=${SMTP_FROM:-paperless@localhost}
|
||||
- SMTP_ENCRYPTION_KEY=${SMTP_ENCRYPTION_KEY:-}
|
||||
- POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0}
|
||||
- MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6}
|
||||
- IMAP_HOST=${IMAP_HOST:-}
|
||||
|
||||
@@ -17,7 +17,7 @@ export class UserSettings {
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
SmtpUser!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
@Column({ type: 'varchar', length: 512, nullable: true })
|
||||
SmtpPass!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Repository } from 'typeorm';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
import * as crypto from 'crypto';
|
||||
import { UserSettings } from '../database/entities/user-settings.entity';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
|
||||
export interface UserSettingsDto {
|
||||
smtpHost: string | null;
|
||||
smtpPort: number | null;
|
||||
@@ -16,10 +20,46 @@ export interface UserSettingsDto {
|
||||
|
||||
@Injectable()
|
||||
export class UserSettingsService {
|
||||
private readonly logger = new Logger(UserSettingsService.name);
|
||||
private readonly encKey: Buffer | null;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(UserSettings)
|
||||
private readonly repo: Repository<UserSettings>,
|
||||
) {}
|
||||
private readonly configService: ConfigService,
|
||||
) {
|
||||
const raw = this.configService.get<string>('SMTP_ENCRYPTION_KEY', '');
|
||||
if (raw && raw.length === 64) {
|
||||
this.encKey = Buffer.from(raw, 'hex');
|
||||
} else {
|
||||
this.encKey = null;
|
||||
this.logger.warn(
|
||||
'SMTP_ENCRYPTION_KEY fehlt oder hat falsche Länge (64 Hex-Zeichen = 32 Bytes erforderlich). Passwörter werden im Klartext gespeichert.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private encrypt(plaintext: string): string {
|
||||
if (!this.encKey) return plaintext;
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, this.encKey, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
return `enc:${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted.toString('hex')}`;
|
||||
}
|
||||
|
||||
private decrypt(stored: string): string {
|
||||
if (!this.encKey || !stored.startsWith('enc:')) return stored;
|
||||
const parts = stored.split(':');
|
||||
if (parts.length !== 4) return stored;
|
||||
const [, ivHex, authTagHex, encryptedHex] = parts;
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const authTag = Buffer.from(authTagHex, 'hex');
|
||||
const encrypted = Buffer.from(encryptedHex, 'hex');
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, this.encKey, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
||||
}
|
||||
|
||||
async getSettings(userId: string): Promise<UserSettingsDto> {
|
||||
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
||||
@@ -48,7 +88,7 @@ export class UserSettingsService {
|
||||
if (data.smtpSecure !== undefined) entity.SmtpSecure = data.smtpSecure;
|
||||
if (data.smtpUser !== undefined) entity.SmtpUser = data.smtpUser;
|
||||
if (data.smtpPass !== undefined && data.smtpPass !== null && data.smtpPass !== '') {
|
||||
entity.SmtpPass = data.smtpPass;
|
||||
entity.SmtpPass = this.encrypt(data.smtpPass);
|
||||
}
|
||||
if (data.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
||||
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
||||
@@ -88,7 +128,7 @@ export class UserSettingsService {
|
||||
port: entity.SmtpPort ?? 587,
|
||||
secure: entity.SmtpSecure,
|
||||
user: entity.SmtpUser ?? '',
|
||||
pass: entity.SmtpPass,
|
||||
pass: this.decrypt(entity.SmtpPass),
|
||||
from: entity.SmtpFrom ?? entity.SmtpUser ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user