Use nodemailer-openpgp plugin to encrypt verification emails
This commit is contained in:
parent
e98bd1b431
commit
7179afaf6f
@ -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
|
@ -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"
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -9,6 +9,7 @@
|
||||
"port": "465",
|
||||
"tls": "true",
|
||||
"starttls": "true",
|
||||
"pgp": "true",
|
||||
"user": "user@gmail.com",
|
||||
"pass": "password"
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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: {
|
||||
|
@ -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": ""
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,14 +125,16 @@ class PublicKey {
|
||||
* @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 });
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user