Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

68 changed files with 1607 additions and 5244 deletions

View File

@ -1,2 +0,0 @@
node_modules
npm-debug.log

View File

@ -0,0 +1,12 @@
branch-defaults:
release/prod:
environment: keyserver-prod
release/test:
environment: keyserver-test
global:
application_name: keyserver
default_ec2_keyname: null
default_platform: Node.js
default_region: eu-west-1
profile: eb-cli
sc: git

View File

@ -1,74 +0,0 @@
{
"parserOptions": {
"ecmaVersion": 2018
},
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"rules": {
"strict": ["error", "global"],
/* possible errors */
"no-console": 0,
"no-empty": ["error", { "allowEmptyCatch": true }], // disallow empty block statements
"require-atomic-updates": 0, // disallow assignments that can lead to race conditions due to usage of await or yield
/* best practices */
"curly": 2, // enforce consistent brace style for all control statements
"no-return-await": 2, // disallows unnecessary return await
"no-eval": 2, // disallow the use of eval()
"no-extend-native": 2, // disallow extending native types
"no-global-assign": 2, // disallow assignments to native objects or read-only global variables
"no-implicit-coercion": 2, // disallow shorthand type conversions
"no-implicit-globals": 2, // disallow var and named function declarations in the global scope
"no-implied-eval": 2, // disallow the use of eval()-like methods
"no-lone-blocks": 2, // disallow unnecessary nested blocks
"no-unused-vars": ["error", {"ignoreRestSiblings": true}], // disallow unused variables
"no-useless-escape": 0, // disallow unnecessary escape characters
/* Stylistic Issues */
"array-bracket-newline": ["warn", "consistent"], // enforce consisten line breaks after opening and before closing array brackets
"array-bracket-spacing": 1, // enforce consistent spacing inside array brackets
"block-spacing": 1, // enforce consistent spacing inside single-line blocks
"brace-style": ["warn", "1tbs", { "allowSingleLine": true }], // enforce consistent brace style for blocks
"comma-spacing": 1, // enforce consistent spacing before and after commas
"computed-property-spacing": 1, // enforce consistent spacing inside computed property brackets
"eol-last": 1, // enforce at least one newline at the end of files
"func-call-spacing": 1, // require or disallow spacing between function identifiers and their invocations
"indent": ["warn", 2, {"MemberExpression": 0, "SwitchCase": 1}], // enforce consistent indentation
"key-spacing": ["warn", { "mode": "minimum" }], // enforce consistent spacing before and after keywords
"keyword-spacing": 1, // enforce consistent spacing between keys and values in object literal properties
"linebreak-style": 1, // enforce consistent linebreak style
"lines-between-class-members": 1, // require or disallow an empty line between class members
"new-parens": ["warn"], // require parens when invoking constructors
"no-multiple-empty-lines": ["warn", {"max": 1}], // disallow multiple empty lines
"no-trailing-spaces": 1, // disallow trailing whitespace at the end of lines
"no-var": 1, // require let or const instead of var
"object-curly-newline": ["warn", {"consistent": true}], // enforce consistent line breaks inside braces
"object-curly-spacing": ["warn", "never"], // enforce consistent spacing inside braces
"one-var": ["warn", "never"], // enforce variables to be declared either together or separately in functions
"padded-blocks": ["warn", "never"], // require or disallow padding within blocks
"prefer-object-spread": 1, // disallow using Object.assign with an object literal as the first argument and prefer the use of object spread instead.
"quotes": ["warn", "single", {"avoidEscape": true}], // enforce the consistent use of single quotes
"semi": ["warn", "always"], // require or disallow semicolons instead of ASI
"semi-spacing": 1, // enforce consistent spacing before and after semicolons
"space-before-blocks": 1, // enforce consistent spacing before blocks
"space-before-function-paren": ["warn", { "anonymous": "never", "named": "never", "asyncArrow": "always" }], // enforce consistent spacing before function definition opening parenthesis
"space-in-parens": ["warn", "never"], // enforce consistent spacing inside parentheses
"space-infix-ops": 1, // require spacing around operators
/* ES6 */
"arrow-body-style": ["warn", "as-needed"], // require braces around arrow function bodies
"arrow-parens": ["warn", "as-needed"], // require parentheses around arrow function arguments
"arrow-spacing": 1, // enforce consistent spacing before and after the arrow in arrow functions
"no-useless-constructor": 1, // disallow unnecessary constructors
"object-shorthand": ["warn", "always", {"avoidQuotes": true}], // require or disallow method and property shorthand syntax for object literals
"prefer-arrow-callback": ["warn", {"allowNamedFunctions": true}], // require arrow functions as callbacks
"prefer-const": ["warn", {"destructuring": "all"}], // require const declarations for variables that are never reassigned after declared
"prefer-template": 1, // require template literals instead of string concatenation
"template-curly-spacing": ["warn", "never"] // require or disallow spacing around embedded expressions of template strings
},
"root": true
}

View File

@ -1,44 +0,0 @@
name: Build & publish images
on: [push]
env:
REGISTRY: git.plantroon.com
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push-image:
runs-on: ubuntu-latest
container:
image: catthehacker/ubuntu:act-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.CR_PAT }}
- name: Extract Docker metadata
id: meta
uses: https://github.com/docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: git.plantroon.com/aux/keyserver
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: true
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.meta.outputs.tags }}

2
.gitignore vendored
View File

@ -34,5 +34,3 @@ node_modules
# Optional REPL history # Optional REPL history
.node_repl_history .node_repl_history
config/development.js

4
.jscsrc Normal file
View File

@ -0,0 +1,4 @@
{
"disallowTrailingWhitespace": true,
"validateIndentation": 2
}

25
.jshintrc Normal file
View File

@ -0,0 +1,25 @@
{
"strict": true,
"node": true,
"nonew": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"newcap": true,
"regexp": true,
"evil": true,
"eqnull": true,
"expr": true,
"undef": true,
"unused": true,
"esnext": true,
"globals": {
"describe" : true,
"it" : true,
"before" : true,
"beforeEach" : true,
"after" : true,
"afterEach" : true
}
}

View File

@ -1,30 +1,17 @@
sudo: false
language: node_js language: node_js
node_js: node_js:
- "10" - "4"
- "5"
env: - "6"
- NODE_ENV=integration LOG_LEVEL=warn MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test before_script:
- npm install -g grunt-cli
- sleep 15
- mongo test_db --eval 'db.addUser("travis", "test");'
notifications:
email:
- build@mailvelope.com
services: services:
- mongodb - mongodb
env:
before_script: - NODE_ENV=integration MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test
- mongo test_db --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});'
before_deploy:
- npm run release
deploy:
skip_cleanup: true
provider: elasticbeanstalk
access_key_id: $AWS_ACCESS_KEY
secret_access_key: $AWS_SECRET_ACCESS_KEY
region: eu-central-1
app: mailvelope-key-server
env: MailvelopeKeyServer-env
zip_file: release.zip
bucket_name: elasticbeanstalk-eu-central-1-936909551620
bucket_path: keyserver
on:
repo: mailvelope/keyserver
branch: master

View File

@ -1,31 +0,0 @@
Mailvelope Key Server Changelog
===============================
v3.0.0
-------
__Mar 4, 2019__
* Add upload, update and removal for single user IDs
* Migrate to koa 2 with async/await instead of generators
* Use eslint instead of jscs/jshint
* Use winston instead of npmlog
* Use co-body directly instead of koa-body
* Send email message with PGP inline not PGP/MIME
* Use OpenPGP.js directly instead of nodemailer-openpgp plugin
* Use native ES6 string templates instead of nodemailer template engine
* Update OpenPGP.js to 4.4 and other dependencies
v2.0.0
-------
__Aug 15, 2017__
* Add release npm script for travis deployment
* Use eslint instead of jscs/jshint
* Use ES6 destructuring
* Replace grunt with npm scripts
* Update dependencies
v1.0.0
-------
__Jun 13, 2016__
* Initial release

View File

@ -1,9 +0,0 @@
FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD [ "node", "index.js" ]

43
Gruntfile.js Normal file
View File

@ -0,0 +1,43 @@
'use strict';
module.exports = function(grunt) {
grunt.initConfig({
jshint: {
all: ['*.js', 'src/**/*.js', 'test/**/*.js'],
options: {
jshintrc: '.jshintrc'
}
},
jscs: {
src: ['*.js', 'src/**/*.js', 'test/**/*.js'],
options: {
config: ".jscsrc",
esnext: true, // If you use ES6 http://jscs.info/overview.html#esnext
verbose: true, // If you need output with rule names http://jscs.info/overview.html#verbose
}
},
mochaTest: {
test: {
options: {
reporter: 'spec'
},
src: [
'test/unit/*.js',
'test/integration/*.js',
]
}
}
});
// Load the plugin(s)
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-jscs');
grunt.loadNpmTasks('grunt-mocha-test');
// Default task(s).
grunt.registerTask('test', ['jshint', 'jscs', 'mochaTest']);
};

147
README.md
View File

@ -31,18 +31,13 @@ Try out the server here: [https://keys.mailvelope.com](https://keys.mailvelope.c
# API # Api
The key server provides a modern RESTful API, but is also backwards compatible to the OpenPGP HTTP Keyserver Protocol (HKP). The following properties are enforced by the key server to enable reliable automatic key look in user agents: The key server provides a modern RESTful api, but is also backwards compatible to the OpenPGP HTTP Keyserver Protocol (HKP).
* Only public keys with at least one verified email address are served ## HKP api
* There can be only one public key per verified email address at a given time
* A key ID specified in a query must be at least 16 hex characters (64-bit long key ID)
* Key ID collisions are checked upon key upload to prevent collision attacks
## HKP API The HKP apis are not documented here. Please refer to the [HKP specification](https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00) to learn more. The server generally implements the full specification, but has some constraints to improve the security for automatic key lookup:
The HKP APIs are not documented here. Please refer to the [HKP specification](https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00) to learn more. The server generally implements the full specification, but has some constraints to improve the security for automatic key lookup:
#### Accepted `search` parameters #### Accepted `search` parameters
* Email addresses * Email addresses
@ -57,7 +52,7 @@ The HKP APIs are not documented here. Please refer to the [HKP specification](ht
#### Accepted `options` parameters #### Accepted `options` parameters
* mr * mr
## REST API ## REST api
### Lookup a key ### Lookup a key
@ -79,6 +74,12 @@ GET /api/v1/key?fingerprint=e3317db04d3958fd5f662c37b8e4105cc9dedc77
GET /api/v1/key?email=user@example.com GET /api/v1/key?email=user@example.com
``` ```
#### By email address (shorthand link for sharing)
```
GET /user/user@example.com
```
#### Payload (JSON): #### Payload (JSON):
```json ```json
@ -124,48 +125,48 @@ POST /api/v1/key
```json ```json
{ {
"publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----" "publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----",
"primaryEmail": "user@example.com"
} }
``` ```
* **publicKeyArmored**: The ascii armored public PGP key to be uploaded * **publicKeyArmored**: The ascii armored public PGP key to be uploaded
* **primaryEmail (optional)**: The ascii armored block is parsed to check for user ids, so this parameter is purely optional. Normally a verification email is sent to every user id found in the pgp key. To prevent this behaviour, user agents can specify the user's primary email address to send out only one email.
E.g. to upload a key from shell:
```bash
curl https://keys.mailvelope.com/api/v1/key --data "{\"publicKeyArmored\":\"$( \
gpg --armor --export-options export-minimal --export $GPGKEYID | sed ':a;N;$!ba;s/\n/\\n/g' \
)\"}"
```
### Verify uploaded key (via link in email) ### Verify uploaded key
``` ```
GET /api/v1/key?op=verify&keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c GET /api/v1/verify?keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
``` ```
### Request key removal ### Request key removal
#### Via delete request
``` ```
DELETE /api/v1/key?keyId=b8e4105cc9dedc77 OR ?email=user@example.com DELETE /api/v1/key?keyId=b8e4105cc9dedc77 OR ?email=user@example.com
``` ```
### Verify key removal (via link in email) #### Via link
``` ```
GET /api/v1/key?op=verifyRemove&keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c GET /api/v1/removeKey?keyId=b8e4105cc9dedc77 OR ?email=user@example.com
``` ```
# Language & DB ### Verify key removal
The server is written is in JavaScript ES7 and runs on [Node.js](https://nodejs.org/) v8+. ```
GET /api/v1/verifyRemove?keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
It uses [MongoDB](https://www.mongodb.com/) v3.2+ as its database. ```
# Getting started
## Installation
### Node.js (Mac OS) # Development
The server is written is in JavaScript ES6 and runs on [Node.js](https://nodejs.org/) v4+. It uses [MongoDB](https://www.mongodb.com/) v2.4+ as its database.
## Install Node.js (Mac OS)
This is how to install node on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [Node.js download page](https://nodejs.org/en/download/). This is how to install node on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [Node.js download page](https://nodejs.org/en/download/).
@ -174,7 +175,7 @@ brew update
brew install node brew install node
``` ```
### MongoDB (Mac OS) ## Setup local MongoDB (Mac OS)
This is the installation guide to get a local development installation on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [MongoDB Getting Started Guide](https://docs.mongodb.com/getting-started/shell/). This is the installation guide to get a local development installation on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [MongoDB Getting Started Guide](https://docs.mongodb.com/getting-started/shell/).
@ -190,7 +191,7 @@ Now the mongo daemon should be running in the background. To have mongo start au
brew services start mongodb brew services start mongodb
``` ```
Now you can use the `mongo` CLI client to create a new test database. The username and password used here match the ones in the `config/development.js` file. **Be sure to change them for production use**: Now you can use the `mongo` CLI client to create a new test database. **The username and password used here match the ones in the `config/development.js` file. Be sure to change them for production use**:
```shell ```shell
mongo mongo
@ -198,72 +199,16 @@ use keyserver-test
db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite", db:"keyserver-test" }] }) db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite", db:"keyserver-test" }] })
``` ```
### Dependencies ## Setup SMTP user
```shell The key server uses [nodemailer](https://nodemailer.com) to send out emails upon public key upload to verify email address ownership. To test this feature locally, open the `config/development.js` file and change the `email.auth.user` and `email.auth.pass` attributes to your Gmail test account. Make sure that `email.auth.user` and `email.sender.email` match. Otherwise the Gmail SMTP server will block any emails you try to send. Also, make sure to enable `Allow less secure apps` in the [Gmail security settings](https://myaccount.google.com/security#connectedapps). You can read more on this in the [Nodemailer documentation](https://nodemailer.com/using-gmail/).
npm install
```
## Configuration
Configuration settings may be provided as environment variables. The files in the config directory read the environment variables and define configuration values for settings with no corresponding environment variable. Warning: Default settings are only provided for a small minority of settings in these files (as most of them are very individual like host/user/password)!
If settings are configured in multiple places, the priority ranking is as follows (individually for each setting):
1. Environment variable
2. config/production.js or config/development.js (depending on NODE_ENV)
3. config/default.js
### Development
If you don't use environment variables to configure settings, create `config/development.js` and use `config/default.js` as a template. Creating `development.js` instead of just editing `config/default.js` is recommended to prevent accidental commits of locally used settings.
### Production
For production use, settings configuration with environment variables is recommended as `NODE_ENV=production` is REQUIRED to be set as environment variable to instruct node.js to adapt e.g. logging to production use.
*Other settings you may also configure within `config/production.js` and use `config/default.js` as a template; but ensure then the environment variable `NODE_ENV=production` or `production.js` will not be read!*
### Settings
Available settings with its environment-variable-names, possible/example values and meaning (if not self-explainable). Defaults **bold**:
* NODE_ENV=development|production
(no default + needs to be set as environment variable!)
* LOG_LEVEL=**silly**|error|warn|info|debug
* PORT=**8888**
(application server port)
* HTTPS_UPGRADE=true
(upgrade HTTP requests to HTTPS and use [HSTS](https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security))
* HTTPS_KEY_PIN=base64_encoded_sha256
(optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning))
* HTTPS_KEY_PIN_BACKUP=base64_encoded_sha256
(optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning))
* MONGO_URI=127.0.0.1:27017/test_db
* MONGO_USER=db_user
* MONGO_PASS=db_password
* SMTP_HOST=127.0.0.1
* SMTP_PORT=465
* SMTP_TLS=true
* SMTP_STARTTLS=true
* SMTP_PGP=true
(encrypt verification message with public key (allows to verify presence + usability of private key at owner of the mail-address))
* SMTP_USER=smtp_user
* SMTP_PASS=smtp_pass
* SENDER_NAME="OpenPGP Key Server"
* SENDER_EMAIL=noreply@example.com
* PUBLIC_KEY_PURGE_TIME=**30**
(number of days after which uploaded keys are deleted if they have not been verified)
### Notes on SMTP
The key server uses [nodemailer](https://nodemailer.com) to send out emails upon public key upload to verify email address ownership. To test this feature locally, configure `SMTP_USER` and `SMTP_PASS` settings to your Gmail test account. Make sure that `SMTP_USER` and `SENDER_EMAIL` match. Otherwise the Gmail SMTP server will block any emails you try to send. Also, make sure to enable `Allow less secure apps` in the [Gmail security settings](https://myaccount.google.com/security#connectedapps). You can read more on this in the [Nodemailer documentation](https://nodemailer.com/using-gmail/).
For production you should use a service like [Amazon SES](https://aws.amazon.com/ses/), [Mailgun](https://www.mailgun.com/) or [Sendgrid](https://sendgrid.com/solutions/transactional-email/). Nodemailer supports all of these out of the box. For production you should use a service like [Amazon SES](https://aws.amazon.com/ses/), [Mailgun](https://www.mailgun.com/) or [Sendgrid](https://sendgrid.com/solutions/transactional-email/). Nodemailer supports all of these out of the box.
## Run tests ## Install dependencies and run tests
```shell ```shell
npm test npm install && npm test
``` ```
## Start local server ## Start local server
@ -274,6 +219,28 @@ npm start
# Production
The `config/development.js` file can be used to configure a local development installation. For production use, the following environment variables need to be set:
* NODE_ENV=production
* MONGO_URI=127.0.0.1:27017/test_db
* MONGO_USER=db_user
* MONGO_PASS=db_password
* 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="OpenPGP Key Server"
* SENDER_EMAIL=noreply@example.com
* HTTPS_UPGRADE=true (upgrade HTTP requests to HTTPS and use [HSTS](https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security))
* HTTPS_KEY_PIN=base64_encoded_sha256 (optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning))
* HTTPS_KEY_PIN_BACKUP=base64_encoded_sha256 (optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning))
# License # License

View File

@ -1,14 +1,7 @@
'use strict';
module.exports = { module.exports = {
log: { log: {
level: process.env.LOG_LEVEL || 'silly' level: 'silly'
},
papertrail: {
host: process.env.PAPERTRAIL_HOST,
port: process.env.PAPERTRAIL_PORT
}, },
server: { server: {
@ -38,10 +31,6 @@ module.exports = {
name: process.env.SENDER_NAME, name: process.env.SENDER_NAME,
email: process.env.SENDER_EMAIL email: process.env.SENDER_EMAIL
} }
},
publicKey: {
purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30
} }
}; };

25
config/development.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
mongo: {
uri: '127.0.0.1:27017/keyserver-test',
user: 'keyserver-user',
pass: 'trfepCpjhVrqgpXFWsEF'
},
email: {
host: 'smtp.gmail.com',
port: 465,
tls: true,
starttls: true,
pgp: true,
auth: {
user: 'user@gmail.com',
pass: 'password'
},
sender: {
name: 'OpenPGP Key Server',
email: 'user@gmail.com'
}
}
};

View File

@ -1,5 +1,7 @@
'use strict';
module.exports = { module.exports = {
}; log: {
level: 'warn'
}
};

View File

@ -1,5 +1,7 @@
'use strict';
module.exports = { module.exports = {
}; log: {
level: 'error'
}
};

View File

@ -1,26 +0,0 @@
version: '3'
services:
mongodb:
image: mongo
volumes:
- ./data/db:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
networks:
- backend
env_file:
- env.sample
keyserver:
image: git.plantroon.com/aux/keyserver:master
ports:
- "12345:3000"
depends_on:
- mongodb
networks:
- backend
env_file:
- env.sample
networks:
backend:

View File

@ -1,30 +0,0 @@
NODE_ENV=production
LOG_LEVEL=debug
PORT=3000
PAPERTRAIL_HOST=''
PAPERTRAIL_PORT=''
MONGO_URI=mongodb:27017/keyserver_db
MONGO_USER=keyserver
MONGO_PASS=changeme
MONGO_INITDB_ROOT_USERNAME=keyserver
MONGO_INITDB_ROOT_PASSWORD=changeme
MONGO_INITDB_DATABASE=keyserver_db
SENDER_NAME=keyserver
SENDER_EMAIL=changeme
SMTP_HOST=changeme
SMTP_PORT=587
SMTP_TLS=false
SMTP_STARTTLS=true
SMTP_PGP=''
SMTP_USER=''
SMTP_PASS=''
HTTPS_UPGRADE=true
HTTPS_KEY_PIN=''
HTTPS_KEY_PIN_BACKUP=''
PUBLIC_KEY_PURGE_TIME=30

View File

@ -20,11 +20,9 @@
const cluster = require('cluster'); const cluster = require('cluster');
const numCPUs = require('os').cpus().length; const numCPUs = require('os').cpus().length;
const config = require('config'); const config = require('config');
const log = require('winston'); const log = require('npmlog');
const papertrail = require('./src/dao/papertrail');
log.level = config.log.level; log.level = config.log.level; // set log level depending on process.env.NODE_ENV
papertrail.init(config.papertrail);
// //
// Start worker cluster depending on number of CPUs // Start worker cluster depending on number of CPUs
@ -34,13 +32,13 @@ if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) { for (let i = 0; i < numCPUs; i++) {
cluster.fork(); cluster.fork();
} }
cluster.on('fork', worker => log.info('cluster', `Forked worker #${worker.id} [pid:${worker.process.pid}]`)); cluster.on('fork', worker => log.info('cluster', 'Forked worker #%s [pid:%s]', worker.id, worker.process.pid));
cluster.on('exit', worker => { cluster.on('exit', worker => {
log.warn('cluster', `Worker #${worker.id} [pid:${worker.process.pid}] died`); log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid);
cluster.fork(); setTimeout(() => cluster.fork(), 5000);
}); });
} else { } else {
require('./src'); require('./src/app');
} }
// //
@ -60,4 +58,4 @@ process.on('SIGINT', () => {
process.on('uncaughtException', err => { process.on('uncaughtException', err => {
log.error('index', 'Uncaught exception', err); log.error('index', 'Uncaught exception', err);
process.exit(1); process.exit(1);
}); });

View File

@ -1,10 +0,0 @@
{
"key_not_found": "Schlüssel nicht gefunden",
"verify_key_subject": "Bestätigen Sie Ihre E-Mail-Adresse",
"verify_key_text": "Hallo {0},\n\nbitte bestätigen Sie Ihre E-Mail-Adresse {1}.\nKlicken Sie hierzu auf den folgenden Link:\n\n{2}\n\nNach der Bestätigung Ihrer E-Mail-Adresse ist ihr öffentlicher Schlüssel in unserem Schlüssel Verzeichnis verfügbar.\n\nWeitere Informationen finden Sie unter {3}.\n\nIhr Mailvelope Team",
"verify_success_header": "E-Mail Adresse {0} erfolgreich verifiziert",
"verify_success_link": "Ihr öffentlicher OpenPGP Schlüssel ist ab jetzt unter folgendem Link verfügbar:",
"verify_removal_subject": "Entfernen Ihres Schlüssels bestätigen",
"verify_removal_text": "Hallo {0},\n\nbitte bestätigen Sie das Entfernen Ihrer E-Mail-Adresse {1} von unserem Key Server ({2}).\nKlicken Sie hierzu auf den folgenden Link:\n\n{3}\n\nIhr Mailvelope Team",
"removal_success": "E-Mail Adresse {0} aus dem Schlüssel Verzeichnis entfernt"
}

View File

@ -1,10 +0,0 @@
{
"key_not_found": "Key not found",
"verify_key_subject": "Verify your email address",
"verify_key_text": "Hello {0},\n\nplease verify your email address {1} by clicking on the following link:\n\n{2}\n\nAfter verification of your email address, your public key is available in our key directory.\n\nYou can find more info at {3}.\n\nGreetings from the Mailvelope Team",
"verify_success_header": "Email address {0} successfully verified",
"verify_success_link": "Your public OpenPGP key is now available at the following link:",
"verify_removal_subject": "Verify key removal",
"verify_removal_text": "Hello {0},\n\nplease verify removal of your email address {1} from our key server ({2}) by clicking on the following link:\n\n{3}\n\nGreetings from the Mailvelope Team",
"removal_success": "Email address {0} removed from the key directory"
}

View File

@ -1,12 +0,0 @@
db.createUser(
{
user: process.env.MONGO_INITDB_ROOT_USERNAME,
pwd: process.env.MONGO_INITDB_ROOT_PASSWORD,
roles: [
{
role: "readWrite",
db: process.env.MONGO_INITDB_DATABASE
}
]
}
);

2754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,41 @@
{ {
"name": "mailvelope-keyserver", "name": "mailvelope-keyserver",
"version": "3.0.0", "version": "1.0.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/mailvelope/keyserver.git" "url": "https://github.com/mailvelope/keyserver.git"
}, },
"engines": { "engines": {
"node": ">=10", "node": ">=4"
"npm": ">=6"
}, },
"scripts": { "scripts": {
"start": "node index.js", "start": ": ${NODE_ENV=development} && node index.js",
"test": "npm run test:lint && npm run test:unit && npm run test:integration", "test": ": ${NODE_ENV=development} && grunt test"
"test:lint": "eslint --ignore-pattern \"**/*.min.js\" config src test *.js",
"test:unit": "mocha --config test/.mocharc.js ./test/unit/",
"test:integration": "mocha --config test/.mocharc.js ./test/integration",
"release": "npm run release:install && npm run release:archive",
"release:install": "rm -rf node_modules/ && npm ci --production",
"release:archive": "zip -rq release.zip package.json package-lock.json node_modules/ *.js src/ config/ locales/"
}, },
"dependencies": { "dependencies": {
"co-body": "6.1.0", "addressparser": "^1.0.1",
"config": "3.3.6", "co": "^4.6.0",
"koa": "2.13.1", "co-body": "^4.2.0",
"koa-ejs": "4.3.0", "config": "^1.20.4",
"koa-locales": "1.12.0", "koa": "^1.2.0",
"koa-router": "10.0.0", "koa-router": "^5.4.0",
"koa-static": "5.0.0", "koa-static": "^2.0.0",
"mongodb": "3.6.6", "mongodb": "^2.1.20",
"nodemailer": "6.6.0", "nodemailer": "^2.4.2",
"openpgp": "4.5.5", "nodemailer-openpgp": "^1.0.2",
"winston": "3.3.3", "npmlog": "^2.0.4",
"winston-papertrail": "1.0.5" "openpgp": "^2.3.0"
}, },
"devDependencies": { "devDependencies": {
"bootstrap": "^3.4.1", "chai": "^3.5.0",
"chai": "^4.3.4", "co-mocha": "^1.1.2",
"chai-as-promised": "^7.1.1", "grunt": "^1.0.1",
"eslint": "^7.26.0", "grunt-contrib-jshint": "^1.0.0",
"jquery": "^3.6.0", "grunt-jscs": "^2.8.0",
"mocha": "^8.4.0", "grunt-mocha-test": "^0.12.7",
"sinon": "^10.0.0", "mocha": "^2.5.3",
"supertest": "^6.1.3" "sinon": "^1.17.4",
"supertest": "^1.2.0"
} }
} }

42
res/aws_release.sh Executable file
View File

@ -0,0 +1,42 @@
#!/bin/sh
# go to root
cd `dirname $0`
cd ..
if [ "$1" != "prod" ] && [ "$1" != "test" ] ; then
echo 'Usage: ./res/aws_release prod|test'
exit 0
fi
# switch branch
git checkout master
git branch -D release/$1
git checkout -b release/$1
git merge master --no-edit
# abort if tests fail
set -e
# build and test
rm -rf node_modules
npm install
npm test
# install only production dependencies
rm -rf node_modules/
npm install --production
# delete .gitignore files before adding to git for aws deployment
find node_modules/ -name ".gitignore" -exec rm -rf {} \;
# Add runtime dependencies to git
sed -i "" '/node_modules/d' .gitignore
git add .gitignore node_modules/
git commit -m "Update release"
# push to aws
eb deploy keyserver-$1
# switch back to master branch
git checkout master

153
src/app.js Normal file
View File

@ -0,0 +1,153 @@
/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const co = require('co');
const app = require('koa')();
const log = require('npmlog');
const config = require('config');
const router = require('koa-router')();
const util = require('./service/util');
const Mongo = require('./dao/mongo');
const Email = require('./email/email');
const PGP = require('./service/pgp');
const PublicKey = require('./service/public-key');
const HKP = require('./route/hkp');
const REST = require('./route/rest');
let mongo, email, pgp, publicKey, hkp, rest;
//
// Configure koa HTTP server
//
// HKP routes
router.post('/pks/add', function *() {
yield hkp.add(this);
});
router.get('/pks/lookup', function *() {
yield hkp.lookup(this);
});
// REST api routes
router.post('/api/v1/key', function *() {
yield rest.create(this);
});
router.get('/api/v1/key', function *() {
yield rest.read(this);
});
router.del('/api/v1/key', function *() {
yield rest.remove(this);
});
// links for verification, removal and sharing
router.get('/api/v1/verify', function *() {
yield rest.verify(this);
});
router.get('/api/v1/removeKey', function *() {
yield rest.remove(this);
});
router.get('/api/v1/verifyRemove', function *() {
yield rest.verifyRemove(this);
});
router.get('/user/:search', function *() {
yield rest.share(this);
});
// Redirect all http traffic to https
app.use(function *(next) {
if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(this)) {
this.redirect('https://' + this.hostname + this.url);
} else {
yield next;
}
});
// Set HTTP response headers
app.use(function *(next) {
// HSTS
if (util.isTrue(config.server.httpsUpgrade)) {
this.set('Strict-Transport-Security', 'max-age=16070400');
}
// HPKP
if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) {
this.set('Public-Key-Pins', 'pin-sha256="' + config.server.httpsKeyPin + '"; pin-sha256="' + config.server.httpsKeyPinBackup + '"; max-age=16070400');
}
// CSP
this.set('Content-Security-Policy', "default-src 'self'; object-src 'none'");
// Prevent rendering website in foreign iframe (Clickjacking)
this.set('X-Frame-Options', 'DENY');
// CORS
this.set('Access-Control-Allow-Origin', '*');
this.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
this.set('Access-Control-Allow-Headers', 'Content-Type');
this.set('Cache-Control', 'no-cache');
this.set('Connection', 'keep-alive');
yield next;
});
app.use(router.routes());
app.use(router.allowedMethods());
// serve static files
app.use(require('koa-static')(__dirname + '/static'));
app.on('error', (error, ctx) => {
if (error.status) {
log.verbose('app', 'Request faild: %s, %s', error.status, error.message);
} else {
log.error('app', 'Unknown error', error, ctx);
}
});
//
// Module initialization
//
function injectDependencies() {
mongo = new Mongo();
email = new Email();
pgp = new PGP();
publicKey = new PublicKey(pgp, mongo, email);
hkp = new HKP(publicKey);
rest = new REST(publicKey);
}
//
// Start app ... connect to the database and start listening
//
if (!global.testing) { // don't automatically start server in tests
co(function *() {
let app = yield init();
app.listen(config.server.port);
log.info('app', 'Ready to rock! Listening on http://localhost:' + config.server.port);
}).catch(err => log.error('app', 'Initialization failed!', err));
}
function *init() {
log.level = config.log.level; // set log level depending on process.env.NODE_ENV
injectDependencies();
email.init(config.email);
log.info('app', 'Connecting to MongoDB ...');
yield mongo.init(config.mongo);
return app;
}
module.exports = init;

View File

@ -1,91 +0,0 @@
/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const Koa = require('koa');
const serve = require('koa-static');
const router = require('koa-router')();
const render = require('koa-ejs');
const locales = require('koa-locales');
const config = require('config');
const path = require('path');
const middleware = require('./middleware');
const Mongo = require('../dao/mongo');
const Email = require('../email/email');
const HKP = require('../route/hkp');
const REST = require('../route/rest');
const PGP = require('../service/pgp');
const PublicKey = require('../service/public-key');
const app = new Koa();
render(app, {
root: path.join(__dirname, '../view')
});
locales(app, {
defaultLocale: 'en',
dirs: [path.join(__dirname, '../../locales')],
localeAlias: {'de-DE': 'de', 'de-de': 'de', 'de-AT': 'de', 'de-at': 'de', 'de-CH': 'de', 'de-ch': 'de', 'de-LI': 'de', 'de-li': 'de'},
writeCookie: false
});
let hkp;
let rest;
app.use(async (ctx, next) => {
ctx.state = ctx.state || {};
ctx.state.__ = ctx.__.bind(ctx);
await next();
});
// UI views
router.get('/', ctx => ctx.render('index'));
router.redirect('/index.html', '/');
router.get('/manage.html', ctx => ctx.render('manage'));
// HKP and REST api routes
router.post('/pks/add', ctx => hkp.add(ctx));
router.get('/pks/lookup', ctx => hkp.lookup(ctx));
router.post('/api/v1/key', ctx => rest.create(ctx));
router.get('/api/v1/key', ctx => rest.query(ctx));
router.del('/api/v1/key', ctx => rest.remove(ctx));
// setup koa middlewares
app.on('error', middleware.logUnknownError);
app.use(middleware.upgradeToHTTPS);
app.use(middleware.setHTTPResponseHeaders);
app.use(router.routes());
app.use(router.allowedMethods());
app.use(serve(path.join(__dirname, '../static')));
async function init() {
// inject dependencies
const mongo = new Mongo();
const email = new Email();
const pgp = new PGP();
const publicKey = new PublicKey(pgp, mongo, email);
hkp = new HKP(publicKey);
rest = new REST(publicKey);
// init DAOs
email.init(config.email);
await mongo.init(config.mongo);
return app;
}
module.exports = init;

View File

@ -1,59 +0,0 @@
/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const log = require('winston');
const config = require('config');
const util = require('../service/util');
exports.upgradeToHTTPS = async function(ctx, next) {
if (util.isTrue(config.server.httpsUpgrade) && util.checkHTTP(ctx)) {
ctx.redirect(`https://${ctx.hostname}${ctx.url}`);
} else {
await next();
}
};
exports.setHTTPResponseHeaders = async function(ctx, next) {
// HSTS
if (util.isTrue(config.server.httpsUpgrade)) {
ctx.set('Strict-Transport-Security', 'max-age=16070400');
}
// HPKP
if (config.server.httpsKeyPin && config.server.httpsKeyPinBackup) {
ctx.set('Public-Key-Pins', `pin-sha256="${config.server.httpsKeyPin}"; pin-sha256="${config.server.httpsKeyPinBackup}"; max-age=16070400`);
}
// CSP
ctx.set('Content-Security-Policy', "default-src 'self'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'");
// Prevent rendering website in foreign iframe (Clickjacking)
ctx.set('X-Frame-Options', 'DENY');
// CORS
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
ctx.set('Access-Control-Allow-Headers', 'Content-Type');
ctx.set('Connection', 'keep-alive');
await next();
};
exports.logUnknownError = function(error, ctx) {
if (error.status) {
log.verbose('middleware', `Request failed: ${error.status} ${error.message}`);
} else {
log.error('middleware', 'Unknown error', error, ctx);
}
};

View File

@ -17,13 +17,13 @@
'use strict'; 'use strict';
const log = require('winston');
const MongoClient = require('mongodb').MongoClient; const MongoClient = require('mongodb').MongoClient;
/** /**
* A simple wrapper around the official MongoDB client. * A simple wrapper around the official MongoDB client.
*/ */
class Mongo { class Mongo {
/** /**
* Initializes the database client by connecting to the MongoDB. * Initializes the database client by connecting to the MongoDB.
* @param {String} uri The mongodb uri * @param {String} uri The mongodb uri
@ -31,11 +31,9 @@ class Mongo {
* @param {String} pass The database user's password * @param {String} pass The database user's password
* @yield {undefined} * @yield {undefined}
*/ */
async init({uri, user, pass}) { *init(options) {
log.info('mongo', 'Connecting to MongoDB ...'); let uri = 'mongodb://' + options.user + ':' + options.pass + '@' + options.uri;
const url = `mongodb://${user}:${pass}@${uri}`; this._db = yield MongoClient.connect(uri);
this._client = await MongoClient.connect(url, {useNewUrlParser: true, useUnifiedTopology: true});
this._db = this._client.db();
} }
/** /**
@ -43,7 +41,7 @@ class Mongo {
* @yield {undefined} * @yield {undefined}
*/ */
disconnect() { disconnect() {
return this._client.close(); return this._db.close();
} }
/** /**
@ -53,7 +51,7 @@ class Mongo {
* @yield {Object} The operation result * @yield {Object} The operation result
*/ */
create(document, type) { create(document, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.insertOne(document); return col.insertOne(document);
} }
@ -64,7 +62,7 @@ class Mongo {
* @yield {Object} The operation result * @yield {Object} The operation result
*/ */
batch(documents, type) { batch(documents, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.insertMany(documents); return col.insertMany(documents);
} }
@ -76,8 +74,8 @@ class Mongo {
* @yield {Object} The operation result * @yield {Object} The operation result
*/ */
update(query, diff, type) { update(query, diff, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.updateOne(query, {$set: diff}); return col.updateOne(query, { $set:diff });
} }
/** /**
@ -87,7 +85,7 @@ class Mongo {
* @yield {Object} The document object * @yield {Object} The document object
*/ */
get(query, type) { get(query, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.findOne(query); return col.findOne(query);
} }
@ -98,7 +96,7 @@ class Mongo {
* @yield {Array} An array of document objects * @yield {Array} An array of document objects
*/ */
list(query, type) { list(query, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.find(query).toArray(); return col.find(query).toArray();
} }
@ -109,7 +107,7 @@ class Mongo {
* @yield {Object} The operation result * @yield {Object} The operation result
*/ */
remove(query, type) { remove(query, type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.deleteMany(query); return col.deleteMany(query);
} }
@ -119,9 +117,10 @@ class Mongo {
* @yield {Object} The operation result * @yield {Object} The operation result
*/ */
clear(type) { clear(type) {
const col = this._db.collection(type); let col = this._db.collection(type);
return col.deleteMany({}); return col.deleteMany({});
} }
} }
module.exports = Mongo; module.exports = Mongo;

View File

@ -1,52 +0,0 @@
/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const log = require('winston');
const {SPLAT} = require('triple-beam');
const config = require('config');
require('winston-papertrail');
log.exitOnError = false;
log.level = config.log.level;
// Reformat logging text, due to deprecated logger usage
const formatLogs = log.format(info => {
info.message = `${info.message} -> ${info[SPLAT].join(', ')}`;
return info;
});
exports.init = function({host, port}) {
if (host && port) {
log.add(new log.transports.Papertrail({
format: formatLogs(),
level: config.log.level,
host,
port
}));
return;
}
if (process.env.NODE_ENV !== 'production') {
log.add(new log.transports.Console({
format: log.format.combine(
formatLogs(),
log.format.simple()
)
}));
}
};

View File

@ -17,15 +17,16 @@
'use strict'; 'use strict';
const log = require('winston'); const log = require('npmlog');
const util = require('../service/util'); const util = require('../service/util');
const openpgp = require('openpgp');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
/** /**
* A simple wrapper around Nodemailer to send verification emails * A simple wrapper around Nodemailer to send verification emails
*/ */
class Email { class Email {
/** /**
* Create an instance of the reusable nodemailer SMTP transport. * Create an instance of the reusable nodemailer SMTP transport.
* @param {string} host SMTP server's hostname: 'smtp.gmail.com' * @param {string} host SMTP server's hostname: 'smtp.gmail.com'
@ -36,85 +37,86 @@ class Email {
* @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. * @param {boolean} pgp (optional) if outgoing emails are encrypted to the user's public key.
*/ */
init({host, port = 465, auth, tls, starttls, pgp, sender}) { init(options) {
this._transporter = nodemailer.createTransport({ this._transport = nodemailer.createTransport({
host, host: options.host,
port, port: options.port || 465,
auth, auth: options.auth,
secure: (tls !== undefined) ? util.isTrue(tls) : true, secure: (options.tls !== undefined) ? util.isTrue(options.tls) : true,
requireTLS: (starttls !== undefined) ? util.isTrue(starttls) : true, requireTLS: (options.starttls !== undefined) ? util.isTrue(options.starttls) : true,
}); });
this._usePGPEncryption = util.isTrue(pgp); if (util.isTrue(options.pgp)) {
this._sender = sender; this._transport.use('stream', openpgpEncrypt());
}
this._sender = options.sender;
} }
/** /**
* Send the verification email to the user using a template. * Send the verification email to the user using a template.
* @param {Object} template the email template function to use * @param {Object} template the email template to use
* @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' } * @param {Object} userId user id document
* @param {string} keyId key id of public key * @param {string} keyId key id of public key
* @param {Object} origin origin of the server * @param {Object} origin origin of the server
* @yield {Object} reponse object containing SMTP info * @yield {Object} send response from the SMTP server
*/ */
async send({template, userId, keyId, origin, publicKeyArmored}) { *send(options) {
const compiled = template({ let template = options.template, userId = options.userId, keyId = options.keyId, origin = options.origin;
...userId, let message = {
origin, from: this._sender,
keyId to: userId,
}); subject: template.subject,
if (this._usePGPEncryption && publicKeyArmored) { text: template.text,
compiled.text = await this._pgpEncrypt(compiled.text, publicKeyArmored); html: template.html,
} params: {
const sendOptions = { name: userId.name,
from: {name: this._sender.name, address: this._sender.email}, baseUrl: util.url(origin),
to: {name: userId.name, address: userId.email}, keyId: keyId,
subject: compiled.subject, nonce: userId.nonce
text: compiled.text }
}; };
return this._sendHelper(sendOptions); return yield this._sendHelper(message);
}
/**
* Encrypt the message body using OpenPGP.js
* @param {string} plaintext the plaintext message body
* @param {string} publicKeyArmored the recipient's public key
* @return {string} the encrypted PGP message block
*/
async _pgpEncrypt(plaintext, publicKeyArmored) {
const {keys: [key], err} = await openpgp.key.readArmored(publicKeyArmored);
if (err) {
log.error('email', 'Reading armored key failed.', err, publicKeyArmored);
}
const now = new Date();
// set message creation date if key has been created with future creation date
const msgCreationDate = key.primaryKey.created > now ? key.primaryKey.created : now;
try {
const ciphertext = await openpgp.encrypt({
message: openpgp.message.fromText(plaintext),
publicKeys: key,
date: msgCreationDate
});
return ciphertext.data;
} catch (error) {
log.error('email', 'Encrypting message failed.', error, publicKeyArmored);
util.throw(400, 'Encrypting message for verification email failed.', error);
}
} }
/** /**
* A generic method to send an email message via nodemailer. * A generic method to send an email message via nodemailer.
* @param {Object} sendoptions object: { from: ..., to: ..., subject: ..., text: ... } * @param {Object} from sender user id object: { name:'Jon Smith', email:'j@smith.com' }
* @param {Object} to recipient user id object: { name:'Jon Smith', email:'j@smith.com' }
* @param {string} subject message subject
* @param {string} text message plaintext body template
* @param {string} html message html body template
* @param {Object} params (optional) nodermailer template parameters
* @yield {Object} reponse object containing SMTP info * @yield {Object} reponse object containing SMTP info
*/ */
async _sendHelper(sendOptions) { *_sendHelper(options) {
let template = {
subject: options.subject,
text: options.text,
html: options.html,
encryptionKeys: [options.to.publicKeyArmored]
};
let sender = {
from: {
name: options.from.name,
address: options.from.email
}
};
let recipient = {
to: {
name: options.to.name,
address: options.to.email
}
};
let params = options.params || {};
try { try {
const info = await this._transporter.sendMail(sendOptions); let sendFn = this._transport.templateSender(template, sender);
let info = yield sendFn(recipient, params);
if (!this._checkResponse(info)) { if (!this._checkResponse(info)) {
log.warn('email', 'Message may not have been received.', info); log.warn('email', 'Message may not have been received.', info);
} }
return info; return info;
} catch (error) { } catch(error) {
log.error('email', 'Sending message failed.', error); log.error('email', 'Sending message failed.', error, options);
util.throw(500, 'Sending email to user failed'); util.throw(500, 'Sending email to user failed');
} }
} }
@ -128,6 +130,7 @@ class Email {
_checkResponse(info) { _checkResponse(info) {
return /^2/.test(info.response); return /^2/.test(info.response);
} }
} }
module.exports = Email; module.exports = Email;

View File

@ -1,21 +0,0 @@
'use strict';
const util = require('../service/util');
function verifyKey(ctx, {name, email, nonce, origin, keyId}) {
const link = `${util.url(origin)}/api/v1/key?op=verify&keyId=${keyId}&nonce=${nonce}`;
return {
subject: ctx.__('verify_key_subject'),
text: ctx.__('verify_key_text', [name, email, link, origin.host])
};
}
function verifyRemove(ctx, {name, email, nonce, origin, keyId}) {
const link = `${util.url(origin)}/api/v1/key?op=verifyRemove&keyId=${keyId}&nonce=${nonce}`;
return {
subject: ctx.__('verify_removal_subject'),
text: ctx.__('verify_removal_text', [name, email, origin.host, link])
};
}
module.exports = {verifyKey, verifyRemove};

12
src/email/templates.json Normal file
View File

@ -0,0 +1,12 @@
{
"verifyKey": {
"subject": "Verify Your Key",
"text": "Hello {{name}},\n\nplease click here to verify your key:\n\n{{baseUrl}}/api/v1/verify?keyId={{keyId}}&nonce={{nonce}}",
"html": "<p>Hello {{name}},</p><p>please <a href=\"{{baseUrl}}/api/v1/verify?keyId={{keyId}}&nonce={{nonce}}\">click here to verify</a> your key.</p>"
},
"verifyRemove": {
"subject": "Verify Key Removal",
"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": "<p>Hello {{name}},</p><p>please <a href=\"{{baseUrl}}/api/v1/verifyRemove?keyId={{keyId}}&nonce={{nonce}}\">click here to verify</a> the removal of your key.</p>"
}
}

View File

@ -1,33 +0,0 @@
/**
* Mailvelope - secure email with OpenPGP encryption for Webmail
* Copyright (C) 2016 Mailvelope GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
'use strict';
const log = require('winston');
const config = require('config');
const init = require('./app');
(async () => {
try {
const app = await init();
app.listen(config.server.port);
log.info('app', `Listening on http://localhost:${config.server.port}`);
} catch (err) {
log.error('app', 'Initialization failed!', err);
throw err;
}
})();

View File

@ -25,6 +25,7 @@ const util = require('../service/util');
* See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 * See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
*/ */
class HKP { class HKP {
/** /**
* Create an instance of the HKP server * Create an instance of the HKP server
* @param {Object} publicKey An instance of the public key service * @param {Object} publicKey An instance of the public key service
@ -37,13 +38,14 @@ class HKP {
* Public key upload via http POST * Public key upload via http POST
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async add(ctx) { *add(ctx) {
const {keytext: publicKeyArmored} = await parse.form(ctx, {limit: '1mb'}); let body = yield parse.form(ctx, { limit: '1mb' });
let publicKeyArmored = body.keytext;
if (!publicKeyArmored) { if (!publicKeyArmored) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
const origin = util.origin(ctx); let origin = util.origin(ctx);
await this._publicKey.put({publicKeyArmored, origin}, ctx); yield this._publicKey.put({ publicKeyArmored, origin });
ctx.body = 'Upload successful. Check your inbox to verify your email address.'; ctx.body = 'Upload successful. Check your inbox to verify your email address.';
ctx.status = 201; ctx.status = 201;
} }
@ -52,11 +54,11 @@ class HKP {
* Public key lookup via http GET * Public key lookup via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async lookup(ctx) { *lookup(ctx) {
const params = this.parseQueryString(ctx); let params = this.parseQueryString(ctx);
const key = await this._publicKey.get(params, ctx); let key = yield this._publicKey.get(params);
this.setGetHeaders(ctx, params); this.setGetHeaders(ctx, params);
await this.setGetBody(ctx, params, key); this.setGetBody(ctx, params, key);
} }
/** /**
@ -66,19 +68,19 @@ class HKP {
* @return {Object} The query parameters or undefined for an invalid request * @return {Object} The query parameters or undefined for an invalid request
*/ */
parseQueryString(ctx) { parseQueryString(ctx) {
const params = { let params = {
op: ctx.query.op, // operation ... only 'get' is supported op: ctx.query.op, // operation ... only 'get' is supported
mr: ctx.query.options === 'mr' // machine readable mr: ctx.query.options === 'mr' // machine readable
}; };
if (this.checkId(ctx.query.search)) { if (this.checkId(ctx.query.search)) {
const id = ctx.query.search.replace(/^0x/, ''); let id = ctx.query.search.replace(/^0x/, '');
params.keyId = util.isKeyId(id) ? id : undefined; params.keyId = util.isKeyId(id) ? id : undefined;
params.fingerprint = util.isFingerPrint(id) ? id : undefined; params.fingerprint = util.isFingerPrint(id) ? id : undefined;
} else if (util.isEmail(ctx.query.search)) { } else if (util.isEmail(ctx.query.search)) {
params.email = ctx.query.search; params.email = ctx.query.search;
} }
if (['get', 'index', 'vindex'].indexOf(params.op) === -1) { if (['get','index','vindex'].indexOf(params.op) === -1) {
ctx.throw(501, 'Not implemented!'); ctx.throw(501, 'Not implemented!');
} else if (!params.keyId && !params.fingerprint && !params.email) { } else if (!params.keyId && !params.fingerprint && !params.email) {
ctx.throw(501, 'Not implemented!'); ctx.throw(501, 'Not implemented!');
@ -119,27 +121,24 @@ class HKP {
* @param {Object} params The parsed query string parameters * @param {Object} params The parsed query string parameters
* @param {Object} key The public key document * @param {Object} key The public key document
*/ */
async setGetBody(ctx, params, key) { setGetBody(ctx, params, key) {
if (params.op === 'get') { if (params.op === 'get') {
if (params.mr) { ctx.body = key.publicKeyArmored;
ctx.body = key.publicKeyArmored; } else if (['index','vindex'].indexOf(params.op) !== -1) {
} else { const VERSION = 1, COUNT = 1; // number of keys
await ctx.render('key-armored', {query: params, key}); let fp = key.fingerprint.toUpperCase();
} let algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
} else if (['index', 'vindex'].indexOf(params.op) !== -1) { let created = key.created ? (key.created.getTime() / 1000) : '';
const VERSION = 1;
const COUNT = 1; // number of keys
const fp = key.fingerprint.toUpperCase();
const algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
const created = key.created ? (key.created.getTime() / 1000) : '';
ctx.body = `info:${VERSION}:${COUNT}\npub:${fp}:${algo}:${key.keySize}:${created}::\n`; ctx.body = 'info:' + VERSION + ':' + COUNT + '\n' +
'pub:' + fp + ':' + algo + ':' + key.keySize + ':' + created + '::\n';
for (const uid of key.userIds) { for (let uid of key.userIds) {
ctx.body += `uid:${encodeURIComponent(`${uid.name} <${uid.email}>`)}:::\n`; ctx.body += 'uid:' + encodeURIComponent(uid.name + ' <' + uid.email + '>') + ':::\n';
} }
} }
} }
} }
module.exports = HKP; module.exports = HKP;

View File

@ -24,6 +24,7 @@ const util = require('../service/util');
* The REST api to provide additional functionality on top of HKP * The REST api to provide additional functionality on top of HKP
*/ */
class REST { class REST {
/** /**
* Create an instance of the REST server * Create an instance of the REST server
* @param {Object} publicKey An instance of the public key service * @param {Object} publicKey An instance of the public key service
@ -34,63 +35,79 @@ class REST {
} }
/** /**
* Public key / user ID upload via http POST * Public key upload via http POST
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async create(ctx) { *create(ctx) {
const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'}); let q = yield parse.json(ctx, { limit: '1mb' });
if (!publicKeyArmored) { let publicKeyArmored = q.publicKeyArmored, primaryEmail = q.primaryEmail;
if (!publicKeyArmored || (primaryEmail && !util.isEmail(primaryEmail))) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
const origin = util.origin(ctx); let origin = util.origin(ctx);
await this._publicKey.put({emails, publicKeyArmored, origin}, ctx); yield this._publicKey.put({ publicKeyArmored, primaryEmail, origin });
ctx.body = 'Upload successful. Check your inbox to verify your email address.'; ctx.body = 'Upload successful. Check your inbox to verify your email address.';
ctx.status = 201; ctx.status = 201;
} }
/**
* Public key query via http GET
* @param {Object} ctx The koa request/response context
*/
async query(ctx) {
const op = ctx.query.op;
if (op === 'verify' || op === 'verifyRemove') {
return this[op](ctx); // delegate operation
}
// do READ if no 'op' provided
const q = {keyId: ctx.query.keyId, fingerprint: ctx.query.fingerprint, email: ctx.query.email};
if (!util.isKeyId(q.keyId) && !util.isFingerPrint(q.fingerprint) && !util.isEmail(q.email)) {
ctx.throw(400, 'Invalid request!');
}
ctx.body = await this._publicKey.get(q, ctx);
}
/** /**
* Verify a public key's user id via http GET * Verify a public key's user id via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async verify(ctx) { *verify(ctx) {
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; let q = { keyId:ctx.query.keyId, nonce:ctx.query.nonce };
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
const {email} = await this._publicKey.verify(q); yield this._publicKey.verify(q);
// create link for sharing // create link for sharing
const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`); let link = util.url(util.origin(ctx), '/user/' + q.keyId.toUpperCase());
await ctx.render('verify-success', {email, link}); ctx.body = `<p>Email address successfully verified!</p><p>Link to share your key: <a href="${link}" target="_blank">${link}</a></p>`;
ctx.set('Content-Type', 'text/html; charset=utf-8');
}
/**
* Public key fetch via http GET
* @param {Object} ctx The koa request/response context
*/
*read(ctx) {
let q = { keyId:ctx.query.keyId, fingerprint:ctx.query.fingerprint, email:ctx.query.email };
if (!util.isKeyId(q.keyId) && !util.isFingerPrint(q.fingerprint) && !util.isEmail(q.email)) {
ctx.throw(400, 'Invalid request!');
}
ctx.body = yield this._publicKey.get(q);
}
/**
* Public key fetch via http GET (shorthand link for sharing)
* @param {Object} ctx The koa request/response context
*/
*share(ctx) {
let q, search = ctx.params.search;
if (util.isEmail(search)) {
q = { email:search };
} else if (util.isKeyId(search)) {
q = { keyId:search };
} else if (util.isFingerPrint(search)) {
q = { fingerprint:search };
}
if (!q) {
ctx.throw(400, 'Invalid request!');
}
ctx.body = (yield this._publicKey.get(q)).publicKeyArmored;
} }
/** /**
* Request public key removal via http DELETE * Request public key removal via http DELETE
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async remove(ctx) { *remove(ctx) {
const q = {keyId: ctx.query.keyId, email: ctx.query.email, origin: util.origin(ctx)}; let q = { keyId:ctx.query.keyId, email:ctx.query.email, origin:util.origin(ctx) };
if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) { if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
await this._publicKey.requestRemove(q, ctx); yield this._publicKey.requestRemove(q);
ctx.body = 'Check your inbox to verify the removal of your email address.'; ctx.body = 'Check your inbox to verify the removal of your key.';
ctx.status = 202; ctx.status = 202;
} }
@ -98,14 +115,15 @@ class REST {
* Verify public key removal via http GET * Verify public key removal via http GET
* @param {Object} ctx The koa request/response context * @param {Object} ctx The koa request/response context
*/ */
async verifyRemove(ctx) { *verifyRemove(ctx) {
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; let q = { keyId:ctx.query.keyId, nonce:ctx.query.nonce };
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
ctx.throw(400, 'Invalid request!'); ctx.throw(400, 'Invalid request!');
} }
const {email} = await this._publicKey.verifyRemove(q); yield this._publicKey.verifyRemove(q);
await ctx.render('removal-success', {email}); ctx.body = 'Key successfully removed!';
} }
} }
module.exports = REST; module.exports = REST;

View File

@ -17,9 +17,10 @@
'use strict'; 'use strict';
const log = require('winston'); const log = require('npmlog');
const util = require('./util'); const util = require('./util');
const openpgp = require('openpgp'); const openpgp = require('openpgp');
const addressparser = require('addressparser');
const KEY_BEGIN = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; const KEY_BEGIN = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----'; const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
@ -28,22 +29,18 @@ const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
* A simple wrapper around OpenPGP.js * A simple wrapper around OpenPGP.js
*/ */
class PGP { class PGP {
constructor() {
openpgp.config.show_version = false;
openpgp.config.show_comment = false;
}
/** /**
* Parse an ascii armored pgp key block and get its parameters. * Parse an ascii armored pgp key block and get its parameters.
* @param {String} publicKeyArmored ascii armored pgp key block * @param {String} publicKeyArmored ascii armored pgp key block
* @return {Object} public key document to persist * @return {Object} public key document to persist
*/ */
async parseKey(publicKeyArmored) { parseKey(publicKeyArmored) {
publicKeyArmored = this.trimKey(publicKeyArmored); publicKeyArmored = this.trimKey(publicKeyArmored);
const r = await openpgp.key.readArmored(publicKeyArmored); let r = openpgp.key.readArmored(publicKeyArmored);
if (r.err) { if (r.err) {
const error = r.err[0]; let error = r.err[0];
log.error('pgp', 'Failed to parse PGP key:\n%s', publicKeyArmored, error); log.error('pgp', 'Failed to parse PGP key:\n%s', publicKeyArmored, error);
util.throw(500, 'Failed to parse PGP key'); util.throw(500, 'Failed to parse PGP key');
} else if (!r.keys || r.keys.length !== 1 || !r.keys[0].primaryKey) { } else if (!r.keys || r.keys.length !== 1 || !r.keys[0].primaryKey) {
@ -51,39 +48,33 @@ class PGP {
} }
// verify primary key // verify primary key
const key = r.keys[0]; let key = r.keys[0];
const primaryKey = key.primaryKey; let primaryKey = key.primaryKey;
const now = new Date(); if (key.verifyPrimaryKey() !== openpgp.enums.keyStatus.valid) {
const verifyDate = primaryKey.created > now ? primaryKey.created : now;
if (await key.verifyPrimaryKey(verifyDate) !== openpgp.enums.keyStatus.valid) {
util.throw(400, 'Invalid PGP key: primary key verification failed'); util.throw(400, 'Invalid PGP key: primary key verification failed');
} }
// accept version 4 keys only // accept version 4 keys only
const keyId = primaryKey.getKeyId().toHex(); let keyId = primaryKey.getKeyId().toHex();
const fingerprint = primaryKey.getFingerprint(); let fingerprint = primaryKey.fingerprint;
if (!util.isKeyId(keyId) || !util.isFingerPrint(fingerprint)) { if (!util.isKeyId(keyId) || !util.isFingerPrint(fingerprint)) {
util.throw(400, 'Invalid PGP key: only v4 keys are accepted'); util.throw(400, 'Invalid PGP key: only v4 keys are accepted');
} }
// check for at least one valid user id // check for at least one valid user id
const userIds = await this.parseUserIds(key.users, primaryKey, verifyDate); let userIds = this.parseUserIds(key.users, primaryKey);
if (!userIds.length) { if (!userIds.length) {
util.throw(400, 'Invalid PGP key: invalid user IDs'); util.throw(400, 'Invalid PGP key: invalid user ids');
} }
// get algorithm details from primary key
const keyInfo = key.primaryKey.getAlgorithmInfo();
// public key document that is stored in the database // public key document that is stored in the database
return { return {
keyId, keyId,
fingerprint, fingerprint,
userIds, userIds,
created: primaryKey.created, created: primaryKey.created,
uploaded: new Date(), algorithm: primaryKey.algorithm,
algorithm: keyInfo.algorithm, keySize: primaryKey.getBitSize(),
keySize: keyInfo.bits,
publicKeyArmored publicKeyArmored
}; };
} }
@ -117,81 +108,35 @@ class PGP {
/** /**
* Parse an array of user ids and verify signatures * Parse an array of user ids and verify signatures
* @param {Array} users A list of openpgp.js user objects * @param {Array} users A list of openpgp.js user objects
* @param {Object} primaryKey The primary key packet of the key
* @param {Date} verifyDate Verify user IDs at this point in time
* @return {Array} An array of user id objects * @return {Array} An array of user id objects
*/ */
async parseUserIds(users, primaryKey, verifyDate = new Date()) { parseUserIds(users, primaryKey) {
if (!users || !users.length) { if (!users || !users.length) {
util.throw(400, 'Invalid PGP key: no user ID found'); util.throw(400, 'Invalid PGP key: no user id found');
} }
// at least one user id must be valid, revoked or expired // at least one user id signature must be valid
const result = []; let result = [];
for (const user of users) { for (let user of users) {
const userStatus = await user.verify(primaryKey, verifyDate); let oneValid = false;
if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) { for (let cert of user.selfCertifications) {
try { if (user.isValidSelfCertificate(primaryKey, cert)) {
const uid = openpgp.util.parseUserId(user.userId.userid); oneValid = true;
if (util.isEmail(uid.email)) { }
// map to local user id object format }
result.push({ if (oneValid && user.userId && user.userId.userid) {
status: userStatus, let uid = addressparser(user.userId.userid)[0];
name: uid.name, if (util.isEmail(uid.address)) {
email: util.normalizeEmail(uid.email), result.push(uid);
verified: false }
});
}
} catch (e) {}
} }
} }
return result; // map to local user id object format
} return result.map(uid => ({
name: uid.name,
/** email: uid.address.toLowerCase(),
* Remove user IDs from armored key block which are not in array of user IDs verified: false
* @param {Array} userIds user IDs to be kept }));
* @param {String} armored armored key block to be filtered
* @return {String} filtered amored key block
*/
async filterKeyByUserIds(userIds, armored) {
const emails = userIds.map(({email}) => email);
const {keys: [key]} = await openpgp.key.readArmored(armored);
key.users = key.users.filter(({userId}) => !userId || emails.includes(util.normalizeEmail(userId.email)));
return key.armor();
}
/**
* Merge (update) armored key blocks
* @param {String} srcArmored source amored key block
* @param {String} dstArmored destination armored key block
* @return {String} merged armored key block
*/
async updateKey(srcArmored, dstArmored) {
const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored);
if (srcErr) {
log.error('pgp', 'Failed to parse source PGP key for update:\n%s', srcArmored, srcErr);
util.throw(500, 'Failed to parse PGP key');
}
const {keys: [dstKey], err: dstErr} = await openpgp.key.readArmored(dstArmored);
if (dstErr) {
log.error('pgp', 'Failed to parse destination PGP key for update:\n%s', dstArmored, dstErr);
util.throw(500, 'Failed to parse PGP key');
}
await dstKey.update(srcKey);
return dstKey.armor();
}
/**
* Remove user ID from armored key block
* @param {String} email email of user ID to be removed
* @param {String} publicKeyArmored amored key block to be filtered
* @return {String} filtered armored key block
*/
async removeUserId(email, publicKeyArmored) {
const {keys: [key]} = await openpgp.key.readArmored(publicKeyArmored);
key.users = key.users.filter(({userId}) => !userId || util.normalizeEmail(userId.email) !== email);
return key.armor();
} }
} }
module.exports = PGP; module.exports = PGP;

View File

@ -17,9 +17,8 @@
'use strict'; 'use strict';
const config = require('config');
const util = require('./util'); const util = require('./util');
const tpl = require('../email/templates'); const tpl = require('../email/templates.json');
/** /**
* Database documents have the format: * Database documents have the format:
@ -36,19 +35,18 @@ const tpl = require('../email/templates');
* } * }
* ], * ],
* created: Sat Oct 17 2015 12:17:03 GMT+0200 (CEST), // key creation time as JavaScript Date * created: Sat Oct 17 2015 12:17:03 GMT+0200 (CEST), // key creation time as JavaScript Date
* uploaded: Sat Oct 17 2015 12:17:03 GMT+0200 (CEST), // time of key upload as JavaScript Date
* algorithm: 'rsa_encrypt_sign', // primary key alogrithm * algorithm: 'rsa_encrypt_sign', // primary key alogrithm
* keySize: 4096, // key length in bits * keySize: 4096, // key length in bits
* publicKeyArmored: '-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----' * publicKeyArmored: '-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----'
* } * }
*/ */
const DB_TYPE = 'publickey'; const DB_TYPE = 'publickey';
const KEY_STATUS_VALID = 3;
/** /**
* A service that handlers PGP public keys queries to the database * A service that handlers PGP public keys queries to the database
*/ */
class PublicKey { class PublicKey {
/** /**
* Create an instance of the service * Create an instance of the service
* @param {Object} pgp An instance of the OpenPGP.js wrapper * @param {Object} pgp An instance of the OpenPGP.js wrapper
@ -63,138 +61,64 @@ class PublicKey {
/** /**
* Persist a new public key * Persist a new public key
* @param {Array} emails (optional) The emails to upload/update
* @param {String} publicKeyArmored The ascii armored pgp key block * @param {String} publicKeyArmored The ascii armored pgp key block
* @param {String} primaryEmail (optional) The key's primary email address
* @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' } * @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' }
* @param {Object} ctx Context * @yield {undefined}
* @return {Promise}
*/ */
async put({emails = [], publicKeyArmored, origin}, ctx) { *put(options) {
emails = emails.map(util.normalizeEmail);
// lazily purge old/unverified keys on every key upload
await this._purgeOldUnverified();
// parse key block // parse key block
const key = await this._pgp.parseKey(publicKeyArmored); let publicKeyArmored = options.publicKeyArmored, primaryEmail = options.primaryEmail, origin = options.origin;
// if emails array is empty, all userIds of the key will be submitted let key = this._pgp.parseKey(publicKeyArmored);
if (emails.length) { // check for existing verfied key by id or email addresses
// keep submitted user IDs only let verified = yield this.getVerified(key);
key.userIds = key.userIds.filter(({email}) => emails.includes(email));
if (key.userIds.length !== emails.length) {
util.throw(400, 'Provided email address does not match a valid user ID of the key');
}
}
// check for existing verified key with same id
const verified = await this.getVerified({keyId: key.keyId});
if (verified) { if (verified) {
key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored); util.throw(304, 'Key for this user already exists');
// reduce new key to verified user IDs
const filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored);
// update verified key with new key
key.publicKeyArmored = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored);
} else {
key.userIds = key.userIds.filter(userId => userId.status === KEY_STATUS_VALID);
if (!key.userIds.length) {
util.throw(400, 'Invalid PGP key: no valid user IDs found');
}
await this._addKeyArmored(key.userIds, key.publicKeyArmored);
// new key, set armored to null
key.publicKeyArmored = null;
} }
// send mails to verify user ids
await this._sendVerifyEmail(key, origin, ctx);
// store key in database // store key in database
await this._persistKey(key); yield this._persisKey(key);
// send mails to verify user ids (send only one if primary email is provided)
yield this._sendVerifyEmail(key, primaryEmail, origin);
} }
/** /**
* Delete all keys where no user id has been verified after x days. * Persist the public key and its user ids in the database.
* @return {Promise} * @param {Object} key public key parameters
* @yield {undefined} The persisted user id documents
*/ */
async _purgeOldUnverified() { *_persisKey(key) {
// create date in the past to compare with // delete old/unverified key
const xDaysAgo = new Date(); yield this._mongo.remove({ fingerprint:key.fingerprint }, DB_TYPE);
xDaysAgo.setDate(xDaysAgo.getDate() - config.publicKey.purgeTimeInDays); // generate nonces for verification
// remove unverified keys older than x days (or no 'uploaded' attribute) for (let uid of key.userIds) {
return this._mongo.remove({ uid.nonce = util.random();
'userIds.verified': {$ne: true}, }
uploaded: {$lt: xDaysAgo} // persist new key
}, DB_TYPE); let r = yield this._mongo.create(key, DB_TYPE);
} if (r.insertedCount !== 1) {
util.throw(500, 'Failed to persist key');
/**
* Merge existing and new user IDs
* @param {Array} existingUsers source user IDs
* @param {Array} newUsers new user IDs
* @param {String} publicKeyArmored armored key block of new user IDs
* @return {Array} merged user IDs
*/
async _mergeUsers(existingUsers, newUsers, publicKeyArmored) {
const result = [];
// existing verified valid or revoked users
const verifiedUsers = existingUsers.filter(userId => userId.verified);
// valid new users which are not yet verified
const validUsers = newUsers.filter(userId => userId.status === KEY_STATUS_VALID && !this._includeEmail(verifiedUsers, userId));
// pending users are not verified, not newly submitted
const pendingUsers = existingUsers.filter(userId => !userId.verified && !this._includeEmail(validUsers, userId));
await this._addKeyArmored(validUsers, publicKeyArmored);
result.push(...validUsers, ...pendingUsers, ...verifiedUsers);
return result;
}
/**
* Create amored key block which contains the corresponding user ID only and add it to the user ID object
* @param {Array} userIds user IDs to be extended
* @param {String} PublicKeyArmored armored key block to be filtered
* @return {Promise}
*/
async _addKeyArmored(userIds, publicKeyArmored) {
for (const userId of userIds) {
userId.publicKeyArmored = await this._pgp.filterKeyByUserIds([userId], publicKeyArmored);
userId.notify = true;
} }
}
_includeEmail(users, user) {
return users.find(({email}) => email === user.email);
} }
/** /**
* 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 {Object} origin the server's origin (required for email links) * @param {Object} origin the server's origin (required for email links)
* @param {Object} ctx Context * @yield {undefined}
* @return {Promise}
*/ */
async _sendVerifyEmail({userIds, keyId}, origin, ctx) { *_sendVerifyEmail(key, primaryEmail, origin) {
for (const userId of userIds) { let userIds = key.userIds, keyId = key.keyId;
if (userId.notify && userId.notify === true) { // check for primary email (send only one email)
// generate nonce for verification let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
userId.nonce = util.random(); if (primaryUserId) {
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored}); userIds = [primaryUserId];
}
} }
} // send emails
for (let userId of userIds) {
/** userId.publicKeyArmored = key.publicKeyArmored; // set key for encryption
* Persist the public key and its user ids in the database. yield this._email.send({ template:tpl.verifyKey, userId, keyId, origin });
* @param {Object} key public key parameters
* @return {Promise}
*/
async _persistKey(key) {
// delete old/unverified key
await this._mongo.remove({keyId: key.keyId}, DB_TYPE);
// generate nonces for verification
for (const userId of key.userIds) {
// remove status from user
delete userId.status;
// remove notify flag from user
delete userId.notify;
}
// persist new key
const r = await this._mongo.create(key, DB_TYPE);
if (r.insertedCount !== 1) {
util.throw(500, 'Failed to persist key');
} }
} }
@ -202,42 +126,20 @@ class PublicKey {
* Verify a user id by proving knowledge of the nonce. * Verify a user id by proving knowledge of the nonce.
* @param {string} keyId Correspronding public key id * @param {string} keyId Correspronding public key id
* @param {string} nonce The verification nonce proving email address ownership * @param {string} nonce The verification nonce proving email address ownership
* @return {Promise} The email that has been verified * @yield {undefined}
*/ */
async verify({keyId, nonce}) { *verify(options) {
let keyId = options.keyId, nonce = options.nonce;
// look for verification nonce in database // look for verification nonce in database
const query = {keyId, 'userIds.nonce': nonce}; let query = { keyId, 'userIds.nonce':nonce };
const key = await this._mongo.get(query, DB_TYPE); let key = yield this._mongo.get(query, DB_TYPE);
if (!key) { if (!key) {
util.throw(404, 'User ID not found'); util.throw(404, 'User id not found');
}
await this._removeKeysWithSameEmail(key, nonce);
let {publicKeyArmored, email} = key.userIds.find(userId => userId.nonce === nonce);
// update armored key
if (key.publicKeyArmored) {
publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored);
} }
// flag the user id as verified // flag the user id as verified
await this._mongo.update(query, { yield this._mongo.update(query, {
publicKeyArmored,
'userIds.$.verified': true, 'userIds.$.verified': true,
'userIds.$.nonce': null, 'userIds.$.nonce': null
'userIds.$.publicKeyArmored': null
}, DB_TYPE);
return {email};
}
/**
* Removes keys with the same email address
* @param {String} options.keyId source key ID
* @param {Array} options.userIds user IDs of source key
* @param {Array} nonce relevant nonce
* @return {Promise}
*/
async _removeKeysWithSameEmail({keyId, userIds}, nonce) {
return this._mongo.remove({
keyId: {$ne: keyId},
'userIds.email': userIds.find(u => u.nonce === nonce).email
}, DB_TYPE); }, DB_TYPE);
} }
@ -248,9 +150,10 @@ class PublicKey {
* @param {Array} userIds A list of user ids to check * @param {Array} userIds A list of user ids to check
* @param {string} fingerprint The public key fingerprint * @param {string} fingerprint The public key fingerprint
* @param {string} keyId (optional) The public key id * @param {string} keyId (optional) The public key id
* @return {Object} The verified key document * @yield {Object} The verified key document
*/ */
async getVerified({userIds, fingerprint, keyId}) { *getVerified(options) {
let fingerprint = options.fingerprint, userIds = options.userIds, keyId = options.keyId;
let queries = []; let queries = [];
// query by fingerprint // query by fingerprint
if (fingerprint) { if (fingerprint) {
@ -271,13 +174,13 @@ class PublicKey {
queries = queries.concat(userIds.map(uid => ({ queries = queries.concat(userIds.map(uid => ({
userIds: { userIds: {
$elemMatch: { $elemMatch: {
'email': util.normalizeEmail(uid.email), 'email': uid.email.toLowerCase(),
'verified': true 'verified': true
} }
} }
}))); })));
} }
return this._mongo.get({$or: queries}, DB_TYPE); return yield this._mongo.get({ $or:queries }, DB_TYPE);
} }
/** /**
@ -286,15 +189,15 @@ class PublicKey {
* @param {string} fingerprint (optional) The public key fingerprint * @param {string} fingerprint (optional) The public key fingerprint
* @param {string} keyId (optional) The public key id * @param {string} keyId (optional) The public key id
* @param {String} email (optional) The user's email address * @param {String} email (optional) The user's email address
* @param {Object} ctx Context * @yield {Object} The public key document
* @return {Object} The public key document
*/ */
async get({fingerprint, keyId, email}, ctx) { *get(options) {
let fingerprint = options.fingerprint, keyId = options.keyId, email = options.email;
// look for verified key // look for verified key
const userIds = email ? [{email}] : undefined; let userIds = email ? [{ email:email }] : undefined;
const key = await this.getVerified({keyId, fingerprint, userIds}); let key = yield this.getVerified({ keyId, fingerprint, userIds });
if (!key) { if (!key) {
util.throw(404, ctx.__('key_not_found')); util.throw(404, 'Key not found');
} }
// clean json return value (_id, nonce) // clean json return value (_id, nonce)
delete key._id; delete key._id;
@ -314,19 +217,19 @@ class PublicKey {
* @param {String} keyId (optional) The public key id * @param {String} keyId (optional) The public key id
* @param {String} email (optional) The user's email address * @param {String} email (optional) The user's email address
* @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' } * @param {Object} origin Required for links to the keyserver e.g. { protocol:'https', host:'openpgpkeys@example.com' }
* @param {Object} ctx Context * @yield {undefined}
* @return {Promise}
*/ */
async requestRemove({keyId, email, origin}, ctx) { *requestRemove(options) {
let keyId = options.keyId, email = options.email, origin = options.origin;
// flag user ids for removal // flag user ids for removal
const key = await this._flagForRemove(keyId, email); let key = yield this._flagForRemove(keyId, email);
if (!key) { if (!key) {
util.throw(404, 'User ID not found'); util.throw(404, 'User id not found');
} }
// send verification mails // send verification mails
keyId = key.keyId; // get keyId in case request was by email keyId = key.keyId; // get keyId in case request was by email
for (const userId of key.userIds) { for (let userId of key.userIds) {
await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin}); yield this._email.send({ template:tpl.verifyRemove, userId, keyId, origin });
} }
} }
@ -335,28 +238,27 @@ class PublicKey {
* saving it. Either a key id or email address must be provided * saving it. Either a key id or email address must be provided
* @param {String} keyId (optional) The public key id * @param {String} keyId (optional) The public key id
* @param {String} email (optional) The user's email address * @param {String} email (optional) The user's email address
* @return {Array} A list of user ids with nonces * @yield {Array} A list of user ids with nonces
*/ */
async _flagForRemove(keyId, email) { *_flagForRemove(keyId, email) {
email = util.normalizeEmail(email); let query = email ? { 'userIds.email':email } : { keyId };
const query = email ? {'userIds.email': email} : {keyId}; let key = yield this._mongo.get(query, DB_TYPE);
const key = await this._mongo.get(query, DB_TYPE);
if (!key) { if (!key) {
return; return;
} }
// flag only the provided user id // flag only the provided user id
if (email) { if (email) {
const nonce = util.random(); let nonce = util.random();
await this._mongo.update(query, {'userIds.$.nonce': nonce}, DB_TYPE); yield this._mongo.update(query, { 'userIds.$.nonce':nonce }, DB_TYPE);
const uid = key.userIds.find(u => u.email === email); let uid = key.userIds.find(u => u.email === email);
uid.nonce = nonce; uid.nonce = nonce;
return {userIds: [uid], keyId: key.keyId}; return { userIds:[uid], keyId:key.keyId };
} }
// flag all key user ids // flag all key user ids
if (keyId) { if (keyId) {
for (const uid of key.userIds) { for (let uid of key.userIds) {
const nonce = util.random(); let nonce = util.random();
await this._mongo.update({'userIds.email': uid.email}, {'userIds.$.nonce': nonce}, DB_TYPE); yield this._mongo.update({ 'userIds.email':uid.email }, { 'userIds.$.nonce':nonce }, DB_TYPE);
uid.nonce = nonce; uid.nonce = nonce;
} }
return key; return key;
@ -368,33 +270,19 @@ class PublicKey {
* Also deletes all user id documents of that key id. * Also deletes all user id documents of that key id.
* @param {string} keyId public key id * @param {string} keyId public key id
* @param {string} nonce The verification nonce proving email address ownership * @param {string} nonce The verification nonce proving email address ownership
* @return {Promise} * @yield {undefined}
*/ */
async verifyRemove({keyId, nonce}) { *verifyRemove(options) {
let keyId = options.keyId, nonce = options.nonce;
// check if key exists in database // check if key exists in database
const flagged = await this._mongo.get({keyId, 'userIds.nonce': nonce}, DB_TYPE); let flagged = yield this._mongo.get({ keyId, 'userIds.nonce':nonce }, DB_TYPE);
if (!flagged) { if (!flagged) {
util.throw(404, 'User ID not found'); util.throw(404, 'User id not found');
} }
if (flagged.userIds.length === 1) { // delete the key
// delete the key yield this._mongo.remove({ keyId }, DB_TYPE);
await this._mongo.remove({keyId}, DB_TYPE);
return flagged.userIds[0];
}
// update the key
const rmIdx = flagged.userIds.findIndex(userId => userId.nonce === nonce);
const rmUserId = flagged.userIds[rmIdx];
if (rmUserId.verified) {
if (flagged.userIds.filter(({verified}) => verified).length > 1) {
flagged.publicKeyArmored = await this._pgp.removeUserId(rmUserId.email, flagged.publicKeyArmored);
} else {
flagged.publicKeyArmored = null;
}
}
flagged.userIds.splice(rmIdx, 1);
await this._mongo.update({keyId}, flagged, DB_TYPE);
return rmUserId;
} }
} }
module.exports = PublicKey; module.exports = PublicKey;

View File

@ -25,7 +25,7 @@ const crypto = require('crypto');
* @return {boolean} If data is a string * @return {boolean} If data is a string
*/ */
exports.isString = function(data) { exports.isString = function(data) {
return typeof data === 'string' || String.prototype.isPrototypeOf(data); // eslint-disable-line no-prototype-builtins return typeof data === 'string' || String.prototype.isPrototypeOf(data);
}; };
/** /**
@ -37,7 +37,7 @@ exports.isTrue = function(data) {
if (this.isString(data)) { if (this.isString(data)) {
return data === 'true'; return data === 'true';
} else { } else {
return Boolean(data); return !!data;
} }
}; };
@ -78,18 +78,6 @@ exports.isEmail = function(data) {
return re.test(data); return re.test(data);
}; };
/**
* Normalize email address to lowercase.
* @param {string} email The email address
* @return {string} lowercase email address
*/
exports.normalizeEmail = function(email) {
if (email) {
email = email.toLowerCase();
}
return email;
};
/** /**
* Create an error with a custom status attribute e.g. for http codes. * Create an error with a custom status attribute e.g. for http codes.
* @param {number} status The error's http status code * @param {number} status The error's http status code
@ -97,7 +85,7 @@ exports.normalizeEmail = function(email) {
* @return {Error} The resulting error object * @return {Error} The resulting error object
*/ */
exports.throw = function(status, message) { exports.throw = function(status, message) {
const err = new Error(message); let err = new Error(message);
err.status = status; err.status = status;
err.expose = true; // display message to the client err.expose = true; // display message to the client
throw err; throw err;
@ -155,7 +143,7 @@ exports.origin = function(ctx) {
* @return {string} The complete url * @return {string} The complete url
*/ */
exports.url = function(origin, resource) { exports.url = function(origin, resource) {
return `${origin.protocol}://${origin.host}${resource || ''}`; return origin.protocol + '://' + origin.host + (resource || '');
}; };
/** /**
@ -166,4 +154,4 @@ exports.url = function(origin, resource) {
*/ */
exports.hkpUrl = function(ctx) { exports.hkpUrl = function(ctx) {
return (this.checkHTTPS(ctx) ? 'hkps://' : 'hkp://') + ctx.host; return (this.checkHTTPS(ctx) ? 'hkps://' : 'hkp://') + ctx.host;
}; };

File diff suppressed because one or more lines are too long

View File

@ -30,21 +30,6 @@ body {
color: #777; color: #777;
border-top: 1px solid #e5e5e5; border-top: 1px solid #e5e5e5;
} }
.footer nav {
display: block;
float: right;
}
.footer ul {
display: block;
padding-left: 10px;
}
.footer li {
display: inline;
margin: 0 8px;
}
.footer a {
color: inherit;
}
/* Customize container */ /* Customize container */
@media (min-width: 768px) { @media (min-width: 768px) {

80
src/static/demo.html Normal file
View File

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="description" content="An OpenPGP public key server that verifies users by sending an encrypted verification email.">
<meta name="author" content="Tankred Hase">
<title>Mailvelope Key Server</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/jumbotron-narrow.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="index.html">Home</a></li>
<li role="presentation" class="active"><a href="#">Demo</a></li>
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
</ul>
</nav>
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="row marketing">
<div class="col-lg-12">
<h2>OpenPGP key upload</h2>
<form action="/pks/add" method="post">
<p><textarea class="form-control" name="keytext" rows="5" spellcheck="false" placeholder="Paste PGP PUBLIC KEY BLOCK here ..." required></textarea></p>
<input class="btn btn-primary btn-lg" type="submit" value="Upload">
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div class="col-lg-12">
<h2>OpenPGP key lookup</h2>
<form action="/pks/lookup" method="get">
<input class="hidden" type="radio" name="op" value="get" checked="checked">
<div class="input-group input-group-lg">
<input class="form-control" name="search" type="text" spellcheck="false" placeholder="Email address or Key ID e.g. 0x11A1A9C84B18732F" required>
<span class="input-group-btn">
<input class="btn btn-default" type="submit" value="Search">
</span>
</div><!-- /input-group -->
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div class="col-lg-12">
<h2>OpenPGP key removal</h2>
<form action="/api/v1/removeKey" method="get">
<div class="input-group input-group-lg">
<input class="form-control" name="email" type="email" spellcheck="false" placeholder="Email address" required>
<span class="input-group-btn">
<input class="btn btn-default" type="submit" value="Delete">
</span>
</div><!-- /input-group -->
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div class="col-lg-12">
<h2>HKP and REST Apis</h2>
<p>The server offers a modern REST api over HTTPS with HSTS and public key pinning that can be integrated into any app architecture. It is also compatible to the OpenPGP HTTP Keyserver Protocol (HKP). Just copy and paste <a href="hkps://keys.mailvelope.com" target="_blank">hkps://keys.mailvelope.com</a> into your current OpenPGP plugin and go. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#api" target="_blank">Learn more</a>.</p>
</div>
</div> <!-- /row marketing -->
<footer class="footer">
<p>&copy; 2016 Mailvelope GmbH</p>
</footer>
</div> <!-- /container -->
</body>
</html>

67
src/static/index.html Normal file
View File

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="description" content="An OpenPGP public key server that verifies users by sending an encrypted verification email.">
<meta name="author" content="Tankred Hase">
<title>Mailvelope Key Server</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/jumbotron-narrow.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active"><a href="#">Home</a></li>
<li role="presentation"><a href="demo.html">Demo</a></li>
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
</ul>
</nav>
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="jumbotron">
<h1>Secure. Easy.</h1>
<p class="lead">The Mailvelope OpenPGP public key server is the first of its kind. It allows automatic public key lookup to make email privacy <strong>just as painless as modern messengers</strong>.</p>
<p><a class="btn btn-lg btn-success" href="demo.html" role="button">Try it now</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>Privacy made Easy</h4>
<p>Automatic key lookup in OpenPGP mail user agents makes reading and writing encrypted email just as painless as modern messenengers.</p>
<h4>No Web of Trust</h4>
<p>No more key signing parties or publishing your social network online. You can even delete your public key at anytime. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#why-not-use-web-of-trust" target="_blank">Learn more</a>.</p>
<h4>Secure REST Api</h4>
<p>The server offers a modern REST api over HTTPS with HSTS and public key pinning that can be integrated into any app architecture. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#rest-api" target="_blank">Learn more</a>.</p>
</div>
<div class="col-lg-6">
<h4>Verify Yourself</h4>
<p>The server verifies email address ownership as well as private key ownership by sending an encrypted verification email.</p>
<h4>Completely Open</h4>
<p>The code is licensed under the AGPL v3.0 which means you are free to host your own key directory under your domain. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#development" target="_blank">Learn more</a>.</p>
<h4>HKP Compatible</h4>
<p>No need to update your current OpenPGP plugin. Just copy and paste <a href="hkps://keys.mailvelope.com" target="_blank">hkps://keys.mailvelope.com</a> into your settings and go. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#hkp-api" target="_blank">Learn more</a>.</p>
</div>
</div>
<footer class="footer">
<p>&copy; 2016 Mailvelope GmbH</p>
</footer>
</div> <!-- /container -->
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@ -1,53 +0,0 @@
/* eslint-disable */
;(function($) {
'use strict';
$('.progress-bar').css('width', '100%');
// POST key form
$('#addKey form').submit(function(e) {
e.preventDefault();
$('#addKey .alert').addClass('hidden');
$('#addKey .progress').removeClass('hidden');
$.ajax({
method: 'POST',
url: '/api/v1/key',
data: JSON.stringify({ publicKeyArmored:$('#addKey textarea').val() }),
contentType: 'application/json',
}).done(function(data, textStatus, xhr) {
if (xhr.status === 304) {
alert('addKey', 'danger', 'Key already exists!');
} else {
alert('addKey', 'success', xhr.responseText);
}
})
.fail(function(xhr) {
alert('addKey', 'danger', xhr.responseText);
});
});
// DELETE key form
$('#removeKey form').submit(function(e) {
e.preventDefault();
$('#removeKey .alert').addClass('hidden');
$('#removeKey .progress').removeClass('hidden');
var email = $('#removeKey input[type="email"]').val();
$.ajax({
method: 'DELETE',
url: '/api/v1/key?email=' + encodeURIComponent(email)
}).done(function(data, textStatus, xhr) {
alert('removeKey', 'success', xhr.responseText);
})
.fail(function(xhr) {
alert('removeKey', 'danger', xhr.responseText);
});
});
function alert(region, outcome, text) {
$('#' + region + ' .progress').addClass('hidden');
$('#' + region + ' .alert-' + outcome + ' span').text(text);
$('#' + region + ' .alert-' + outcome).removeClass('hidden');
}
}(jQuery));

View File

@ -1,9 +0,0 @@
<footer class="footer">
<nav>
<ul>
<li><a target="_blank" href="https://www.mailvelope.com/imprint">Imprint</a></li> |
<li><a target="_blank" href="https://www.mailvelope.com/privacy-service">Privacy</a></li>
</ul>
</nav>
<p>&copy; 2021 Mailvelope GmbH</p>
</footer>

View File

@ -1,46 +0,0 @@
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation" class="active"><a href="/">Home</a></li>
<li role="presentation"><a href="/manage.html">Manage Keys</a></li>
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
</ul>
</nav>
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="jumbotron">
<h1>Secure. Easy.</h1>
<p class="lead">The Mailvelope OpenPGP public key server is the first of its kind. It allows automatic public key lookup to make email privacy <strong>just as painless as modern messengers</strong>.</p>
<p><a class="btn btn-lg btn-success" href="manage.html" role="button">Try it now</a></p>
</div>
<div class="row marketing">
<div class="col-lg-6">
<h4>Privacy made Easy</h4>
<p>Automatic key lookup in OpenPGP mail user agents makes reading and writing encrypted email just as painless as modern messenengers.</p>
<h4>No Web of Trust</h4>
<p>No more key signing parties or publishing your social network online. You can even delete your public key at anytime. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#why-not-use-web-of-trust" target="_blank">Learn more</a></p>
<h4>Secure REST API</h4>
<p>The server offers a modern REST API over HTTPS with HSTS and public key pinning that can be integrated into any app architecture. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#rest-api" target="_blank">Learn more</a></p>
</div>
<div class="col-lg-6">
<h4>Verify Yourself</h4>
<p>The server verifies email address ownership as well as private key ownership by sending an encrypted verification email.</p>
<h4>Completely Open</h4>
<p>The code is licensed under the AGPL v3.0 which means you are free to host your own key directory under your domain. <a href="https://github.com/mailvelope/keyserver" target="_blank">Learn more</a></p>
<h4>HKP Compatible</h4>
<p>No need to update your current OpenPGP plugin. Just copy and paste <a href="hkps://keys.mailvelope.com" target="_blank">hkps://keys.mailvelope.com</a> into your settings and go. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#hkp-api" target="_blank">Learn more</a></p>
</div>
</div>
<%- include('footer') %>
</div> <!-- /container -->

View File

@ -1,23 +0,0 @@
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="/">Home</a></li>
<li role="presentation"><a href="/manage.html">Manage Keys</a></li>
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
</ul>
</nav>
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="row marketing">
<div class="col-lg-12">
<h4><%= query.email ? `Email: ${query.email}` : query.fingerprint ? `Fingerprint: ${query.fingerprint}` : `Key ID: ${query.keyId}` %></h4>
<pre><%= key.publicKeyArmored %></pre>
</div> <!-- /col-lg-12 -->
</div>
<%- include('footer') %>
</div> <!-- /container -->

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<meta name="description" content="An OpenPGP public key server that verifies users by sending an encrypted verification email.">
<meta name="author" content="Mailvelope GmbH">
<title>Mailvelope Key Server</title>
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/jumbotron-narrow.css">
</head>
<body>
<%- body %>
</body>
</html>

View File

@ -1,80 +0,0 @@
<div class="container">
<div class="header clearfix">
<nav>
<ul class="nav nav-pills pull-right">
<li role="presentation"><a href="/">Home</a></li>
<li role="presentation" class="active"><a href="/manage.html">Manage Keys</a></li>
<li role="presentation"><a href="https://github.com/mailvelope/keyserver" target="_blank">GitHub</a></li>
</ul>
</nav>
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="row marketing">
<div id="addKey" class="col-lg-12">
<h2>OpenPGP key upload</h2>
<div class="alert alert-success hidden" role="alert">
<strong>Success!</strong> <span></span>
</div>
<div class="alert alert-danger hidden" role="alert">
<strong>Error!</strong> <span></span>
</div>
<div class="progress hidden">
<div class="progress-bar progress-bar-striped active" role="progressbar"></div>
</div>
<form action="/pks/add" method="post">
<p><textarea class="form-control" name="keytext" rows="5" spellcheck="false" placeholder="Paste PGP PUBLIC KEY BLOCK here ..." required></textarea></p>
<input class="btn btn-primary btn-lg" type="submit" value="Upload">
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div class="col-lg-12">
<h2>OpenPGP key lookup</h2>
<form action="/pks/lookup" method="get">
<input class="hidden" type="radio" name="op" value="get" checked="checked">
<div class="input-group input-group-lg">
<input class="form-control" name="search" type="text" spellcheck="false" placeholder="Email address or Key ID e.g. 0x11A1A9C84B18732F" required>
<span class="input-group-btn">
<input class="btn btn-default" type="submit" value="Search">
</span>
</div><!-- /input-group -->
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div id="removeKey" class="col-lg-12">
<h2>OpenPGP key removal</h2>
<div class="alert alert-success hidden" role="alert">
<strong>Success!</strong> <span></span>
</div>
<div class="alert alert-danger hidden" role="alert">
<strong>Error!</strong> <span></span>
</div>
<div class="progress hidden">
<div class="progress-bar progress-bar-striped active" role="progressbar"></div>
</div>
<form>
<div class="input-group input-group-lg">
<input class="form-control" name="email" type="email" spellcheck="false" placeholder="Email address" required>
<span class="input-group-btn">
<input class="btn btn-default" type="submit" value="Delete">
</span>
</div><!-- /input-group -->
</form>
<hr>
</div> <!-- /col-lg-12 -->
<div class="col-lg-12">
<h2>HKP and REST Apis</h2>
<p>The server offers a modern REST api over HTTPS with HSTS and public key pinning that can be integrated into any app architecture. It is also compatible to the OpenPGP HTTP Keyserver Protocol (HKP). Just copy and paste <a href="hkps://keys.mailvelope.com" target="_blank">hkps://keys.mailvelope.com</a> into your current OpenPGP plugin and go. <a href="https://github.com/mailvelope/keyserver/blob/master/README.md#api" target="_blank">Learn more</a>.</p>
</div>
</div> <!-- /row marketing -->
<%- include('footer') %>
</div> <!-- /container -->
<script src="js/jquery.min.js"></script>
<script src="js/manage.js"></script>

View File

@ -1,15 +0,0 @@
<div class="container">
<div class="header clearfix">
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="row marketing">
<div class="col-lg-12">
<h3><%= __('removal_success', [email]) %></h3>
</div> <!-- /col-lg-12 -->
</div>
<%- include('footer') %>
</div> <!-- /container -->

View File

@ -1,16 +0,0 @@
<div class="container">
<div class="header clearfix">
<h3 class="text-muted">Mailvelope Key Server</h3>
</div>
<div class="row marketing">
<div class="col-lg-12">
<h3><%= __('verify_success_header', [email]) %></h3>
<p><%= __('verify_success_link') %> <a href="<%= link %>" target="_blank"><%= link %></a></p>
</div> <!-- /col-lg-12 -->
</div>
<%- include('footer') %>
</div> <!-- /container -->

View File

@ -1,16 +0,0 @@
{
"extends": "../.eslintrc.json",
"rules": {
"no-shadow": 1
},
"globals": {
"expect": true,
"sinon": true
},
"env": {
"mocha": true
}
}

View File

@ -1,7 +0,0 @@
'use strict'
module.exports = {
recursive: true,
require: ['./test/setup.js']
}

261
test/fixtures/key3.asc vendored
View File

@ -1,261 +0,0 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
=0IPf
-----END PGP PRIVATE KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
0llyb/EoHbuIzVg=
=yqus
-----END PGP PUBLIC KEY BLOCK-----

156
test/fixtures/key4.asc vendored
View File

@ -1,156 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
ZFXRFPEmGmdLGZcxFHcsXgmmfg==
=XwSC
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PRIVATE KEY BLOCK-----
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
lzEUdyxeCaZ+
=owDa
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -1,40 +0,0 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)
mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+
fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5
GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0
JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS
YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6
AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki
Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf
9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rDRwc7BzAEQAAEBAAAAAAAA
AAAAAAAA/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQN
DAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/
2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
MjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAAFABQDASIAAhEBAxEB/8QAHwAAAQUB
AQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQID
AAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0
NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKT
lJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl
5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL
/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHB
CSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpj
ZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3
uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIR
AxEAPwD3+iiigAooooA//9mIuQQTAQIAIwUCUzxDqQIbLwcLCQgHAwIBBhUIAgkK
CwQWAgMBAh4BAheAAAoJEEpjYTpNbkCU9PEEAKMMaXjhGdgDISBXAAEVXL6MB3x1
d/7zBdnUljh1gM34TSKvbeZf7h/1DNgLbJFfSF3KiLViiqRVOumIkjwNIMZPqYtu
WoEcElY50mvTETzOKemCt1GYI0GhOY2uZOVRtQLrkX0CB9r5hEQalkrnjNKlbghj
LfOYu1uARF16cZUWuI0EUmEvTgEEAOkfz7QRWiWk+I6tdMqgEpOLKsFTLHOh3Inz
OZUnccxMRT++J2lDDMhLChz+d0MUxdBq6rrGoEIP2bYE9AjdR1DNedsuwAjnadYI
io6TMzk0ApagqHJcr1jhQfi/0sBhCCX+y0ghK8KAbiYnyXPMQFa9F19CbYaFvrj/
dXk0N16bABEBAAGJAT0EGAECAAkFAlJhL04CGy4AqAkQSmNhOk1uQJSdIAQZAQIA
BgUCUmEvTgAKCRDghPdEbCAsl7qiBADZpokQgEhe2Cuz7xZIniTcM3itFdxdpRl/
rrumN0P2cXbcHOMUfpnvwkgZrFEcl0ztvTloTxi7Mzx/c0iVPQXQ4ur9Mjaa5hT1
/9TYNAG5/7ApMHrb48QtWCL0yxcLVC/+7+jUtm2abFMUU4PfnEqzFlkjY4mPalCm
o5tbbszw2VwFBADDZgDd8Vzfyo8r49jitnJNF1u+PLJf7XN6oijzCftAJDBez44Z
ofZ8ahPfkAhJe6opxaqgS47s4FIQVOEJcF9RgwLTU6uooSzA+b9XfNmQu7TWrXZQ
zBlpyHbxDAr9hmXLiKg0Pa11rOPXu7atTZ3C2Ic97WIyoaBUyhCKt8tz6Q==
=MVfN
-----END PGP PUBLIC KEY BLOCK-----

View File

@ -1,64 +1,59 @@
'use strict'; 'use strict';
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
const request = require('supertest'); const request = require('supertest');
const Mongo = require('../../src/dao/mongo'); const Mongo = require('../../src/dao/mongo');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const templates = require('../../src/email/templates');
const config = require('config'); const config = require('config');
const fs = require('fs'); const fs = require('fs');
const log = require('winston'); const expect = require('chai').expect;
const sinon = require('sinon');
describe('Koa App (HTTP Server) Integration Tests', function() { describe('Koa App (HTTP Server) Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
const sandbox = sinon.createSandbox(); let app, mongo,
let app; sendEmailStub, publicKeyArmored, emailParams;
let mongo;
let sendEmailStub;
let publicKeyArmored;
let emailParams;
const DB_TYPE_PUB_KEY = 'publickey'; const DB_TYPE_PUB_KEY = 'publickey';
const DB_TYPE_USER_ID = 'userid'; const DB_TYPE_USER_ID = 'userid';
const primaryEmail = 'safewithme.testuser@gmail.com'; const primaryEmail = 'safewithme.testuser@gmail.com';
const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9'; const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9';
before(async () => { before(function *() {
sandbox.stub(log); publicKeyArmored = fs.readFileSync(__dirname + '/../key1.asc', 'utf8');
publicKeyArmored = fs.readFileSync(`${__dirname}/../fixtures/key1.asc`, 'utf8');
mongo = new Mongo(); mongo = new Mongo();
await mongo.init(config.mongo); yield mongo.init(config.mongo);
const paramMatcher = sinon.match(params => { sendEmailStub = sinon.stub().returns(Promise.resolve({ response:'250' }));
sendEmailStub.withArgs(sinon.match(recipient => {
return recipient.to.address === primaryEmail;
}), sinon.match(params => {
emailParams = params; emailParams = params;
return Boolean(params.nonce); return !!params.nonce;
}); }));
const ctxMatcher = sinon.match(ctx => Boolean(ctx)); sinon.stub(nodemailer, 'createTransport').returns({
sandbox.spy(templates, 'verifyKey').withArgs(ctxMatcher, paramMatcher); templateSender: () => { return sendEmailStub; },
sandbox.spy(templates, 'verifyRemove').withArgs(ctxMatcher, paramMatcher); use: function() {}
sendEmailStub = sandbox.stub().returns(Promise.resolve({response: '250'}));
sendEmailStub.withArgs(sinon.match(sendOptions => sendOptions.to.address === primaryEmail));
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendEmailStub
}); });
const init = require('../../src/app'); global.testing = true;
app = await init(); let init = require('../../src/app');
app = yield init();
}); });
beforeEach(async () => { beforeEach(function *() {
await mongo.clear(DB_TYPE_PUB_KEY); yield mongo.clear(DB_TYPE_PUB_KEY);
await mongo.clear(DB_TYPE_USER_ID); yield mongo.clear(DB_TYPE_USER_ID);
emailParams = null; emailParams = null;
}); });
after(async () => { after(function *() {
sandbox.restore(); nodemailer.createTransport.restore();
await mongo.clear(DB_TYPE_PUB_KEY); yield mongo.clear(DB_TYPE_PUB_KEY);
await mongo.clear(DB_TYPE_USER_ID); yield mongo.clear(DB_TYPE_USER_ID);
await mongo.disconnect(); yield mongo.disconnect();
}); });
describe('REST api', () => { describe('REST api', () => {
@ -66,15 +61,34 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
it('should return 400 for an invalid pgp key', done => { it('should return 400 for an invalid pgp key', done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored: 'foo'}) .send({ publicKeyArmored:'foo' })
.expect(400) .expect(400)
.end(done); .end(done);
}); });
it('should return 201', done => { it('should return 400 for an invalid primaryEmail', done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored}) .send({ publicKeyArmored, primaryEmail:'foo' })
.expect(400)
.end(done);
});
it('should return 201 with primaryEmail', done => {
request(app.listen())
.post('/api/v1/key')
.send({ publicKeyArmored, primaryEmail })
.expect(201)
.end(() => {
expect(emailParams).to.exist;
done();
});
});
it('should return 201 without primaryEmail', done => {
request(app.listen())
.post('/api/v1/key')
.send({ publicKeyArmored })
.expect(201) .expect(201)
.end(() => { .end(() => {
expect(emailParams).to.exist; expect(emailParams).to.exist;
@ -83,32 +97,32 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
}); });
}); });
describe('GET /api/v1/key?op=verify', () => { describe('GET /api/v1/verify', () => {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored}) .send({ publicKeyArmored, primaryEmail })
.expect(201) .expect(201)
.end(done); .end(done);
}); });
it('should return 200 for valid params', done => { it('should return 200 for valid params', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`) .get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 400 for missing keyid and', done => { it('should return 400 for missing keyid and', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verify&nonce=${emailParams.nonce}`) .get('/api/v1/verify?nonce=' + emailParams.nonce)
.expect(400) .expect(400)
.end(done); .end(done);
}); });
it('should return 400 for missing nonce', done => { it('should return 400 for missing nonce', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}`) .get('/api/v1/verify?keyId=' + emailParams.keyId)
.expect(400) .expect(400)
.end(done); .end(done);
}); });
@ -118,7 +132,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored}) .send({ publicKeyArmored, primaryEmail })
.expect(201) .expect(201)
.end(done); .end(done);
}); });
@ -126,7 +140,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
describe('Not yet verified', () => { describe('Not yet verified', () => {
it('should return 404', done => { it('should return 404', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?keyId=${emailParams.keyId}`) .get('/api/v1/key?keyId=' + emailParams.keyId)
.expect(404).end(done); .expect(404).end(done);
}); });
}); });
@ -134,21 +148,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
describe('Verified', () => { describe('Verified', () => {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`) .get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 200 and get key by id', done => { it('should return 200 and get key by id', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?keyId=${emailParams.keyId}`) .get('/api/v1/key?keyId=' + emailParams.keyId)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 200 and get key email address', done => { it('should return 200 and get key email address', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?email=${primaryEmail}`) .get('/api/v1/key?email=' + primaryEmail)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
@ -176,25 +190,95 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
}); });
}); });
describe('GET /user/:search (sharing link)', () => {
beforeEach(done => {
request(app.listen())
.post('/api/v1/key')
.send({ publicKeyArmored, primaryEmail })
.expect(201)
.end(done);
});
describe('Not yet verified', () => {
it('should return 404', done => {
request(app.listen())
.get('/user/' + primaryEmail)
.expect(404)
.end(done);
});
});
describe('Verified', () => {
beforeEach(done => {
request(app.listen())
.get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
.expect(200)
.end(done);
});
it('should return 200 for email address', done => {
request(app.listen())
.get('/user/' + primaryEmail)
.expect(200, publicKeyArmored)
.end(done);
});
it('should return 200 for key id', done => {
request(app.listen())
.get('/user/' + emailParams.keyId)
.expect(200, publicKeyArmored)
.end(done);
});
it('should return 200 for fingerprint', done => {
request(app.listen())
.get('/user/' + fingerprint)
.expect(200, publicKeyArmored)
.end(done);
});
it('should return 400 for invalid email', done => {
request(app.listen())
.get('/user/a@bco')
.expect(400)
.end(done);
});
it('should return 404 for unkown email', done => {
request(app.listen())
.get('/user/a@b.co')
.expect(404)
.end(done);
});
it('should return 404 for missing email', done => {
request(app.listen())
.get('/user/')
.expect(404)
.end(done);
});
});
});
describe('DELETE /api/v1/key', () => { describe('DELETE /api/v1/key', () => {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored}) .send({ publicKeyArmored, primaryEmail })
.expect(201) .expect(201)
.end(done); .end(done);
}); });
it('should return 202 for key id', done => { it('should return 202 for key id', done => {
request(app.listen()) request(app.listen())
.del(`/api/v1/key?keyId=${emailParams.keyId}`) .del('/api/v1/key?keyId=' + emailParams.keyId)
.expect(202) .expect(202)
.end(done); .end(done);
}); });
it('should return 202 for email address', done => { it('should return 202 for email address', done => {
request(app.listen()) request(app.listen())
.del(`/api/v1/key?email=${primaryEmail}`) .del('/api/v1/key?email=' + primaryEmail)
.expect(202) .expect(202)
.end(done); .end(done);
}); });
@ -214,15 +298,32 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
}); });
}); });
describe('GET /api/v1/key?op=verifyRemove', () => { describe('GET /api/v1/removeKey', () => {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.post('/api/v1/key') .post('/api/v1/key')
.send({publicKeyArmored}) .send({ publicKeyArmored, primaryEmail })
.expect(201) .expect(201)
.end(() => { .end(done);
});
it('should return 202 for key id', done => {
request(app.listen())
.get('/api/v1/removeKey?keyId=' + emailParams.keyId)
.expect(202)
.end(done);
});
});
describe('GET /api/v1/verifyRemove', () => {
beforeEach(done => {
request(app.listen())
.post('/api/v1/key')
.send({ publicKeyArmored, primaryEmail })
.expect(201)
.end(function() {
request(app.listen()) request(app.listen())
.del(`/api/v1/key?keyId=${emailParams.keyId}`) .del('/api/v1/key?keyId=' + emailParams.keyId)
.expect(202) .expect(202)
.end(done); .end(done);
}); });
@ -230,21 +331,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
it('should return 200 for key id', done => { it('should return 200 for key id', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verifyRemove&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`) .get('/api/v1/verifyRemove?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 400 for invalid params', done => { it('should return 400 for invalid params', done => {
request(app.listen()) request(app.listen())
.get('/api/v1/key?op=verifyRemove') .get('/api/v1/verifyRemove')
.expect(400) .expect(400)
.end(done); .end(done);
}); });
it('should return 404 for unknown key id', done => { it('should return 404 for unknown key id', done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verifyRemove&keyId=0123456789ABCDEF&nonce=${emailParams.nonce}`) .get('/api/v1/verifyRemove?keyId=0123456789ABCDEF&nonce=' + emailParams.nonce)
.expect(404) .expect(404)
.end(done); .end(done);
}); });
@ -266,7 +367,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
request(app.listen()) request(app.listen())
.post('/pks/add') .post('/pks/add')
.type('form') .type('form')
.send(`keytext=${encodeURIComponent(publicKeyArmored)}`) .send('keytext=' + encodeURIComponent(publicKeyArmored))
.expect(201) .expect(201)
.end(done); .end(done);
}); });
@ -277,7 +378,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
request(app.listen()) request(app.listen())
.post('/pks/add') .post('/pks/add')
.type('form') .type('form')
.send(`keytext=${encodeURIComponent(publicKeyArmored)}`) .send('keytext=' + encodeURIComponent(publicKeyArmored))
.expect(201) .expect(201)
.end(done); .end(done);
}); });
@ -285,7 +386,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
describe('Not yet verified', () => { describe('Not yet verified', () => {
it('should return 404', done => { it('should return 404', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`) .get('/pks/lookup?op=get&search=0x' + emailParams.keyId)
.expect(404) .expect(404)
.end(done); .end(done);
}); });
@ -294,51 +395,51 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
describe('Verified', () => { describe('Verified', () => {
beforeEach(done => { beforeEach(done => {
request(app.listen()) request(app.listen())
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`) .get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 200 for key id', done => { it('should return 200 for key id', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`) .get('/pks/lookup?op=get&search=0x' + emailParams.keyId)
.expect(200) .expect(200, publicKeyArmored)
.end(done); .end(done);
}); });
it('should return 200 for fingerprint', done => { it('should return 200 for fingerprint', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&search=0x${fingerprint}`) .get('/pks/lookup?op=get&search=0x' + fingerprint)
.expect(200) .expect(200, publicKeyArmored)
.end(done); .end(done);
}); });
it('should return 200 for correct email address', done => { it('should return 200 for correct email address', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&search=${primaryEmail}`) .get('/pks/lookup?op=get&search=' + primaryEmail)
.expect(200) .expect(200, publicKeyArmored)
.end(done); .end(done);
}); });
it('should return 200 for "mr" option', done => { it('should return 200 for "mr" option', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&options=mr&search=${primaryEmail}`) .get('/pks/lookup?op=get&options=mr&search=' + primaryEmail)
.expect('Content-Type', 'application/pgp-keys; charset=utf-8') .expect('Content-Type', 'application/pgp-keys; charset=utf-8')
.expect('Content-Disposition', 'attachment; filename=openpgpkey.asc') .expect('Content-Disposition', 'attachment; filename=openpgpkey.asc')
.expect(200) .expect(200, publicKeyArmored)
.end(done); .end(done);
}); });
it('should return 200 for "vindex" op', done => { it('should return 200 for "vindex" op', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=vindex&search=0x${emailParams.keyId}`) .get('/pks/lookup?op=vindex&search=0x' + emailParams.keyId)
.expect(200) .expect(200)
.end(done); .end(done);
}); });
it('should return 200 for "index" with "mr" option', done => { it('should return 200 for "index" with "mr" option', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=index&options=mr&search=0x${emailParams.keyId}`) .get('/pks/lookup?op=index&options=mr&search=0x' + emailParams.keyId)
.expect('Content-Type', 'text/plain; charset=utf-8') .expect('Content-Type', 'text/plain; charset=utf-8')
.expect(200) .expect(200)
.end(done); .end(done);
@ -367,7 +468,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
it('should return 501 for a invalid key id format', done => { it('should return 501 for a invalid key id format', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=get&search=${emailParams.keyId}`) .get('/pks/lookup?op=get&search=' + emailParams.keyId)
.expect(501) .expect(501)
.end(done); .end(done);
}); });
@ -388,11 +489,12 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
it('should return 501 (Not implemented) for "x-email" op', done => { it('should return 501 (Not implemented) for "x-email" op', done => {
request(app.listen()) request(app.listen())
.get(`/pks/lookup?op=x-email&search=0x${emailParams.keyId}`) .get('/pks/lookup?op=x-email&search=0x' + emailParams.keyId)
.expect(501) .expect(501)
.end(done); .end(done);
}); });
}); });
}); });
}); });
});
});

View File

@ -1,27 +1,24 @@
'use strict'; 'use strict';
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
const expect = require('chai').expect;
const config = require('config'); const config = require('config');
const Email = require('../../src/email/email'); const Email = require('../../src/email/email');
const tpl = require('../../src/email/templates'); const tpl = require('../../src/email/templates.json');
describe('Email Integration Tests', function() { describe('Email Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
let email; let email, keyId, userId, origin, publicKeyArmored;
let keyId;
let userId;
let origin;
let publicKeyArmored;
const recipient = {name: 'Test User', email: 'safewithme.testuser@gmail.com'}; const recipient = { name:'Test User', email:'safewithme.testuser@gmail.com' };
const ctx = {__: key => key}; before(function() {
publicKeyArmored = require('fs').readFileSync(__dirname + '/../key1.asc', 'utf8');
before(() => {
publicKeyArmored = require('fs').readFileSync(`${__dirname}/../fixtures/key1.asc`, 'utf8');
origin = { origin = {
protocol: 'http', protocol: 'http',
host: `localhost:${config.server.port}` host: 'localhost:' + config.server.port
}; };
email = new Email(); email = new Email();
email.init(config.email); email.init(config.email);
@ -37,39 +34,40 @@ describe('Email Integration Tests', function() {
}; };
}); });
describe('_sendHelper', () => { describe("_sendHelper", () => {
it('should work', async () => { it('should work', function *() {
const mailOptions = { let mailOptions = {
from: {name: email._sender.name, address: email._sender.email}, from: email._sender,
to: {name: recipient.name, address: recipient.email}, to: recipient,
subject: 'Hello ✔', // Subject line subject: 'Hello ✔', // Subject line
text: 'Hello world 🐴', // plaintext body text: 'Hello world 🐴', // plaintext body
html: '<b>Hello world 🐴</b>' // html body html: '<b>Hello world 🐴</b>' // html body
}; };
const info = await email._sendHelper(mailOptions); let info = yield email._sendHelper(mailOptions);
expect(info).to.exist; expect(info).to.exist;
}); });
}); });
describe('send verifyKey template', () => { describe("send verifyKey template", () => {
it('should send plaintext email', async () => { it('should send plaintext email', function *() {
delete userId.publicKeyArmored; delete userId.publicKeyArmored;
await email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin}); yield email.send({ template:tpl.verifyKey, userId, keyId, origin });
}); });
it('should send pgp encrypted email', async () => { it('should send pgp encrypted email', function *() {
await email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin}); yield email.send({ template:tpl.verifyKey, userId, keyId, origin });
}); });
}); });
describe('send verifyRemove template', () => { describe("send verifyRemove template", () => {
it('should send plaintext email', async () => { it('should send plaintext email', function *() {
delete userId.publicKeyArmored; delete userId.publicKeyArmored;
await email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin}); yield email.send({ template:tpl.verifyRemove, userId, keyId, origin });
}); });
it('should send pgp encrypted email', async () => { it('should send pgp encrypted email', function *() {
await email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin}); yield email.send({ template:tpl.verifyRemove, userId, keyId, origin });
}); });
}); });
});
});

View File

@ -1,99 +1,98 @@
'use strict'; 'use strict';
const log = require('winston'); require('co-mocha')(require('mocha')); // monkey patch mocha for generators
const config = require('config'); const config = require('config');
const Mongo = require('../../src/dao/mongo'); const Mongo = require('../../src/dao/mongo');
const expect = require('chai').expect;
describe('Mongo Integration Tests', function() { describe('Mongo Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
const DB_TYPE = 'apple'; const DB_TYPE = 'apple';
const sandbox = sinon.createSandbox();
let mongo; let mongo;
before(async () => { before(function *() {
sandbox.stub(log);
mongo = new Mongo(); mongo = new Mongo();
await mongo.init(config.mongo); yield mongo.init(config.mongo);
}); });
beforeEach(async () => { beforeEach(function *() {
await mongo.clear(DB_TYPE); yield mongo.clear(DB_TYPE);
}); });
after(async () => { after(function *() {
sandbox.restore(); yield mongo.clear(DB_TYPE);
await mongo.clear(DB_TYPE); yield mongo.disconnect();
await mongo.disconnect();
}); });
describe('create', () => { describe("create", () => {
it('should insert a document', async () => { it('should insert a document', function *() {
const r = await mongo.create({_id: '0'}, DB_TYPE); let r = yield mongo.create({ _id:'0' }, DB_TYPE);
expect(r.insertedCount).to.equal(1); expect(r.insertedCount).to.equal(1);
}); });
it('should fail if two with the same ID are inserted', async () => { it('should fail if two with the same ID are inserted', function *() {
let r = await mongo.create({_id: '0'}, DB_TYPE); let r = yield mongo.create({ _id:'0' }, DB_TYPE);
expect(r.insertedCount).to.equal(1); expect(r.insertedCount).to.equal(1);
try { try {
r = await mongo.create({_id: '0'}, DB_TYPE); r = yield mongo.create({ _id:'0' }, DB_TYPE);
} catch (e) { } catch(e) {
expect(e.message).to.match(/duplicate/); expect(e.message).to.match(/duplicate/);
} }
}); });
}); });
describe('batch', () => { describe("batch", () => {
it('should insert a document', async () => { it('should insert a document', function *() {
const r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE); let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
expect(r.insertedCount).to.equal(2); expect(r.insertedCount).to.equal(2);
}); });
it('should fail if docs with the same ID are inserted', async () => { it('should fail if docs with the same ID are inserted', function *() {
let r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE); let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
expect(r.insertedCount).to.equal(2); expect(r.insertedCount).to.equal(2);
try { try {
r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE); r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
} catch (e) { } catch(e) {
expect(e.message).to.match(/duplicate/); expect(e.message).to.match(/duplicate/);
} }
}); });
}); });
describe('update', () => { describe("update", () => {
it('should update a document', async () => { it('should update a document', function *() {
let r = await mongo.create({_id: '0'}, DB_TYPE); let r = yield mongo.create({ _id:'0' }, DB_TYPE);
r = await mongo.update({_id: '0'}, {foo: 'bar'}, DB_TYPE); r = yield mongo.update({ _id:'0' }, { foo:'bar' }, DB_TYPE);
expect(r.modifiedCount).to.equal(1); expect(r.modifiedCount).to.equal(1);
r = await mongo.get({_id: '0'}, DB_TYPE); r = yield mongo.get({ _id:'0' }, DB_TYPE);
expect(r.foo).to.equal('bar'); expect(r.foo).to.equal('bar');
}); });
}); });
describe('get', () => { describe("get", () => {
it('should get a document', async () => { it('should get a document', function *() {
let r = await mongo.create({_id: '0'}, DB_TYPE); let r = yield mongo.create({ _id:'0' }, DB_TYPE);
r = await mongo.get({_id: '0'}, DB_TYPE); r = yield mongo.get({ _id:'0' }, DB_TYPE);
expect(r).to.exist; expect(r).to.exist;
}); });
}); });
describe('list', () => { describe("list", () => {
it('should list documents', async () => { it('should list documents', function *() {
let r = await mongo.batch([{_id: '0', foo: 'bar'}, {_id: '1', foo: 'bar'}], DB_TYPE); let r = yield mongo.batch([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }], DB_TYPE);
r = await mongo.list({foo: 'bar'}, DB_TYPE); r = yield mongo.list({ foo:'bar' }, DB_TYPE);
expect(r).to.deep.equal([{_id: '0', foo: 'bar'}, {_id: '1', foo: 'bar'}], DB_TYPE); expect(r).to.deep.equal([{ _id:'0', foo:'bar' }, { _id:'1', foo:'bar' }], DB_TYPE);
}); });
}); });
describe('remove', () => { describe("remove", () => {
it('should remove a document', async () => { it('should remove a document', function *() {
let r = await mongo.create({_id: '0'}, DB_TYPE); let r = yield mongo.create({ _id:'0' }, DB_TYPE);
r = await mongo.remove({_id: '0'}, DB_TYPE); r = yield mongo.remove({ _id:'0' }, DB_TYPE);
r = await mongo.get({_id: '0'}, DB_TYPE); r = yield mongo.get({ _id:'0' }, DB_TYPE);
expect(r).to.not.exist; expect(r).to.not.exist;
}); });
}); });
});
});

View File

@ -1,343 +1,241 @@
'use strict'; 'use strict';
const log = require('winston'); require('co-mocha')(require('mocha')); // monkey patch mocha for generators
const config = require('config'); const config = require('config');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const Email = require('../../src/email/email'); const Email = require('../../src/email/email');
const Mongo = require('../../src/dao/mongo'); const Mongo = require('../../src/dao/mongo');
const PGP = require('../../src/service/pgp'); const PGP = require('../../src/service/pgp');
const PublicKey = require('../../src/service/public-key'); const PublicKey = require('../../src/service/public-key');
const templates = require('../../src/email/templates'); const expect = require('chai').expect;
const sinon = require('sinon');
describe('Public Key Integration Tests', function() { describe('Public Key Integration Tests', function() {
this.timeout(20000); this.timeout(20000);
const sandbox = sinon.createSandbox(); let publicKey, email, mongo, pgp,
let publicKey; sendEmailStub, publicKeyArmored, emailParams;
let email;
let mongo;
let pgp;
let sendEmailStub;
let publicKeyArmored;
let publicKeyArmored2;
let mailsSent;
const ctx = {__: key => key};
const DB_TYPE = 'publickey'; const DB_TYPE = 'publickey';
const primaryEmail = 'test1@example.com'; const primaryEmail = 'test1@example.com';
const origin = {host: 'localhost', protocol: 'http'}; const origin = { host:'localhost', protocol:'http' };
before(async () => { before(function *() {
publicKeyArmored = require('fs').readFileSync(`${__dirname}/../fixtures/key3.asc`, 'utf8'); publicKeyArmored = require('fs').readFileSync(__dirname + '/../key3.asc', 'utf8');
publicKeyArmored2 = require('fs').readFileSync(`${__dirname}/../fixtures/key4.asc`, 'utf8');
sinon.stub(log, 'info');
mongo = new Mongo(); mongo = new Mongo();
await mongo.init(config.mongo); yield mongo.init(config.mongo);
}); });
beforeEach(async () => { beforeEach(function *() {
await mongo.clear(DB_TYPE); yield mongo.clear(DB_TYPE);
emailParams = null;
mailsSent = []; sendEmailStub = sinon.stub().returns(Promise.resolve({ response:'250' }));
const paramMatcher = sinon.match(params => { sendEmailStub.withArgs(sinon.match(recipient => {
mailsSent[mailsSent.length] = {params}; return recipient.to.address === primaryEmail;
expect(params.nonce).to.exist; }), sinon.match(params => {
expect(params.keyId).to.exist; emailParams = params;
return true; return params.nonce !== undefined && params.keyId !== undefined;
});
const ctxMatcher = sinon.match(context => Boolean(context));
sandbox.spy(templates, 'verifyKey').withArgs(ctxMatcher, paramMatcher);
sandbox.spy(templates, 'verifyRemove').withArgs(ctxMatcher, paramMatcher);
sendEmailStub = sinon.stub().returns(Promise.resolve({response: '250'}));
sendEmailStub.withArgs(sinon.match(sendOptions => {
mailsSent[mailsSent.length - 1].to = sendOptions.to.address;
return true;
})); }));
sandbox.stub(nodemailer, 'createTransport').returns({ sinon.stub(nodemailer, 'createTransport').returns({
sendMail: sendEmailStub templateSender: () => { return sendEmailStub; }
}); });
email = new Email(nodemailer); email = new Email(nodemailer);
email.init({ email.init({
host: 'localhost', host: 'localhost',
auth: {user: 'user', pass: 'pass'}, auth: { user:'user', pass:'pass' },
sender: {name: 'Foo Bar', emails: 'foo@bar.com'} sender: { name:'Foo Bar', email:'foo@bar.com' }
}); });
pgp = new PGP(); pgp = new PGP();
publicKey = new PublicKey(pgp, mongo, email); publicKey = new PublicKey(pgp, mongo, email);
}); });
afterEach(() => { afterEach(() => {
sandbox.restore(); nodemailer.createTransport.restore();
}); });
after(async () => { after(function *() {
await mongo.clear(DB_TYPE); yield mongo.clear(DB_TYPE);
await mongo.disconnect(); yield mongo.disconnect();
log.info.restore();
}); });
describe('put', () => { describe('put', () => {
it('should persist key and send verification email', async () => { it('should persist key and send verification email with primaryEmail', function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
expect(mailsSent.length).to.equal(4); expect(emailParams.nonce).to.exist;
});
it('should persist key and send verification email without primaryEmail', function *() {
yield publicKey.put({ publicKeyArmored, origin });
expect(emailParams.nonce).to.exist;
}); });
it('should work twice if not yet verified', async () => { it('should work twice if not yet verified', function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
expect(mailsSent.length).to.equal(4); expect(emailParams.nonce).to.exist;
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); emailParams = null;
expect(mailsSent.length).to.equal(8); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
expect(emailParams.nonce).to.exist;
}); });
it.skip('should throw 304 if key already exists', async () => { it('should throw 304 if key already exists', function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
await publicKey.verify(mailsSent[0].params); yield publicKey.verify(emailParams);
try { try {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
expect(false).to.be.true; expect(false).to.be.true;
} catch (e) { } catch(e) {
expect(e.status).to.equal(304); expect(e.status).to.equal(304);
} }
}); });
it('should work for a key with an existing/verified email address to allow key update without an extra delete step in between', async () => {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
await publicKey.verify(mailsSent[1].params);
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}, ctx);
expect(mailsSent.length).to.equal(5);
});
});
describe('_purgeOldUnverified', () => {
let key;
beforeEach(async () => {
key = await pgp.parseKey(publicKeyArmored);
});
it('should work for no keys', async () => {
const r = await publicKey._purgeOldUnverified();
expect(r.deletedCount).to.equal(0);
});
it('should not remove a current unverified key', async () => {
await publicKey._persistKey(key);
const r = await publicKey._purgeOldUnverified();
expect(r.deletedCount).to.equal(0);
});
it('should not remove a current verified key', async () => {
key.userIds[0].verified = true;
await publicKey._persistKey(key);
const r = await publicKey._purgeOldUnverified();
expect(r.deletedCount).to.equal(0);
});
it('should not remove an old verified key', async () => {
key.uploaded.setDate(key.uploaded.getDate() - 31);
key.userIds[0].verified = true;
await publicKey._persistKey(key);
const r = await publicKey._purgeOldUnverified();
expect(r.deletedCount).to.equal(0);
});
it('should remove an old unverified key', async () => {
key.uploaded.setDate(key.uploaded.getDate() - 31);
await publicKey._persistKey(key);
const r = await publicKey._purgeOldUnverified();
expect(r.deletedCount).to.equal(1);
});
}); });
describe('verify', () => { describe('verify', () => {
it('should update the document', async () => { beforeEach(function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
const emailParams = mailsSent[0].params; });
await publicKey.verify(emailParams);
const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE); it('should update the document', function *() {
yield publicKey.verify(emailParams);
let gotten = yield mongo.get({ keyId:emailParams.keyId }, DB_TYPE);
expect(gotten.userIds[0].verified).to.be.true; expect(gotten.userIds[0].verified).to.be.true;
expect(gotten.userIds[0].nonce).to.be.null; expect(gotten.userIds[0].nonce).to.be.null;
expect(gotten.userIds[1].verified).to.be.false; expect(gotten.userIds[1].verified).to.be.false;
expect(gotten.userIds[1].nonce).to.exist; expect(gotten.userIds[1].nonce).to.exist;
}); });
it('should not find the document', async () => { it('should not find the document', function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
const emailParams = mailsSent[0].params;
try { try {
await publicKey.verify({keyId: emailParams.keyId, nonce: 'fake_nonce'}); yield publicKey.verify({ keyId:emailParams.keyId, nonce:'fake_nonce' });
expect(true).to.be.false; expect(true).to.be.false;
} catch (e) { } catch(e) {
expect(e.status).to.equal(404); expect(e.status).to.equal(404);
} }
const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE); let gotten = yield mongo.get({ keyId:emailParams.keyId }, DB_TYPE);
expect(gotten.userIds[0].verified).to.be.false; expect(gotten.userIds[0].verified).to.be.false;
expect(gotten.userIds[0].nonce).to.equal(emailParams.nonce); expect(gotten.userIds[0].nonce).to.equal(emailParams.nonce);
expect(gotten.userIds[1].verified).to.be.false; expect(gotten.userIds[1].verified).to.be.false;
expect(gotten.userIds[1].nonce).to.exist; expect(gotten.userIds[1].nonce).to.exist;
}); });
it('should verify a second key for an already verified user id and delete the old key', async () => {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
await publicKey.verify(mailsSent[1].params);
let firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
expect(firstKey).to.exist;
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}, ctx);
await publicKey.verify(mailsSent[4].params);
firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
expect(firstKey).to.not.exist;
const secondKey = await publicKey.getVerified({keyId: mailsSent[4].params.keyId});
expect(secondKey).to.exist;
});
it('should delete other keys with the same user id when verifying', async () => {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
await publicKey.put({emails: [], publicKeyArmored: publicKeyArmored2, origin}, ctx);
expect(mailsSent[1].to).to.equal(mailsSent[4].to);
await publicKey.verify(mailsSent[1].params);
const firstKey = await publicKey.getVerified({keyId: mailsSent[1].params.keyId});
expect(firstKey).to.exist;
const secondKey = await mongo.get({keyId: mailsSent[4].params.keyId}, DB_TYPE);
expect(secondKey).to.not.exist;
});
it('should be able to verify multiple user ids', async () => {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
expect(mailsSent.length).to.equal(4);
await publicKey.verify(mailsSent[0].params);
await publicKey.verify(mailsSent[1].params);
await publicKey.verify(mailsSent[2].params);
await publicKey.verify(mailsSent[3].params);
const gotten = await mongo.get({keyId: mailsSent[0].params.keyId}, DB_TYPE);
expect(gotten.userIds[0].verified).to.be.true;
expect(gotten.userIds[1].verified).to.be.true;
expect(gotten.userIds[2].verified).to.be.true;
expect(gotten.userIds[3].verified).to.be.true;
});
}); });
describe('getVerified', () => { describe('getVerified', () => {
let key; let key;
describe('should find a verified key', () => { describe('should find a verified key', () => {
beforeEach(async () => { beforeEach(function *() {
key = await pgp.parseKey(publicKeyArmored); key = pgp.parseKey(publicKeyArmored);
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
await publicKey.verify(mailsSent[0].params); yield publicKey.verify(emailParams);
}); });
it('by fingerprint', async () => { it('by fingerprint', function *() {
const verified = await publicKey.getVerified({fingerprint: key.fingerprint}); let verified = yield publicKey.getVerified({ fingerprint:key.fingerprint });
expect(verified).to.exist; expect(verified).to.exist;
}); });
it('by all userIds', async () => { it('by all userIds', function *() {
const verified = await publicKey.getVerified({userIds: key.userIds}); let verified = yield publicKey.getVerified({ userIds:key.userIds });
expect(verified).to.exist; expect(verified).to.exist;
}); });
it('by verified userId', async () => { it('by verified userId', function *() {
const verified = await publicKey.getVerified({userIds: [key.userIds[0]]}); let verified = yield publicKey.getVerified({ userIds:[key.userIds[0]] });
expect(verified).to.exist; expect(verified).to.exist;
}); });
it('by unverified userId', async () => { it('by unverified userId', function *() {
const verified = await publicKey.getVerified({userIds: [key.userIds[1]]}); let verified = yield publicKey.getVerified({ userIds:[key.userIds[1]] });
expect(verified).to.not.exist; expect(verified).to.not.exist;
}); });
it('by keyId', async () => { it('by keyId', function *() {
const verified = await publicKey.getVerified({keyId: key.keyId}); let verified = yield publicKey.getVerified({ keyId:key.keyId });
expect(verified).to.exist; expect(verified).to.exist;
}); });
it('by all params', async () => { it('by all params', function *() {
const verified = await publicKey.getVerified(key); let verified = yield publicKey.getVerified(key);
expect(verified).to.exist; expect(verified).to.exist;
}); });
}); });
describe('should not find an unverified key', () => { describe('should not find an unverified key', () => {
beforeEach(async () => { beforeEach(function *() {
key = await pgp.parseKey(publicKeyArmored); key = pgp.parseKey(publicKeyArmored);
key.userIds[0].verified = false; key.userIds[0].verified = false;
await mongo.create(key, DB_TYPE); yield mongo.create(key, DB_TYPE);
}); });
it('by fingerprint', async () => { it('by fingerprint', function *() {
const verified = await publicKey.getVerified({fingerprint: key.fingerprint}); let verified = yield publicKey.getVerified({ fingerprint:key.fingerprint });
expect(verified).to.not.exist; expect(verified).to.not.exist;
}); });
it('by userIds', async () => { it('by userIds', function *() {
const verified = await publicKey.getVerified({userIds: key.userIds}); let verified = yield publicKey.getVerified({ userIds:key.userIds });
expect(verified).to.not.exist; expect(verified).to.not.exist;
}); });
it('by keyId', async () => { it('by keyId', function *() {
const verified = await publicKey.getVerified({keyId: key.keyId}); let verified = yield publicKey.getVerified({ keyId:key.keyId });
expect(verified).to.not.exist; expect(verified).to.not.exist;
}); });
it('by all params', async () => { it('by all params', function *() {
const verified = await publicKey.getVerified(key); let verified = yield publicKey.getVerified(key);
expect(verified).to.not.exist; expect(verified).to.not.exist;
}); });
}); });
}); });
describe('get', () => { describe('get', () => {
let emailParams; beforeEach(function *() {
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
beforeEach(async () => {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
emailParams = mailsSent[0].params;
}); });
it('should return verified key by key id', async () => { it('should return verified key by key id', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const key = await publicKey.get({keyId: emailParams.keyId}, ctx); let key = yield publicKey.get({ keyId:emailParams.keyId });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should return verified key by key id (uppercase)', async () => { it('should return verified key by key id (uppercase)', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const key = await publicKey.get({keyId: emailParams.keyId.toUpperCase()}, ctx); let key = yield publicKey.get({ keyId:emailParams.keyId.toUpperCase() });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should return verified key by fingerprint', async () => { it('should return verified key by fingerprint', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const fingerprint = (await pgp.parseKey(publicKeyArmored)).fingerprint; let fingerprint = pgp.parseKey(publicKeyArmored).fingerprint;
const key = await publicKey.get({fingerprint}, ctx); let key = yield publicKey.get({ fingerprint });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should return verified key by fingerprint (uppercase)', async () => { it('should return verified key by fingerprint (uppercase)', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const fingerprint = (await pgp.parseKey(publicKeyArmored)).fingerprint.toUpperCase(); let fingerprint = pgp.parseKey(publicKeyArmored).fingerprint.toUpperCase();
const key = await publicKey.get({fingerprint}, ctx); let key = yield publicKey.get({ fingerprint });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should return verified key by email address', async () => { it('should return verified key by email address', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const key = await publicKey.get({email: primaryEmail}, ctx); let key = yield publicKey.get({ email:primaryEmail });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should return verified key by email address (uppercase)', async () => { it('should return verified key by email address (uppercase)', function *() {
await publicKey.verify(emailParams); yield publicKey.verify(emailParams);
const key = await publicKey.get({email: primaryEmail.toUpperCase()}, ctx); let key = yield publicKey.get({ email:primaryEmail.toUpperCase() });
expect(key.publicKeyArmored).to.exist; expect(key.publicKeyArmored).to.exist;
}); });
it('should throw 404 for unverified key', async () => { it('should throw 404 for unverified key', function *() {
try { try {
await publicKey.get({keyId: emailParams.keyId}, ctx); yield publicKey.get({ keyId:emailParams.keyId });
expect(false).to.be.true; expect(false).to.be.true;
} catch (e) { } catch(e) {
expect(e.status).to.equal(404); expect(e.status).to.equal(404);
} }
}); });
@ -346,33 +244,39 @@ describe('Public Key Integration Tests', function() {
describe('requestRemove', () => { describe('requestRemove', () => {
let keyId; let keyId;
beforeEach(async () => { beforeEach(function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
keyId = mailsSent[0].params.keyId; keyId = emailParams.keyId;
}); });
it('should work for verified key', async () => { it('should work for verified key', function *() {
await publicKey.verify(mailsSent[0].params); yield publicKey.verify(emailParams);
await publicKey.requestRemove({keyId, origin}, ctx); emailParams = null;
expect(mailsSent.length).to.equal(8); yield publicKey.requestRemove({ keyId, origin });
expect(emailParams.keyId).to.exist;
expect(emailParams.nonce).to.exist;
}); });
it('should work for unverified key', async () => { it('should work for unverified key', function *() {
await publicKey.requestRemove({keyId, origin}, ctx); emailParams = null;
expect(mailsSent.length).to.equal(8); yield publicKey.requestRemove({ keyId, origin });
expect(emailParams.keyId).to.exist;
expect(emailParams.nonce).to.exist;
}); });
it('should work by email address', async () => { it('should work by email address', function *() {
await publicKey.requestRemove({email: primaryEmail, origin}, ctx); emailParams = null;
expect(mailsSent.length).to.equal(5); yield publicKey.requestRemove({ email:primaryEmail, origin });
expect(emailParams.keyId).to.exist;
expect(emailParams.nonce).to.exist;
}); });
it('should throw 404 for no key', async () => { it('should throw 404 for no key', function *() {
await mongo.remove({keyId}, DB_TYPE); yield mongo.remove({ keyId }, DB_TYPE);
try { try {
await publicKey.requestRemove({keyId, origin}, ctx); yield publicKey.requestRemove({ keyId, origin });
expect(false).to.be.true; expect(false).to.be.true;
} catch (e) { } catch(e) {
expect(e.status).to.equal(404); expect(e.status).to.equal(404);
} }
}); });
@ -381,76 +285,28 @@ describe('Public Key Integration Tests', function() {
describe('verifyRemove', () => { describe('verifyRemove', () => {
let keyId; let keyId;
beforeEach(async () => { beforeEach(function *() {
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx); yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
keyId = mailsSent[0].params.keyId; keyId = emailParams.keyId;
emailParams = null;
yield publicKey.requestRemove({ keyId, origin });
}); });
afterEach(() => { it('should remove key', function *() {
mailsSent = []; yield publicKey.verifyRemove(emailParams);
}); let key = yield mongo.get({ keyId }, DB_TYPE);
it('should remove unverified user ID', async () => {
await publicKey.requestRemove({keyId, origin}, ctx);
const key = await mongo.get({keyId}, DB_TYPE);
expect(key.userIds[0].verified).to.be.false;
expect(key.userIds[0].email).to.equal(primaryEmail);
await publicKey.verifyRemove(mailsSent[4].params);
const modifiedKey = await mongo.get({keyId}, DB_TYPE);
expect(modifiedKey.userIds[0].email).to.not.equal(primaryEmail);
});
it('should remove single verfied user ID', async () => {
await publicKey.verify(mailsSent[0].params);
const key = await mongo.get({keyId}, DB_TYPE);
expect(key.userIds[0].verified).to.be.true;
expect(key.userIds[0].email).to.equal(primaryEmail);
const keyFromArmored = await pgp.parseKey(key.publicKeyArmored);
expect(keyFromArmored.userIds.find(userId => userId.email === primaryEmail)).not.to.be.undefined;
await publicKey.requestRemove({keyId, origin}, ctx);
await publicKey.verifyRemove(mailsSent[4].params);
const modifiedKey = await mongo.get({keyId}, DB_TYPE);
expect(modifiedKey.userIds[0].email).to.not.equal(primaryEmail);
expect(modifiedKey.publicKeyArmored).to.be.null;
});
it('should remove verfied user ID', async () => {
await publicKey.verify(mailsSent[0].params);
await publicKey.verify(mailsSent[1].params);
const key = await mongo.get({keyId}, DB_TYPE);
expect(key.userIds[0].verified).to.be.true;
expect(key.userIds[1].verified).to.be.true;
const emails = [key.userIds[0].email, key.userIds[1].email];
const keyFromArmored = await pgp.parseKey(key.publicKeyArmored);
expect(keyFromArmored.userIds.filter(userId => emails.includes(userId.email)).length).to.equal(2);
await publicKey.requestRemove({keyId, origin}, ctx);
await publicKey.verifyRemove(mailsSent[5].params);
const modifiedKey = await mongo.get({keyId}, DB_TYPE);
expect(modifiedKey.userIds[0].email).to.equal(emails[0]);
expect(modifiedKey.userIds[1].email).to.not.equal(emails[1]);
expect(modifiedKey.publicKeyArmored).not.to.be.null;
const keyFromModifiedArmored = await pgp.parseKey(modifiedKey.publicKeyArmored);
expect(keyFromModifiedArmored.userIds.filter(userId => emails.includes(userId.email)).length).to.equal(1);
});
it('should remove key', async () => {
await publicKey.requestRemove({keyId, origin}, ctx);
await publicKey.verifyRemove(mailsSent[4].params);
await publicKey.verifyRemove(mailsSent[5].params);
await publicKey.verifyRemove(mailsSent[6].params);
await publicKey.verifyRemove(mailsSent[7].params);
const key = await mongo.get({keyId}, DB_TYPE);
expect(key).to.not.exist; expect(key).to.not.exist;
}); });
it('should throw 404 for no key', async () => { it('should throw 404 for no key', function *() {
await mongo.remove({keyId}, DB_TYPE); yield mongo.remove({ keyId }, DB_TYPE);
try { try {
await publicKey.verifyRemove(mailsSent[1].params); yield publicKey.verifyRemove(emailParams);
expect(false).to.be.true; expect(false).to.be.true;
} catch (e) { } catch(e) {
expect(e.status).to.equal(404); expect(e.status).to.equal(404);
} }
}); });
}); });
});
});

232
test/key3.asc Normal file
View File

@ -0,0 +1,232 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=sqvb
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQcYBFdZY4oBEADHN/tWY4tMdT20T6AzC7VyCNFu5UjSNtw74GHPlyoHuDi4wBLK
J21YfgSEEqv9kvA9BGgT5c68nY2eu6GEE2WQNz90N5xIUTJrhsp2bCcitYgXqvkB
e0U9Ybv3rGcdd/MIdvj2m71N7eHmJy7s1yevhWXpcII7oPTBa5StFr+fs77+LUwL
lOMacwn0KDKFcs7pVI1mJ+0B+2gcE/oXYHtJoCkMnABOO+xG0EtMS1z1amXZJLNB
Wy2WKAv2rosrtHR/Qj/st0fl781WK9E9qVzpttsBuxwOHjJwn/WGRMirj9cl8IuL
us9Iti9e0z1J5d3b3V2APv+U0/WNco2QdstiPiCGBGAVMCrnh7jqfI6WsX2xCRCQ
ObUNVlYKPYrZYhID6diSAXyWdPjewhVS095H3B+8bZk8hnkU72+Z+7lU/UI/Lf8U
OsUaNSaVtktHzvrYErAv+//HlWVCBi6CuWB0SDslZ+V4SS5CzNPpkQ6krvOluJr0
WrmLMwdqwJ7DWxjh+tcuDYot3e7pKsFjDf2lwaUiO9Z00Uf4O8kH9P5ZAzuXNtzh
k/ZESCa4N99R3OuF11127NifMHXeLwtcUblWtW1GUeScvR2OiH7GRlItY9C4RPWY
IqfwDokkcmmv26be5BqI11KiJ4f/dhOthUCsbgeVAdqjtOFSsDHG9JYIewARAQAB
AA/9Etcyh+sGI4b6/PCC4BD9afl3hRteFbNmhKsl1PIg4XYEt0RDAqdT6giQ+MSj
S2n4Gm0uQqN7N89Ws2pfThRfiJIRCDayKwyyzgSDZUu5L8knQ8XBoug7liCGHFhL
sDfF3kkSJpB4CMS0loWiJHf8otbk2nzvdCA2xYwdFXmPSdU//N3f0UCVcczrZhHf
JUvEUcDTVpP0EDnskKs6/bb8MexZtX2TcdKs981/MYn3EqarVyvnYAj1eLv01bGQ
K+P3GIn1bbevrwlMzBd8xG4eAWRvtewyLQuiDZCzMa2TpNYHrOjg6agTLnc8Z6Vm
qHR61O5Mh3JtzW92S5hH1x/FACyIyigLiWIEz/fMEKitkiih1poMkdCAcZPCCkNK
GlSM0eoe5tJE5qR92jxElnH4aH2uDhKKIPiW+ur/0SY2uTYpDBtstojtGBvqB0/D
WRIlEqVydIKF4CfqApa89qCX48SPr4Oddoq4uF0XBrqobEd95PL/GNEw3Iz5ZuiI
VhAdWJC6jX/X2fSdaZcHsM3+Av5tSkPyFlz8/Kv6Pha7GZ2KwD9nTxhvYhcIFbbP
QgBYqXLC7mHSmnRPhicgrmEKERRdXyWwBg0cCDa4nr5fu1o/xBsVDFgMryb8v6Wa
SO09WivRnNrayxFlksBS6gBKWZ2xPDCLv26U0xfYAredMqEIAMi7Gl+envH3jErp
Qz/axY9rVMOVhMI3BeNZ9M4q0a2SReMovwRqiQ1FpuCxV9BjSJ99QotUEJShWPRn
uBC1FSm8vKJf1j74WgGLN6Nt47x5JCCkPrnl5MlRHGcoy2lEO+Jh5ELkpRGwxdsJ
qMmCVbBzmSFGWvwtGUgq1MM70fPltSF3uBqAL8L1vLxnRiWu4cVm9Re0nr4UdM0j
8UZr+JOUyLp/XVXMpN04B+W3UMhWM6nMr5er6OnLioG+hhJiTLiQ8Z1uw6Q4wH8G
YqQqjoveVLRZi0GU5n+9F2CFScX0HZkx8Qq+UvBj+U09jhUyv5TyhJt5WQPj8pLT
iYToIbkIAP4SSfzpDe2mgvJMSfFa5Zx+8CSjHW5P7lQF1J2z5Gegb4Klir3OB2Zb
n+DHPrqAwq6cNUuWEH1JLKhkpPPcX60ZM2NbwO5ZotWYGFybGvxcqYP33uEFiVeY
dougy9Feif7G/sHViEjJHIy0NFGesPhMJ1Gwy+nBUwdyHCWQmafSvpC3A9ozeEMl
hnRpfBWK8g/kRWBrwcqy6GvMaCzUSHQY5VyUbggzRB6YlMaXp+GBLF4fehBSc71K
UWttfLZw+QkRYooI0TPnJVJgyR3hf4lCLEP8JXNpej9qYg7rz8JWAurDOgVDMTiZ
5gePO3l1BeBRCrWFOhLeaUGrdGK2pdMIANUK6OxzGz//709jH1UAOgYvD/F9Qz6F
SR2kQ9dH4zm10sbufvjI0I8PLOuEcoFSEbjv6YXnaDBfDzehWkVy1otUuTPbEW3n
7ootyAnxKqTBMN/XqmqO23OTWZw+4bAaEON6kafYKEkr88AMSuKPFmkzCvAEFqif
wsQa7MybamEnIacCqfJ9BQOC0USZFEYlvxjZLDO6XXwiLtuExlawMBOiPmb+00IJ
waGRraUVbQR5v8zlPXn9LzoXhXL/8OCoyap/mF/ERxkFhjyl96jW6T2e9hgF9aK7
6Z17LcahNUwsLl0TGus45s/ljpxNHHED2bAiykrlqVUg1XPOJkO8wNCErrQdVGVz
dCBVc2VyIDx0ZXN0MUBleGFtcGxlLmNvbT6JAj8EEwEIACkFAldZY4oCGwMFCQeG
H4AHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRBAAaEnqQ3o4Y8eD/0SUKel
5N0/Qowm9eVQ3DsrckqoAHL6E+iVLzM8qvUm4hd1HuSTr386IvX7PrukZ9M5isMv
xD3GKD+R93v4Ag5BNDiOdGPXDqZuY7brNSsiez2QYWyEELrNrlw4CV+lboMfi02D
SHnhL58crkWId0Zn3DAsZ2xq4zgPdnMz0ryFjGCMmRzbMffYaMuT7Y3zdwfXK0nl
1dV5uH5qEyeNBuobYaui1KY2WB5FObbfHWY9j2UQu1Gce2xM2hmTowHXZZc7gARl
E6aT22X0YAzprjhE4XfetTkHU/mSgJeX3RZEbQFa66PT9pBj6b+BdZuuCK5E5ICS
nK2gv6hwPv2zxZz/F/UwBoXpIb1qeuTEyfk08ceMGILhUGvn0DmeGkD6hyltqBsO
RNBYne4CU+Ss5pDF/rvL+FdFgBkPvDY1Z6JsgCGn1ft8HXvR8A48prw9Ty/dJsXe
BseNdvTAuAAE2BH9ongmspALRcu8G/CIMSdU4spAAbN9szq3gSU3YUWav48fRLY/
99EhPITvqGafYWsAimWyPMEqI+CPL4C1HUQEO0jpJztfOhS6pxHU6Ap9MmICruXN
rH8UyLCfkx4+JV8eY4lt3Jl/77b2D4JQUSeoFdNe4Tn4aFR4UP7l/FOa8DYzZ1Sp
2+Pum1h3pjFGT2d106rg8oB/m8KljhmlK8SaM7QdVGVzdCBVc2VyIDx0ZXN0MkBl
eGFtcGxlLmNvbT6JAj8EEwEIACkFAldZY64CGwMFCQeGH4AHCwkIBwMCAQYVCAIJ
CgsEFgIDAQIeAQIXgAAKCRBAAaEnqQ3o4ZCGD/9hcfSpltZhG281bkt7a/LHYBRo
F+hnLrNZck6ZWvQ6wFu1fjD09U2LsKrxYmrAUSY5w0dHk9nsTLTWr/pMrw/BGeLL
PsX2Fzo4Hz+BsZioeL6mKKWzRCILMy6mWfe/ivFRdCJK9AlH+9Sq+jBaRFTwrgXL
uKZNb88qiCEVoZ1Y5s+N539w/oZkG9zK5viAD4/FTcL2Xtu76cfXjQQlFXU1ladG
hVx4SRjPkmROlQ8ESkSio0JM9r2EqbIu+OXNL5GvLy9djCwciEkN5dtWbGeNJ4Fr
CDyxJQDLiXHXz61KFMPX6s9oWu0VOmz8JhbLmkO00d15G+4SPLpcTcwehuXfBhOT
hXatyM8HaQFsAwQ8WiBFGV8psnr24N44LaQo8Nvj9UI2b6FekhxR5tq6Lav5hD7k
PrZ0qtLji+lQXGkRcaKXkwapzsAX39tDKSxiu7mzAn+xUBSRFihnwdE/acMGMLPi
T+Cb6sCi6xsypeaLJ/ipcFwzb03IKxQ/u4I5aEamxePbKtZUtHArRdSLrsJnD3vx
ljjRfVYsoXIzIM5eR3VoW4bvnjTXtdmIFt0zuMYjr3p/HGZ2MR28Xp48ALIGEdyl
lQnj5Qg03BLgogKT/Ei3SS6HDrWrBli5Ad+Qozvz6rKIiamXjh1uRBHS8RhOie76
qvVUurB9CL6QEblEZLQdVGVzdCBVc2VyIDx0ZXN0M0BleGFtcGxlLmNvbT6JAj8E
EwEIACkFAldZY8gCGwMFCQeGH4AHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAK
CRBAAaEnqQ3o4UEfEACvk0tQs6xelccXKoAo+5UiUZ4oM0ByF4SRoTkO7sZ+Cn9c
sfv4aBOl9S0Wcm4HN7xANCni6x+k44qNKEde2FNcnCBBwgOhh2OKoCVKSmQyYdrr
+mV5cwiG4oFSM73QOBKOD/fK3ywfOA0Rcy/rRI0iCSfwD3BKIUKHBhl3mptExxa6
/Vhki0yk1ams9rXsI6rC9GpLaW5n5l6hKkTxh1yTX+lILISaovnL7vOjweoPIrP9
BczA+o4ZuHwBkLkkxILCz2QZrxTnd9ugSyvHEinMfuEAdReL+Tmn5/ZEP4v2mItx
6UETBqNcGXnhKEhrT1QUeMWIO/thwr3CysZTox+T2mHtcm3LEa+LsZu+9ht+ngwW
E4OvEwg7lDY9eAnussU6rAHcJXVHfHwG/MKYzbhrb78n7o6STSHTEXrktkE6ccxB
PuEK5YLPtyqs7WYdC9fCYarh7NtuPr1lSsH27rdELM/7+WdWZ3bJeOABye4K37pF
9n5rHKQofDV/TGB/tafhX/phvZnYHUwJncyLFIYh6gdrV20NOjK+uiCBNjWuPXBc
gz52LkmVavy7ycfIz9ZcNeSrf73xxXy4tB39m928N9ePayB/OGNpj5iu1KJvFqkw
AxPDLDH4nEvF2Es3WXEy82ydZnXSh3dzzLZmT4d2/XKrcUiTvKFAShOcKpSpZ7Q0
VGVzdCBVc2VyIChDb21tZW50IGFib3V0IHN0dWZmLikgPHRlc3Q0QGV4YW1wbGUu
Y29tPokCPwQTAQgAKQUCV1lj+QIbAwUJB4YfgAcLCQgHAwIBBhUIAgkKCwQWAgMB
Ah4BAheAAAoJEEABoSepDejh/eIP/3kwibQIbnY4tnOtSCrGFOcG5UAANDYXJadK
XRlW4WZBUOtAl88WXf7SjRJzu+TlOSf+GFN8Av/Q8cFbFpruhJlcQqD9GmXUPsd8
+llI0W4FpRbQ0qableeY9ZFoc/VvlVhzw6USUiQgS60P/A9dpco8sh0LRHTe2hl7
OHFbdLXHWs19jqZNNycaGvMiZuXKBnMYpo+fh88kjYuAmNfmwMs7QTNZWMqQrH44
d4zuJmXbxdiCr6hbX8gT43oWHFXhqfr/6eZ6hp5S/0zrEUsF28vCFs/C30r1wIcH
a2vSRvIdhPeVBMNj34VVp1IcFO91OL8VTl1fnHsuUKhr+iB2SqFbxR+rLzOdXrZ6
10zifWEZDN5Wkhplc6N5wrggKU3r4Q6RVsOYpD+I5nytSXYd6ihFXVOJOC4AErfW
Ki0dn7GTwHDUfFvEajfVaLKtb1cvXfEVPFvqg5BGixEWQQqJZwr/rgPWpQwQtAuO
Q4OQgXwf6W3vUwKsNiH1QVrkfDPUfJkGkw5JiRXE8oo9vnp2kgmhTdeiKPQcfr+0
JpL99SxZJU+6R8QKbPOaGMllJquyaN7VBBPFC14pEGwK2Kn4jVhjU4OHE6TFIZPt
xh5+bZmJNNGeUTX8cTS5OULCXGkqYezSdW1o8vt8ujG17MWsQZn/HwG513+vAd3R
doShBVBJnQcXBFdZY4oBEADMgfdHy9rlCbjKDr9CcO1cEKgJ5+mD+j2+zOIjNPTH
L7NVPmqpY2nkbslwpoCSnMGkVF/cIOCO3jsus9D+k50K4CH1179Lh8qNXMSnWkYr
mLcklq2sDa/oWHl/p0wJnS/RAEPbFFag8f7UVyzOIh80AW5ygILpPlgvWSHByzsm
D7X9jJj+ikLoiYrh/QuYKPHiKIT4I0e+jbhmKAxo0p7ER+GSjJJ4qVad3vjMOHIN
iMKW07mmoyhvOYuqJ7+AYfndgrNKm56zh7EORuLkhACS/dLBWxCLHPWMAjhTSahu
tfo+ZXXHHTwWtsQOQqBImeKM0Jx8zbdMfUymhX+zxIL4Nq9bDkcV0F3h+W6J3iWY
l9piyNVc/xC2kPYNn/XDULZcBPva/3gVkbmzeOPC4cCJTiM82VJSIAxc0Khnmprc
YO0x4PaXk7gm8pPWVHnKOF14KfDWt18hi6L+vJUTObcLmIeQt6kfmDnIsc09URYj
q/T1VxzYvbMV+/3yxk/4GAhfv417MQ0Z7xmYJhmymXMkWzwkV56p2KzwZmavkmKA
lgI7tueKcMp4OOSyEykVNZQikYjz1ELblS4vDGa8QX2aM75Db1nJVMhLR8XpxTDD
7w6mo7vdKUuaY8vRuI4K81mGSjW5+sAiarx5HKaxjvhRDNOYzJjzEDZPT4xvZOyC
HQARAQABAA/2KxumLS0/2237eCbuLpsjYSB5FvC9DQ1grx8oCUyQsBWc8YmT8a0S
IdbOpBkpyPRTCFN8542UeQyx8RHyvIB0KSvKGZf0iMAK2dWDn4JuXTwPnIpIbIvA
E0N3UcAipvB8FCV9f5c1G5aLrFg9opJvYohFL5Y27paCjIn4pN+9noQrHrj7VKY1
mNqZxBH0Y5D5DSkR8d2xXNMn0bYquj8G6+Iz1L18OiwSBlWHfG8+WIwQPOR1Xexm
rFJF9xrs5b15W7g9Uxg7XxVijHiMVplcvzJagUkxE+xmqtRf/Ti8NJHXxvRRlQaL
kUAyfOi9NGl9dVrz9+vAmodWsikPofx3UYpuaa3giUxlWDqA5jdl56wjCQnEAbcd
aUviB2m2/sJ2VxRqPw6iKdNLAK8Sd6VvfTZ0szw50GpkzDjgOi8hfHknWJgg3rbu
j1IpmDwpBsAPusLBZtYUFFZsGawAExiVpLaseZh+eFLVjt6T3JIVUVN4JVdjPNU5
0b3q4onuiEtTIn1Ga3v83UePz525nVWXN+kTHDiko/hFYlfcwWSYwQHoqtRdDV3v
1zlo/bDHkdQQF4rC4PsTJmsED9RZV8tBqe3k9SfVoy7OT14OLVJ+2V3lYJgd1rCD
MP/2CD66rSIys40vEOjdqf05/wQGBGXM6Ox4g1zUHPa0IT7g5D3IWQgA3oT4iUXV
5lQXYWtiHQeSxFzcYHXYfNI/xKm/k0Ddl/9fsC6zLk8MbhDB0+7GpMsoEHwW4lC/
G92qArAaD4xagzilA/keqHbcJsU3hFrPXbpdOQeGMn4pmykoQRENeQrtqlIg4CLJ
npP7/faa6HUbIJaCCOEBQ9kHZnBIkrFFMfvwFSX2caFifWgY/KDloV56qet45/FE
gv7XHCYPP/rp0WKTWURFvuUC2XuT3Mm18mbUm0lpd0pAhETkBJ9U/apsvBSykdrB
yGh2R7CBZH9OXR04ns6nu1LBEpiQXlkGF3ZdURiV3PobvhTaHtp0D+CQtj7eHomv
Muki7B3FDwdWlQgA60c3g3tfCqruyegOjFFqrGRbiyPMzC4xZuRtUhEyyQLax/FY
rM/uGEJn+l2cwWnBrOmI6TJe9O0apE7KyL/aEyPqP3kemCCKMrTDD3N4YCS2pVEL
lOfKAetucHvRdF2jcumw4nXJdDXo+NSoOx4ZG2eMNt1JfaUaaVzBqEjDoXnou9dV
UzMO/iiZ0ybDSPb44ybMrczZesGXZJqllD2sLxxTXLSvjAKkfcQvlP5DIR8KdSWD
pt7r/uy5BhZPNHHdK/L/BYK8XLP40MvXTwmI20KhN1Wg8mQp2r0pEHHV2JA7dyVU
EMKJremFWbefQEuHdhyqlxS/g1Rl6hjARv1DaQgAmQ2lOc+wMV5W/G760PGJg35q
qANIc2d7ux0iT+eLDurZKGSWdscgHPaX3AhXzFTVURlIGta9YLnWr3GFhIEfROxn
svPm64VOq1NiC7/b0RgnfbHGvsCjjaoHcMV0liJDhmed4MinDsY8vCGRlbKW6Mmr
KseFvKJOmEOGGUXTY7BEUmJkIo0BolnWrHv4oxwA2hpp5zJeZh8M69ZwGARMAhwU
w47S3WOWRj1kfwrP2FWmcu3wFKg+zaIr361hrFlmgCXPtftyei5coqjLdLvAuiAk
PInBpBq51WMKwN8IuwTRnmXzYuJo+XrQJFrNeaUGITbImkkS++1P78E+cWA3HHtP
iQIlBBgBCAAPBQJXWWOKAhsMBQkHhh+AAAoJEEABoSepDejhaJcP/2FjFE3nZ7Rz
lIAFeo9+9On3rmZ97az/xIYFaiNFTNIGm3m6158KngwIL/GpS6JHKQ/KPhO7Yxcr
BqYMzvZJplyfCXTdv4tXsHPDmhgfqhK3CXOGPAQSbNflmEM6bVIJabgYon2cSKas
J2j/PlsoyXO24yjf6vbMXMiGkL67byBkgCe73k54fIOL7KXHRu7uDIXYFD9AKW99
jrzxipDfqXtJdFyQ0NLT2mQAVxo/GYemqA4W19sksLZaltN+q4SWV4jpgUc1nyUu
6j2Zx6yBixhzBcKrpiqJifFTBBIxXD40zf+Vx5G6sSYzrGQ9nIw0GtvFyIUKkhFf
Q/TpDHrhL7jnkgnqLypBY4oPCXpK2Ym8meDAnDGbgcYNSUJDvx4iPemcHkMziPM5
neR9DrkUgg8k916EJM7rUV7KBFVzwd1LebxWtR/KiSbta6CxD15rhA/EESRuV3Ra
hF09W5u1iE5lfpdjpYjEmf4jFLJeOtWWHrxaZqQfeXxP6ZAuCYCDPUDr0vZyAHQF
Gghb1xsDWs+z2s4ycpIEFAgQUR5Q/yzIdubMToXSPpGQZVQ7i8zxWgRP6o3JmZ0E
31zZ05oh2wvT/SlpHp5jrB9gqPGYVmPwHV6tbURUMGepmFP2H4uvnT2k2Kfux5Eq
Qeh1ZTzVvm3VhScYKhDPg8x2OpDvKAoo
=qFMO
-----END PGP PRIVATE KEY BLOCK-----

View File

@ -1,10 +0,0 @@
'use strict';
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const sinon = require('sinon');
chai.use(chaiAsPromised);
global.expect = chai.expect;
global.sinon = sinon;

View File

@ -1,34 +1,37 @@
'use strict'; 'use strict';
const log = require('winston'); require('co-mocha')(require('mocha')); // monkey patch mocha for generators
const expect = require('chai').expect;
const log = require('npmlog');
const Email = require('../../src/email/email'); const Email = require('../../src/email/email');
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const sinon = require('sinon');
describe('Email Unit Tests', () => { describe('Email Unit Tests', () => {
const sandbox = sinon.createSandbox(); let email, sendFnStub;
let email;
let sendFnStub;
const template = () => ({ let template = {
subject: 'foo', subject: 'foo',
text: 'bar', text: 'bar',
html: '<strong>bar</strong>' html: '<strong>bar</strong>'
}); };
const sender = { let sender = {
name: 'Foo Bar', name: 'Foo Bar',
email: 'foo@bar.com' email: 'foo@bar.com'
}; };
const userId1 = { let userId1 = {
name: 'name1', name: 'name1',
email: 'email1', email: 'email1',
nonce: 'qwertzuioasdfghjkqwertzuio' nonce: 'qwertzuioasdfghjkqwertzuio'
}; };
const keyId = '0123456789ABCDF0'; let keyId = '0123456789ABCDF0';
const origin = { let origin = {
protocol: 'http', protocol: 'http',
host: 'localhost:8888' host: 'localhost:8888'
}; };
const mailOptions = { let mailOptions = {
from: sender, from: sender,
to: sender, to: sender,
subject: 'Hello ✔', // Subject line subject: 'Hello ✔', // Subject line
@ -37,66 +40,74 @@ describe('Email Unit Tests', () => {
}; };
beforeEach(() => { beforeEach(() => {
sendFnStub = sandbox.stub(); sendFnStub = sinon.stub();
sandbox.stub(nodemailer, 'createTransport').returns({ sinon.stub(nodemailer, 'createTransport').returns({
sendMail: sendFnStub templateSender: () => { return sendFnStub; }
}); });
sandbox.stub(log); sinon.stub(log, 'warn');
sinon.stub(log, 'error');
email = new Email(nodemailer); email = new Email(nodemailer);
email.init({ email.init({
host: 'host', host: 'host',
auth: {user: 'user', pass: 'pass'}, auth: { user:'user', pass:'pass' },
sender sender: sender
}); });
expect(email._sender).to.equal(sender); expect(email._sender).to.equal(sender);
}); });
afterEach(() => { afterEach(() => {
sandbox.restore(); nodemailer.createTransport.restore();
log.warn.restore();
log.error.restore();
}); });
describe('send', () => { describe("send", () => {
beforeEach(() => { beforeEach(() => {
sandbox.stub(email, '_sendHelper').returns(Promise.resolve({response: '250'})); sinon.stub(email, '_sendHelper').returns(Promise.resolve({ response:'250' }));
}); });
it('should work', async () => { afterEach(() => {
const info = await email.send({template, userId: userId1, keyId, origin}); email._sendHelper.restore();
});
it('should work', function *() {
let info = yield email.send({ template, userId:userId1, keyId, origin});
expect(info.response).to.match(/^250/); expect(info.response).to.match(/^250/);
}); });
}); });
describe('_sendHelper', () => { describe("_sendHelper", () => {
it('should work', async () => { it('should work', function *() {
sendFnStub.returns(Promise.resolve({response: '250'})); sendFnStub.returns(Promise.resolve({ response:'250' }));
const info = await email._sendHelper(mailOptions); let info = yield email._sendHelper(mailOptions);
expect(info.response).to.match(/^250/); expect(info.response).to.match(/^250/);
}); });
it('should log warning for reponse error', async () => { it('should log warning for reponse error', function *() {
sendFnStub.returns(Promise.resolve({response: '554'})); sendFnStub.returns(Promise.resolve({ response:'554' }));
const info = await email._sendHelper(mailOptions); let info = yield email._sendHelper(mailOptions);
expect(info.response).to.match(/^554/); expect(info.response).to.match(/^554/);
expect(log.warn.calledOnce).to.be.true; expect(log.warn.calledOnce).to.be.true;
}); });
it('should fail', async () => { it('should fail', function *() {
sendFnStub.returns(Promise.reject(new Error('boom'))); sendFnStub.returns(Promise.reject(new Error('boom')));
try { try {
await email._sendHelper(mailOptions); yield email._sendHelper(mailOptions);
} catch (e) { } catch(e) {
expect(log.error.calledOnce).to.be.true; expect(log.error.calledOnce).to.be.true;
expect(e.status).to.equal(500); expect(e.status).to.equal(500);
expect(e.message).to.match(/failed/); expect(e.message).to.match(/failed/);
} }
}); });
}); });
});
});

View File

@ -1,132 +1,119 @@
'use strict'; 'use strict';
const fs = require('fs'); const fs = require('fs');
const log = require('winston'); const expect = require('chai').expect;
const log = require('npmlog');
const openpgp = require('openpgp'); const openpgp = require('openpgp');
const PGP = require('../../src/service/pgp'); const PGP = require('../../src/service/pgp');
const sinon = require('sinon');
describe('PGP Unit Tests', () => { describe('PGP Unit Tests', () => {
const sandbox = sinon.createSandbox(); let pgp, key1Armored, key2Armored, key3Armored;
let pgp;
let key1Armored;
let key2Armored;
let key3Armored;
let key5Armored;
before(() => {
key1Armored = fs.readFileSync(`${__dirname}/../fixtures/key1.asc`, 'utf8');
key2Armored = fs.readFileSync(`${__dirname}/../fixtures/key2.asc`, 'utf8');
key3Armored = fs.readFileSync(`${__dirname}/../fixtures/key3.asc`, 'utf8');
key5Armored = fs.readFileSync(`${__dirname}/../fixtures/key5.asc`, 'utf8');
});
beforeEach(() => { beforeEach(() => {
sandbox.stub(log); key1Armored = fs.readFileSync(__dirname + '/../key1.asc', 'utf8');
key2Armored = fs.readFileSync(__dirname + '/../key2.asc', 'utf8');
key3Armored = fs.readFileSync(__dirname + '/../key3.asc', 'utf8');
pgp = new PGP(); pgp = new PGP();
}); });
afterEach(() => {
sandbox.restore();
});
describe('parseKey', () => { describe('parseKey', () => {
it('should should throw error on key parsing', async () => { it('should should throw error on key parsing', () => {
sandbox.stub(openpgp.key, 'readArmored').returns({err: [new Error()]}); let readStub = sinon.stub(openpgp.key, 'readArmored').returns({err:[new Error()]});
await expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/Failed to parse/); sinon.stub(log, 'error');
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/Failed to parse/);
expect(log.error.calledOnce).to.be.true; expect(log.error.calledOnce).to.be.true;
log.error.restore();
readStub.restore();
}); });
it('should should throw error when more than one key', () => { it('should should throw error when more than one key', () => {
sandbox.stub(openpgp.key, 'readArmored').returns({keys: [{}, {}]}); let readStub = sinon.stub(openpgp.key, 'readArmored').returns({keys:[{},{}]});
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only one key/); expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only one key/);
readStub.restore();
}); });
it('should should throw error when primaryKey not verfied', () => { it('should should throw error when more than one key', () => {
sandbox.stub(openpgp.key, 'readArmored').returns({ let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
keys: [{ keys: [{
primaryKey: {}, primaryKey: {},
verifyPrimaryKey() { return false; } verifyPrimaryKey: function() { return false; }
}] }]
}); });
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/primary key verification/); expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/primary key verification/);
readStub.restore();
}); });
it('should only accept 16 char key id', () => { it('should only accept 16 char key id', () => {
sandbox.stub(openpgp.key, 'readArmored').returns({ let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
keys: [{ keys: [{
primaryKey: { primaryKey: {
getFingerprint() { fingerprint: '4277257930867231ce393fb8dbc0b3d92b1b86e9',
return '4277257930867231ce393fb8dbc0b3d92b1b86e9'; getKeyId: function() {
},
getKeyId() {
return { return {
toHex() { return 'asdf'; } toHex:function() { return 'asdf'; }
}; };
} }
}, },
verifyPrimaryKey() { return openpgp.enums.keyStatus.valid; } verifyPrimaryKey: function() { return openpgp.enums.keyStatus.valid; }
}] }]
}); });
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only v4 keys/); expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only v4 keys/);
readStub.restore();
}); });
it('should only accept version 4 fingerprint', () => { it('should only accept version 4 fingerprint', () => {
sandbox.stub(openpgp.key, 'readArmored').returns({ let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
keys: [{ keys: [{
primaryKey: { primaryKey: {
getFingerprint() { fingerprint: '4277257930867231ce393fb8dbc0b3d92b1b86e',
return '4277257930867231ce393fb8dbc0b3d92b1b86e'; getKeyId: function() {
},
getKeyId() {
return { return {
toHex() { return 'dbc0b3d92b1b86e9'; } toHex:function() { return 'dbc0b3d92b1b86e9'; }
}; };
} }
}, },
verifyPrimaryKey() { return openpgp.enums.keyStatus.valid; } verifyPrimaryKey: function() { return openpgp.enums.keyStatus.valid; }
}] }]
}); });
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only v4 keys/); expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only v4 keys/);
readStub.restore();
}); });
it('should only accept valid user ids', () => { it('should only accept valid user ids', () => {
sandbox.stub(pgp, 'parseUserIds').returns([]); sinon.stub(pgp, 'parseUserIds').returns([]);
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/invalid user IDs/); expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/invalid user ids/);
}); });
it('should be able to parse RSA key', async () => { it('should be able to parse RSA key', () => {
const params = await pgp.parseKey(key1Armored); let params = pgp.parseKey(key1Armored);
expect(params.keyId).to.equal('dbc0b3d92b1b86e9'); expect(params.keyId).to.equal('dbc0b3d92b1b86e9');
expect(params.fingerprint).to.equal('4277257930867231ce393fb8dbc0b3d92b1b86e9'); expect(params.fingerprint).to.equal('4277257930867231ce393fb8dbc0b3d92b1b86e9');
expect(params.userIds[0].name).to.equal('safewithme testuser'); expect(params.userIds[0].name).to.equal('safewithme testuser');
expect(params.userIds[0].email).to.equal('safewithme.testuser@gmail.com'); expect(params.userIds[0].email).to.equal('safewithme.testuser@gmail.com');
expect(params.created.getTime()).to.exist; expect(params.created.getTime()).to.exist;
expect(params.uploaded.getTime()).to.exist;
expect(params.algorithm).to.equal('rsa_encrypt_sign'); expect(params.algorithm).to.equal('rsa_encrypt_sign');
expect(params.keySize).to.equal(2048); expect(params.keySize).to.equal(2048);
expect(params.publicKeyArmored).to.equal(key1Armored); expect(params.publicKeyArmored).to.equal(key1Armored);
}); });
/* test key2 has expired */ it('should be able to parse RSA/ECC key', () => {
it.skip('should be able to parse RSA/ECC key', async () => { let params = pgp.parseKey(key2Armored);
const params = await pgp.parseKey(key2Armored);
expect(params.keyId).to.equal('b8e4105cc9dedc77'); expect(params.keyId).to.equal('b8e4105cc9dedc77');
expect(params.fingerprint).to.equal('e3317db04d3958fd5f662c37b8e4105cc9dedc77'); expect(params.fingerprint).to.equal('e3317db04d3958fd5f662c37b8e4105cc9dedc77');
expect(params.userIds.length).to.equal(1); expect(params.userIds.length).to.equal(1);
expect(params.created.getTime()).to.exist; expect(params.created.getTime()).to.exist;
expect(params.uploaded.getTime()).to.exist;
expect(params.algorithm).to.equal('rsa_encrypt_sign'); expect(params.algorithm).to.equal('rsa_encrypt_sign');
expect(params.keySize).to.equal(4096); expect(params.keySize).to.equal(4096);
expect(params.publicKeyArmored).to.equal(pgp.trimKey(key2Armored)); expect(params.publicKeyArmored).to.equal(pgp.trimKey(key2Armored));
}); });
it('should be able to parse komplex key', async () => { it('should be able to parse komplex key', () => {
const params = await pgp.parseKey(key3Armored); let params = pgp.parseKey(key3Armored);
expect(params.keyId).to.equal('4001a127a90de8e1'); expect(params.keyId).to.equal('4001a127a90de8e1');
expect(params.fingerprint).to.equal('04062c70b446e33016e219a74001a127a90de8e1'); expect(params.fingerprint).to.equal('04062c70b446e33016e219a74001a127a90de8e1');
expect(params.userIds.length).to.equal(4); expect(params.userIds.length).to.equal(4);
expect(params.created.getTime()).to.exist; expect(params.created.getTime()).to.exist;
expect(params.uploaded.getTime()).to.exist;
expect(params.algorithm).to.equal('rsa_encrypt_sign'); expect(params.algorithm).to.equal('rsa_encrypt_sign');
expect(params.keySize).to.equal(4096); expect(params.keySize).to.equal(4096);
expect(params.publicKeyArmored).to.equal(pgp.trimKey(key3Armored)); expect(params.publicKeyArmored).to.equal(pgp.trimKey(key3Armored));
@ -135,12 +122,12 @@ describe('PGP Unit Tests', () => {
describe('trimKey', () => { describe('trimKey', () => {
it('should be the same as key1', () => { it('should be the same as key1', () => {
const trimmed = pgp.trimKey(key1Armored); let trimmed = pgp.trimKey(key1Armored);
expect(trimmed).to.equal(key1Armored); expect(trimmed).to.equal(key1Armored);
}); });
it('should not be the same as key2', () => { it('should not be the same as key2', () => {
const trimmed = pgp.trimKey(key2Armored); let trimmed = pgp.trimKey(key2Armored);
expect(trimmed).to.not.equal(key2Armored); expect(trimmed).to.not.equal(key2Armored);
}); });
}); });
@ -150,22 +137,22 @@ describe('PGP Unit Tests', () => {
const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----'; const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
it('should return true for valid key block', () => { it('should return true for valid key block', () => {
const input = KEY_BEGIN + KEY_END; let input = KEY_BEGIN + KEY_END;
expect(pgp.validateKeyBlock(input)).to.be.true; expect(pgp.validateKeyBlock(input)).to.be.true;
}); });
it('should return false for invalid key block', () => { it('should return false for invalid key block', () => {
const input = KEY_END + KEY_BEGIN; let input = KEY_END + KEY_BEGIN;
expect(pgp.validateKeyBlock(input)).to.be.false; expect(pgp.validateKeyBlock(input)).to.be.false;
}); });
it('should return false for invalid key block', () => { it('should return false for invalid key block', () => {
const input = KEY_END; let input = KEY_END;
expect(pgp.validateKeyBlock(input)).to.be.false; expect(pgp.validateKeyBlock(input)).to.be.false;
}); });
it('should return false for invalid key block', () => { it('should return false for invalid key block', () => {
const input = KEY_BEGIN; let input = KEY_BEGIN;
expect(pgp.validateKeyBlock(input)).to.be.false; expect(pgp.validateKeyBlock(input)).to.be.false;
}); });
}); });
@ -173,75 +160,33 @@ describe('PGP Unit Tests', () => {
describe('parseUserIds', () => { describe('parseUserIds', () => {
let key; let key;
beforeEach(async () => { beforeEach(() => {
key = (await openpgp.key.readArmored(key1Armored)).keys[0]; key = openpgp.key.readArmored(key1Armored).keys[0];
}); });
it('should parse a valid user id', async () => { it('should parse a valid user id', () => {
const parsed = await pgp.parseUserIds(key.users, key.primaryKey); let parsed = pgp.parseUserIds(key.users, key.primaryKey);
expect(parsed[0].name).to.equal('safewithme testuser'); expect(parsed[0].name).to.equal('safewithme testuser');
expect(parsed[0].email).to.equal('safewithme.testuser@gmail.com'); expect(parsed[0].email).to.equal('safewithme.testuser@gmail.com');
}); });
it('should throw for an empty user ids array', () => it('should throw for an empty user ids array', () => {
expect(pgp.parseUserIds([], key.primaryKey)).to.eventually.be.rejectedWith(/no user ID/) expect(pgp.parseUserIds.bind(pgp, [], key.primaryKey)).to.throw(/no user id/);
); });
it('should return no user id for an invalid signature', async () => { it('should return no user id for an invalid signature', () => {
key.users[0].userId.userid = 'fake@example.com'; key.users[0].userId.userid = 'fake@example.com';
const parsed = await pgp.parseUserIds(key.users, key.primaryKey); let parsed = pgp.parseUserIds(key.users, key.primaryKey);
expect(parsed.length).to.equal(0); expect(parsed.length).to.equal(0);
}); });
it('should throw for an invalid email address', async () => { it('should throw for a invalid email address', () => {
let verifyStub = sinon.stub(key.users[0], 'isValidSelfCertificate').returns(true);
key.users[0].userId.userid = 'safewithme testuser <safewithme.testusergmail.com>'; key.users[0].userId.userid = 'safewithme testuser <safewithme.testusergmail.com>';
const parsed = await pgp.parseUserIds(key.users, key.primaryKey); let parsed = pgp.parseUserIds(key.users, key.primaryKey);
expect(parsed.length).to.equal(0); expect(parsed.length).to.equal(0);
verifyStub.restore();
}); });
}); });
describe('filterKeyByUserIds', () => { });
it('should filter user IDs', async () => {
const email = 'test1@example.com';
const {keys: [key]} = await openpgp.key.readArmored(key3Armored);
expect(key.users.length).to.equal(4);
const filtered = await pgp.filterKeyByUserIds([{email}], key3Armored);
const {keys: [filteredKey]} = await openpgp.key.readArmored(filtered);
expect(filteredKey.users.length).to.equal(1);
expect(filteredKey.users[0].userId.email).to.equal(email);
});
it('should not filter user attributes', async () => {
const email = 'test@example.com';
const {keys: [key]} = await openpgp.key.readArmored(key5Armored);
expect(key.users.length).to.equal(2);
const filtered = await pgp.filterKeyByUserIds([{email}], key5Armored);
const {keys: [filteredKey]} = await openpgp.key.readArmored(filtered);
expect(filteredKey.users.length).to.equal(2);
expect(filteredKey.users[0].userId).to.exist;
expect(filteredKey.users[1].userAttribute).to.exist;
});
});
describe('removeUserId', () => {
it('should remove user IDs', async () => {
const email = 'test1@example.com';
const {keys: [key]} = await openpgp.key.readArmored(key3Armored);
expect(key.users.length).to.equal(4);
const reduced = await pgp.removeUserId(email, key3Armored);
const {keys: [reducedKey]} = await openpgp.key.readArmored(reduced);
expect(reducedKey.users.length).to.equal(3);
expect(reducedKey.users.includes(({userId}) => userId.email === email)).to.be.false;
});
it('should not remove user attributes', async () => {
const email = 'test@example.com';
const {keys: [key]} = await openpgp.key.readArmored(key5Armored);
expect(key.users.length).to.equal(2);
const reduced = await pgp.removeUserId(email, key5Armored);
const {keys: [reducedKey]} = await openpgp.key.readArmored(reduced);
expect(reducedKey.users.length).to.equal(1);
expect(reducedKey.users[0].userAttribute).to.exist;
});
});
});

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const expect = require('chai').expect;
const util = require('../../src/service/util'); const util = require('../../src/service/util');
describe('Util Unit Tests', () => { describe('Util Unit Tests', () => {
@ -116,7 +117,7 @@ describe('Util Unit Tests', () => {
try { try {
util.throw(500, 'boom'); util.throw(500, 'boom');
expect(true).to.be.false; expect(true).to.be.false;
} catch (e) { } catch(e) {
expect(e.message).to.equal('boom'); expect(e.message).to.equal('boom');
expect(e.status).to.equal(500); expect(e.status).to.equal(500);
expect(e.expose).to.be.true; expect(e.expose).to.be.true;
@ -136,19 +137,20 @@ describe('Util Unit Tests', () => {
describe('origin', () => { describe('origin', () => {
it('should work', () => { it('should work', () => {
expect(util.origin({secure: true, host: 'h', protocol: 'p'})).to.exist; expect(util.origin({ secure:true, host:'h', protocol:'p' })).to.exist;
}); });
}); });
describe('url', () => { describe('url', () => {
it('should work with resource', () => { it('should work with resource', () => {
const url = util.url({host: 'localhost', protocol: 'http'}, '/foo'); let url = util.url({ host:'localhost', protocol:'http'}, '/foo');
expect(url).to.equal('http://localhost/foo'); expect(url).to.equal('http://localhost/foo');
}); });
it('should work without resource', () => { it('should work without resource', () => {
const url = util.url({host: 'localhost', protocol: 'http'}); let url = util.url({ host:'localhost', protocol:'http'});
expect(url).to.equal('http://localhost'); expect(url).to.equal('http://localhost');
}); });
}); });
});
});