Use nodemailer-openpgp plugin to encrypt verification emails
This commit is contained in:
parent
e98bd1b431
commit
7179afaf6f
@ -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
|
@ -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"
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -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
|
||||||
|
@ -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: {
|
||||||
|
@ -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": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -122,17 +122,19 @@ class PublicKey {
|
|||||||
/**
|
/**
|
||||||
* Send verification emails to the public keys user ids for verification.
|
* Send verification emails to the public keys user ids for verification.
|
||||||
* If a primary email address is provided only one email will be sent.
|
* If a primary email address is provided only one email will be sent.
|
||||||
* @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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user