diff --git a/.travis.yml b/.travis.yml index 1e4f551..dcd0a8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 \ No newline at end of file + - 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 \ No newline at end of file diff --git a/README.md b/README.md index 467eb33..2625541 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/package.json b/package.json index d54fabf..f664212 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/res/credentials.json b/res/credentials.json index 8c6954d..05bd932 100644 --- a/res/credentials.json +++ b/res/credentials.json @@ -9,6 +9,7 @@ "port": "465", "tls": "true", "starttls": "true", + "pgp": "true", "user": "user@gmail.com", "pass": "password" }, diff --git a/src/app.js b/src/app.js index 550e16b..14cdcc1 100644 --- a/src/app.js +++ b/src/app.js @@ -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 diff --git a/src/email/email.js b/src/email/email.js index 72737b1..43a58b1 100644 --- a/src/email/email.js +++ b/src/email/email.js @@ -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: { diff --git a/src/email/templates.json b/src/email/templates.json index 0b5d607..44e2647 100644 --- a/src/email/templates.json +++ b/src/email/templates.json @@ -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": "" } } \ No newline at end of file diff --git a/src/service/public-key.js b/src/service/public-key.js index 5a86fd0..d28c02a 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -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 }); } } diff --git a/test/integration/app-test.js b/test/integration/app-test.js index 403b5a3..a0e7741 100644 --- a/test/integration/app-test.js +++ b/test/integration/app-test.js @@ -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; diff --git a/test/integration/email-test.js b/test/integration/email-test.js index f4b96b6..9650c2a 100644 --- a/test/integration/email-test.js +++ b/test/integration/email-test.js @@ -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 }); }); });