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

This commit is contained in:
2026-05-06 20:10:20 +02:00
parent 609e4beab2
commit 8212f733ab
3 changed files with 46 additions and 5 deletions
+1
View File
@@ -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 ?? '',
};
}