fix(email): enclose PGP encryption logic in try/catch (#2519)

pull/2511/head
TheCatLady 2 years ago committed by GitHub
parent 61681857b1
commit a76b608ab7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,7 @@
import { randomBytes } from 'crypto';
import * as openpgp from 'openpgp';
import { Transform, TransformCallback } from 'stream';
import logger from '../../logger';
interface EncryptorOptions {
signingKey?: string;
@ -36,133 +37,149 @@ class PGPEncryptor extends Transform {
// Actually do stuff
_flush = async (callback: TransformCallback): Promise<void> => {
// Reconstruct message as buffer
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
if (!validPublicKeys.length) {
this.push(message);
return callback();
}
// 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,
});
}
try {
// Reconstruct message as buffer
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
if (!validPublicKeys.length) {
this.push(message);
return 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;
// 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);
/**
* This is done to prevent the last header
* from being missed
* In this loop original headers are split up into two parts,
* one for the email that is sent
* and one for the encrypted content
*/
if (i === linesInHeader.length - 1) {
previousHeader.push(line);
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');
/**
* 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
* Concatenate everything into single strings
* and add pgp headers to the email headers
*/
if (
/^(content-type|content-transfer-encoding):/i.test(previousHeader[0])
) {
contentHeaders.push(previousHeader);
} else {
emailHeaders.push(previousHeader);
}
previousHeader = [line];
}
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,
});
// 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();
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();
}
};
}

Loading…
Cancel
Save