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_USER=${SMTP_USER:-}
|
||||||
- SMTP_PASS=${SMTP_PASS:-}
|
- SMTP_PASS=${SMTP_PASS:-}
|
||||||
- SMTP_FROM=${SMTP_FROM:-paperless@localhost}
|
- SMTP_FROM=${SMTP_FROM:-paperless@localhost}
|
||||||
|
- SMTP_ENCRYPTION_KEY=${SMTP_ENCRYPTION_KEY:-}
|
||||||
- POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0}
|
- POSTPROCESSING_ERROR_TAG=${POSTPROCESSING_ERROR_TAG:-0}
|
||||||
- MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6}
|
- MANUELL_BEARBEITEN_TAG=${MANUELL_BEARBEITEN_TAG:-6}
|
||||||
- IMAP_HOST=${IMAP_HOST:-}
|
- IMAP_HOST=${IMAP_HOST:-}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export class UserSettings {
|
|||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||||
SmtpUser!: string | null;
|
SmtpUser!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
@Column({ type: 'varchar', length: 512, nullable: true })
|
||||||
SmtpPass!: string | null;
|
SmtpPass!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
@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 { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import * as nodemailer from 'nodemailer';
|
import * as nodemailer from 'nodemailer';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
import { UserSettings } from '../database/entities/user-settings.entity';
|
import { UserSettings } from '../database/entities/user-settings.entity';
|
||||||
|
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
|
||||||
export interface UserSettingsDto {
|
export interface UserSettingsDto {
|
||||||
smtpHost: string | null;
|
smtpHost: string | null;
|
||||||
smtpPort: number | null;
|
smtpPort: number | null;
|
||||||
@@ -16,10 +20,46 @@ export interface UserSettingsDto {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserSettingsService {
|
export class UserSettingsService {
|
||||||
|
private readonly logger = new Logger(UserSettingsService.name);
|
||||||
|
private readonly encKey: Buffer | null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(UserSettings)
|
@InjectRepository(UserSettings)
|
||||||
private readonly repo: Repository<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> {
|
async getSettings(userId: string): Promise<UserSettingsDto> {
|
||||||
const entity = await this.repo.findOne({ where: { UserId: userId } });
|
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.smtpSecure !== undefined) entity.SmtpSecure = data.smtpSecure;
|
||||||
if (data.smtpUser !== undefined) entity.SmtpUser = data.smtpUser;
|
if (data.smtpUser !== undefined) entity.SmtpUser = data.smtpUser;
|
||||||
if (data.smtpPass !== undefined && data.smtpPass !== null && data.smtpPass !== '') {
|
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.smtpFrom !== undefined) entity.SmtpFrom = data.smtpFrom;
|
||||||
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
if (data.mailSignatureHtml !== undefined) entity.MailSignatureHtml = data.mailSignatureHtml;
|
||||||
@@ -88,7 +128,7 @@ export class UserSettingsService {
|
|||||||
port: entity.SmtpPort ?? 587,
|
port: entity.SmtpPort ?? 587,
|
||||||
secure: entity.SmtpSecure,
|
secure: entity.SmtpSecure,
|
||||||
user: entity.SmtpUser ?? '',
|
user: entity.SmtpUser ?? '',
|
||||||
pass: entity.SmtpPass,
|
pass: this.decrypt(entity.SmtpPass),
|
||||||
from: entity.SmtpFrom ?? entity.SmtpUser ?? '',
|
from: entity.SmtpFrom ?? entity.SmtpUser ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user