Use nodemailer-openpgp plugin to encrypt verification emails

This commit is contained in:
Tankred Hase 2016-06-02 16:19:54 +02:00
parent e98bd1b431
commit 7179afaf6f
10 changed files with 55 additions and 24 deletions

View File

@ -14,4 +14,4 @@ notifications:
services: services:
- mongodb - mongodb
env: env:
- MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test SMTP_HOST=127.0.0.1 SMTP_PORT=465 SMTP_TLS=true SMTP_STARTTLS=true SMTP_USER=smtp_user SMTP_PASS=smtp_pass SENDER_NAME=Travis SENDER_EMAIL=travis@mailvelope.com - MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test SMTP_HOST=127.0.0.1 SMTP_PORT=465 SMTP_TLS=true SMTP_STARTTLS=true SMTP_PGP=true SMTP_USER=smtp_user SMTP_PASS=smtp_pass SENDER_NAME=Travis SENDER_EMAIL=travis@mailvelope.com

View File

@ -15,13 +15,13 @@ The web of trust raises some valid privacy concerns. Not only is a user's social
### Usability ### Usability
The main issue with the Web of Trust though is that it does not scale in terms of usability. The goal of this key server is to enable a better user experience for OpenPGP user agents by providing a more reliable source of public keys, where users verify their email address after key upload. This prevents user A from uploading a public key for user B. With this property in place, automatic key lookup is more reliable than with standard SKS servers. The main issue with the Web of Trust though is that it does not scale in terms of usability. The goal of this key server is to enable a better user experience for OpenPGP user agents by providing a more reliable source of public keys. Similar to messengers like Signal, users verify their email address by clicking on a link of a PGP encrypted message. This prevents user A from uploading a public key for user B. With this property in place, automatic key lookup is more reliable than with standard SKS servers.
This requires more trust to be placed in the service provider that hosts a key server, but we believe that this trade-off is necessary to improve the user experience for average users. Tech-savvy users or users with a threat model that requires stronger security may still choose to verify PGP key fingerprints just as before. This requires more trust to be placed in the service provider that hosts a key server, but we believe that this trade-off is necessary to improve the user experience for average users. Tech-savvy users or users with a threat model that requires stronger security may still choose to verify PGP key fingerprints just as before.
## Standardization and (De)centralization ## Standardization and (De)centralization
The idea is that an identity provider such as an email provider can host their own key server under a common `openpgpkeys` subdomain. An OpenPGP supporting user agent should attempt to lookup keys under the user's domain e.g. `https://openpgpkeys.example.com` for `user@example.com` first. User agents can host their own fallback key server as well, in case a mail provider does not provide its own key directory. The idea is that an identity provider such as an email provider can host their own key directory under a common `openpgpkeys` subdomain. An OpenPGP supporting user agent should attempt to lookup keys under the user's domain e.g. `https://openpgpkeys.example.com` for `user@example.com` first. User agents can host their own fallback key server as well, in case a mail provider does not provide its own key directory.
@ -224,6 +224,7 @@ The `credentials.json` file can be used to configure a local development install
* SMTP_PORT=465 * SMTP_PORT=465
* SMTP_TLS=true * SMTP_TLS=true
* SMTP_STARTTLS=true * SMTP_STARTTLS=true
* SMTP_PGP=true
* SMTP_USER=smtp_user * SMTP_USER=smtp_user
* SMTP_PASS=smtp_pass * SMTP_PASS=smtp_pass
* SENDER_NAME="OpenPGP Key Server" * SENDER_NAME="OpenPGP Key Server"

View File

@ -23,6 +23,7 @@
"mongodb": "^2.1.20", "mongodb": "^2.1.20",
"node-uuid": "^1.4.7", "node-uuid": "^1.4.7",
"nodemailer": "^2.4.2", "nodemailer": "^2.4.2",
"nodemailer-openpgp": "^1.0.2",
"npmlog": "^2.0.4", "npmlog": "^2.0.4",
"openpgp": "^2.3.0" "openpgp": "^2.3.0"
}, },

View File

@ -9,6 +9,7 @@
"port": "465", "port": "465",
"tls": "true", "tls": "true",
"starttls": "true", "starttls": "true",
"pgp": "true",
"user": "user@gmail.com", "user": "user@gmail.com",
"pass": "password" "pass": "password"
}, },

View File

@ -24,6 +24,7 @@ const config = require('config');
const router = require('koa-router')(); const router = require('koa-router')();
const openpgp = require('openpgp'); const openpgp = require('openpgp');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
const Mongo = require('./dao/mongo'); const Mongo = require('./dao/mongo');
const Email = require('./email/email'); const Email = require('./email/email');
const UserId = require('./service/user-id'); const UserId = require('./service/user-id');
@ -100,12 +101,13 @@ function injectDependencies() {
user: process.env.MONGO_USER || credentials.mongo.user, user: process.env.MONGO_USER || credentials.mongo.user,
password: process.env.MONGO_PASS || credentials.mongo.pass password: process.env.MONGO_PASS || credentials.mongo.pass
}); });
email = new Email(nodemailer); email = new Email(nodemailer, openpgpEncrypt);
email.init({ email.init({
host: process.env.SMTP_HOST || credentials.smtp.host, host: process.env.SMTP_HOST || credentials.smtp.host,
port: process.env.SMTP_PORT || credentials.smtp.port, port: process.env.SMTP_PORT || credentials.smtp.port,
tls: (process.env.SMTP_TLS || credentials.smtp.tls) === 'true', tls: (process.env.SMTP_TLS || credentials.smtp.tls) === 'true',
starttls: (process.env.SMTP_STARTTLS || credentials.smtp.starttls) === 'true', starttls: (process.env.SMTP_STARTTLS || credentials.smtp.starttls) === 'true',
pgp: (process.env.SMTP_PGP || credentials.smtp.pgp) === 'true',
auth: { auth: {
user: process.env.SMTP_USER || credentials.smtp.user, user: process.env.SMTP_USER || credentials.smtp.user,
pass: process.env.SMTP_PASS || credentials.smtp.pass pass: process.env.SMTP_PASS || credentials.smtp.pass

View File

@ -29,8 +29,9 @@ class Email {
* Create an instance of the email object. * Create an instance of the email object.
* @param {Object} mailer An instance of nodemailer * @param {Object} mailer An instance of nodemailer
*/ */
constructor(mailer) { constructor(mailer, openpgpEncrypt) {
this._mailer = mailer; this._mailer = mailer;
this._openpgpEncrypt = openpgpEncrypt;
} }
/** /**
@ -41,6 +42,7 @@ class Email {
* @param {string} port (optional) SMTP server's SMTP port. Defaults to 465. * @param {string} port (optional) SMTP server's SMTP port. Defaults to 465.
* @param {boolean} tls (optional) if TSL should be used. Defaults to true. * @param {boolean} tls (optional) if TSL should be used. Defaults to true.
* @param {boolean} starttls (optional) force STARTTLS to prevent downgrade attack. Defaults to true. * @param {boolean} starttls (optional) force STARTTLS to prevent downgrade attack. Defaults to true.
* @param {boolean} pgp (optional) if outgoing emails are encrypted to the user's public key.
*/ */
init(options) { init(options) {
this._transport = this._mailer.createTransport({ this._transport = this._mailer.createTransport({
@ -50,6 +52,9 @@ class Email {
secure: (options.tls !== undefined) ? options.tls : true, secure: (options.tls !== undefined) ? options.tls : true,
requireTLS: (options.starttls !== undefined) ? options.starttls : true, requireTLS: (options.starttls !== undefined) ? options.starttls : true,
}); });
if (options.pgp) {
this._transport.use('stream', this._openpgpEncrypt());
}
this._sender = options.sender; this._sender = options.sender;
} }
@ -92,7 +97,8 @@ class Email {
let template = { let template = {
subject: options.subject, subject: options.subject,
text: options.text, text: options.text,
html: options.html html: options.html,
encryptionKeys: [options.to.publicKeyArmored]
}; };
let sender = { let sender = {
from: { from: {

View File

@ -1,12 +1,12 @@
{ {
"verifyKey": { "verifyKey": {
"subject": "Verify Your Key", "subject": "Verify Your Key",
"text": "Hello {{name}},\n\nplease click here to verify your key: {{baseUrl}}/api/v1/verify?keyid={{keyid}}&nonce={{nonce}}", "text": "Hello {{name}},\n\nplease click here to verify your key:\n\n{{baseUrl}}/api/v1/verify?keyid={{keyid}}&nonce={{nonce}}",
"html": "" "html": ""
}, },
"verifyRemove": { "verifyRemove": {
"subject": "Verify Key Removal", "subject": "Verify Key Removal",
"text": "Hello {{name}},\n\nplease click here to verify the removal of your key: {{baseUrl}}/api/v1/verifyRemove?keyid={{keyid}}&nonce={{nonce}}", "text": "Hello {{name}},\n\nplease click here to verify the removal of your key:\n\n{{baseUrl}}/api/v1/verifyRemove?keyid={{keyid}}&nonce={{nonce}}",
"html": "" "html": ""
} }
} }

View File

@ -69,7 +69,7 @@ class PublicKey {
// store key in database // store key in database
let userIds = yield this._persisKey(publicKeyArmored, params); let userIds = yield this._persisKey(publicKeyArmored, params);
// send mails to verify user ids (send only one if primary email is provided) // send mails to verify user ids (send only one if primary email is provided)
yield this._sendVerifyEmail(userIds, primaryEmail, origin); yield this._sendVerifyEmail(userIds, primaryEmail, origin, publicKeyArmored);
} }
/** /**
@ -125,14 +125,16 @@ class PublicKey {
* @param {Array} userIds user id documents containg the verification nonces * @param {Array} userIds user id documents containg the verification nonces
* @param {string} primaryEmail the public key's primary email address * @param {string} primaryEmail the public key's primary email address
* @param {Object} origin the server's origin (required for email links) * @param {Object} origin the server's origin (required for email links)
* @param {String} publicKeyArmored The ascii armored pgp key block
* @yield {undefined} * @yield {undefined}
*/ */
*_sendVerifyEmail(userIds, primaryEmail, origin) { *_sendVerifyEmail(userIds, primaryEmail, origin, publicKeyArmored) {
let primaryUserId = userIds.find(uid => uid.email === primaryEmail); let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
if (primaryUserId) { if (primaryUserId) {
userIds = [primaryUserId]; userIds = [primaryUserId];
} }
for (let userId of userIds) { for (let userId of userIds) {
userId.publicKeyArmored = publicKeyArmored; // set key for encryption
yield this._email.send({ template:tpl.verifyKey, userId, origin }); yield this._email.send({ template:tpl.verifyKey, userId, origin });
} }
} }

View File

@ -46,7 +46,8 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
return !!params.nonce; return !!params.nonce;
})); }));
sinon.stub(nodemailer, 'createTransport').returns({ sinon.stub(nodemailer, 'createTransport').returns({
templateSender: () => { return sendEmailStub; } templateSender: () => { return sendEmailStub; },
use: function() {}
}); });
global.testing = true; global.testing = true;

View File

@ -7,6 +7,7 @@ const log = require('npmlog');
const config = require('config'); const config = require('config');
const Email = require('../../src/email/email'); const Email = require('../../src/email/email');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
const tpl = require('../../src/email/templates.json'); const tpl = require('../../src/email/templates.json');
log.level = config.log.level; log.level = config.log.level;
@ -14,7 +15,7 @@ log.level = config.log.level;
describe('Email Integration Tests', function() { describe('Email Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
let email, credentials, userId, origin; let email, credentials, userId, origin, publicKeyArmored;
before(function() { before(function() {
try { try {
@ -24,22 +25,18 @@ describe('Email Integration Tests', function() {
this.skip(); this.skip();
return; return;
} }
userId = { publicKeyArmored = require('fs').readFileSync(__dirname + '/../key1.asc', 'utf8');
name: credentials.sender.name,
email: credentials.sender.email,
keyid: '0123456789ABCDF0',
nonce: 'qwertzuioasdfghjkqwertzuio'
};
origin = { origin = {
protocol: 'http', protocol: 'http',
host: 'localhost:' + config.server.port host: 'localhost:' + config.server.port
}; };
email = new Email(nodemailer); email = new Email(nodemailer, openpgpEncrypt);
email.init({ email.init({
host: process.env.SMTP_HOST || credentials.smtp.host, host: process.env.SMTP_HOST || credentials.smtp.host,
port: process.env.SMTP_PORT || credentials.smtp.port, port: process.env.SMTP_PORT || credentials.smtp.port,
tls: (process.env.SMTP_TLS || credentials.smtp.tls) === 'true', tls: (process.env.SMTP_TLS || credentials.smtp.tls) === 'true',
starttls: (process.env.SMTP_STARTTLS || credentials.smtp.starttls) === 'true', starttls: (process.env.SMTP_STARTTLS || credentials.smtp.starttls) === 'true',
pgp: (process.env.SMTP_PGP || credentials.smtp.pgp) === 'true',
auth: { auth: {
user: process.env.SMTP_USER || credentials.smtp.user, user: process.env.SMTP_USER || credentials.smtp.user,
pass: process.env.SMTP_PASS || credentials.smtp.pass pass: process.env.SMTP_PASS || credentials.smtp.pass
@ -51,6 +48,16 @@ describe('Email Integration Tests', function() {
}); });
}); });
beforeEach(() => {
userId = {
name: credentials.sender.name,
email: credentials.sender.email,
keyid: '0123456789ABCDF0',
nonce: 'qwertzuioasdfghjkqwertzuio',
publicKeyArmored
};
});
describe("_sendHelper", () => { describe("_sendHelper", () => {
it('should work', function *() { it('should work', function *() {
let mailOptions = { let mailOptions = {
@ -66,13 +73,23 @@ describe('Email Integration Tests', function() {
}); });
describe("send verifyKey template", () => { describe("send verifyKey template", () => {
it('should work', function *() { it('should send plaintext email', function *() {
delete userId.publicKeyArmored;
yield email.send({ template:tpl.verifyKey, userId, origin });
});
it('should send pgp encrypted email', function *() {
yield email.send({ template:tpl.verifyKey, userId, origin }); yield email.send({ template:tpl.verifyKey, userId, origin });
}); });
}); });
describe("send verifyRemove template", () => { describe("send verifyRemove template", () => {
it('should work', function *() { it('should send plaintext email', function *() {
delete userId.publicKeyArmored;
yield email.send({ template:tpl.verifyRemove, userId, origin });
});
it('should send pgp encrypted email', function *() {
yield email.send({ template:tpl.verifyRemove, userId, origin }); yield email.send({ template:tpl.verifyRemove, userId, origin });
}); });
}); });