Skip to content

Commit f4784d5

Browse files
TaaviENickOvt
andauthored
feat: S/MIME at-rest encryption support (#1011)
* S/MIME at-rest encryption support * Add ASN1 reference in smime.js * Minor fixes * Reformat smime.js * Always ensure MIME-Version header exists in outer headers * Unwrap result for zonemta-wildduck legacy shim * Do PKCS#1 v1.5 padding support check earlier during init * Generalize log messages where applicable * Generalize cipher selection * Fix typo in consts.js, text cleanup * Fix linter warning * Fix excessive logging * Avoid excessive DB queries * Improve docstrings * Move S/MIME into a library * update package-lock * bump deps --------- Co-authored-by: Nikolai Ovtsinnikov <nikolai@zone.ee>
1 parent 2484db1 commit f4784d5

18 files changed

Lines changed: 2131 additions & 611 deletions

lib/api/messages.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
5959
const putMessage = util.promisify(messageHandler.put.bind(messageHandler));
6060
const updateMessage = util.promisify(messageHandler.update.bind(messageHandler));
6161

62-
const encryptMessage = util.promisify(messageHandler.encryptMessage.bind(messageHandler));
6362

6463
const getMailboxCounter = tools.getMailboxCounter;
6564
const asyncForward = util.promisify(forward);
@@ -2543,12 +2542,29 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
25432542
if ((userData.encryptMessages || mailboxData.encryptMessages) && !result.value.draft) {
25442543
// encrypt message if global encryption ON or encrypted target mailbox
25452544
try {
2546-
let encrypted = await encryptMessage(userData.pubKey, raw);
2547-
if (encrypted) {
2548-
raw = encrypted;
2545+
let encryptResult = await messageHandler.encryptMessageAsync(tools.getUserEncryptionKey(userData), raw);
2546+
if (encryptResult) {
2547+
raw = encryptResult.raw;
2548+
} else {
2549+
log.error('ENCRYPT', 'Encryption returned false for user %s, message stored unencrypted (source: api_messages)', user);
2550+
server.loggelf({
2551+
short_message: '[ENCRYPTSKIP] Encryption returned false during API message upload',
2552+
_mail_action: 'encrypt_skip',
2553+
_user: user,
2554+
_ip: result.value.ip || req.remoteAddress,
2555+
_source: 'api_messages'
2556+
});
25492557
}
25502558
} catch (err) {
2551-
// ignore
2559+
server.loggelf({
2560+
short_message: '[ENCRYPTFAIL] Encryption failed during API message upload',
2561+
_mail_action: 'encrypt_fail',
2562+
_user: user,
2563+
_error: err.message,
2564+
_code: err.code || 'EncryptionError',
2565+
_ip: result.value.ip || req.remoteAddress,
2566+
_source: 'api_messages'
2567+
});
25522568
}
25532569
}
25542570

lib/api/submit.js

Lines changed: 69 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
5656
recipients: true,
5757
encryptMessages: true,
5858
pubKey: true,
59+
smimeCerts: true,
60+
smimeCipher: true,
61+
smimeKeyTransport: true,
5962
disabled: true,
6063
suspended: true,
6164
fromWhitelist: true,
@@ -529,56 +532,79 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
529532
}
530533

531534
// Checks if the message needs to be encrypted before storing it
532-
messageHandler.encryptMessage(
533-
userData.encryptMessages ? userData.pubKey : false,
534-
{ chunks: collector.chunks, chunklen: collector.chunklen },
535-
(err, encrypted) => {
536-
let raw = false;
537-
if (!err && encrypted) {
538-
// message was encrypted, so use the result instead of raw
539-
raw = encrypted;
540-
}
541-
542-
let meta = {
543-
source: 'API',
544-
from: compiledEnvelope.from,
545-
to: compiledEnvelope.to,
546-
origin: options.ip,
547-
sess: options.sess,
548-
time: new Date()
549-
};
550-
551-
if (options.meta) {
552-
Object.keys(options.meta || {}).forEach(key => {
553-
if (!(key in meta)) {
554-
meta[key] = options.meta[key];
555-
}
535+
(async () => {
536+
let raw = false;
537+
try {
538+
let encryptResult = await messageHandler.encryptMessageAsync(
539+
userData.encryptMessages ? tools.getUserEncryptionKey(userData) : false,
540+
{ chunks: collector.chunks, chunklen: collector.chunklen }
541+
);
542+
if (encryptResult) {
543+
raw = encryptResult.raw;
544+
} else {
545+
log.error('ENCRYPT', 'Encryption returned false for user %s, message stored unencrypted (source: api_submit)', userData._id);
546+
server.loggelf({
547+
short_message: '[ENCRYPTFAIL] Encryption returned false during API submit',
548+
_mail_action: 'encrypt_skip',
549+
_user: userData._id,
550+
_ip: options.ip,
551+
_sess: options.sess,
552+
_source: 'api_submit'
556553
});
557554
}
555+
} catch (err) {
556+
server.loggelf({
557+
short_message: '[ENCRYPTFAIL] Encryption failed during API submit',
558+
_mail_action: 'encrypt_fail',
559+
_user: userData._id,
560+
_error: err.message,
561+
_code: err.code || 'EncryptionError',
562+
_ip: options.ip,
563+
_sess: options.sess,
564+
_source: 'api_submit'
565+
});
566+
}
558567

559-
let messageOptions = {
560-
user: userData._id,
561-
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox
562-
? new ObjectId(options.mailbox)
563-
: options.isDraft
564-
? '\\Drafts'
565-
: '\\Sent',
568+
let meta = {
569+
source: 'API',
570+
from: compiledEnvelope.from,
571+
to: compiledEnvelope.to,
572+
origin: options.ip,
573+
sess: options.sess,
574+
time: new Date()
575+
};
576+
577+
if (options.meta) {
578+
Object.keys(options.meta || {}).forEach(key => {
579+
if (!(key in meta)) {
580+
meta[key] = options.meta[key];
581+
}
582+
});
583+
}
566584

567-
outbound,
585+
let messageOptions = {
586+
user: userData._id,
587+
[options.mailbox ? 'mailbox' : 'specialUse']: options.mailbox
588+
? new ObjectId(options.mailbox)
589+
: options.isDraft
590+
? '\\Drafts'
591+
: '\\Sent',
568592

569-
meta,
593+
outbound,
570594

571-
date: false,
572-
flags: ['\\Seen'].concat(options.isDraft ? '\\Draft' : [])
573-
};
595+
meta,
574596

575-
if (raw) {
576-
messageOptions.raw = raw;
577-
} else {
578-
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
579-
}
597+
date: false,
598+
flags: ['\\Seen'].concat(options.isDraft ? '\\Draft' : [])
599+
};
580600

581-
messageHandler.add(messageOptions, (err, success, info) => {
601+
if (raw) {
602+
messageOptions.raw = raw;
603+
} else {
604+
messageOptions.raw = Buffer.concat(collector.chunks, collector.chunklen);
605+
}
606+
607+
messageHandler.add(messageOptions, (err, success, info) => {
582608
if (err) {
583609
log.error('API', 'SUBMITFAIL user=%s error=%s', user, err.message);
584610
err.responseCode = 500;
@@ -621,8 +647,7 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
621647
}
622648
done();
623649
});
624-
}
625-
);
650+
})();
626651
});
627652
});
628653
});

0 commit comments

Comments
 (0)