mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
fix(email): enclose PGP encryption logic in try/catch (#2519)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import * as openpgp from 'openpgp';
|
import * as openpgp from 'openpgp';
|
||||||
import { Transform, TransformCallback } from 'stream';
|
import { Transform, TransformCallback } from 'stream';
|
||||||
|
import logger from '../../logger';
|
||||||
|
|
||||||
interface EncryptorOptions {
|
interface EncryptorOptions {
|
||||||
signingKey?: string;
|
signingKey?: string;
|
||||||
@@ -36,133 +37,149 @@ class PGPEncryptor extends Transform {
|
|||||||
|
|
||||||
// Actually do stuff
|
// Actually do stuff
|
||||||
_flush = async (callback: TransformCallback): Promise<void> => {
|
_flush = async (callback: TransformCallback): Promise<void> => {
|
||||||
// Reconstruct message as buffer
|
|
||||||
const message = Buffer.concat(this._messageChunks, this._messageLength);
|
const message = Buffer.concat(this._messageChunks, this._messageLength);
|
||||||
const validPublicKeys = await Promise.all(
|
|
||||||
this._encryptionKeys.map((armoredKey) => openpgp.readKey({ armoredKey }))
|
|
||||||
);
|
|
||||||
let privateKey: openpgp.PrivateKey | undefined;
|
|
||||||
|
|
||||||
// Just return the message if there is no one to encrypt for
|
try {
|
||||||
if (!validPublicKeys.length) {
|
// Reconstruct message as buffer
|
||||||
this.push(message);
|
const validPublicKeys = await Promise.all(
|
||||||
return callback();
|
this._encryptionKeys.map((armoredKey) =>
|
||||||
}
|
openpgp.readKey({ armoredKey })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let privateKey: openpgp.PrivateKey | undefined;
|
||||||
|
|
||||||
// Only sign the message if private key and password exist
|
// Just return the message if there is no one to encrypt for
|
||||||
if (this._signingKey && this._password) {
|
if (!validPublicKeys.length) {
|
||||||
privateKey = await openpgp.decryptKey({
|
this.push(message);
|
||||||
privateKey: await openpgp.readPrivateKey({
|
return callback();
|
||||||
armoredKey: this._signingKey,
|
}
|
||||||
|
|
||||||
|
// Only sign the message if private key and password exist
|
||||||
|
if (this._signingKey && this._password) {
|
||||||
|
privateKey = await openpgp.decryptKey({
|
||||||
|
privateKey: await openpgp.readPrivateKey({
|
||||||
|
armoredKey: this._signingKey,
|
||||||
|
}),
|
||||||
|
passphrase: this._password,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailPartDelimiter = '\r\n\r\n';
|
||||||
|
const messageParts = message.toString().split(emailPartDelimiter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In this loop original headers are split up into two parts,
|
||||||
|
* one for the email that is sent
|
||||||
|
* and one for the encrypted content
|
||||||
|
*/
|
||||||
|
const header = messageParts.shift() as string;
|
||||||
|
const emailHeaders: string[][] = [];
|
||||||
|
const contentHeaders: string[][] = [];
|
||||||
|
const linesInHeader = header.split('\r\n');
|
||||||
|
let previousHeader: string[] = [];
|
||||||
|
for (let i = 0; i < linesInHeader.length; i++) {
|
||||||
|
const line = linesInHeader[i];
|
||||||
|
/**
|
||||||
|
* If it is a multi-line header (current line starts with whitespace)
|
||||||
|
* or it's the first line in the iteration
|
||||||
|
* add the current line with previous header and move on
|
||||||
|
*/
|
||||||
|
if (/^\s/.test(line) || i === 0) {
|
||||||
|
previousHeader.push(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is done to prevent the last header
|
||||||
|
* from being missed
|
||||||
|
*/
|
||||||
|
if (i === linesInHeader.length - 1) {
|
||||||
|
previousHeader.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need to seperate the actual content headers
|
||||||
|
* so that we can add it as a header for the encrypted content
|
||||||
|
* So that the content will be displayed properly after decryption
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
|
||||||
|
) {
|
||||||
|
contentHeaders.push(previousHeader);
|
||||||
|
} else {
|
||||||
|
emailHeaders.push(previousHeader);
|
||||||
|
}
|
||||||
|
previousHeader = [line];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a new boundary for the email content
|
||||||
|
const boundary = 'nm_' + randomBytes(14).toString('hex');
|
||||||
|
/**
|
||||||
|
* Concatenate everything into single strings
|
||||||
|
* and add pgp headers to the email headers
|
||||||
|
*/
|
||||||
|
const emailHeadersRaw =
|
||||||
|
emailHeaders.map((line) => line.join('\r\n')).join('\r\n') +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' +
|
||||||
|
'\r\n' +
|
||||||
|
' boundary="' +
|
||||||
|
boundary +
|
||||||
|
'"' +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Description: OpenPGP encrypted message' +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit';
|
||||||
|
const contentHeadersRaw = contentHeaders
|
||||||
|
.map((line) => line.join('\r\n'))
|
||||||
|
.join('\r\n');
|
||||||
|
|
||||||
|
const encryptedMessage = await openpgp.encrypt({
|
||||||
|
message: await openpgp.createMessage({
|
||||||
|
text:
|
||||||
|
contentHeadersRaw +
|
||||||
|
emailPartDelimiter +
|
||||||
|
messageParts.join(emailPartDelimiter),
|
||||||
}),
|
}),
|
||||||
passphrase: this._password,
|
encryptionKeys: validPublicKeys,
|
||||||
|
signingKeys: privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const body =
|
||||||
|
'--' +
|
||||||
|
boundary +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: application/pgp-encrypted\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'Version: 1\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
'--' +
|
||||||
|
boundary +
|
||||||
|
'\r\n' +
|
||||||
|
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
|
||||||
|
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
|
||||||
|
'Content-Transfer-Encoding: 7bit\r\n' +
|
||||||
|
'\r\n' +
|
||||||
|
encryptedMessage +
|
||||||
|
'\r\n--' +
|
||||||
|
boundary +
|
||||||
|
'--\r\n';
|
||||||
|
|
||||||
|
this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body));
|
||||||
|
callback();
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(
|
||||||
|
'Something went wrong while encrypting email message with OpenPGP. Sending email without encryption',
|
||||||
|
{
|
||||||
|
label: 'Notifications',
|
||||||
|
errorMessage: e.message,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.push(message);
|
||||||
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailPartDelimiter = '\r\n\r\n';
|
|
||||||
const messageParts = message.toString().split(emailPartDelimiter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* In this loop original headers are split up into two parts,
|
|
||||||
* one for the email that is sent
|
|
||||||
* and one for the encrypted content
|
|
||||||
*/
|
|
||||||
const header = messageParts.shift() as string;
|
|
||||||
const emailHeaders: string[][] = [];
|
|
||||||
const contentHeaders: string[][] = [];
|
|
||||||
const linesInHeader = header.split('\r\n');
|
|
||||||
let previousHeader: string[] = [];
|
|
||||||
for (let i = 0; i < linesInHeader.length; i++) {
|
|
||||||
const line = linesInHeader[i];
|
|
||||||
/**
|
|
||||||
* If it is a multi-line header (current line starts with whitespace)
|
|
||||||
* or it's the first line in the iteration
|
|
||||||
* add the current line with previous header and move on
|
|
||||||
*/
|
|
||||||
if (/^\s/.test(line) || i === 0) {
|
|
||||||
previousHeader.push(line);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is done to prevent the last header
|
|
||||||
* from being missed
|
|
||||||
*/
|
|
||||||
if (i === linesInHeader.length - 1) {
|
|
||||||
previousHeader.push(line);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We need to seperate the actual content headers
|
|
||||||
* so that we can add it as a header for the encrypted content
|
|
||||||
* So that the content will be displayed properly after decryption
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
|
|
||||||
) {
|
|
||||||
contentHeaders.push(previousHeader);
|
|
||||||
} else {
|
|
||||||
emailHeaders.push(previousHeader);
|
|
||||||
}
|
|
||||||
previousHeader = [line];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a new boundary for the email content
|
|
||||||
const boundary = 'nm_' + randomBytes(14).toString('hex');
|
|
||||||
/**
|
|
||||||
* Concatenate everything into single strings
|
|
||||||
* and add pgp headers to the email headers
|
|
||||||
*/
|
|
||||||
const emailHeadersRaw =
|
|
||||||
emailHeaders.map((line) => line.join('\r\n')).join('\r\n') +
|
|
||||||
'\r\n' +
|
|
||||||
'Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";' +
|
|
||||||
'\r\n' +
|
|
||||||
' boundary="' +
|
|
||||||
boundary +
|
|
||||||
'"' +
|
|
||||||
'\r\n' +
|
|
||||||
'Content-Description: OpenPGP encrypted message' +
|
|
||||||
'\r\n' +
|
|
||||||
'Content-Transfer-Encoding: 7bit';
|
|
||||||
const contentHeadersRaw = contentHeaders
|
|
||||||
.map((line) => line.join('\r\n'))
|
|
||||||
.join('\r\n');
|
|
||||||
|
|
||||||
const encryptedMessage = await openpgp.encrypt({
|
|
||||||
message: await openpgp.createMessage({
|
|
||||||
text:
|
|
||||||
contentHeadersRaw +
|
|
||||||
emailPartDelimiter +
|
|
||||||
messageParts.join(emailPartDelimiter),
|
|
||||||
}),
|
|
||||||
encryptionKeys: validPublicKeys,
|
|
||||||
signingKeys: privateKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const body =
|
|
||||||
'--' +
|
|
||||||
boundary +
|
|
||||||
'\r\n' +
|
|
||||||
'Content-Type: application/pgp-encrypted\r\n' +
|
|
||||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
|
||||||
'\r\n' +
|
|
||||||
'Version: 1\r\n' +
|
|
||||||
'\r\n' +
|
|
||||||
'--' +
|
|
||||||
boundary +
|
|
||||||
'\r\n' +
|
|
||||||
'Content-Type: application/octet-stream; name=encrypted.asc\r\n' +
|
|
||||||
'Content-Disposition: inline; filename=encrypted.asc\r\n' +
|
|
||||||
'Content-Transfer-Encoding: 7bit\r\n' +
|
|
||||||
'\r\n' +
|
|
||||||
encryptedMessage +
|
|
||||||
'\r\n--' +
|
|
||||||
boundary +
|
|
||||||
'--\r\n';
|
|
||||||
|
|
||||||
this.push(Buffer.from(emailHeadersRaw + emailPartDelimiter + body));
|
|
||||||
callback();
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user