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:
- mongodb
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
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.
## 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_TLS=true
* SMTP_STARTTLS=true
* SMTP_PGP=true
* SMTP_USER=smtp_user
* SMTP_PASS=smtp_pass
* SENDER_NAME="OpenPGP Key Server"

View File

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

View File

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

View File

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

View File

@ -29,8 +29,9 @@ class Email {
* Create an instance of the email object.
* @param {Object} mailer An instance of nodemailer
*/
constructor(mailer) {
constructor(mailer, openpgpEncrypt) {
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 {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} pgp (optional) if outgoing emails are encrypted to the user's public key.
*/
init(options) {
this._transport = this._mailer.createTransport({
@ -50,6 +52,9 @@ class Email {
secure: (options.tls !== undefined) ? options.tls : true,
requireTLS: (options.starttls !== undefined) ? options.starttls : true,
});
if (options.pgp) {
this._transport.use('stream', this._openpgpEncrypt());
}
this._sender = options.sender;
}
@ -92,7 +97,8 @@ class Email {
let template = {
subject: options.subject,
text: options.text,
html: options.html
html: options.html,
encryptionKeys: [options.to.publicKeyArmored]
};
let sender = {
from: {

View File

@ -1,12 +1,12 @@
{
"verifyKey": {
"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": ""
},
"verifyRemove": {
"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": ""
}
}

View File

@ -69,7 +69,7 @@ class PublicKey {
// store key in database
let userIds = yield this._persisKey(publicKeyArmored, params);
// 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);
}
/**
@ -122,17 +122,19 @@ class PublicKey {
/**
* Send verification emails to the public keys user ids for verification.
* If a primary email address is provided only one email will be sent.
* @param {Array} userIds user id documents containg the verification nonces
* @param {string} primaryEmail the public key's primary email address
* @param {Object} origin the server's origin (required for email links)
* @param {Array} userIds user id documents containg the verification nonces
* @param {string} primaryEmail the public key's primary email address
* @param {Object} origin the server's origin (required for email links)
* @param {String} publicKeyArmored The ascii armored pgp key block
* @yield {undefined}
*/
*_sendVerifyEmail(userIds, primaryEmail, origin) {
*_sendVerifyEmail(userIds, primaryEmail, origin, publicKeyArmored) {
let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
if (primaryUserId) {
userIds = [primaryUserId];
}
for (let userId of userIds) {
userId.publicKeyArmored = publicKeyArmored; // set key for encryption
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;
}));
sinon.stub(nodemailer, 'createTransport').returns({
templateSender: () => { return sendEmailStub; }
templateSender: () => { return sendEmailStub; },
use: function() {}
});
global.testing = true;

View File

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