Compare commits
No commits in common. "master" and "v1.0.0" have entirely different histories.
@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
npm-debug.log
|
|
12
.elasticbeanstalk/config.yml
Normal file
12
.elasticbeanstalk/config.yml
Normal 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
|
@ -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
|
|
||||||
}
|
|
@ -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
2
.gitignore
vendored
@ -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
4
.jscsrc
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"disallowTrailingWhitespace": true,
|
||||||
|
"validateIndentation": 2
|
||||||
|
}
|
25
.jshintrc
Normal file
25
.jshintrc
Normal 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
|
||||||
|
}
|
||||||
|
}
|
39
.travis.yml
39
.travis.yml
@ -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
|
|
31
Changelog.md
31
Changelog.md
@ -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
|
|
@ -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
43
Gruntfile.js
Normal 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
147
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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
25
config/development.js
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
|
log: {
|
||||||
|
level: 'warn'
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
@ -1,5 +1,7 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
|
log: {
|
||||||
|
level: 'error'
|
||||||
|
}
|
||||||
|
|
||||||
};
|
};
|
@ -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:
|
|
30
env.sample
30
env.sample
@ -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
|
|
14
index.js
14
index.js
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -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"
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
@ -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
2754
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@ -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
42
res/aws_release.sh
Executable 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
153
src/app.js
Normal 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;
|
@ -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;
|
|
@ -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);
|
|
||||||
}
|
|
||||||
};
|
|
@ -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,7 +74,7 @@ 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;
|
@ -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()
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
@ -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: {
|
||||||
|
name: userId.name,
|
||||||
|
baseUrl: util.url(origin),
|
||||||
|
keyId: keyId,
|
||||||
|
nonce: userId.nonce
|
||||||
}
|
}
|
||||||
const sendOptions = {
|
|
||||||
from: {name: this._sender.name, address: this._sender.email},
|
|
||||||
to: {name: userId.name, address: userId.email},
|
|
||||||
subject: compiled.subject,
|
|
||||||
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;
|
@ -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
12
src/email/templates.json
Normal 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>"
|
||||||
|
}
|
||||||
|
}
|
33
src/index.js
33
src/index.js
@ -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;
|
|
||||||
}
|
|
||||||
})();
|
|
@ -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,12 +68,12 @@ 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)) {
|
||||||
@ -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 {
|
|
||||||
await ctx.render('key-armored', {query: params, key});
|
|
||||||
}
|
|
||||||
} else if (['index','vindex'].indexOf(params.op) !== -1) {
|
} else if (['index','vindex'].indexOf(params.op) !== -1) {
|
||||||
const VERSION = 1;
|
const VERSION = 1, COUNT = 1; // number of keys
|
||||||
const COUNT = 1; // number of keys
|
let fp = key.fingerprint.toUpperCase();
|
||||||
const fp = key.fingerprint.toUpperCase();
|
let algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
|
||||||
const algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
|
let created = key.created ? (key.created.getTime() / 1000) : '';
|
||||||
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;
|
@ -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;
|
@ -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,80 +108,34 @@ 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 signature must be valid
|
||||||
|
let result = [];
|
||||||
|
for (let user of users) {
|
||||||
|
let oneValid = false;
|
||||||
|
for (let cert of user.selfCertifications) {
|
||||||
|
if (user.isValidSelfCertificate(primaryKey, cert)) {
|
||||||
|
oneValid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oneValid && user.userId && user.userId.userid) {
|
||||||
|
let uid = addressparser(user.userId.userid)[0];
|
||||||
|
if (util.isEmail(uid.address)) {
|
||||||
|
result.push(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// at least one user id must be valid, revoked or expired
|
|
||||||
const result = [];
|
|
||||||
for (const user of users) {
|
|
||||||
const userStatus = await user.verify(primaryKey, verifyDate);
|
|
||||||
if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) {
|
|
||||||
try {
|
|
||||||
const uid = openpgp.util.parseUserId(user.userId.userid);
|
|
||||||
if (util.isEmail(uid.email)) {
|
|
||||||
// map to local user id object format
|
// map to local user id object format
|
||||||
result.push({
|
return result.map(uid => ({
|
||||||
status: userStatus,
|
|
||||||
name: uid.name,
|
name: uid.name,
|
||||||
email: util.normalizeEmail(uid.email),
|
email: uid.address.toLowerCase(),
|
||||||
verified: false
|
verified: false
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove user IDs from armored key block which are not in array of user IDs
|
|
||||||
* @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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
|
||||||
}, DB_TYPE);
|
|
||||||
}
|
}
|
||||||
|
// persist new key
|
||||||
/**
|
let r = yield this._mongo.create(key, DB_TYPE);
|
||||||
* Merge existing and new user IDs
|
if (r.insertedCount !== 1) {
|
||||||
* @param {Array} existingUsers source user IDs
|
util.throw(500, 'Failed to persist key');
|
||||||
* @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
|
||||||
/**
|
yield this._email.send({ template:tpl.verifyKey, userId, keyId, origin });
|
||||||
* Persist the public key and its user ids in the database.
|
|
||||||
* @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
|
||||||
await this._mongo.remove({keyId}, DB_TYPE);
|
yield 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;
|
@ -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 || '');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
6
src/static/css/bootstrap.min.css
vendored
6
src/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
@ -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
80
src/static/demo.html
Normal 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>© 2016 Mailvelope GmbH</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
67
src/static/index.html
Normal file
67
src/static/index.html
Normal 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>© 2016 Mailvelope GmbH</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div> <!-- /container -->
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
2
src/static/js/jquery.min.js
vendored
2
src/static/js/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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));
|
|
@ -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>© 2021 Mailvelope GmbH</p>
|
|
||||||
</footer>
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -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>
|
|
@ -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>
|
|
@ -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 -->
|
|
@ -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 -->
|
|
@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../.eslintrc.json",
|
|
||||||
|
|
||||||
"rules": {
|
|
||||||
"no-shadow": 1
|
|
||||||
},
|
|
||||||
|
|
||||||
"globals": {
|
|
||||||
"expect": true,
|
|
||||||
"sinon": true
|
|
||||||
},
|
|
||||||
|
|
||||||
"env": {
|
|
||||||
"mocha": true
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
|
|
||||||
'use strict'
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
recursive: true,
|
|
||||||
require: ['./test/setup.js']
|
|
||||||
}
|
|
261
test/fixtures/key3.asc
vendored
261
test/fixtures/key3.asc
vendored
@ -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
156
test/fixtures/key4.asc
vendored
@ -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-----
|
|
40
test/fixtures/key5.asc
vendored
40
test/fixtures/key5.asc
vendored
@ -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-----
|
|
@ -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', () => {
|
||||||
@ -71,7 +66,26 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
|||||||
.end(done);
|
.end(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 201', done => {
|
it('should return 400 for an invalid primaryEmail', done => {
|
||||||
|
request(app.listen())
|
||||||
|
.post('/api/v1/key')
|
||||||
|
.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())
|
request(app.listen())
|
||||||
.post('/api/v1/key')
|
.post('/api/v1/key')
|
||||||
.send({ publicKeyArmored })
|
.send({ publicKeyArmored })
|
||||||
@ -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())
|
request(app.listen())
|
||||||
.del(`/api/v1/key?keyId=${emailParams.keyId}`)
|
.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())
|
||||||
|
.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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -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 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -1,341 +1,239 @@
|
|||||||
'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 = [];
|
|
||||||
const paramMatcher = sinon.match(params => {
|
|
||||||
mailsSent[mailsSent.length] = {params};
|
|
||||||
expect(params.nonce).to.exist;
|
|
||||||
expect(params.keyId).to.exist;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
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 = sinon.stub().returns(Promise.resolve({ response:'250' }));
|
||||||
sendEmailStub.withArgs(sinon.match(sendOptions => {
|
sendEmailStub.withArgs(sinon.match(recipient => {
|
||||||
mailsSent[mailsSent.length - 1].to = sendOptions.to.address;
|
return recipient.to.address === primaryEmail;
|
||||||
return true;
|
}), sinon.match(params => {
|
||||||
|
emailParams = params;
|
||||||
|
return params.nonce !== undefined && params.keyId !== undefined;
|
||||||
}));
|
}));
|
||||||
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,31 +244,37 @@ 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
232
test/key3.asc
Normal 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-----
|
@ -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;
|
|
@ -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,61 +40,68 @@ 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);
|
||||||
@ -99,4 +109,5 @@ describe('Email Unit Tests', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
@ -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;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
@ -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', () => {
|
||||||
@ -142,13 +143,14 @@ describe('Util Unit Tests', () => {
|
|||||||
|
|
||||||
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');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
Loading…
Reference in New Issue
Block a user