Compare commits
145 Commits
Author | SHA1 | Date | |
---|---|---|---|
7adfc6aa1d | |||
b8b71481c6 | |||
|
cd62e63cc2 | ||
|
f118ec64b0 | ||
|
fe1cb9f439 | ||
|
8a9a7be0c7 | ||
|
6f21406afd | ||
|
9c3ddbfef2 | ||
|
665708dbb9 | ||
|
93af442fff | ||
|
769d7d1c52 | ||
|
11f99f8d40 | ||
|
b15879f31b | ||
|
ff6f9f7c63 | ||
|
f399da9614 | ||
|
3367f08647 | ||
|
9064089482 | ||
|
d14b0dc390 | ||
|
e7ba187221 | ||
|
9e7149108a | ||
|
f88f4d5d2e | ||
|
b83f13cde2 | ||
|
9159bd5a47 | ||
|
1fcf791560 | ||
|
aad782573d | ||
|
1da22c1029 | ||
|
cee14ba99c | ||
|
9db75f4034 | ||
|
46b474a748 | ||
|
914e63a8ee | ||
|
8d18614f17 | ||
|
3f498495c1 | ||
|
02adaad939 | ||
|
1651571d36 | ||
|
a2b941b0ae | ||
|
0baf3fc857 | ||
|
92df122435 | ||
|
0852822055 | ||
|
1c53ff7f17 | ||
|
e259c0f51f | ||
|
6ec72aef06 | ||
|
656aa9b6ed | ||
|
190738fd0d | ||
|
f890ffa8cd | ||
|
b3a2171a72 | ||
|
d80108915e | ||
|
b1848bf8e6 | ||
|
1de83fe5d5 | ||
|
b93db84c6a | ||
|
77fc0fd195 | ||
|
b738e1bc5c | ||
|
164585b406 | ||
|
2b969c0382 | ||
|
0400b9c9d9 | ||
|
b74563b3ec | ||
|
5fa36e6d52 | ||
|
4c28da4eab | ||
|
1e2c85621b | ||
|
aa850377d5 | ||
|
0d6a9fdae5 | ||
|
258117d36d | ||
|
5b86a77338 | ||
|
fe55578268 | ||
|
2af8310070 | ||
|
afacbf413f | ||
|
74063915c7 | ||
|
80c760681c | ||
|
ba6f75984e | ||
|
35dbc08015 | ||
|
a156f05002 | ||
|
e3a2a1ff20 | ||
|
7f5ad65c61 | ||
|
7d3a64c84d | ||
|
95ff2d9247 | ||
|
8c76281666 | ||
|
a52cef2771 | ||
|
4081463dfa | ||
|
49b24a5cb4 | ||
|
3dfa447fcf | ||
|
1557a5f925 | ||
|
59a77fd01e | ||
|
5778f8fa13 | ||
|
ba671126db | ||
|
874903c64b | ||
|
26807e03b1 | ||
|
5ecc728564 | ||
|
7178a12ed5 | ||
|
158a7418d0 | ||
|
20593a0adc | ||
|
21118c0b1d | ||
|
c773da3f60 | ||
|
80a8028f86 | ||
|
d8039ea976 | ||
|
e9251d5203 | ||
|
750cf3d897 | ||
|
f224f32e66 | ||
|
7800dafce3 | ||
|
bbf24d6c53 | ||
|
b9380f9f20 | ||
|
ffbee07c5c | ||
|
eb9ecea7e5 | ||
|
2db8127931 | ||
|
b397fa00cd | ||
|
252053dd13 | ||
|
d7ef68cd97 | ||
|
469afdac91 | ||
|
9f922ce116 | ||
|
b721dc9f9b | ||
|
505b337d9a | ||
|
b2455393b2 | ||
|
9898383230 | ||
|
cb37c834d8 | ||
|
a47a0162a6 | ||
|
5674a2e8c9 | ||
|
2fcedd9f09 | ||
|
07c6ccd717 | ||
|
41cd62668b | ||
|
3df1f3ad5d | ||
|
4294c6c372 | ||
|
66cd1bc3a0 | ||
|
7a6b8c5d27 | ||
|
e8c3820c49 | ||
|
e75f37031a | ||
|
7350b07540 | ||
|
95b4f5e471 | ||
|
80425e3c58 | ||
|
3ffe4fa666 | ||
|
7510e75688 | ||
|
ebc7dd9ada | ||
|
92e5db544a | ||
|
7920eebd4b | ||
|
a273e378d9 | ||
|
516df397c2 | ||
|
7c16ccd40a | ||
|
615da99b77 | ||
|
f33f45dde9 | ||
|
af086e705d | ||
|
2bddbd6a93 | ||
|
7d93b882a5 | ||
|
1a7b57777b | ||
|
1ab934da31 | ||
|
a60d8b86a9 | ||
|
b29c8308a8 | ||
|
e09454242d | ||
|
d4c1e7ba06 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
npm-debug.log
|
@ -1,12 +0,0 @@
|
||||
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
|
74
.eslintrc.json
Normal file
74
.eslintrc.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"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
|
||||
}
|
44
.gitea/workflows/docker-publish.yaml
Normal file
44
.gitea/workflows/docker-publish.yaml
Normal file
@ -0,0 +1,44 @@
|
||||
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,3 +34,5 @@ node_modules
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
config/development.js
|
||||
|
25
.jshintrc
25
.jshintrc
@ -1,25 +0,0 @@
|
||||
{
|
||||
"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,17 +1,30 @@
|
||||
sudo: false
|
||||
language: node_js
|
||||
node_js:
|
||||
- "4"
|
||||
- "5"
|
||||
- "6"
|
||||
before_script:
|
||||
- npm install -g grunt-cli
|
||||
- sleep 15
|
||||
- mongo test_db --eval 'db.addUser("travis", "test");'
|
||||
notifications:
|
||||
email:
|
||||
- build@mailvelope.com
|
||||
- "10"
|
||||
|
||||
env:
|
||||
- NODE_ENV=integration LOG_LEVEL=warn MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test
|
||||
|
||||
services:
|
||||
- mongodb
|
||||
env:
|
||||
- NODE_ENV=integration MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test
|
||||
|
||||
before_script:
|
||||
- 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
Normal file
31
Changelog.md
Normal file
@ -0,0 +1,31 @@
|
||||
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
|
9
Dockerfile
Normal file
9
Dockerfile
Normal file
@ -0,0 +1,9 @@
|
||||
FROM node:16
|
||||
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "node", "index.js" ]
|
43
Gruntfile.js
43
Gruntfile.js
@ -1,43 +0,0 @@
|
||||
'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']);
|
||||
|
||||
};
|
131
README.md
131
README.md
@ -31,13 +31,18 @@ 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 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:
|
||||
|
||||
## HKP api
|
||||
* Only public keys with at least one verified email address are served
|
||||
* 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
|
||||
|
||||
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:
|
||||
## 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:
|
||||
|
||||
#### Accepted `search` parameters
|
||||
* Email addresses
|
||||
@ -52,7 +57,7 @@ The HKP apis are not documented here. Please refer to the [HKP specification](ht
|
||||
#### Accepted `options` parameters
|
||||
* mr
|
||||
|
||||
## REST api
|
||||
## REST API
|
||||
|
||||
### Lookup a key
|
||||
|
||||
@ -74,12 +79,6 @@ GET /api/v1/key?fingerprint=e3317db04d3958fd5f662c37b8e4105cc9dedc77
|
||||
GET /api/v1/key?email=user@example.com
|
||||
```
|
||||
|
||||
#### By email address (shorthand link for sharing)
|
||||
|
||||
```
|
||||
GET /user/user@example.com
|
||||
```
|
||||
|
||||
#### Payload (JSON):
|
||||
|
||||
```json
|
||||
@ -125,48 +124,48 @@ POST /api/v1/key
|
||||
|
||||
```json
|
||||
{
|
||||
"publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----",
|
||||
"primaryEmail": "user@example.com"
|
||||
"publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----"
|
||||
}
|
||||
```
|
||||
|
||||
* **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
|
||||
### Verify uploaded key (via link in email)
|
||||
|
||||
```
|
||||
GET /api/v1/verify?keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
|
||||
GET /api/v1/key?op=verify&keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
|
||||
```
|
||||
|
||||
### Request key removal
|
||||
|
||||
#### Via delete request
|
||||
|
||||
```
|
||||
DELETE /api/v1/key?keyId=b8e4105cc9dedc77 OR ?email=user@example.com
|
||||
```
|
||||
|
||||
#### Via link
|
||||
### Verify key removal (via link in email)
|
||||
|
||||
```
|
||||
GET /api/v1/removeKey?keyId=b8e4105cc9dedc77 OR ?email=user@example.com
|
||||
GET /api/v1/key?op=verifyRemove&keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
|
||||
```
|
||||
|
||||
### Verify key removal
|
||||
# Language & DB
|
||||
|
||||
```
|
||||
GET /api/v1/verifyRemove?keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
|
||||
```
|
||||
The server is written is in JavaScript ES7 and runs on [Node.js](https://nodejs.org/) v8+.
|
||||
|
||||
It uses [MongoDB](https://www.mongodb.com/) v3.2+ as its database.
|
||||
|
||||
|
||||
# Getting started
|
||||
## Installation
|
||||
|
||||
# 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)
|
||||
### 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/).
|
||||
|
||||
@ -175,7 +174,7 @@ brew update
|
||||
brew install node
|
||||
```
|
||||
|
||||
## Setup local MongoDB (Mac OS)
|
||||
### 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/).
|
||||
|
||||
@ -191,7 +190,7 @@ Now the mongo daemon should be running in the background. To have mongo start au
|
||||
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
|
||||
mongo
|
||||
@ -199,31 +198,46 @@ use keyserver-test
|
||||
db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite", db:"keyserver-test" }] })
|
||||
```
|
||||
|
||||
## Setup SMTP user
|
||||
|
||||
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/).
|
||||
|
||||
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.
|
||||
|
||||
## Install dependencies and run tests
|
||||
### Dependencies
|
||||
|
||||
```shell
|
||||
npm install && npm test
|
||||
npm install
|
||||
```
|
||||
|
||||
## Start local server
|
||||
## Configuration
|
||||
|
||||
```shell
|
||||
npm start
|
||||
```
|
||||
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
|
||||
|
||||
# Production
|
||||
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.
|
||||
|
||||
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:
|
||||
### Production
|
||||
|
||||
* NODE_ENV=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
|
||||
@ -232,13 +246,32 @@ The `config/development.js` file can be used to configure a local development in
|
||||
* 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
|
||||
* 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))
|
||||
* 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.
|
||||
|
||||
## Run tests
|
||||
|
||||
```shell
|
||||
npm test
|
||||
```
|
||||
|
||||
## Start local server
|
||||
|
||||
```shell
|
||||
npm start
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,7 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
log: {
|
||||
level: 'silly'
|
||||
level: process.env.LOG_LEVEL || 'silly'
|
||||
},
|
||||
|
||||
papertrail: {
|
||||
host: process.env.PAPERTRAIL_HOST,
|
||||
port: process.env.PAPERTRAIL_PORT
|
||||
},
|
||||
|
||||
server: {
|
||||
@ -31,6 +38,10 @@ module.exports = {
|
||||
name: process.env.SENDER_NAME,
|
||||
email: process.env.SENDER_EMAIL
|
||||
}
|
||||
},
|
||||
|
||||
publicKey: {
|
||||
purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -1,25 +0,0 @@
|
||||
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,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
log: {
|
||||
level: 'warn'
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -1,7 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
log: {
|
||||
level: 'error'
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
26
docker-compose.yml
Normal file
26
docker-compose.yml
Normal file
@ -0,0 +1,26 @@
|
||||
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
Normal file
30
env.sample
Normal file
@ -0,0 +1,30 @@
|
||||
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
|
16
index.js
16
index.js
@ -20,9 +20,11 @@
|
||||
const cluster = require('cluster');
|
||||
const numCPUs = require('os').cpus().length;
|
||||
const config = require('config');
|
||||
const log = require('npmlog');
|
||||
const log = require('winston');
|
||||
const papertrail = require('./src/dao/papertrail');
|
||||
|
||||
log.level = config.log.level; // set log level depending on process.env.NODE_ENV
|
||||
log.level = config.log.level;
|
||||
papertrail.init(config.papertrail);
|
||||
|
||||
//
|
||||
// Start worker cluster depending on number of CPUs
|
||||
@ -32,13 +34,13 @@ if (cluster.isMaster) {
|
||||
for (let i = 0; i < numCPUs; i++) {
|
||||
cluster.fork();
|
||||
}
|
||||
cluster.on('fork', worker => log.info('cluster', 'Forked worker #%s [pid:%s]', worker.id, worker.process.pid));
|
||||
cluster.on('fork', worker => log.info('cluster', `Forked worker #${worker.id} [pid:${worker.process.pid}]`));
|
||||
cluster.on('exit', worker => {
|
||||
log.warn('cluster', 'Worker #%s [pid:%s] died', worker.id, worker.process.pid);
|
||||
setTimeout(() => cluster.fork(), 5000);
|
||||
log.warn('cluster', `Worker #${worker.id} [pid:${worker.process.pid}] died`);
|
||||
cluster.fork();
|
||||
});
|
||||
} else {
|
||||
require('./src/app');
|
||||
require('./src');
|
||||
}
|
||||
|
||||
//
|
||||
@ -58,4 +60,4 @@ process.on('SIGINT', () => {
|
||||
process.on('uncaughtException', err => {
|
||||
log.error('index', 'Uncaught exception', err);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
10
locales/de.json
Normal file
10
locales/de.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"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"
|
||||
}
|
10
locales/en.json
Normal file
10
locales/en.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"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"
|
||||
}
|
12
mongo-init.js
Normal file
12
mongo-init.js
Normal file
@ -0,0 +1,12 @@
|
||||
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
Normal file
2754
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@ -1,41 +1,47 @@
|
||||
{
|
||||
"name": "mailvelope-keyserver",
|
||||
"version": "1.0.0",
|
||||
"version": "3.0.0",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mailvelope/keyserver.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=10",
|
||||
"npm": ">=6"
|
||||
},
|
||||
"scripts": {
|
||||
"start": ": ${NODE_ENV=development} && node index.js",
|
||||
"test": ": ${NODE_ENV=development} && grunt test"
|
||||
"start": "node index.js",
|
||||
"test": "npm run test:lint && npm run test:unit && npm run test:integration",
|
||||
"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": {
|
||||
"addressparser": "^1.0.1",
|
||||
"co": "^4.6.0",
|
||||
"co-body": "^4.2.0",
|
||||
"config": "^1.20.4",
|
||||
"koa": "^1.2.0",
|
||||
"koa-router": "^5.4.0",
|
||||
"koa-static": "^2.0.0",
|
||||
"mongodb": "^2.1.20",
|
||||
"nodemailer": "^2.4.2",
|
||||
"nodemailer-openpgp": "^1.0.2",
|
||||
"npmlog": "^2.0.4",
|
||||
"openpgp": "^2.3.0"
|
||||
"co-body": "6.1.0",
|
||||
"config": "3.3.6",
|
||||
"koa": "2.13.1",
|
||||
"koa-ejs": "4.3.0",
|
||||
"koa-locales": "1.12.0",
|
||||
"koa-router": "10.0.0",
|
||||
"koa-static": "5.0.0",
|
||||
"mongodb": "3.6.6",
|
||||
"nodemailer": "6.6.0",
|
||||
"openpgp": "4.5.5",
|
||||
"winston": "3.3.3",
|
||||
"winston-papertrail": "1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^3.5.0",
|
||||
"co-mocha": "^1.1.2",
|
||||
"grunt": "^1.0.1",
|
||||
"grunt-contrib-jshint": "^1.0.0",
|
||||
"grunt-jscs": "^2.8.0",
|
||||
"grunt-mocha-test": "^0.12.7",
|
||||
"mocha": "^2.5.3",
|
||||
"sinon": "^1.17.4",
|
||||
"supertest": "^1.2.0"
|
||||
"bootstrap": "^3.4.1",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^7.26.0",
|
||||
"jquery": "^3.6.0",
|
||||
"mocha": "^8.4.0",
|
||||
"sinon": "^10.0.0",
|
||||
"supertest": "^6.1.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
#!/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
153
src/app.js
@ -1,153 +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 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;
|
91
src/app/index.js
Normal file
91
src/app/index.js
Normal file
@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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;
|
59
src/app/middleware.js
Normal file
59
src/app/middleware.js
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
const log = require('winston');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
|
||||
/**
|
||||
* A simple wrapper around the official MongoDB client.
|
||||
*/
|
||||
class Mongo {
|
||||
|
||||
/**
|
||||
* Initializes the database client by connecting to the MongoDB.
|
||||
* @param {String} uri The mongodb uri
|
||||
@ -31,9 +31,11 @@ class Mongo {
|
||||
* @param {String} pass The database user's password
|
||||
* @yield {undefined}
|
||||
*/
|
||||
*init(options) {
|
||||
let uri = 'mongodb://' + options.user + ':' + options.pass + '@' + options.uri;
|
||||
this._db = yield MongoClient.connect(uri);
|
||||
async init({uri, user, pass}) {
|
||||
log.info('mongo', 'Connecting to MongoDB ...');
|
||||
const url = `mongodb://${user}:${pass}@${uri}`;
|
||||
this._client = await MongoClient.connect(url, {useNewUrlParser: true, useUnifiedTopology: true});
|
||||
this._db = this._client.db();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -41,7 +43,7 @@ class Mongo {
|
||||
* @yield {undefined}
|
||||
*/
|
||||
disconnect() {
|
||||
return this._db.close();
|
||||
return this._client.close();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -51,7 +53,7 @@ class Mongo {
|
||||
* @yield {Object} The operation result
|
||||
*/
|
||||
create(document, type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.insertOne(document);
|
||||
}
|
||||
|
||||
@ -62,7 +64,7 @@ class Mongo {
|
||||
* @yield {Object} The operation result
|
||||
*/
|
||||
batch(documents, type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.insertMany(documents);
|
||||
}
|
||||
|
||||
@ -74,8 +76,8 @@ class Mongo {
|
||||
* @yield {Object} The operation result
|
||||
*/
|
||||
update(query, diff, type) {
|
||||
let col = this._db.collection(type);
|
||||
return col.updateOne(query, { $set:diff });
|
||||
const col = this._db.collection(type);
|
||||
return col.updateOne(query, {$set: diff});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -85,7 +87,7 @@ class Mongo {
|
||||
* @yield {Object} The document object
|
||||
*/
|
||||
get(query, type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.findOne(query);
|
||||
}
|
||||
|
||||
@ -96,7 +98,7 @@ class Mongo {
|
||||
* @yield {Array} An array of document objects
|
||||
*/
|
||||
list(query, type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.find(query).toArray();
|
||||
}
|
||||
|
||||
@ -107,7 +109,7 @@ class Mongo {
|
||||
* @yield {Object} The operation result
|
||||
*/
|
||||
remove(query, type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.deleteMany(query);
|
||||
}
|
||||
|
||||
@ -117,10 +119,9 @@ class Mongo {
|
||||
* @yield {Object} The operation result
|
||||
*/
|
||||
clear(type) {
|
||||
let col = this._db.collection(type);
|
||||
const col = this._db.collection(type);
|
||||
return col.deleteMany({});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Mongo;
|
||||
module.exports = Mongo;
|
||||
|
52
src/dao/papertrail.js
Normal file
52
src/dao/papertrail.js
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* 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,16 +17,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
const log = require('winston');
|
||||
const util = require('../service/util');
|
||||
const openpgp = require('openpgp');
|
||||
const nodemailer = require('nodemailer');
|
||||
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
|
||||
|
||||
/**
|
||||
* A simple wrapper around Nodemailer to send verification emails
|
||||
*/
|
||||
class Email {
|
||||
|
||||
/**
|
||||
* Create an instance of the reusable nodemailer SMTP transport.
|
||||
* @param {string} host SMTP server's hostname: 'smtp.gmail.com'
|
||||
@ -37,86 +36,85 @@ class Email {
|
||||
* @param {boolean} starttls (optional) force STARTTLS to prevent downgrade attack. Defaults to true.
|
||||
* @param {boolean} pgp (optional) if outgoing emails are encrypted to the user's public key.
|
||||
*/
|
||||
init(options) {
|
||||
this._transport = nodemailer.createTransport({
|
||||
host: options.host,
|
||||
port: options.port || 465,
|
||||
auth: options.auth,
|
||||
secure: (options.tls !== undefined) ? util.isTrue(options.tls) : true,
|
||||
requireTLS: (options.starttls !== undefined) ? util.isTrue(options.starttls) : true,
|
||||
init({host, port = 465, auth, tls, starttls, pgp, sender}) {
|
||||
this._transporter = nodemailer.createTransport({
|
||||
host,
|
||||
port,
|
||||
auth,
|
||||
secure: (tls !== undefined) ? util.isTrue(tls) : true,
|
||||
requireTLS: (starttls !== undefined) ? util.isTrue(starttls) : true,
|
||||
});
|
||||
if (util.isTrue(options.pgp)) {
|
||||
this._transport.use('stream', openpgpEncrypt());
|
||||
}
|
||||
this._sender = options.sender;
|
||||
this._usePGPEncryption = util.isTrue(pgp);
|
||||
this._sender = sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the verification email to the user using a template.
|
||||
* @param {Object} template the email template to use
|
||||
* @param {Object} userId user id document
|
||||
* @param {Object} template the email template function to use
|
||||
* @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' }
|
||||
* @param {string} keyId key id of public key
|
||||
* @param {Object} origin origin of the server
|
||||
* @yield {Object} send response from the SMTP server
|
||||
* @yield {Object} reponse object containing SMTP info
|
||||
*/
|
||||
*send(options) {
|
||||
let template = options.template, userId = options.userId, keyId = options.keyId, origin = options.origin;
|
||||
let message = {
|
||||
from: this._sender,
|
||||
to: userId,
|
||||
subject: template.subject,
|
||||
text: template.text,
|
||||
html: template.html,
|
||||
params: {
|
||||
name: userId.name,
|
||||
baseUrl: util.url(origin),
|
||||
keyId: keyId,
|
||||
nonce: userId.nonce
|
||||
}
|
||||
async send({template, userId, keyId, origin, publicKeyArmored}) {
|
||||
const compiled = template({
|
||||
...userId,
|
||||
origin,
|
||||
keyId
|
||||
});
|
||||
if (this._usePGPEncryption && publicKeyArmored) {
|
||||
compiled.text = await this._pgpEncrypt(compiled.text, publicKeyArmored);
|
||||
}
|
||||
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 yield this._sendHelper(message);
|
||||
return this._sendHelper(sendOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @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
|
||||
* @param {Object} sendoptions object: { from: ..., to: ..., subject: ..., text: ... }
|
||||
* @yield {Object} reponse object containing SMTP info
|
||||
*/
|
||||
*_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 || {};
|
||||
|
||||
async _sendHelper(sendOptions) {
|
||||
try {
|
||||
let sendFn = this._transport.templateSender(template, sender);
|
||||
let info = yield sendFn(recipient, params);
|
||||
const info = await this._transporter.sendMail(sendOptions);
|
||||
if (!this._checkResponse(info)) {
|
||||
log.warn('email', 'Message may not have been received.', info);
|
||||
}
|
||||
return info;
|
||||
} catch(error) {
|
||||
log.error('email', 'Sending message failed.', error, options);
|
||||
} catch (error) {
|
||||
log.error('email', 'Sending message failed.', error);
|
||||
util.throw(500, 'Sending email to user failed');
|
||||
}
|
||||
}
|
||||
@ -130,7 +128,6 @@ class Email {
|
||||
_checkResponse(info) {
|
||||
return /^2/.test(info.response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = Email;
|
||||
module.exports = Email;
|
||||
|
21
src/email/templates.js
Normal file
21
src/email/templates.js
Normal file
@ -0,0 +1,21 @@
|
||||
'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};
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"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
Normal file
33
src/index.js
Normal file
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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,7 +25,6 @@ const util = require('../service/util');
|
||||
* See https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
|
||||
*/
|
||||
class HKP {
|
||||
|
||||
/**
|
||||
* Create an instance of the HKP server
|
||||
* @param {Object} publicKey An instance of the public key service
|
||||
@ -38,14 +37,13 @@ class HKP {
|
||||
* Public key upload via http POST
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*add(ctx) {
|
||||
let body = yield parse.form(ctx, { limit: '1mb' });
|
||||
let publicKeyArmored = body.keytext;
|
||||
async add(ctx) {
|
||||
const {keytext: publicKeyArmored} = await parse.form(ctx, {limit: '1mb'});
|
||||
if (!publicKeyArmored) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
let origin = util.origin(ctx);
|
||||
yield this._publicKey.put({ publicKeyArmored, origin });
|
||||
const origin = util.origin(ctx);
|
||||
await this._publicKey.put({publicKeyArmored, origin}, ctx);
|
||||
ctx.body = 'Upload successful. Check your inbox to verify your email address.';
|
||||
ctx.status = 201;
|
||||
}
|
||||
@ -54,11 +52,11 @@ class HKP {
|
||||
* Public key lookup via http GET
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*lookup(ctx) {
|
||||
let params = this.parseQueryString(ctx);
|
||||
let key = yield this._publicKey.get(params);
|
||||
async lookup(ctx) {
|
||||
const params = this.parseQueryString(ctx);
|
||||
const key = await this._publicKey.get(params, ctx);
|
||||
this.setGetHeaders(ctx, params);
|
||||
this.setGetBody(ctx, params, key);
|
||||
await this.setGetBody(ctx, params, key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,19 +66,19 @@ class HKP {
|
||||
* @return {Object} The query parameters or undefined for an invalid request
|
||||
*/
|
||||
parseQueryString(ctx) {
|
||||
let params = {
|
||||
const params = {
|
||||
op: ctx.query.op, // operation ... only 'get' is supported
|
||||
mr: ctx.query.options === 'mr' // machine readable
|
||||
};
|
||||
if (this.checkId(ctx.query.search)) {
|
||||
let id = ctx.query.search.replace(/^0x/, '');
|
||||
const id = ctx.query.search.replace(/^0x/, '');
|
||||
params.keyId = util.isKeyId(id) ? id : undefined;
|
||||
params.fingerprint = util.isFingerPrint(id) ? id : undefined;
|
||||
} else if (util.isEmail(ctx.query.search)) {
|
||||
params.email = ctx.query.search;
|
||||
}
|
||||
|
||||
if (['get','index','vindex'].indexOf(params.op) === -1) {
|
||||
if (['get', 'index', 'vindex'].indexOf(params.op) === -1) {
|
||||
ctx.throw(501, 'Not implemented!');
|
||||
} else if (!params.keyId && !params.fingerprint && !params.email) {
|
||||
ctx.throw(501, 'Not implemented!');
|
||||
@ -121,24 +119,27 @@ class HKP {
|
||||
* @param {Object} params The parsed query string parameters
|
||||
* @param {Object} key The public key document
|
||||
*/
|
||||
setGetBody(ctx, params, key) {
|
||||
async setGetBody(ctx, params, key) {
|
||||
if (params.op === 'get') {
|
||||
ctx.body = key.publicKeyArmored;
|
||||
} else if (['index','vindex'].indexOf(params.op) !== -1) {
|
||||
const VERSION = 1, COUNT = 1; // number of keys
|
||||
let fp = key.fingerprint.toUpperCase();
|
||||
let algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
|
||||
let created = key.created ? (key.created.getTime() / 1000) : '';
|
||||
if (params.mr) {
|
||||
ctx.body = key.publicKeyArmored;
|
||||
} else {
|
||||
await ctx.render('key-armored', {query: params, key});
|
||||
}
|
||||
} else if (['index', 'vindex'].indexOf(params.op) !== -1) {
|
||||
const VERSION = 1;
|
||||
const COUNT = 1; // number of keys
|
||||
const fp = key.fingerprint.toUpperCase();
|
||||
const algo = (key.algorithm.indexOf('rsa') !== -1) ? 1 : '';
|
||||
const created = key.created ? (key.created.getTime() / 1000) : '';
|
||||
|
||||
ctx.body = 'info:' + VERSION + ':' + COUNT + '\n' +
|
||||
'pub:' + fp + ':' + algo + ':' + key.keySize + ':' + created + '::\n';
|
||||
ctx.body = `info:${VERSION}:${COUNT}\npub:${fp}:${algo}:${key.keySize}:${created}::\n`;
|
||||
|
||||
for (let uid of key.userIds) {
|
||||
ctx.body += 'uid:' + encodeURIComponent(uid.name + ' <' + uid.email + '>') + ':::\n';
|
||||
for (const uid of key.userIds) {
|
||||
ctx.body += `uid:${encodeURIComponent(`${uid.name} <${uid.email}>`)}:::\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = HKP;
|
||||
module.exports = HKP;
|
||||
|
@ -24,7 +24,6 @@ const util = require('../service/util');
|
||||
* The REST api to provide additional functionality on top of HKP
|
||||
*/
|
||||
class REST {
|
||||
|
||||
/**
|
||||
* Create an instance of the REST server
|
||||
* @param {Object} publicKey An instance of the public key service
|
||||
@ -35,79 +34,63 @@ class REST {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public key upload via http POST
|
||||
* Public key / user ID upload via http POST
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*create(ctx) {
|
||||
let q = yield parse.json(ctx, { limit: '1mb' });
|
||||
let publicKeyArmored = q.publicKeyArmored, primaryEmail = q.primaryEmail;
|
||||
if (!publicKeyArmored || (primaryEmail && !util.isEmail(primaryEmail))) {
|
||||
async create(ctx) {
|
||||
const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'});
|
||||
if (!publicKeyArmored) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
let origin = util.origin(ctx);
|
||||
yield this._publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
const origin = util.origin(ctx);
|
||||
await this._publicKey.put({emails, publicKeyArmored, origin}, ctx);
|
||||
ctx.body = 'Upload successful. Check your inbox to verify your email address.';
|
||||
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
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*verify(ctx) {
|
||||
let q = { keyId:ctx.query.keyId, nonce:ctx.query.nonce };
|
||||
async verify(ctx) {
|
||||
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce};
|
||||
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
yield this._publicKey.verify(q);
|
||||
const {email} = await this._publicKey.verify(q);
|
||||
// create link for sharing
|
||||
let link = util.url(util.origin(ctx), '/user/' + q.keyId.toUpperCase());
|
||||
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;
|
||||
const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`);
|
||||
await ctx.render('verify-success', {email, link});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request public key removal via http DELETE
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*remove(ctx) {
|
||||
let q = { keyId:ctx.query.keyId, email:ctx.query.email, origin:util.origin(ctx) };
|
||||
async remove(ctx) {
|
||||
const q = {keyId: ctx.query.keyId, email: ctx.query.email, origin: util.origin(ctx)};
|
||||
if (!util.isKeyId(q.keyId) && !util.isEmail(q.email)) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
yield this._publicKey.requestRemove(q);
|
||||
ctx.body = 'Check your inbox to verify the removal of your key.';
|
||||
await this._publicKey.requestRemove(q, ctx);
|
||||
ctx.body = 'Check your inbox to verify the removal of your email address.';
|
||||
ctx.status = 202;
|
||||
}
|
||||
|
||||
@ -115,15 +98,14 @@ class REST {
|
||||
* Verify public key removal via http GET
|
||||
* @param {Object} ctx The koa request/response context
|
||||
*/
|
||||
*verifyRemove(ctx) {
|
||||
let q = { keyId:ctx.query.keyId, nonce:ctx.query.nonce };
|
||||
async verifyRemove(ctx) {
|
||||
const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce};
|
||||
if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) {
|
||||
ctx.throw(400, 'Invalid request!');
|
||||
}
|
||||
yield this._publicKey.verifyRemove(q);
|
||||
ctx.body = 'Key successfully removed!';
|
||||
const {email} = await this._publicKey.verifyRemove(q);
|
||||
await ctx.render('removal-success', {email});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = REST;
|
||||
module.exports = REST;
|
||||
|
@ -17,10 +17,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const log = require('npmlog');
|
||||
const log = require('winston');
|
||||
const util = require('./util');
|
||||
const openpgp = require('openpgp');
|
||||
const addressparser = require('addressparser');
|
||||
|
||||
const KEY_BEGIN = '-----BEGIN PGP PUBLIC KEY BLOCK-----';
|
||||
const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
|
||||
@ -29,18 +28,22 @@ const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
|
||||
* A simple wrapper around OpenPGP.js
|
||||
*/
|
||||
class PGP {
|
||||
constructor() {
|
||||
openpgp.config.show_version = false;
|
||||
openpgp.config.show_comment = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an ascii armored pgp key block and get its parameters.
|
||||
* @param {String} publicKeyArmored ascii armored pgp key block
|
||||
* @return {Object} public key document to persist
|
||||
*/
|
||||
parseKey(publicKeyArmored) {
|
||||
async parseKey(publicKeyArmored) {
|
||||
publicKeyArmored = this.trimKey(publicKeyArmored);
|
||||
|
||||
let r = openpgp.key.readArmored(publicKeyArmored);
|
||||
const r = await openpgp.key.readArmored(publicKeyArmored);
|
||||
if (r.err) {
|
||||
let error = r.err[0];
|
||||
const error = r.err[0];
|
||||
log.error('pgp', 'Failed to parse PGP key:\n%s', publicKeyArmored, error);
|
||||
util.throw(500, 'Failed to parse PGP key');
|
||||
} else if (!r.keys || r.keys.length !== 1 || !r.keys[0].primaryKey) {
|
||||
@ -48,33 +51,39 @@ class PGP {
|
||||
}
|
||||
|
||||
// verify primary key
|
||||
let key = r.keys[0];
|
||||
let primaryKey = key.primaryKey;
|
||||
if (key.verifyPrimaryKey() !== openpgp.enums.keyStatus.valid) {
|
||||
const key = r.keys[0];
|
||||
const primaryKey = key.primaryKey;
|
||||
const now = new Date();
|
||||
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');
|
||||
}
|
||||
|
||||
// accept version 4 keys only
|
||||
let keyId = primaryKey.getKeyId().toHex();
|
||||
let fingerprint = primaryKey.fingerprint;
|
||||
const keyId = primaryKey.getKeyId().toHex();
|
||||
const fingerprint = primaryKey.getFingerprint();
|
||||
if (!util.isKeyId(keyId) || !util.isFingerPrint(fingerprint)) {
|
||||
util.throw(400, 'Invalid PGP key: only v4 keys are accepted');
|
||||
}
|
||||
|
||||
// check for at least one valid user id
|
||||
let userIds = this.parseUserIds(key.users, primaryKey);
|
||||
const userIds = await this.parseUserIds(key.users, primaryKey, verifyDate);
|
||||
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
|
||||
return {
|
||||
keyId,
|
||||
fingerprint,
|
||||
userIds,
|
||||
created: primaryKey.created,
|
||||
algorithm: primaryKey.algorithm,
|
||||
keySize: primaryKey.getBitSize(),
|
||||
uploaded: new Date(),
|
||||
algorithm: keyInfo.algorithm,
|
||||
keySize: keyInfo.bits,
|
||||
publicKeyArmored
|
||||
};
|
||||
}
|
||||
@ -108,35 +117,81 @@ class PGP {
|
||||
/**
|
||||
* Parse an array of user ids and verify signatures
|
||||
* @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
|
||||
*/
|
||||
parseUserIds(users, primaryKey) {
|
||||
async parseUserIds(users, primaryKey, verifyDate = new Date()) {
|
||||
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
|
||||
result.push({
|
||||
status: userStatus,
|
||||
name: uid.name,
|
||||
email: util.normalizeEmail(uid.email),
|
||||
verified: false
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
// map to local user id object format
|
||||
return result.map(uid => ({
|
||||
name: uid.name,
|
||||
email: uid.address.toLowerCase(),
|
||||
verified: false
|
||||
}));
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PGP;
|
||||
module.exports = PGP;
|
||||
|
@ -17,8 +17,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const util = require('./util');
|
||||
const tpl = require('../email/templates.json');
|
||||
const tpl = require('../email/templates');
|
||||
|
||||
/**
|
||||
* Database documents have the format:
|
||||
@ -35,18 +36,19 @@ const tpl = require('../email/templates.json');
|
||||
* }
|
||||
* ],
|
||||
* 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
|
||||
* keySize: 4096, // key length in bits
|
||||
* publicKeyArmored: '-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----'
|
||||
* }
|
||||
*/
|
||||
const DB_TYPE = 'publickey';
|
||||
const KEY_STATUS_VALID = 3;
|
||||
|
||||
/**
|
||||
* A service that handlers PGP public keys queries to the database
|
||||
*/
|
||||
class PublicKey {
|
||||
|
||||
/**
|
||||
* Create an instance of the service
|
||||
* @param {Object} pgp An instance of the OpenPGP.js wrapper
|
||||
@ -61,64 +63,138 @@ class PublicKey {
|
||||
|
||||
/**
|
||||
* 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} 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' }
|
||||
* @yield {undefined}
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise}
|
||||
*/
|
||||
*put(options) {
|
||||
async put({emails = [], publicKeyArmored, origin}, ctx) {
|
||||
emails = emails.map(util.normalizeEmail);
|
||||
// lazily purge old/unverified keys on every key upload
|
||||
await this._purgeOldUnverified();
|
||||
// parse key block
|
||||
let publicKeyArmored = options.publicKeyArmored, primaryEmail = options.primaryEmail, origin = options.origin;
|
||||
let key = this._pgp.parseKey(publicKeyArmored);
|
||||
// check for existing verfied key by id or email addresses
|
||||
let verified = yield this.getVerified(key);
|
||||
if (verified) {
|
||||
util.throw(304, 'Key for this user already exists');
|
||||
const key = await this._pgp.parseKey(publicKeyArmored);
|
||||
// if emails array is empty, all userIds of the key will be submitted
|
||||
if (emails.length) {
|
||||
// keep submitted user IDs only
|
||||
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) {
|
||||
key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored);
|
||||
// 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
|
||||
yield this._persisKey(key);
|
||||
// send mails to verify user ids (send only one if primary email is provided)
|
||||
yield this._sendVerifyEmail(key, primaryEmail, origin);
|
||||
await this._persistKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist the public key and its user ids in the database.
|
||||
* @param {Object} key public key parameters
|
||||
* @yield {undefined} The persisted user id documents
|
||||
* Delete all keys where no user id has been verified after x days.
|
||||
* @return {Promise}
|
||||
*/
|
||||
*_persisKey(key) {
|
||||
// delete old/unverified key
|
||||
yield this._mongo.remove({ fingerprint:key.fingerprint }, DB_TYPE);
|
||||
// generate nonces for verification
|
||||
for (let uid of key.userIds) {
|
||||
uid.nonce = util.random();
|
||||
}
|
||||
// persist new key
|
||||
let r = yield this._mongo.create(key, DB_TYPE);
|
||||
if (r.insertedCount !== 1) {
|
||||
util.throw(500, 'Failed to persist key');
|
||||
async _purgeOldUnverified() {
|
||||
// create date in the past to compare with
|
||||
const xDaysAgo = new Date();
|
||||
xDaysAgo.setDate(xDaysAgo.getDate() - config.publicKey.purgeTimeInDays);
|
||||
// remove unverified keys older than x days (or no 'uploaded' attribute)
|
||||
return this._mongo.remove({
|
||||
'userIds.verified': {$ne: true},
|
||||
uploaded: {$lt: xDaysAgo}
|
||||
}, DB_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge existing and new user IDs
|
||||
* @param {Array} existingUsers source user IDs
|
||||
* @param {Array} newUsers new user IDs
|
||||
* @param {String} publicKeyArmored armored key block of new user IDs
|
||||
* @return {Array} merged user IDs
|
||||
*/
|
||||
async _mergeUsers(existingUsers, newUsers, publicKeyArmored) {
|
||||
const result = [];
|
||||
// existing verified valid or revoked users
|
||||
const verifiedUsers = existingUsers.filter(userId => userId.verified);
|
||||
// valid new users which are not yet verified
|
||||
const validUsers = newUsers.filter(userId => userId.status === KEY_STATUS_VALID && !this._includeEmail(verifiedUsers, userId));
|
||||
// pending users are not verified, not newly submitted
|
||||
const pendingUsers = existingUsers.filter(userId => !userId.verified && !this._includeEmail(validUsers, userId));
|
||||
await this._addKeyArmored(validUsers, publicKeyArmored);
|
||||
result.push(...validUsers, ...pendingUsers, ...verifiedUsers);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create amored key block which contains the corresponding user ID only and add it to the user ID object
|
||||
* @param {Array} userIds user IDs to be extended
|
||||
* @param {String} PublicKeyArmored armored key block to be filtered
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _addKeyArmored(userIds, publicKeyArmored) {
|
||||
for (const userId of userIds) {
|
||||
userId.publicKeyArmored = await this._pgp.filterKeyByUserIds([userId], publicKeyArmored);
|
||||
userId.notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
_includeEmail(users, user) {
|
||||
return users.find(({email}) => email === user.email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send verification emails to the public keys user ids for verification.
|
||||
* If a primary email address is provided only one email will be sent.
|
||||
* @param {Array} userIds user id documents containg the verification nonces
|
||||
* @param {string} primaryEmail the public key's primary email address
|
||||
* @param {Object} origin the server's origin (required for email links)
|
||||
* @yield {undefined}
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise}
|
||||
*/
|
||||
*_sendVerifyEmail(key, primaryEmail, origin) {
|
||||
let userIds = key.userIds, keyId = key.keyId;
|
||||
// check for primary email (send only one email)
|
||||
let primaryUserId = userIds.find(uid => uid.email === primaryEmail);
|
||||
if (primaryUserId) {
|
||||
userIds = [primaryUserId];
|
||||
async _sendVerifyEmail({userIds, keyId}, origin, ctx) {
|
||||
for (const userId of userIds) {
|
||||
if (userId.notify && userId.notify === true) {
|
||||
// generate nonce for verification
|
||||
userId.nonce = util.random();
|
||||
await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored});
|
||||
}
|
||||
}
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,20 +202,42 @@ class PublicKey {
|
||||
* Verify a user id by proving knowledge of the nonce.
|
||||
* @param {string} keyId Correspronding public key id
|
||||
* @param {string} nonce The verification nonce proving email address ownership
|
||||
* @yield {undefined}
|
||||
* @return {Promise} The email that has been verified
|
||||
*/
|
||||
*verify(options) {
|
||||
let keyId = options.keyId, nonce = options.nonce;
|
||||
async verify({keyId, nonce}) {
|
||||
// look for verification nonce in database
|
||||
let query = { keyId, 'userIds.nonce':nonce };
|
||||
let key = yield this._mongo.get(query, DB_TYPE);
|
||||
const query = {keyId, 'userIds.nonce': nonce};
|
||||
const key = await this._mongo.get(query, DB_TYPE);
|
||||
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
|
||||
yield this._mongo.update(query, {
|
||||
await this._mongo.update(query, {
|
||||
publicKeyArmored,
|
||||
'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);
|
||||
}
|
||||
|
||||
@ -150,10 +248,9 @@ class PublicKey {
|
||||
* @param {Array} userIds A list of user ids to check
|
||||
* @param {string} fingerprint The public key fingerprint
|
||||
* @param {string} keyId (optional) The public key id
|
||||
* @yield {Object} The verified key document
|
||||
* @return {Object} The verified key document
|
||||
*/
|
||||
*getVerified(options) {
|
||||
let fingerprint = options.fingerprint, userIds = options.userIds, keyId = options.keyId;
|
||||
async getVerified({userIds, fingerprint, keyId}) {
|
||||
let queries = [];
|
||||
// query by fingerprint
|
||||
if (fingerprint) {
|
||||
@ -174,13 +271,13 @@ class PublicKey {
|
||||
queries = queries.concat(userIds.map(uid => ({
|
||||
userIds: {
|
||||
$elemMatch: {
|
||||
'email': uid.email.toLowerCase(),
|
||||
'email': util.normalizeEmail(uid.email),
|
||||
'verified': true
|
||||
}
|
||||
}
|
||||
})));
|
||||
}
|
||||
return yield this._mongo.get({ $or:queries }, DB_TYPE);
|
||||
return this._mongo.get({$or: queries}, DB_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -189,15 +286,15 @@ class PublicKey {
|
||||
* @param {string} fingerprint (optional) The public key fingerprint
|
||||
* @param {string} keyId (optional) The public key id
|
||||
* @param {String} email (optional) The user's email address
|
||||
* @yield {Object} The public key document
|
||||
* @param {Object} ctx Context
|
||||
* @return {Object} The public key document
|
||||
*/
|
||||
*get(options) {
|
||||
let fingerprint = options.fingerprint, keyId = options.keyId, email = options.email;
|
||||
async get({fingerprint, keyId, email}, ctx) {
|
||||
// look for verified key
|
||||
let userIds = email ? [{ email:email }] : undefined;
|
||||
let key = yield this.getVerified({ keyId, fingerprint, userIds });
|
||||
const userIds = email ? [{email}] : undefined;
|
||||
const key = await this.getVerified({keyId, fingerprint, userIds});
|
||||
if (!key) {
|
||||
util.throw(404, 'Key not found');
|
||||
util.throw(404, ctx.__('key_not_found'));
|
||||
}
|
||||
// clean json return value (_id, nonce)
|
||||
delete key._id;
|
||||
@ -217,19 +314,19 @@ class PublicKey {
|
||||
* @param {String} keyId (optional) The public key id
|
||||
* @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' }
|
||||
* @yield {undefined}
|
||||
* @param {Object} ctx Context
|
||||
* @return {Promise}
|
||||
*/
|
||||
*requestRemove(options) {
|
||||
let keyId = options.keyId, email = options.email, origin = options.origin;
|
||||
async requestRemove({keyId, email, origin}, ctx) {
|
||||
// flag user ids for removal
|
||||
let key = yield this._flagForRemove(keyId, email);
|
||||
const key = await this._flagForRemove(keyId, email);
|
||||
if (!key) {
|
||||
util.throw(404, 'User id not found');
|
||||
util.throw(404, 'User ID not found');
|
||||
}
|
||||
// send verification mails
|
||||
keyId = key.keyId; // get keyId in case request was by email
|
||||
for (let userId of key.userIds) {
|
||||
yield this._email.send({ template:tpl.verifyRemove, userId, keyId, origin });
|
||||
for (const userId of key.userIds) {
|
||||
await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin});
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,27 +335,28 @@ class PublicKey {
|
||||
* saving it. Either a key id or email address must be provided
|
||||
* @param {String} keyId (optional) The public key id
|
||||
* @param {String} email (optional) The user's email address
|
||||
* @yield {Array} A list of user ids with nonces
|
||||
* @return {Array} A list of user ids with nonces
|
||||
*/
|
||||
*_flagForRemove(keyId, email) {
|
||||
let query = email ? { 'userIds.email':email } : { keyId };
|
||||
let key = yield this._mongo.get(query, DB_TYPE);
|
||||
async _flagForRemove(keyId, email) {
|
||||
email = util.normalizeEmail(email);
|
||||
const query = email ? {'userIds.email': email} : {keyId};
|
||||
const key = await this._mongo.get(query, DB_TYPE);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
// flag only the provided user id
|
||||
if (email) {
|
||||
let nonce = util.random();
|
||||
yield this._mongo.update(query, { 'userIds.$.nonce':nonce }, DB_TYPE);
|
||||
let uid = key.userIds.find(u => u.email === email);
|
||||
const nonce = util.random();
|
||||
await this._mongo.update(query, {'userIds.$.nonce': nonce}, DB_TYPE);
|
||||
const uid = key.userIds.find(u => u.email === email);
|
||||
uid.nonce = nonce;
|
||||
return { userIds:[uid], keyId:key.keyId };
|
||||
return {userIds: [uid], keyId: key.keyId};
|
||||
}
|
||||
// flag all key user ids
|
||||
if (keyId) {
|
||||
for (let uid of key.userIds) {
|
||||
let nonce = util.random();
|
||||
yield this._mongo.update({ 'userIds.email':uid.email }, { 'userIds.$.nonce':nonce }, DB_TYPE);
|
||||
for (const uid of key.userIds) {
|
||||
const nonce = util.random();
|
||||
await this._mongo.update({'userIds.email': uid.email}, {'userIds.$.nonce': nonce}, DB_TYPE);
|
||||
uid.nonce = nonce;
|
||||
}
|
||||
return key;
|
||||
@ -270,19 +368,33 @@ class PublicKey {
|
||||
* Also deletes all user id documents of that key id.
|
||||
* @param {string} keyId public key id
|
||||
* @param {string} nonce The verification nonce proving email address ownership
|
||||
* @yield {undefined}
|
||||
* @return {Promise}
|
||||
*/
|
||||
*verifyRemove(options) {
|
||||
let keyId = options.keyId, nonce = options.nonce;
|
||||
async verifyRemove({keyId, nonce}) {
|
||||
// check if key exists in database
|
||||
let flagged = yield this._mongo.get({ keyId, 'userIds.nonce':nonce }, DB_TYPE);
|
||||
const flagged = await this._mongo.get({keyId, 'userIds.nonce': nonce}, DB_TYPE);
|
||||
if (!flagged) {
|
||||
util.throw(404, 'User id not found');
|
||||
util.throw(404, 'User ID not found');
|
||||
}
|
||||
// delete the key
|
||||
yield this._mongo.remove({ keyId }, DB_TYPE);
|
||||
if (flagged.userIds.length === 1) {
|
||||
// delete the key
|
||||
await this._mongo.remove({keyId}, DB_TYPE);
|
||||
return flagged.userIds[0];
|
||||
}
|
||||
// update the key
|
||||
const rmIdx = flagged.userIds.findIndex(userId => userId.nonce === nonce);
|
||||
const rmUserId = flagged.userIds[rmIdx];
|
||||
if (rmUserId.verified) {
|
||||
if (flagged.userIds.filter(({verified}) => verified).length > 1) {
|
||||
flagged.publicKeyArmored = await this._pgp.removeUserId(rmUserId.email, flagged.publicKeyArmored);
|
||||
} else {
|
||||
flagged.publicKeyArmored = null;
|
||||
}
|
||||
}
|
||||
flagged.userIds.splice(rmIdx, 1);
|
||||
await this._mongo.update({keyId}, flagged, DB_TYPE);
|
||||
return rmUserId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = PublicKey;
|
||||
module.exports = PublicKey;
|
||||
|
@ -25,7 +25,7 @@ const crypto = require('crypto');
|
||||
* @return {boolean} If data is a string
|
||||
*/
|
||||
exports.isString = function(data) {
|
||||
return typeof data === 'string' || String.prototype.isPrototypeOf(data);
|
||||
return typeof data === 'string' || String.prototype.isPrototypeOf(data); // eslint-disable-line no-prototype-builtins
|
||||
};
|
||||
|
||||
/**
|
||||
@ -37,7 +37,7 @@ exports.isTrue = function(data) {
|
||||
if (this.isString(data)) {
|
||||
return data === 'true';
|
||||
} else {
|
||||
return !!data;
|
||||
return Boolean(data);
|
||||
}
|
||||
};
|
||||
|
||||
@ -78,6 +78,18 @@ exports.isEmail = function(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.
|
||||
* @param {number} status The error's http status code
|
||||
@ -85,7 +97,7 @@ exports.isEmail = function(data) {
|
||||
* @return {Error} The resulting error object
|
||||
*/
|
||||
exports.throw = function(status, message) {
|
||||
let err = new Error(message);
|
||||
const err = new Error(message);
|
||||
err.status = status;
|
||||
err.expose = true; // display message to the client
|
||||
throw err;
|
||||
@ -143,7 +155,7 @@ exports.origin = function(ctx) {
|
||||
* @return {string} The complete url
|
||||
*/
|
||||
exports.url = function(origin, resource) {
|
||||
return origin.protocol + '://' + origin.host + (resource || '');
|
||||
return `${origin.protocol}://${origin.host}${resource || ''}`;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -154,4 +166,4 @@ exports.url = function(origin, resource) {
|
||||
*/
|
||||
exports.hkpUrl = function(ctx) {
|
||||
return (this.checkHTTPS(ctx) ? 'hkps://' : 'hkp://') + ctx.host;
|
||||
};
|
||||
};
|
||||
|
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,6 +30,21 @@ body {
|
||||
color: #777;
|
||||
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 */
|
||||
@media (min-width: 768px) {
|
||||
|
@ -1,80 +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="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>
|
@ -1,67 +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="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
Normal file
2
src/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
53
src/static/js/manage.js
Normal file
53
src/static/js/manage.js
Normal file
@ -0,0 +1,53 @@
|
||||
/* 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));
|
9
src/view/footer.html
Normal file
9
src/view/footer.html
Normal file
@ -0,0 +1,9 @@
|
||||
<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>
|
46
src/view/index.html
Normal file
46
src/view/index.html
Normal file
@ -0,0 +1,46 @@
|
||||
<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 -->
|
23
src/view/key-armored.html
Normal file
23
src/view/key-armored.html
Normal file
@ -0,0 +1,23 @@
|
||||
<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 -->
|
15
src/view/layout.html
Normal file
15
src/view/layout.html
Normal file
@ -0,0 +1,15 @@
|
||||
<!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>
|
80
src/view/manage.html
Normal file
80
src/view/manage.html
Normal file
@ -0,0 +1,80 @@
|
||||
<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>
|
15
src/view/removal-success.html
Normal file
15
src/view/removal-success.html
Normal file
@ -0,0 +1,15 @@
|
||||
<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 -->
|
16
src/view/verify-success.html
Normal file
16
src/view/verify-success.html
Normal file
@ -0,0 +1,16 @@
|
||||
<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 -->
|
16
test/.eslintrc.json
Normal file
16
test/.eslintrc.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "../.eslintrc.json",
|
||||
|
||||
"rules": {
|
||||
"no-shadow": 1
|
||||
},
|
||||
|
||||
"globals": {
|
||||
"expect": true,
|
||||
"sinon": true
|
||||
},
|
||||
|
||||
"env": {
|
||||
"mocha": true
|
||||
}
|
||||
}
|
7
test/.mocharc.js
Normal file
7
test/.mocharc.js
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
recursive: true,
|
||||
require: ['./test/setup.js']
|
||||
}
|
0
test/key1.asc → test/fixtures/key1.asc
vendored
0
test/key1.asc → test/fixtures/key1.asc
vendored
0
test/key2.asc → test/fixtures/key2.asc
vendored
0
test/key2.asc → test/fixtures/key2.asc
vendored
261
test/fixtures/key3.asc
vendored
Normal file
261
test/fixtures/key3.asc
vendored
Normal file
@ -0,0 +1,261 @@
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=0IPf
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBFdZY4oBEADHN/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
|
||||
tB1UZXN0IFVzZXIgPHRlc3QxQGV4YW1wbGUuY29tPokCWQQTAQoAQwIbAwcLCQgH
|
||||
AwIBBhUIAgkKCwQWAgMBAh4BAheAAhkBFiEEBAYscLRG4zAW4hmnQAGhJ6kN6OEF
|
||||
AmCWtCYFCRoz/ZEACgkQQAGhJ6kN6OG1oRAAhOPptjI6sghqkApBglBLTpv/xwfN
|
||||
g21CeZq2AMitXYCkpbQQzOpmGn49YhBnF83RM233LPXUzfTULtRA4ShknoNT0WNv
|
||||
r9rYZdFqPDlDiMn+fTuEAMRyvMzzz0o0nl37YaFC4F15k0oQQ5NLRUzAwWuRn2UH
|
||||
E4pai5jIRMwMd1LlTt/YHSmYzZ8lPRSmjua25rjgBZLFh5lq7mrnZG6McOgXJK4z
|
||||
ALfToWLqBbdpiRF+FumbePxKh54Eyv6Xg0I2TfWbBggF2Pqnw8zNxT/dItQAixka
|
||||
fCvNi5BDcReX1wSveE8oCd2TqRkR0H5IhgKcidV/cSF7Nybkc8sdIo/0D5qL9Lxc
|
||||
xawEH2p8GnJwK7qRJDtK6Y2bnFL9yQkdflorxOkEASBwUim9oeKh4kGlO31iOWg/
|
||||
RTat6SWgouTtWdFToz00f+VfGDH8a3oXYG33qTyLkJ9nlnS0VtUW1YINNsdg/l7m
|
||||
2vwYV2JWaeRih19RE3xuzrvQN4yp3FQJV2Ha6dvwk/Do2MJrB89ZXqtLf/wr3YtP
|
||||
4Ka6xIkbp9W4UvGrcTXKIAx464yQvq3aalwmdbfp2JAz74+41cbgdm5iDGd+fks7
|
||||
wW6VfocE1h3z8ReTcjmqwP3yOv3mlj1T+KNXYqDs/71mfDIc32SW62QV3iSsNv/Y
|
||||
Akbx+QWySbdcADiJAj8EEwEIACkFAldZY4oCGwMFCQeGH4AHCwkIBwMCAQYVCAIJ
|
||||
CgsEFgIDAQIeAQIXgAAKCRBAAaEnqQ3o4Y8eD/0SUKel5N0/Qowm9eVQ3Dsrckqo
|
||||
AHL6E+iVLzM8qvUm4hd1HuSTr386IvX7PrukZ9M5isMvxD3GKD+R93v4Ag5BNDiO
|
||||
dGPXDqZuY7brNSsiez2QYWyEELrNrlw4CV+lboMfi02DSHnhL58crkWId0Zn3DAs
|
||||
Z2xq4zgPdnMz0ryFjGCMmRzbMffYaMuT7Y3zdwfXK0nl1dV5uH5qEyeNBuobYaui
|
||||
1KY2WB5FObbfHWY9j2UQu1Gce2xM2hmTowHXZZc7gARlE6aT22X0YAzprjhE4Xfe
|
||||
tTkHU/mSgJeX3RZEbQFa66PT9pBj6b+BdZuuCK5E5ICSnK2gv6hwPv2zxZz/F/Uw
|
||||
BoXpIb1qeuTEyfk08ceMGILhUGvn0DmeGkD6hyltqBsORNBYne4CU+Ss5pDF/rvL
|
||||
+FdFgBkPvDY1Z6JsgCGn1ft8HXvR8A48prw9Ty/dJsXeBseNdvTAuAAE2BH9ongm
|
||||
spALRcu8G/CIMSdU4spAAbN9szq3gSU3YUWav48fRLY/99EhPITvqGafYWsAimWy
|
||||
PMEqI+CPL4C1HUQEO0jpJztfOhS6pxHU6Ap9MmICruXNrH8UyLCfkx4+JV8eY4lt
|
||||
3Jl/77b2D4JQUSeoFdNe4Tn4aFR4UP7l/FOa8DYzZ1Sp2+Pum1h3pjFGT2d106rg
|
||||
8oB/m8KljhmlK8SaM7QdVGVzdCBVc2VyIDx0ZXN0MkBleGFtcGxlLmNvbT6JAlYE
|
||||
EwEKAEACGwMHCwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgBYhBAQGLHC0RuMwFuIZ
|
||||
p0ABoSepDejhBQJglrQmBQkaM/2RAAoJEEABoSepDejheqcP/R6uhuoD0BOhXNgz
|
||||
CR/ULZ9pBAg9hkhNuuqV4gTHblwL4LFlWJbNNEhnuBfxZm21eYvtisn7I7rl3522
|
||||
KkiwVQqHoQblOpKM2Nt97s8/3jFd9Ds7dNMgasD543HpJArmhMVkUgAyMtQ83nfs
|
||||
WBxsFD24yYhuK1afOfLJ3sHBX/3rs9wKJRJ2hTU9uMtq5rXNLdf18eBZ/z+UqkJD
|
||||
gFqEIQWV4jYEuzpOvc+3/LPCHXZDX2E/RXUvD5T6XpY92Y9rkMWARxn5bxrsbbOc
|
||||
YPybAnY5QBvmRVawXuX51j+b1GTa+TTjUcKngFU2Olhb40mDrgb/1RH0Xd0fnm19
|
||||
54a6kgH2GusQQXTM9bpE7K3AQ5z8ccQ1Qm776nTWTtf8D7dCIZka8x5/F9/s8zcA
|
||||
cHYr8zvamenE1eRtl8+Ypho5/KoE2IbWt0DoHriVWeZF1FNHOzlv2JCBFdzF5SaR
|
||||
GAqHjog4dk3LJtL+ESF/Ttclv6rMRKveVfPe8HvsQMpK58PUWEfefhq1HmdtxqmY
|
||||
a2Y/IcDDFX/xuRezsOVhwTafnPMcKlUWRt2xaitspY0k91hEBIZOVdbu/FSVV27J
|
||||
5bMRdrIcOyORRKcRgzBCwYJsDN3Yun+Utb0v31xXCPKsbddPHAWvN2Vgv60xqnag
|
||||
Gfh6WOjCtZTZ6H+8tJM2dNmWe2MCtB1UZXN0IFVzZXIgPHRlc3QzQGV4YW1wbGUu
|
||||
Y29tPokCVgQTAQoAQAIbAwcLCQgHAwIBBhUIAgkKCwQWAgMBAh4BAheAFiEEBAYs
|
||||
cLRG4zAW4hmnQAGhJ6kN6OEFAmCWtCYFCRoz/ZEACgkQQAGhJ6kN6OFbdg/+IQck
|
||||
Y4fe2948INy39jN0oJned5Z7pNeu0tZ2gPlzg2Ez1aEUWIl+4k4YFYNZ1sz2+xj7
|
||||
ECwUSRHDtjDHUvm59lAmDgaextXvYuhqjwDCtXIsrpQpcqDhMxrbrvqoZpA5HZ89
|
||||
Pt6fR1QpTdqsIZacnodNU6rccyCUNMCmVUxWKr+9PKp4iNdHVDz2K/FZvMQgfQ9X
|
||||
cevjiPnZ/P0BiYngPJ1Q4p40xVOsFDAWWw+THX/bp2UhM6bnrFzcAHVYS2hHYyET
|
||||
z7rx2BYTMUdl4NnRykhcIT3uqe5SqI6uWq7nI8J4YmNrJ0E8MnzjzJOWuWE+NRB4
|
||||
XiXeTcMi5OJ6rJRDJP2sWIlwMA9BAiOMCDAQj6EKZWQr3ZIIKkX0B1W+zXbKKzET
|
||||
PpQAD+V5Nw//85PUyJe7TSub83Yzs+IrIaSS0G/HU/ofp4vyhzs+hODdLINN9xAQ
|
||||
m8ryly6rD8GG4aC9f7+qx8RAUnxgLdYFpqH3mKTbLnFthfaBajj0F43AFtfBOxJT
|
||||
RYM+mgEVBOJLZ8CqQTGNlHTvg+QYTIfaPspcM79HzfcSWSMPoz74EIWZMpcw6Ix6
|
||||
ec3rG1qiKdYBQjHH8myeno3ardobX3DrFvobJ5MFubI7LOQseUUQsBTMTvU2V179
|
||||
qlmAWndQOkOym79B2/mm8J/dr3hGbKxqzx3R+OS0NFRlc3QgVXNlciAoQ29tbWVu
|
||||
dCBhYm91dCBzdHVmZi4pIDx0ZXN0NEBleGFtcGxlLmNvbT6JAlYEEwEKAEACGwMH
|
||||
CwkIBwMCAQYVCAIJCgsEFgIDAQIeAQIXgBYhBAQGLHC0RuMwFuIZp0ABoSepDejh
|
||||
BQJglrQmBQkaM/2RAAoJEEABoSepDejhslUP/0HppC9K8zfmgj5Do6eRIXOXOUGD
|
||||
u2N9PD+lXVD2zDRCBcdsYX67gsXhvD1ew6jkO820TjV0L8BKc6mwekEbQZ8Ro25h
|
||||
0RsmRhN01OVgibhtJ1f2pdqB5IHhork5eEEn9UdpZDbgX/LrtFh+Gbxfp+2t6mxE
|
||||
FqXx+8U+sCNIiwwZi6MaTRba++/jhrqN4SrbSlH7Wy0qYtAUPdZqIjs5eN/JP+OY
|
||||
mrE2KrMkgRW88IjEWfH8LjT2PaNgCa9Ol+wo3MgArK/5KfOEn22Uy6ss3OOCqHez
|
||||
4JMl+sNUxSAdZsviIyujbc5hIoMB/KyLEbA3/d2Hlv8bIU4yhaRv+N807sMeqAWM
|
||||
zcOwXJcOaxi/WdzQaZ668M0JJmqWwgTfjcrgkDGDf65ZyTOO/rTJjnOveHGGKhHp
|
||||
BACAFkYHAhXhfNYfRII8RyWuCq3JoceZze+hZ+6EakKGWhYX3og9WqDtdXlm71k/
|
||||
jDq1ccyt5w1kHTKCu5vI/pPbct8mJb6954O10Xs3x7NVzP1B+1iegrWFEdxdkgH/
|
||||
PpSqSBdCW3kTSwhIBUZdiR1giCkkoPdUVsfTpvfdjzZMUDJi+NPJmQeOYxPHBaNg
|
||||
RTZEYb/0mX/667/tDrHPXrc3FKFgUG+8Zh9Qtk1LpGJw/HeWQAYmTKnbdy+tD221
|
||||
yqnzKQa06wJThfGkuQINBFdZY4oBEADMgfdHy9rlCbjKDr9CcO1cEKgJ5+mD+j2+
|
||||
zOIjNPTHL7NVPmqpY2nkbslwpoCSnMGkVF/cIOCO3jsus9D+k50K4CH1179Lh8qN
|
||||
XMSnWkYrmLcklq2sDa/oWHl/p0wJnS/RAEPbFFag8f7UVyzOIh80AW5ygILpPlgv
|
||||
WSHByzsmD7X9jJj+ikLoiYrh/QuYKPHiKIT4I0e+jbhmKAxo0p7ER+GSjJJ4qVad
|
||||
3vjMOHINiMKW07mmoyhvOYuqJ7+AYfndgrNKm56zh7EORuLkhACS/dLBWxCLHPWM
|
||||
AjhTSahutfo+ZXXHHTwWtsQOQqBImeKM0Jx8zbdMfUymhX+zxIL4Nq9bDkcV0F3h
|
||||
+W6J3iWYl9piyNVc/xC2kPYNn/XDULZcBPva/3gVkbmzeOPC4cCJTiM82VJSIAxc
|
||||
0KhnmprcYO0x4PaXk7gm8pPWVHnKOF14KfDWt18hi6L+vJUTObcLmIeQt6kfmDnI
|
||||
sc09URYjq/T1VxzYvbMV+/3yxk/4GAhfv417MQ0Z7xmYJhmymXMkWzwkV56p2Kzw
|
||||
ZmavkmKAlgI7tueKcMp4OOSyEykVNZQikYjz1ELblS4vDGa8QX2aM75Db1nJVMhL
|
||||
R8XpxTDD7w6mo7vdKUuaY8vRuI4K81mGSjW5+sAiarx5HKaxjvhRDNOYzJjzEDZP
|
||||
T4xvZOyCHQARAQABiQI8BBgBCgAmAhsMFiEEBAYscLRG4zAW4hmnQAGhJ6kN6OEF
|
||||
AmCWtFAFCRoz/b8ACgkQQAGhJ6kN6OGAbxAAhE2IHWcQqKvmZc6Q5ZGpYmsna9n+
|
||||
mFYXBMm1WEhlYFhC8+J1va9ACMBqvuiJ93mRqsK/NqR6SDi9hBNyu4jv+01Jcib0
|
||||
aalJJB91C5KzkJYHE8TXrcj4cBEQJf9NzSNCtpKg8trLWCGCci67vMuzHPI1WK6r
|
||||
7N1VW25L0seN3eCCZ64FEZOMgFsirP7BX91w5nau3v3o6FiPg9obV3QTuLkfk40Z
|
||||
9a82uJzRyuS+4TixGlhxx+V73EBOJ5DDkcDuSy3g2h8SsXS+NE137WOvHXIcNepP
|
||||
fIDI3gcer2+c2qXXTR+AVCPsI3jMfb7xVxRiS7yPpOsQeQB6XjRxg26LBRqQY+yA
|
||||
ANcQLPPsdWW/dwuRDB0+OV/jBGyf6KIpd+CObWO7XvjxFUPfyDlPLLlSbCFLSnx0
|
||||
XhXwnKCNA+PaCRcQPbR8UnkSrfVFOUv6gy47RJRk5BVQ2whwl6xSPa9LxRSh7ZIJ
|
||||
hHVtZEAhnSH0vpbsK+PPiQnjlI1ERWJYx6WEewOIOEkJNSJM7DdLFlngO/6/4vlu
|
||||
WV7oxD/VRXcVPNQR2F7Ep/x8gvQG1CcahHzec4XLZbgVJGP8DL9XXK0WnYM+cRea
|
||||
4zjljwrpf4k1N9fNqBhXtoa14nncIBJ+QtwETUIc68GUhgvydyWCTBYxakDO9f9C
|
||||
0llyb/EoHbuIzVg=
|
||||
=yqus
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
156
test/fixtures/key4.asc
vendored
Normal file
156
test/fixtures/key4.asc
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
-----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
Normal file
40
test/fixtures/key5.asc
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
-----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,59 +1,64 @@
|
||||
'use strict';
|
||||
|
||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||
|
||||
const request = require('supertest');
|
||||
const Mongo = require('../../src/dao/mongo');
|
||||
const nodemailer = require('nodemailer');
|
||||
const templates = require('../../src/email/templates');
|
||||
const config = require('config');
|
||||
const fs = require('fs');
|
||||
const expect = require('chai').expect;
|
||||
const sinon = require('sinon');
|
||||
const log = require('winston');
|
||||
|
||||
describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
this.timeout(20000);
|
||||
|
||||
let app, mongo,
|
||||
sendEmailStub, publicKeyArmored, emailParams;
|
||||
const sandbox = sinon.createSandbox();
|
||||
let app;
|
||||
let mongo;
|
||||
let sendEmailStub;
|
||||
let publicKeyArmored;
|
||||
let emailParams;
|
||||
|
||||
const DB_TYPE_PUB_KEY = 'publickey';
|
||||
const DB_TYPE_USER_ID = 'userid';
|
||||
const primaryEmail = 'safewithme.testuser@gmail.com';
|
||||
const fingerprint = '4277257930867231CE393FB8DBC0B3D92B1B86E9';
|
||||
|
||||
before(function *() {
|
||||
publicKeyArmored = fs.readFileSync(__dirname + '/../key1.asc', 'utf8');
|
||||
mongo = new Mongo();
|
||||
yield mongo.init(config.mongo);
|
||||
before(async () => {
|
||||
sandbox.stub(log);
|
||||
|
||||
sendEmailStub = sinon.stub().returns(Promise.resolve({ response:'250' }));
|
||||
sendEmailStub.withArgs(sinon.match(recipient => {
|
||||
return recipient.to.address === primaryEmail;
|
||||
}), sinon.match(params => {
|
||||
publicKeyArmored = fs.readFileSync(`${__dirname}/../fixtures/key1.asc`, 'utf8');
|
||||
mongo = new Mongo();
|
||||
await mongo.init(config.mongo);
|
||||
|
||||
const paramMatcher = sinon.match(params => {
|
||||
emailParams = params;
|
||||
return !!params.nonce;
|
||||
}));
|
||||
sinon.stub(nodemailer, 'createTransport').returns({
|
||||
templateSender: () => { return sendEmailStub; },
|
||||
use: function() {}
|
||||
return Boolean(params.nonce);
|
||||
});
|
||||
const ctxMatcher = sinon.match(ctx => Boolean(ctx));
|
||||
sandbox.spy(templates, 'verifyKey').withArgs(ctxMatcher, paramMatcher);
|
||||
sandbox.spy(templates, 'verifyRemove').withArgs(ctxMatcher, paramMatcher);
|
||||
|
||||
sendEmailStub = sandbox.stub().returns(Promise.resolve({response: '250'}));
|
||||
sendEmailStub.withArgs(sinon.match(sendOptions => sendOptions.to.address === primaryEmail));
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendEmailStub
|
||||
});
|
||||
|
||||
global.testing = true;
|
||||
let init = require('../../src/app');
|
||||
app = yield init();
|
||||
const init = require('../../src/app');
|
||||
app = await init();
|
||||
});
|
||||
|
||||
beforeEach(function *() {
|
||||
yield mongo.clear(DB_TYPE_PUB_KEY);
|
||||
yield mongo.clear(DB_TYPE_USER_ID);
|
||||
beforeEach(async () => {
|
||||
await mongo.clear(DB_TYPE_PUB_KEY);
|
||||
await mongo.clear(DB_TYPE_USER_ID);
|
||||
emailParams = null;
|
||||
});
|
||||
|
||||
after(function *() {
|
||||
nodemailer.createTransport.restore();
|
||||
yield mongo.clear(DB_TYPE_PUB_KEY);
|
||||
yield mongo.clear(DB_TYPE_USER_ID);
|
||||
yield mongo.disconnect();
|
||||
after(async () => {
|
||||
sandbox.restore();
|
||||
await mongo.clear(DB_TYPE_PUB_KEY);
|
||||
await mongo.clear(DB_TYPE_USER_ID);
|
||||
await mongo.disconnect();
|
||||
});
|
||||
|
||||
describe('REST api', () => {
|
||||
@ -61,34 +66,15 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
it('should return 400 for an invalid pgp key', done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored:'foo' })
|
||||
.send({publicKeyArmored: 'foo'})
|
||||
.expect(400)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 400 for an invalid primaryEmail', done => {
|
||||
it('should return 201', 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())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored })
|
||||
.send({publicKeyArmored})
|
||||
.expect(201)
|
||||
.end(() => {
|
||||
expect(emailParams).to.exist;
|
||||
@ -97,32 +83,32 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/verify', () => {
|
||||
describe('GET /api/v1/key?op=verify', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored, primaryEmail })
|
||||
.send({publicKeyArmored})
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for valid params', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 400 for missing keyid and', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verify?nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verify&nonce=${emailParams.nonce}`)
|
||||
.expect(400)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 400 for missing nonce', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verify?keyId=' + emailParams.keyId)
|
||||
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}`)
|
||||
.expect(400)
|
||||
.end(done);
|
||||
});
|
||||
@ -132,7 +118,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored, primaryEmail })
|
||||
.send({publicKeyArmored})
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
@ -140,7 +126,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
describe('Not yet verified', () => {
|
||||
it('should return 404', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/key?keyId=' + emailParams.keyId)
|
||||
.get(`/api/v1/key?keyId=${emailParams.keyId}`)
|
||||
.expect(404).end(done);
|
||||
});
|
||||
});
|
||||
@ -148,21 +134,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
describe('Verified', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 and get key by id', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/key?keyId=' + emailParams.keyId)
|
||||
.get(`/api/v1/key?keyId=${emailParams.keyId}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 and get key email address', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/key?email=' + primaryEmail)
|
||||
.get(`/api/v1/key?email=${primaryEmail}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
@ -190,95 +176,25 @@ 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', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored, primaryEmail })
|
||||
.send({publicKeyArmored})
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 202 for key id', done => {
|
||||
request(app.listen())
|
||||
.del('/api/v1/key?keyId=' + emailParams.keyId)
|
||||
.del(`/api/v1/key?keyId=${emailParams.keyId}`)
|
||||
.expect(202)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 202 for email address', done => {
|
||||
request(app.listen())
|
||||
.del('/api/v1/key?email=' + primaryEmail)
|
||||
.del(`/api/v1/key?email=${primaryEmail}`)
|
||||
.expect(202)
|
||||
.end(done);
|
||||
});
|
||||
@ -298,32 +214,15 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/removeKey', () => {
|
||||
describe('GET /api/v1/key?op=verifyRemove', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored, primaryEmail })
|
||||
.send({publicKeyArmored})
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 202 for key id', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/removeKey?keyId=' + emailParams.keyId)
|
||||
.expect(202)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/verifyRemove', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.post('/api/v1/key')
|
||||
.send({ publicKeyArmored, primaryEmail })
|
||||
.expect(201)
|
||||
.end(function() {
|
||||
.end(() => {
|
||||
request(app.listen())
|
||||
.del('/api/v1/key?keyId=' + emailParams.keyId)
|
||||
.del(`/api/v1/key?keyId=${emailParams.keyId}`)
|
||||
.expect(202)
|
||||
.end(done);
|
||||
});
|
||||
@ -331,21 +230,21 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
|
||||
it('should return 200 for key id', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verifyRemove?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verifyRemove&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 400 for invalid params', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verifyRemove')
|
||||
.get('/api/v1/key?op=verifyRemove')
|
||||
.expect(400)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 404 for unknown key id', done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verifyRemove?keyId=0123456789ABCDEF&nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verifyRemove&keyId=0123456789ABCDEF&nonce=${emailParams.nonce}`)
|
||||
.expect(404)
|
||||
.end(done);
|
||||
});
|
||||
@ -367,7 +266,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
request(app.listen())
|
||||
.post('/pks/add')
|
||||
.type('form')
|
||||
.send('keytext=' + encodeURIComponent(publicKeyArmored))
|
||||
.send(`keytext=${encodeURIComponent(publicKeyArmored)}`)
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
@ -378,7 +277,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
request(app.listen())
|
||||
.post('/pks/add')
|
||||
.type('form')
|
||||
.send('keytext=' + encodeURIComponent(publicKeyArmored))
|
||||
.send(`keytext=${encodeURIComponent(publicKeyArmored)}`)
|
||||
.expect(201)
|
||||
.end(done);
|
||||
});
|
||||
@ -386,7 +285,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
describe('Not yet verified', () => {
|
||||
it('should return 404', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=get&search=0x' + emailParams.keyId)
|
||||
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`)
|
||||
.expect(404)
|
||||
.end(done);
|
||||
});
|
||||
@ -395,51 +294,51 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
describe('Verified', () => {
|
||||
beforeEach(done => {
|
||||
request(app.listen())
|
||||
.get('/api/v1/verify?keyId=' + emailParams.keyId + '&nonce=' + emailParams.nonce)
|
||||
.get(`/api/v1/key?op=verify&keyId=${emailParams.keyId}&nonce=${emailParams.nonce}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for key id', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=get&search=0x' + emailParams.keyId)
|
||||
.expect(200, publicKeyArmored)
|
||||
.get(`/pks/lookup?op=get&search=0x${emailParams.keyId}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for fingerprint', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=get&search=0x' + fingerprint)
|
||||
.expect(200, publicKeyArmored)
|
||||
.get(`/pks/lookup?op=get&search=0x${fingerprint}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for correct email address', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=get&search=' + primaryEmail)
|
||||
.expect(200, publicKeyArmored)
|
||||
.get(`/pks/lookup?op=get&search=${primaryEmail}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for "mr" option', done => {
|
||||
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-Disposition', 'attachment; filename=openpgpkey.asc')
|
||||
.expect(200, publicKeyArmored)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for "vindex" op', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=vindex&search=0x' + emailParams.keyId)
|
||||
.get(`/pks/lookup?op=vindex&search=0x${emailParams.keyId}`)
|
||||
.expect(200)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('should return 200 for "index" with "mr" option', done => {
|
||||
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(200)
|
||||
.end(done);
|
||||
@ -468,7 +367,7 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
|
||||
it('should return 501 for a invalid key id format', done => {
|
||||
request(app.listen())
|
||||
.get('/pks/lookup?op=get&search=' + emailParams.keyId)
|
||||
.get(`/pks/lookup?op=get&search=${emailParams.keyId}`)
|
||||
.expect(501)
|
||||
.end(done);
|
||||
});
|
||||
@ -489,12 +388,11 @@ describe('Koa App (HTTP Server) Integration Tests', function() {
|
||||
|
||||
it('should return 501 (Not implemented) for "x-email" op', done => {
|
||||
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)
|
||||
.end(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,24 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const config = require('config');
|
||||
const Email = require('../../src/email/email');
|
||||
const tpl = require('../../src/email/templates.json');
|
||||
const tpl = require('../../src/email/templates');
|
||||
|
||||
describe('Email Integration Tests', function() {
|
||||
this.timeout(20000);
|
||||
|
||||
let email, keyId, userId, origin, publicKeyArmored;
|
||||
let email;
|
||||
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'};
|
||||
|
||||
before(function() {
|
||||
publicKeyArmored = require('fs').readFileSync(__dirname + '/../key1.asc', 'utf8');
|
||||
const ctx = {__: key => key};
|
||||
|
||||
before(() => {
|
||||
publicKeyArmored = require('fs').readFileSync(`${__dirname}/../fixtures/key1.asc`, 'utf8');
|
||||
origin = {
|
||||
protocol: 'http',
|
||||
host: 'localhost:' + config.server.port
|
||||
host: `localhost:${config.server.port}`
|
||||
};
|
||||
email = new Email();
|
||||
email.init(config.email);
|
||||
@ -34,40 +37,39 @@ describe('Email Integration Tests', function() {
|
||||
};
|
||||
});
|
||||
|
||||
describe("_sendHelper", () => {
|
||||
it('should work', function *() {
|
||||
let mailOptions = {
|
||||
from: email._sender,
|
||||
to: recipient,
|
||||
describe('_sendHelper', () => {
|
||||
it('should work', async () => {
|
||||
const mailOptions = {
|
||||
from: {name: email._sender.name, address: email._sender.email},
|
||||
to: {name: recipient.name, address: recipient.email},
|
||||
subject: 'Hello ✔', // Subject line
|
||||
text: 'Hello world 🐴', // plaintext body
|
||||
html: '<b>Hello world 🐴</b>' // html body
|
||||
};
|
||||
let info = yield email._sendHelper(mailOptions);
|
||||
const info = await email._sendHelper(mailOptions);
|
||||
expect(info).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe("send verifyKey template", () => {
|
||||
it('should send plaintext email', function *() {
|
||||
describe('send verifyKey template', () => {
|
||||
it('should send plaintext email', async () => {
|
||||
delete userId.publicKeyArmored;
|
||||
yield email.send({ template:tpl.verifyKey, userId, keyId, origin });
|
||||
await email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin});
|
||||
});
|
||||
|
||||
it('should send pgp encrypted email', function *() {
|
||||
yield email.send({ template:tpl.verifyKey, userId, keyId, origin });
|
||||
it('should send pgp encrypted email', async () => {
|
||||
await email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin});
|
||||
});
|
||||
});
|
||||
|
||||
describe("send verifyRemove template", () => {
|
||||
it('should send plaintext email', function *() {
|
||||
describe('send verifyRemove template', () => {
|
||||
it('should send plaintext email', async () => {
|
||||
delete userId.publicKeyArmored;
|
||||
yield email.send({ template:tpl.verifyRemove, userId, keyId, origin });
|
||||
await email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin});
|
||||
});
|
||||
|
||||
it('should send pgp encrypted email', function *() {
|
||||
yield email.send({ template:tpl.verifyRemove, userId, keyId, origin });
|
||||
it('should send pgp encrypted email', async () => {
|
||||
await email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,98 +1,99 @@
|
||||
'use strict';
|
||||
|
||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||
|
||||
const log = require('winston');
|
||||
const config = require('config');
|
||||
const Mongo = require('../../src/dao/mongo');
|
||||
const expect = require('chai').expect;
|
||||
|
||||
describe('Mongo Integration Tests', function() {
|
||||
this.timeout(20000);
|
||||
|
||||
const DB_TYPE = 'apple';
|
||||
const sandbox = sinon.createSandbox();
|
||||
let mongo;
|
||||
|
||||
before(function *() {
|
||||
before(async () => {
|
||||
sandbox.stub(log);
|
||||
|
||||
mongo = new Mongo();
|
||||
yield mongo.init(config.mongo);
|
||||
await mongo.init(config.mongo);
|
||||
});
|
||||
|
||||
beforeEach(function *() {
|
||||
yield mongo.clear(DB_TYPE);
|
||||
beforeEach(async () => {
|
||||
await mongo.clear(DB_TYPE);
|
||||
});
|
||||
|
||||
after(function *() {
|
||||
yield mongo.clear(DB_TYPE);
|
||||
yield mongo.disconnect();
|
||||
after(async () => {
|
||||
sandbox.restore();
|
||||
await mongo.clear(DB_TYPE);
|
||||
await mongo.disconnect();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it('should insert a document', function *() {
|
||||
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
describe('create', () => {
|
||||
it('should insert a document', async () => {
|
||||
const r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
expect(r.insertedCount).to.equal(1);
|
||||
});
|
||||
|
||||
it('should fail if two with the same ID are inserted', function *() {
|
||||
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
it('should fail if two with the same ID are inserted', async () => {
|
||||
let r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
expect(r.insertedCount).to.equal(1);
|
||||
try {
|
||||
r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
} catch(e) {
|
||||
r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
} catch (e) {
|
||||
expect(e.message).to.match(/duplicate/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("batch", () => {
|
||||
it('should insert a document', function *() {
|
||||
let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||
describe('batch', () => {
|
||||
it('should insert a document', async () => {
|
||||
const r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE);
|
||||
expect(r.insertedCount).to.equal(2);
|
||||
});
|
||||
|
||||
it('should fail if docs with the same ID are inserted', function *() {
|
||||
let r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||
it('should fail if docs with the same ID are inserted', async () => {
|
||||
let r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE);
|
||||
expect(r.insertedCount).to.equal(2);
|
||||
try {
|
||||
r = yield mongo.batch([{ _id:'0' }, { _id:'1' }], DB_TYPE);
|
||||
} catch(e) {
|
||||
r = await mongo.batch([{_id: '0'}, {_id: '1'}], DB_TYPE);
|
||||
} catch (e) {
|
||||
expect(e.message).to.match(/duplicate/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it('should update a document', function *() {
|
||||
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
r = yield mongo.update({ _id:'0' }, { foo:'bar' }, DB_TYPE);
|
||||
describe('update', () => {
|
||||
it('should update a document', async () => {
|
||||
let r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
r = await mongo.update({_id: '0'}, {foo: 'bar'}, DB_TYPE);
|
||||
expect(r.modifiedCount).to.equal(1);
|
||||
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||
r = await mongo.get({_id: '0'}, DB_TYPE);
|
||||
expect(r.foo).to.equal('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe("get", () => {
|
||||
it('should get a document', function *() {
|
||||
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||
describe('get', () => {
|
||||
it('should get a document', async () => {
|
||||
let r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
r = await mongo.get({_id: '0'}, DB_TYPE);
|
||||
expect(r).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe("list", () => {
|
||||
it('should list documents', function *() {
|
||||
let r = yield mongo.batch([{ _id:'0', foo:'bar' }, { _id:'1', 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);
|
||||
describe('list', () => {
|
||||
it('should list documents', async () => {
|
||||
let r = await mongo.batch([{_id: '0', foo: 'bar'}, {_id: '1', foo: 'bar'}], DB_TYPE);
|
||||
r = await mongo.list({foo: 'bar'}, DB_TYPE);
|
||||
expect(r).to.deep.equal([{_id: '0', foo: 'bar'}, {_id: '1', foo: 'bar'}], DB_TYPE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("remove", () => {
|
||||
it('should remove a document', function *() {
|
||||
let r = yield mongo.create({ _id:'0' }, DB_TYPE);
|
||||
r = yield mongo.remove({ _id:'0' }, DB_TYPE);
|
||||
r = yield mongo.get({ _id:'0' }, DB_TYPE);
|
||||
describe('remove', () => {
|
||||
it('should remove a document', async () => {
|
||||
let r = await mongo.create({_id: '0'}, DB_TYPE);
|
||||
r = await mongo.remove({_id: '0'}, DB_TYPE);
|
||||
r = await mongo.get({_id: '0'}, DB_TYPE);
|
||||
expect(r).to.not.exist;
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,241 +1,343 @@
|
||||
'use strict';
|
||||
|
||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||
|
||||
const log = require('winston');
|
||||
const config = require('config');
|
||||
const nodemailer = require('nodemailer');
|
||||
const Email = require('../../src/email/email');
|
||||
const Mongo = require('../../src/dao/mongo');
|
||||
const PGP = require('../../src/service/pgp');
|
||||
const PublicKey = require('../../src/service/public-key');
|
||||
const expect = require('chai').expect;
|
||||
const sinon = require('sinon');
|
||||
const templates = require('../../src/email/templates');
|
||||
|
||||
describe('Public Key Integration Tests', function() {
|
||||
this.timeout(20000);
|
||||
|
||||
let publicKey, email, mongo, pgp,
|
||||
sendEmailStub, publicKeyArmored, emailParams;
|
||||
const sandbox = sinon.createSandbox();
|
||||
let publicKey;
|
||||
let email;
|
||||
let mongo;
|
||||
let pgp;
|
||||
let sendEmailStub;
|
||||
let publicKeyArmored;
|
||||
let publicKeyArmored2;
|
||||
let mailsSent;
|
||||
const ctx = {__: key => key};
|
||||
|
||||
const DB_TYPE = 'publickey';
|
||||
const primaryEmail = 'test1@example.com';
|
||||
const origin = { host:'localhost', protocol:'http' };
|
||||
const origin = {host: 'localhost', protocol: 'http'};
|
||||
|
||||
before(function *() {
|
||||
publicKeyArmored = require('fs').readFileSync(__dirname + '/../key3.asc', 'utf8');
|
||||
before(async () => {
|
||||
publicKeyArmored = require('fs').readFileSync(`${__dirname}/../fixtures/key3.asc`, 'utf8');
|
||||
publicKeyArmored2 = require('fs').readFileSync(`${__dirname}/../fixtures/key4.asc`, 'utf8');
|
||||
sinon.stub(log, 'info');
|
||||
mongo = new Mongo();
|
||||
yield mongo.init(config.mongo);
|
||||
await mongo.init(config.mongo);
|
||||
});
|
||||
|
||||
beforeEach(function *() {
|
||||
yield mongo.clear(DB_TYPE);
|
||||
emailParams = null;
|
||||
sendEmailStub = sinon.stub().returns(Promise.resolve({ response:'250' }));
|
||||
sendEmailStub.withArgs(sinon.match(recipient => {
|
||||
return recipient.to.address === primaryEmail;
|
||||
}), sinon.match(params => {
|
||||
emailParams = params;
|
||||
return params.nonce !== undefined && params.keyId !== undefined;
|
||||
beforeEach(async () => {
|
||||
await mongo.clear(DB_TYPE);
|
||||
|
||||
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.withArgs(sinon.match(sendOptions => {
|
||||
mailsSent[mailsSent.length - 1].to = sendOptions.to.address;
|
||||
return true;
|
||||
}));
|
||||
sinon.stub(nodemailer, 'createTransport').returns({
|
||||
templateSender: () => { return sendEmailStub; }
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendEmailStub
|
||||
});
|
||||
email = new Email(nodemailer);
|
||||
email.init({
|
||||
host: 'localhost',
|
||||
auth: { user:'user', pass:'pass' },
|
||||
sender: { name:'Foo Bar', email:'foo@bar.com' }
|
||||
auth: {user: 'user', pass: 'pass'},
|
||||
sender: {name: 'Foo Bar', emails: 'foo@bar.com'}
|
||||
});
|
||||
pgp = new PGP();
|
||||
publicKey = new PublicKey(pgp, mongo, email);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nodemailer.createTransport.restore();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
after(function *() {
|
||||
yield mongo.clear(DB_TYPE);
|
||||
yield mongo.disconnect();
|
||||
after(async () => {
|
||||
await mongo.clear(DB_TYPE);
|
||||
await mongo.disconnect();
|
||||
log.info.restore();
|
||||
});
|
||||
|
||||
describe('put', () => {
|
||||
it('should persist key and send verification email with primaryEmail', function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
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 persist key and send verification email', async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(4);
|
||||
});
|
||||
|
||||
it('should work twice if not yet verified', function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
expect(emailParams.nonce).to.exist;
|
||||
emailParams = null;
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
expect(emailParams.nonce).to.exist;
|
||||
it('should work twice if not yet verified', async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(4);
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(8);
|
||||
});
|
||||
|
||||
it('should throw 304 if key already exists', function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
yield publicKey.verify(emailParams);
|
||||
it.skip('should throw 304 if key already exists', async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
await publicKey.verify(mailsSent[0].params);
|
||||
try {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
expect(false).to.be.true;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
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', () => {
|
||||
beforeEach(function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
});
|
||||
|
||||
it('should update the document', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let gotten = yield mongo.get({ keyId:emailParams.keyId }, DB_TYPE);
|
||||
it('should update the document', async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
const emailParams = mailsSent[0].params;
|
||||
await publicKey.verify(emailParams);
|
||||
const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE);
|
||||
expect(gotten.userIds[0].verified).to.be.true;
|
||||
expect(gotten.userIds[0].nonce).to.be.null;
|
||||
expect(gotten.userIds[1].verified).to.be.false;
|
||||
expect(gotten.userIds[1].nonce).to.exist;
|
||||
});
|
||||
|
||||
it('should not find the document', function *() {
|
||||
it('should not find the document', async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
const emailParams = mailsSent[0].params;
|
||||
try {
|
||||
yield publicKey.verify({ keyId:emailParams.keyId, nonce:'fake_nonce' });
|
||||
await publicKey.verify({keyId: emailParams.keyId, nonce: 'fake_nonce'});
|
||||
expect(true).to.be.false;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(404);
|
||||
}
|
||||
let gotten = yield mongo.get({ keyId:emailParams.keyId }, DB_TYPE);
|
||||
const gotten = await mongo.get({keyId: emailParams.keyId}, DB_TYPE);
|
||||
expect(gotten.userIds[0].verified).to.be.false;
|
||||
expect(gotten.userIds[0].nonce).to.equal(emailParams.nonce);
|
||||
expect(gotten.userIds[1].verified).to.be.false;
|
||||
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', () => {
|
||||
let key;
|
||||
|
||||
describe('should find a verified key', () => {
|
||||
beforeEach(function *() {
|
||||
key = pgp.parseKey(publicKeyArmored);
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
yield publicKey.verify(emailParams);
|
||||
beforeEach(async () => {
|
||||
key = await pgp.parseKey(publicKeyArmored);
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
await publicKey.verify(mailsSent[0].params);
|
||||
});
|
||||
|
||||
it('by fingerprint', function *() {
|
||||
let verified = yield publicKey.getVerified({ fingerprint:key.fingerprint });
|
||||
it('by fingerprint', async () => {
|
||||
const verified = await publicKey.getVerified({fingerprint: key.fingerprint});
|
||||
expect(verified).to.exist;
|
||||
});
|
||||
|
||||
it('by all userIds', function *() {
|
||||
let verified = yield publicKey.getVerified({ userIds:key.userIds });
|
||||
it('by all userIds', async () => {
|
||||
const verified = await publicKey.getVerified({userIds: key.userIds});
|
||||
expect(verified).to.exist;
|
||||
});
|
||||
|
||||
it('by verified userId', function *() {
|
||||
let verified = yield publicKey.getVerified({ userIds:[key.userIds[0]] });
|
||||
it('by verified userId', async () => {
|
||||
const verified = await publicKey.getVerified({userIds: [key.userIds[0]]});
|
||||
expect(verified).to.exist;
|
||||
});
|
||||
|
||||
it('by unverified userId', function *() {
|
||||
let verified = yield publicKey.getVerified({ userIds:[key.userIds[1]] });
|
||||
it('by unverified userId', async () => {
|
||||
const verified = await publicKey.getVerified({userIds: [key.userIds[1]]});
|
||||
expect(verified).to.not.exist;
|
||||
});
|
||||
|
||||
it('by keyId', function *() {
|
||||
let verified = yield publicKey.getVerified({ keyId:key.keyId });
|
||||
it('by keyId', async () => {
|
||||
const verified = await publicKey.getVerified({keyId: key.keyId});
|
||||
expect(verified).to.exist;
|
||||
});
|
||||
|
||||
it('by all params', function *() {
|
||||
let verified = yield publicKey.getVerified(key);
|
||||
it('by all params', async () => {
|
||||
const verified = await publicKey.getVerified(key);
|
||||
expect(verified).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not find an unverified key', () => {
|
||||
beforeEach(function *() {
|
||||
key = pgp.parseKey(publicKeyArmored);
|
||||
beforeEach(async () => {
|
||||
key = await pgp.parseKey(publicKeyArmored);
|
||||
key.userIds[0].verified = false;
|
||||
yield mongo.create(key, DB_TYPE);
|
||||
await mongo.create(key, DB_TYPE);
|
||||
});
|
||||
|
||||
it('by fingerprint', function *() {
|
||||
let verified = yield publicKey.getVerified({ fingerprint:key.fingerprint });
|
||||
it('by fingerprint', async () => {
|
||||
const verified = await publicKey.getVerified({fingerprint: key.fingerprint});
|
||||
expect(verified).to.not.exist;
|
||||
});
|
||||
|
||||
it('by userIds', function *() {
|
||||
let verified = yield publicKey.getVerified({ userIds:key.userIds });
|
||||
it('by userIds', async () => {
|
||||
const verified = await publicKey.getVerified({userIds: key.userIds});
|
||||
expect(verified).to.not.exist;
|
||||
});
|
||||
|
||||
it('by keyId', function *() {
|
||||
let verified = yield publicKey.getVerified({ keyId:key.keyId });
|
||||
it('by keyId', async () => {
|
||||
const verified = await publicKey.getVerified({keyId: key.keyId});
|
||||
expect(verified).to.not.exist;
|
||||
});
|
||||
|
||||
it('by all params', function *() {
|
||||
let verified = yield publicKey.getVerified(key);
|
||||
it('by all params', async () => {
|
||||
const verified = await publicKey.getVerified(key);
|
||||
expect(verified).to.not.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
beforeEach(function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
let emailParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
emailParams = mailsSent[0].params;
|
||||
});
|
||||
|
||||
it('should return verified key by key id', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let key = yield publicKey.get({ keyId:emailParams.keyId });
|
||||
it('should return verified key by key id', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const key = await publicKey.get({keyId: emailParams.keyId}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should return verified key by key id (uppercase)', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let key = yield publicKey.get({ keyId:emailParams.keyId.toUpperCase() });
|
||||
it('should return verified key by key id (uppercase)', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const key = await publicKey.get({keyId: emailParams.keyId.toUpperCase()}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should return verified key by fingerprint', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let fingerprint = pgp.parseKey(publicKeyArmored).fingerprint;
|
||||
let key = yield publicKey.get({ fingerprint });
|
||||
it('should return verified key by fingerprint', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const fingerprint = (await pgp.parseKey(publicKeyArmored)).fingerprint;
|
||||
const key = await publicKey.get({fingerprint}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should return verified key by fingerprint (uppercase)', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let fingerprint = pgp.parseKey(publicKeyArmored).fingerprint.toUpperCase();
|
||||
let key = yield publicKey.get({ fingerprint });
|
||||
it('should return verified key by fingerprint (uppercase)', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const fingerprint = (await pgp.parseKey(publicKeyArmored)).fingerprint.toUpperCase();
|
||||
const key = await publicKey.get({fingerprint}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should return verified key by email address', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let key = yield publicKey.get({ email:primaryEmail });
|
||||
it('should return verified key by email address', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const key = await publicKey.get({email: primaryEmail}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should return verified key by email address (uppercase)', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
let key = yield publicKey.get({ email:primaryEmail.toUpperCase() });
|
||||
it('should return verified key by email address (uppercase)', async () => {
|
||||
await publicKey.verify(emailParams);
|
||||
const key = await publicKey.get({email: primaryEmail.toUpperCase()}, ctx);
|
||||
expect(key.publicKeyArmored).to.exist;
|
||||
});
|
||||
|
||||
it('should throw 404 for unverified key', function *() {
|
||||
it('should throw 404 for unverified key', async () => {
|
||||
try {
|
||||
yield publicKey.get({ keyId:emailParams.keyId });
|
||||
await publicKey.get({keyId: emailParams.keyId}, ctx);
|
||||
expect(false).to.be.true;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(404);
|
||||
}
|
||||
});
|
||||
@ -244,39 +346,33 @@ describe('Public Key Integration Tests', function() {
|
||||
describe('requestRemove', () => {
|
||||
let keyId;
|
||||
|
||||
beforeEach(function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
keyId = emailParams.keyId;
|
||||
beforeEach(async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
keyId = mailsSent[0].params.keyId;
|
||||
});
|
||||
|
||||
it('should work for verified key', function *() {
|
||||
yield publicKey.verify(emailParams);
|
||||
emailParams = null;
|
||||
yield publicKey.requestRemove({ keyId, origin });
|
||||
expect(emailParams.keyId).to.exist;
|
||||
expect(emailParams.nonce).to.exist;
|
||||
it('should work for verified key', async () => {
|
||||
await publicKey.verify(mailsSent[0].params);
|
||||
await publicKey.requestRemove({keyId, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(8);
|
||||
});
|
||||
|
||||
it('should work for unverified key', function *() {
|
||||
emailParams = null;
|
||||
yield publicKey.requestRemove({ keyId, origin });
|
||||
expect(emailParams.keyId).to.exist;
|
||||
expect(emailParams.nonce).to.exist;
|
||||
it('should work for unverified key', async () => {
|
||||
await publicKey.requestRemove({keyId, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(8);
|
||||
});
|
||||
|
||||
it('should work by email address', function *() {
|
||||
emailParams = null;
|
||||
yield publicKey.requestRemove({ email:primaryEmail, origin });
|
||||
expect(emailParams.keyId).to.exist;
|
||||
expect(emailParams.nonce).to.exist;
|
||||
it('should work by email address', async () => {
|
||||
await publicKey.requestRemove({email: primaryEmail, origin}, ctx);
|
||||
expect(mailsSent.length).to.equal(5);
|
||||
});
|
||||
|
||||
it('should throw 404 for no key', function *() {
|
||||
yield mongo.remove({ keyId }, DB_TYPE);
|
||||
it('should throw 404 for no key', async () => {
|
||||
await mongo.remove({keyId}, DB_TYPE);
|
||||
try {
|
||||
yield publicKey.requestRemove({ keyId, origin });
|
||||
await publicKey.requestRemove({keyId, origin}, ctx);
|
||||
expect(false).to.be.true;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(404);
|
||||
}
|
||||
});
|
||||
@ -285,28 +381,76 @@ describe('Public Key Integration Tests', function() {
|
||||
describe('verifyRemove', () => {
|
||||
let keyId;
|
||||
|
||||
beforeEach(function *() {
|
||||
yield publicKey.put({ publicKeyArmored, primaryEmail, origin });
|
||||
keyId = emailParams.keyId;
|
||||
emailParams = null;
|
||||
yield publicKey.requestRemove({ keyId, origin });
|
||||
beforeEach(async () => {
|
||||
await publicKey.put({emails: [], publicKeyArmored, origin}, ctx);
|
||||
keyId = mailsSent[0].params.keyId;
|
||||
});
|
||||
|
||||
it('should remove key', function *() {
|
||||
yield publicKey.verifyRemove(emailParams);
|
||||
let key = yield mongo.get({ keyId }, DB_TYPE);
|
||||
afterEach(() => {
|
||||
mailsSent = [];
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
it('should throw 404 for no key', function *() {
|
||||
yield mongo.remove({ keyId }, DB_TYPE);
|
||||
it('should throw 404 for no key', async () => {
|
||||
await mongo.remove({keyId}, DB_TYPE);
|
||||
try {
|
||||
yield publicKey.verifyRemove(emailParams);
|
||||
await publicKey.verifyRemove(mailsSent[1].params);
|
||||
expect(false).to.be.true;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.status).to.equal(404);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
232
test/key3.asc
232
test/key3.asc
@ -1,232 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=sqvb
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
Qeh1ZTzVvm3VhScYKhDPg8x2OpDvKAoo
|
||||
=qFMO
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
10
test/setup.js
Normal file
10
test/setup.js
Normal file
@ -0,0 +1,10 @@
|
||||
'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,37 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
require('co-mocha')(require('mocha')); // monkey patch mocha for generators
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const log = require('npmlog');
|
||||
const log = require('winston');
|
||||
const Email = require('../../src/email/email');
|
||||
const nodemailer = require('nodemailer');
|
||||
const sinon = require('sinon');
|
||||
|
||||
|
||||
describe('Email Unit Tests', () => {
|
||||
let email, sendFnStub;
|
||||
const sandbox = sinon.createSandbox();
|
||||
let email;
|
||||
let sendFnStub;
|
||||
|
||||
let template = {
|
||||
const template = () => ({
|
||||
subject: 'foo',
|
||||
text: 'bar',
|
||||
html: '<strong>bar</strong>'
|
||||
};
|
||||
let sender = {
|
||||
});
|
||||
const sender = {
|
||||
name: 'Foo Bar',
|
||||
email: 'foo@bar.com'
|
||||
};
|
||||
let userId1 = {
|
||||
const userId1 = {
|
||||
name: 'name1',
|
||||
email: 'email1',
|
||||
nonce: 'qwertzuioasdfghjkqwertzuio'
|
||||
};
|
||||
let keyId = '0123456789ABCDF0';
|
||||
let origin = {
|
||||
const keyId = '0123456789ABCDF0';
|
||||
const origin = {
|
||||
protocol: 'http',
|
||||
host: 'localhost:8888'
|
||||
};
|
||||
let mailOptions = {
|
||||
const mailOptions = {
|
||||
from: sender,
|
||||
to: sender,
|
||||
subject: 'Hello ✔', // Subject line
|
||||
@ -40,74 +37,66 @@ describe('Email Unit Tests', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
sendFnStub = sinon.stub();
|
||||
sinon.stub(nodemailer, 'createTransport').returns({
|
||||
templateSender: () => { return sendFnStub; }
|
||||
sendFnStub = sandbox.stub();
|
||||
sandbox.stub(nodemailer, 'createTransport').returns({
|
||||
sendMail: sendFnStub
|
||||
});
|
||||
|
||||
sinon.stub(log, 'warn');
|
||||
sinon.stub(log, 'error');
|
||||
sandbox.stub(log);
|
||||
|
||||
email = new Email(nodemailer);
|
||||
email.init({
|
||||
host: 'host',
|
||||
auth: { user:'user', pass:'pass' },
|
||||
sender: sender
|
||||
auth: {user: 'user', pass: 'pass'},
|
||||
sender
|
||||
});
|
||||
expect(email._sender).to.equal(sender);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nodemailer.createTransport.restore();
|
||||
log.warn.restore();
|
||||
log.error.restore();
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe("send", () => {
|
||||
describe('send', () => {
|
||||
beforeEach(() => {
|
||||
sinon.stub(email, '_sendHelper').returns(Promise.resolve({ response:'250' }));
|
||||
sandbox.stub(email, '_sendHelper').returns(Promise.resolve({response: '250'}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
email._sendHelper.restore();
|
||||
});
|
||||
|
||||
it('should work', function *() {
|
||||
let info = yield email.send({ template, userId:userId1, keyId, origin});
|
||||
it('should work', async () => {
|
||||
const info = await email.send({template, userId: userId1, keyId, origin});
|
||||
|
||||
expect(info.response).to.match(/^250/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_sendHelper", () => {
|
||||
it('should work', function *() {
|
||||
sendFnStub.returns(Promise.resolve({ response:'250' }));
|
||||
describe('_sendHelper', () => {
|
||||
it('should work', async () => {
|
||||
sendFnStub.returns(Promise.resolve({response: '250'}));
|
||||
|
||||
let info = yield email._sendHelper(mailOptions);
|
||||
const info = await email._sendHelper(mailOptions);
|
||||
|
||||
expect(info.response).to.match(/^250/);
|
||||
});
|
||||
|
||||
it('should log warning for reponse error', function *() {
|
||||
sendFnStub.returns(Promise.resolve({ response:'554' }));
|
||||
it('should log warning for reponse error', async () => {
|
||||
sendFnStub.returns(Promise.resolve({response: '554'}));
|
||||
|
||||
let info = yield email._sendHelper(mailOptions);
|
||||
const info = await email._sendHelper(mailOptions);
|
||||
|
||||
expect(info.response).to.match(/^554/);
|
||||
expect(log.warn.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should fail', function *() {
|
||||
it('should fail', async () => {
|
||||
sendFnStub.returns(Promise.reject(new Error('boom')));
|
||||
|
||||
try {
|
||||
yield email._sendHelper(mailOptions);
|
||||
} catch(e) {
|
||||
await email._sendHelper(mailOptions);
|
||||
} catch (e) {
|
||||
expect(log.error.calledOnce).to.be.true;
|
||||
expect(e.status).to.equal(500);
|
||||
expect(e.message).to.match(/failed/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -1,119 +1,132 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const expect = require('chai').expect;
|
||||
const log = require('npmlog');
|
||||
const log = require('winston');
|
||||
const openpgp = require('openpgp');
|
||||
const PGP = require('../../src/service/pgp');
|
||||
const sinon = require('sinon');
|
||||
|
||||
describe('PGP Unit Tests', () => {
|
||||
let pgp, key1Armored, key2Armored, key3Armored;
|
||||
const sandbox = sinon.createSandbox();
|
||||
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(() => {
|
||||
key1Armored = fs.readFileSync(__dirname + '/../key1.asc', 'utf8');
|
||||
key2Armored = fs.readFileSync(__dirname + '/../key2.asc', 'utf8');
|
||||
key3Armored = fs.readFileSync(__dirname + '/../key3.asc', 'utf8');
|
||||
sandbox.stub(log);
|
||||
pgp = new PGP();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
});
|
||||
|
||||
describe('parseKey', () => {
|
||||
it('should should throw error on key parsing', () => {
|
||||
let readStub = sinon.stub(openpgp.key, 'readArmored').returns({err:[new Error()]});
|
||||
sinon.stub(log, 'error');
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/Failed to parse/);
|
||||
it('should should throw error on key parsing', async () => {
|
||||
sandbox.stub(openpgp.key, 'readArmored').returns({err: [new Error()]});
|
||||
await expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/Failed to parse/);
|
||||
expect(log.error.calledOnce).to.be.true;
|
||||
log.error.restore();
|
||||
readStub.restore();
|
||||
});
|
||||
|
||||
it('should should throw error when more than one key', () => {
|
||||
let readStub = sinon.stub(openpgp.key, 'readArmored').returns({keys:[{},{}]});
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only one key/);
|
||||
readStub.restore();
|
||||
sandbox.stub(openpgp.key, 'readArmored').returns({keys: [{}, {}]});
|
||||
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only one key/);
|
||||
});
|
||||
|
||||
it('should should throw error when more than one key', () => {
|
||||
let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
|
||||
it('should should throw error when primaryKey not verfied', () => {
|
||||
sandbox.stub(openpgp.key, 'readArmored').returns({
|
||||
keys: [{
|
||||
primaryKey: {},
|
||||
verifyPrimaryKey: function() { return false; }
|
||||
verifyPrimaryKey() { return false; }
|
||||
}]
|
||||
});
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/primary key verification/);
|
||||
readStub.restore();
|
||||
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/primary key verification/);
|
||||
});
|
||||
|
||||
it('should only accept 16 char key id', () => {
|
||||
let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
|
||||
sandbox.stub(openpgp.key, 'readArmored').returns({
|
||||
keys: [{
|
||||
primaryKey: {
|
||||
fingerprint: '4277257930867231ce393fb8dbc0b3d92b1b86e9',
|
||||
getKeyId: function() {
|
||||
getFingerprint() {
|
||||
return '4277257930867231ce393fb8dbc0b3d92b1b86e9';
|
||||
},
|
||||
getKeyId() {
|
||||
return {
|
||||
toHex:function() { return 'asdf'; }
|
||||
toHex() { return 'asdf'; }
|
||||
};
|
||||
}
|
||||
},
|
||||
verifyPrimaryKey: function() { return openpgp.enums.keyStatus.valid; }
|
||||
verifyPrimaryKey() { return openpgp.enums.keyStatus.valid; }
|
||||
}]
|
||||
});
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only v4 keys/);
|
||||
readStub.restore();
|
||||
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only v4 keys/);
|
||||
});
|
||||
|
||||
it('should only accept version 4 fingerprint', () => {
|
||||
let readStub = sinon.stub(openpgp.key, 'readArmored').returns({
|
||||
sandbox.stub(openpgp.key, 'readArmored').returns({
|
||||
keys: [{
|
||||
primaryKey: {
|
||||
fingerprint: '4277257930867231ce393fb8dbc0b3d92b1b86e',
|
||||
getKeyId: function() {
|
||||
getFingerprint() {
|
||||
return '4277257930867231ce393fb8dbc0b3d92b1b86e';
|
||||
},
|
||||
getKeyId() {
|
||||
return {
|
||||
toHex:function() { return 'dbc0b3d92b1b86e9'; }
|
||||
toHex() { return 'dbc0b3d92b1b86e9'; }
|
||||
};
|
||||
}
|
||||
},
|
||||
verifyPrimaryKey: function() { return openpgp.enums.keyStatus.valid; }
|
||||
verifyPrimaryKey() { return openpgp.enums.keyStatus.valid; }
|
||||
}]
|
||||
});
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/only v4 keys/);
|
||||
readStub.restore();
|
||||
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/only v4 keys/);
|
||||
});
|
||||
|
||||
it('should only accept valid user ids', () => {
|
||||
sinon.stub(pgp, 'parseUserIds').returns([]);
|
||||
expect(pgp.parseKey.bind(pgp, key3Armored)).to.throw(/invalid user ids/);
|
||||
sandbox.stub(pgp, 'parseUserIds').returns([]);
|
||||
return expect(pgp.parseKey(key3Armored)).to.eventually.be.rejectedWith(/invalid user IDs/);
|
||||
});
|
||||
|
||||
it('should be able to parse RSA key', () => {
|
||||
let params = pgp.parseKey(key1Armored);
|
||||
it('should be able to parse RSA key', async () => {
|
||||
const params = await pgp.parseKey(key1Armored);
|
||||
expect(params.keyId).to.equal('dbc0b3d92b1b86e9');
|
||||
expect(params.fingerprint).to.equal('4277257930867231ce393fb8dbc0b3d92b1b86e9');
|
||||
expect(params.userIds[0].name).to.equal('safewithme testuser');
|
||||
expect(params.userIds[0].email).to.equal('safewithme.testuser@gmail.com');
|
||||
expect(params.created.getTime()).to.exist;
|
||||
expect(params.uploaded.getTime()).to.exist;
|
||||
expect(params.algorithm).to.equal('rsa_encrypt_sign');
|
||||
expect(params.keySize).to.equal(2048);
|
||||
expect(params.publicKeyArmored).to.equal(key1Armored);
|
||||
});
|
||||
|
||||
it('should be able to parse RSA/ECC key', () => {
|
||||
let params = pgp.parseKey(key2Armored);
|
||||
/* test key2 has expired */
|
||||
it.skip('should be able to parse RSA/ECC key', async () => {
|
||||
const params = await pgp.parseKey(key2Armored);
|
||||
expect(params.keyId).to.equal('b8e4105cc9dedc77');
|
||||
expect(params.fingerprint).to.equal('e3317db04d3958fd5f662c37b8e4105cc9dedc77');
|
||||
expect(params.userIds.length).to.equal(1);
|
||||
expect(params.created.getTime()).to.exist;
|
||||
expect(params.uploaded.getTime()).to.exist;
|
||||
expect(params.algorithm).to.equal('rsa_encrypt_sign');
|
||||
expect(params.keySize).to.equal(4096);
|
||||
expect(params.publicKeyArmored).to.equal(pgp.trimKey(key2Armored));
|
||||
});
|
||||
|
||||
it('should be able to parse komplex key', () => {
|
||||
let params = pgp.parseKey(key3Armored);
|
||||
it('should be able to parse komplex key', async () => {
|
||||
const params = await pgp.parseKey(key3Armored);
|
||||
expect(params.keyId).to.equal('4001a127a90de8e1');
|
||||
expect(params.fingerprint).to.equal('04062c70b446e33016e219a74001a127a90de8e1');
|
||||
expect(params.userIds.length).to.equal(4);
|
||||
expect(params.created.getTime()).to.exist;
|
||||
expect(params.uploaded.getTime()).to.exist;
|
||||
expect(params.algorithm).to.equal('rsa_encrypt_sign');
|
||||
expect(params.keySize).to.equal(4096);
|
||||
expect(params.publicKeyArmored).to.equal(pgp.trimKey(key3Armored));
|
||||
@ -122,12 +135,12 @@ describe('PGP Unit Tests', () => {
|
||||
|
||||
describe('trimKey', () => {
|
||||
it('should be the same as key1', () => {
|
||||
let trimmed = pgp.trimKey(key1Armored);
|
||||
const trimmed = pgp.trimKey(key1Armored);
|
||||
expect(trimmed).to.equal(key1Armored);
|
||||
});
|
||||
|
||||
it('should not be the same as key2', () => {
|
||||
let trimmed = pgp.trimKey(key2Armored);
|
||||
const trimmed = pgp.trimKey(key2Armored);
|
||||
expect(trimmed).to.not.equal(key2Armored);
|
||||
});
|
||||
});
|
||||
@ -137,22 +150,22 @@ describe('PGP Unit Tests', () => {
|
||||
const KEY_END = '-----END PGP PUBLIC KEY BLOCK-----';
|
||||
|
||||
it('should return true for valid key block', () => {
|
||||
let input = KEY_BEGIN + KEY_END;
|
||||
const input = KEY_BEGIN + KEY_END;
|
||||
expect(pgp.validateKeyBlock(input)).to.be.true;
|
||||
});
|
||||
|
||||
it('should return false for invalid key block', () => {
|
||||
let input = KEY_END + KEY_BEGIN;
|
||||
const input = KEY_END + KEY_BEGIN;
|
||||
expect(pgp.validateKeyBlock(input)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for invalid key block', () => {
|
||||
let input = KEY_END;
|
||||
const input = KEY_END;
|
||||
expect(pgp.validateKeyBlock(input)).to.be.false;
|
||||
});
|
||||
|
||||
it('should return false for invalid key block', () => {
|
||||
let input = KEY_BEGIN;
|
||||
const input = KEY_BEGIN;
|
||||
expect(pgp.validateKeyBlock(input)).to.be.false;
|
||||
});
|
||||
});
|
||||
@ -160,33 +173,75 @@ describe('PGP Unit Tests', () => {
|
||||
describe('parseUserIds', () => {
|
||||
let key;
|
||||
|
||||
beforeEach(() => {
|
||||
key = openpgp.key.readArmored(key1Armored).keys[0];
|
||||
beforeEach(async () => {
|
||||
key = (await openpgp.key.readArmored(key1Armored)).keys[0];
|
||||
});
|
||||
|
||||
it('should parse a valid user id', () => {
|
||||
let parsed = pgp.parseUserIds(key.users, key.primaryKey);
|
||||
it('should parse a valid user id', async () => {
|
||||
const parsed = await pgp.parseUserIds(key.users, key.primaryKey);
|
||||
expect(parsed[0].name).to.equal('safewithme testuser');
|
||||
expect(parsed[0].email).to.equal('safewithme.testuser@gmail.com');
|
||||
});
|
||||
|
||||
it('should throw for an empty user ids array', () => {
|
||||
expect(pgp.parseUserIds.bind(pgp, [], key.primaryKey)).to.throw(/no user id/);
|
||||
});
|
||||
it('should throw for an empty user ids array', () =>
|
||||
expect(pgp.parseUserIds([], key.primaryKey)).to.eventually.be.rejectedWith(/no user ID/)
|
||||
);
|
||||
|
||||
it('should return no user id for an invalid signature', () => {
|
||||
it('should return no user id for an invalid signature', async () => {
|
||||
key.users[0].userId.userid = 'fake@example.com';
|
||||
let parsed = pgp.parseUserIds(key.users, key.primaryKey);
|
||||
const parsed = await pgp.parseUserIds(key.users, key.primaryKey);
|
||||
expect(parsed.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('should throw for a invalid email address', () => {
|
||||
let verifyStub = sinon.stub(key.users[0], 'isValidSelfCertificate').returns(true);
|
||||
it('should throw for an invalid email address', async () => {
|
||||
key.users[0].userId.userid = 'safewithme testuser <safewithme.testusergmail.com>';
|
||||
let parsed = pgp.parseUserIds(key.users, key.primaryKey);
|
||||
const parsed = await pgp.parseUserIds(key.users, key.primaryKey);
|
||||
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,6 +1,5 @@
|
||||
'use strict';
|
||||
|
||||
const expect = require('chai').expect;
|
||||
const util = require('../../src/service/util');
|
||||
|
||||
describe('Util Unit Tests', () => {
|
||||
@ -117,7 +116,7 @@ describe('Util Unit Tests', () => {
|
||||
try {
|
||||
util.throw(500, 'boom');
|
||||
expect(true).to.be.false;
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
expect(e.message).to.equal('boom');
|
||||
expect(e.status).to.equal(500);
|
||||
expect(e.expose).to.be.true;
|
||||
@ -137,20 +136,19 @@ describe('Util Unit Tests', () => {
|
||||
|
||||
describe('origin', () => {
|
||||
it('should work', () => {
|
||||
expect(util.origin({ secure:true, host:'h', protocol:'p' })).to.exist;
|
||||
expect(util.origin({secure: true, host: 'h', protocol: 'p'})).to.exist;
|
||||
});
|
||||
});
|
||||
|
||||
describe('url', () => {
|
||||
it('should work with resource', () => {
|
||||
let url = util.url({ host:'localhost', protocol:'http'}, '/foo');
|
||||
const url = util.url({host: 'localhost', protocol: 'http'}, '/foo');
|
||||
expect(url).to.equal('http://localhost/foo');
|
||||
});
|
||||
|
||||
it('should work without resource', () => {
|
||||
let url = util.url({ host:'localhost', protocol:'http'});
|
||||
const url = util.url({host: 'localhost', protocol: 'http'});
|
||||
expect(url).to.equal('http://localhost');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user