Compare commits

..

145 Commits

Author SHA1 Message Date
7adfc6aa1d
Add docker-compose
All checks were successful
Build & publish images / build-and-push-image (push) Successful in 24s
2023-06-19 07:41:58 +00:00
b8b71481c6
Add Docker builds
All checks were successful
Build & publish images / build-and-push-image (push) Successful in 6m11s
2023-06-19 06:50:05 +00:00
Stefan
cd62e63cc2 Improve documentation with more details to get started 2021-07-06 14:23:17 +02:00
Thomas Oberndörfer
f118ec64b0 Update dependencies 2021-05-08 18:07:52 +02:00
Thomas Oberndörfer
fe1cb9f439 Rethrow error in init phase 2021-05-08 18:07:52 +02:00
Martin Vietz
8a9a7be0c7 Descripe howto upload a key from shell 2020-06-09 12:02:27 +02:00
Thomas Oberndörfer
6f21406afd Revert query response to JSON 2019-08-09 17:11:04 +02:00
Thomas Oberndörfer
9c3ddbfef2 Ensures reproducible builds with npm ci 2019-08-09 15:12:27 +02:00
Thomas Oberndörfer
665708dbb9 Update dependencies 2019-08-09 14:45:50 +02:00
Thomas Oberndörfer
93af442fff Add locales to release archive 2019-08-09 14:29:36 +02:00
Thomas Oberndörfer
769d7d1c52 Fix default locale setting 2019-06-18 15:23:51 +02:00
Thomas Oberndörfer
11f99f8d40 Set koa-locales options 2019-06-18 12:04:23 +02:00
Thomas Oberndörfer
b15879f31b Update dependencies: koa-locales, mongodb, openpgp. 2019-06-17 17:20:41 +02:00
Thomas Oberndörfer
ff6f9f7c63 Add views for key search result, verify success and removal success pages. 2019-06-17 16:15:07 +02:00
Thomas Oberndörfer
f399da9614 Serve Bootstrap and jQuery from key server origin. Use EJS for view rendering. 2019-06-14 11:17:25 +02:00
Thomas Oberndörfer
3367f08647 Localize verification messages with koa-locales 2019-06-14 09:33:13 +02:00
Thomas Oberndörfer
9064089482 Update dependencies 2019-05-27 14:31:22 +02:00
Thomas Oberndörfer
d14b0dc390 Fix link to key management 2019-03-28 11:48:10 +01:00
Thomas Oberndörfer
e7ba187221 Update mongodb driver to 3.2.2 2019-03-26 17:29:08 +01:00
Thomas Oberndörfer
9e7149108a Fix user ID filtering. Do not filter out user attribute packets. 2019-03-15 16:55:53 +01:00
Thomas Oberndörfer
f88f4d5d2e Replace addressparser with openpgp.util.parseUserId 2019-03-13 11:18:20 +01:00
Thomas Oberndörfer
b83f13cde2 Error handling when encrypting the verification email fails 2019-03-12 10:45:06 +01:00
Thomas Oberndörfer
9159bd5a47 Fix issues with keys that have a creation date that is in the future. 2019-03-07 11:53:27 +01:00
Thomas Oberndörfer
1fcf791560 Fix normalization of email to lowercase 2019-03-06 15:47:46 +01:00
Thomas Oberndörfer
aad782573d Update static pages and linked dependencies 2019-03-06 15:19:20 +01:00
Thomas Oberndörfer
1da22c1029 Update eslint rules 2019-03-06 10:21:29 +01:00
Thomas Oberndörfer
cee14ba99c Extend error logging 2019-03-06 09:52:08 +01:00
Thomas Oberndörfer
9db75f4034 Release v3.0 2019-03-04 18:28:56 +01:00
Thomas Oberndörfer
46b474a748 Update travis deployment 2019-03-04 13:12:46 +01:00
Thomas Oberndörfer
914e63a8ee Hide armored header version and comment 2019-03-02 15:04:13 +01:00
Thomas Oberndörfer
8d18614f17 Add package-lock.json. Set dependency versions fixed. Set min node version to 10. 2019-03-02 12:12:25 +01:00
Thomas Oberndörfer
3f498495c1
Merge pull request #80 from mailvelope/feature/extended_userid_handling
Feature/extended userid handling
2019-03-02 11:28:02 +01:00
Martin Hauck
02adaad939
Add upload, update and removal for single user IDs (emails) 2019-02-25 15:11:40 +01:00
Martin Hauck
1651571d36
Rebase onto dev/pgp-inline, fix unit tests 2019-02-08 17:04:28 +01:00
webwitcher
a2b941b0ae
Update dependencies, fix package conflicts, add packages (ejs, email-templates) 2019-02-08 11:02:03 +01:00
Tankred Hase
0baf3fc857
Double quote escaping not required for ES6 templates 2019-02-08 10:53:26 +01:00
Tankred Hase
92df122435
Fix integration tests for verification email matching 2019-02-08 10:53:26 +01:00
Tankred Hase
0852822055
Fix typo in email docs 2019-02-08 10:53:26 +01:00
Tankred Hase
1c53ff7f17
Fix email unit test 2019-02-08 10:53:26 +01:00
Tankred Hase
e259c0f51f
Upgrade to nodermailer@^4.0.1 2019-02-08 10:53:26 +01:00
Tankred Hase
6ec72aef06
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
2019-02-08 10:53:07 +01:00
Tankred Hase
656aa9b6ed Revert "chore(package): update mocha to version 4.0.1"
This reverts commit 190738fd0d.
2017-10-17 11:18:20 +02:00
greenkeeper[bot]
190738fd0d chore(package): update mocha to version 4.0.1
Closes #52
2017-10-17 10:53:19 +02:00
Thomas Oberndörfer
f890ffa8cd Merge pull request #54 from mailvelope/fix-travis-mongo
Fix mongo config
2017-10-17 10:23:35 +02:00
Tankred Hase
b3a2171a72 Fix mongo config
We need to update to the new mongo 3.x api since the travis build is currently broken. See: https://travis-ci.org/mailvelope/keyserver/builds/282576101#L845
2017-10-17 10:17:54 +02:00
Tankred Hase
d80108915e Merge pull request #47 from mailvelope/dev/fix-parsing
Use co-body directly instead of koa-body (which uses co-body under th…
2017-08-27 16:54:34 +08:00
Tankred Hase
b1848bf8e6 Use co-body directly instead of koa-body (which uses co-body under the hood) 2017-08-27 16:32:25 +08:00
Tankred Hase
1de83fe5d5 Merge pull request #46 from mailvelope/dev/key-update
Allow update of an email address’ key with remove/verify flow in between
2017-08-25 16:27:06 +08:00
Tankred Hase
b93db84c6a Optimize key removal during verification 2017-08-25 16:20:33 +08:00
Tankred Hase
77fc0fd195 Cleanup purge old keys 2017-08-25 16:11:35 +08:00
Tankred Hase
b738e1bc5c Allow update of an email address’ key with remove/verify flow in between 2017-08-25 14:32:39 +08:00
Tankred Hase
164585b406 Don’t use build@mailvelople.com for travis notifications. 2017-08-24 14:19:19 +08:00
Tankred Hase
2b969c0382 Merge pull request #45 from mailvelope/dev/remove-primaryEmail-param
Dev/remove primary email param
2017-08-24 14:13:26 +08:00
Tankred Hase
0400b9c9d9 Fix test 2017-08-24 13:26:39 +08:00
Tankred Hase
b74563b3ec Remove primaryEmail parameter from README 2017-08-24 12:48:48 +08:00
Tankred Hase
5fa36e6d52 Remove primaryEmail parameter from REST api. 2017-08-24 12:48:48 +08:00
Tankred Hase
4c28da4eab Add uploaded attribute to documentation. 2017-08-24 12:48:48 +08:00
Tankred Hase
1e2c85621b Remove primaryEmail parameter from public-key service. 2017-08-24 12:48:48 +08:00
Tankred Hase
aa850377d5 Ignore config/development.js 2017-08-24 12:48:06 +08:00
Tankred Hase
0d6a9fdae5 Use log level env var instead of environment config 2017-08-23 18:31:15 +08:00
Tankred Hase
258117d36d Merge pull request #44 from mailvelope/dev/purge-old-unverified-keys
Dev/purge old unverified keys
2017-08-22 15:31:09 +08:00
Tankred Hase
5b86a77338 Delete redundant test 2017-08-22 15:29:18 +08:00
Tankred Hase
fe55578268 Remove legacy support since all documents now have an uploaded flag. 2017-08-22 15:26:15 +08:00
Tankred Hase
2af8310070 Purge old/unverified keys or keys without an uploaded attribute. 2017-08-22 12:13:15 +08:00
Tankred Hase
afacbf413f Add uploaded date attribute to PGP key document in MongoDB 2017-08-22 11:26:12 +08:00
Tankred Hase
74063915c7 Remove NODE_ENV environment var in nom scripts 2017-08-18 21:36:55 +08:00
Tankred Hase
80c760681c Merge pull request #42 from mailvelope/dev/papertrail
Dev/papertrail
2017-08-18 18:25:10 +08:00
Tankred Hase
ba6f75984e Integrate winston-papertrail plugin 2017-08-18 18:04:43 +08:00
Tankred Hase
35dbc08015 Use winston instead of npmlog 2017-08-18 18:01:34 +08:00
Tankred Hase
a156f05002 Update README 2017-08-17 20:02:38 +08:00
Tankred Hase
e3a2a1ff20 Rename demo.html -> ui.html 2017-08-17 19:53:22 +08:00
Tankred Hase
7f5ad65c61 Merge pull request #41 from mailvelope/dev/app-refactor
Cleanup app/init/koa-middlewares
2017-08-17 19:34:01 +08:00
Tankred Hase
7d3a64c84d Stub log output in public-key integration test 2017-08-17 19:28:08 +08:00
Tankred Hase
95ff2d9247 Cleanup app/init/koa-middlewares 2017-08-17 19:17:36 +08:00
Tankred Hase
8c76281666 Merge pull request #40 from mailvelope/dev/async-await
Dev/async await
2017-08-17 17:46:49 +08:00
Tankred Hase
a52cef2771 Add space after async in async () => 2017-08-17 17:44:26 +08:00
Tankred Hase
4081463dfa Migrate HKP api 2017-08-17 15:37:59 +08:00
Tankred Hase
49b24a5cb4 Migrate to koa 2
Refactor rest api to async/await
2017-08-17 15:34:47 +08:00
Tankred Hase
3dfa447fcf Revert resolves/rejects changes in email unit test. 2017-08-16 17:57:33 +08:00
Tankred Hase
1557a5f925 Migrate public-key service to async/await 2017-08-16 17:55:32 +08:00
Tankred Hase
59a77fd01e Go back to sinon v1.x for now due to failing tests. 2017-08-16 17:39:55 +08:00
Tankred Hase
5778f8fa13 Migrate pgp-test to sinon sandbox 2017-08-16 17:38:19 +08:00
Tankred Hase
ba671126db Migrate email module 2017-08-16 12:27:03 +08:00
Tankred Hase
874903c64b Migrate mongo DAO 2017-08-16 12:03:32 +08:00
Tankred Hase
26807e03b1 Remove co-mocha from test setup 2017-08-16 11:50:03 +08:00
Tankred Hase
5ecc728564 Update dependencies for koa 2 and async/await 2017-08-16 11:49:43 +08:00
Tankred Hase
7178a12ed5 Activate ES2017 in eslint to allow async/await 2017-08-16 11:43:44 +08:00
Tankred Hase
158a7418d0 Merge pull request #39 from mailvelope/dev/eslint
Dev/eslint
2017-08-15 16:38:10 +08:00
Tankred Hase
20593a0adc Revert static/demo.js since it is not transpiled 2017-08-15 16:32:50 +08:00
Tankred Hase
21118c0b1d Fix string in hkp 2017-08-15 16:27:12 +08:00
Tankred Hase
c773da3f60 Fix config/development.js 2017-08-15 16:19:31 +08:00
Tankred Hase
80a8028f86 Remove jshint and jscs configs 2017-08-15 16:14:21 +08:00
Tankred Hase
d8039ea976 Stub npmlog in integration tests 2017-08-15 16:12:51 +08:00
Tankred Hase
e9251d5203 Fix eslint errors 2017-08-15 16:03:06 +08:00
Tankred Hase
750cf3d897 Use eslint instead of jscs/jshint
Add .eslint.rc and test/.eslint.rc
2017-08-15 16:02:38 +08:00
Tankred Hase
f224f32e66 Merge pull request #38 from mailvelope/dev/travis-aws-deploy
Dev/travis aws deploy
2017-08-14 19:34:27 +08:00
Tankred Hase
7800dafce3 Remove .elasticbeanstalk/config.yml and shell script for local deployment 2017-08-14 19:30:48 +08:00
Tankred Hase
bbf24d6c53 Add AWS Elastic Beanstalk deployment plugin to travis config
Run npm release script before deployment
Upgrade to node v8 in travis job
2017-08-14 19:29:16 +08:00
Tankred Hase
b9380f9f20 Add release npm script for travis deployment 2017-08-14 19:27:21 +08:00
Tankred Hase
ffbee07c5c Ignore compatible upgrades to sinon.
Closes #26
2017-08-14 16:01:27 +08:00
Tankred Hase
eb9ecea7e5 Merge pull request #35 from mailvelope/greenkeeper/chai-4.1.1
chore(package): update chai to version 4.1.1
2017-08-14 12:24:47 +08:00
Tankred Hase
2db8127931 Merge pull request #34 from mailvelope/greenkeeper/co-body-5.1.1
fix(package): update co-body to version 5.1.1
2017-08-14 12:20:03 +08:00
Tankred Hase
b397fa00cd Ignore incompatible nodemailer updates.
Nodemailer v3+ no longer has an internal template engine.
Closes #20
2017-08-14 12:18:16 +08:00
Tankred Hase
252053dd13 Ignore npm v5+ package-lock.json to let greenkeeper monitor updates. 2017-08-14 12:05:33 +08:00
Tankred Hase
d7ef68cd97 Merge pull request #19 from mailvelope/greenkeeper/supertest-3.0.0
Update supertest to the latest version 🚀
2017-08-14 12:02:09 +08:00
Tankred Hase
469afdac91 Update to current mongoldb driver 2017-08-14 11:57:55 +08:00
Tankred Hase
9f922ce116 Merge pull request #16 from mailvelope/npm_script
Replace grunt with npm scripts
2017-08-14 11:51:27 +08:00
Tankred Hase
b721dc9f9b Merge pull request #17 from mailvelope/node_v6
Use ES6 destructuring (not available in node v4)
2017-08-14 11:43:54 +08:00
greenkeeper[bot]
505b337d9a chore(package): update chai to version 4.1.1
Closes #33
2017-08-05 07:58:26 +00:00
greenkeeper[bot]
b2455393b2 fix(package): update co-body to version 5.1.1
Closes #23

https://greenkeeper.io/
2017-03-24 03:31:00 +00:00
greenkeeper[bot]
9898383230 chore(package): update supertest to version 3.0.0
https://greenkeeper.io/
2017-01-30 00:24:15 +00:00
Tankred Hase
cb37c834d8 Remove grunt from travis.yml 2017-01-21 12:16:03 +00:00
Tankred Hase
a47a0162a6 Use ES6 destructuring (not available in node v4) 2017-01-21 12:03:06 +00:00
Tankred Hase
5674a2e8c9 Replace grunt with npm scripts 2017-01-21 11:30:26 +00:00
Tankred Hase
2fcedd9f09 Merge pull request #15 from mailvelope/greenkeeper/update-all
Update dependencies to enable Greenkeeper 🌴
2017-01-20 19:24:13 +00:00
Tankred Hase
07c6ccd717 Upgrade node versions 2017-01-20 19:19:15 +00:00
Tankred Hase
41cd62668b Revert back to koa-router@5.x 2017-01-20 19:17:50 +00:00
Tankred Hase
3df1f3ad5d Fix jscs config 2017-01-20 18:51:35 +00:00
greenkeeper[bot]
4294c6c372 chore(package): update dependencies
https://greenkeeper.io/
2017-01-20 13:55:14 +00:00
Tankred Hase
66cd1bc3a0 Update api constraints in README 2016-06-30 09:56:47 +02:00
Tankred Hase
7a6b8c5d27 1.2.6 2016-06-29 11:30:33 +02:00
Tankred Hase
e8c3820c49 Revert "Set Cache-Control for static resources"
This reverts commit 7510e75688.
2016-06-29 11:29:30 +02:00
Tankred Hase
e75f37031a 1.2.5 2016-06-29 11:22:23 +02:00
Tankred Hase
7350b07540 Add imprint and privacy links to page footer 2016-06-29 11:21:34 +02:00
Thomas Oberndörfer
95b4f5e471 1.2.4 2016-06-28 12:33:48 +02:00
Tankred Hase
80425e3c58 1.2.3 2016-06-22 14:37:46 +02:00
Tankred Hase
3ffe4fa666 Update homepage text 2016-06-22 14:37:39 +02:00
Tankred Hase
7510e75688 Set Cache-Control for static resources 2016-06-22 14:37:24 +02:00
Tankred Hase
ebc7dd9ada 1.2.2 2016-06-21 08:32:40 +02:00
Tankred Hase
92e5db544a Cleanup src/app.js 2016-06-21 08:32:19 +02:00
Tankred Hase
7920eebd4b Remve empty line in test 2016-06-17 20:07:18 +02:00
Tankred Hase
a273e378d9 1.2.1 2016-06-17 19:56:47 +02:00
Tankred Hase
516df397c2 Fix bug that prevented verification of multiple user ids 2016-06-17 19:56:33 +02:00
Tankred Hase
7c16ccd40a 1.2.0 2016-06-14 20:30:11 +02:00
Tankred Hase
615da99b77 Delete libs from repo 2016-06-14 20:27:29 +02:00
Tankred Hase
f33f45dde9 Load demo page libs via CDN with subresource integrity 2016-06-14 20:27:29 +02:00
Tankred Hase
af086e705d Do npm install after release to AWS 2016-06-14 20:27:29 +02:00
Tankred Hase
2bddbd6a93 Update demo to reflect key removal api change 2016-06-14 16:47:54 +02:00
Tankred Hase
7d93b882a5 Use DELETE method instead of GET for key removal 2016-06-14 16:46:17 +02:00
Tankred Hase
1a7b57777b 1.1.0 2016-06-14 13:16:48 +02:00
Tankred Hase
1ab934da31 Check for already verified user id on publicKey.verify() 2016-06-14 13:13:34 +02:00
Tankred Hase
a60d8b86a9 Cleanup REST api and use 'op' query param for verbs 2016-06-14 12:10:53 +02:00
Tankred Hase
b29c8308a8 Remove share link (redundant to /pks/lookup) 2016-06-14 10:55:27 +02:00
Tankred Hase
e09454242d Purge key by keyID before persisting new key 2016-06-14 10:42:53 +02:00
Tankred Hase
d4c1e7ba06 Fix links 2016-06-13 12:52:00 +02:00
68 changed files with 5238 additions and 1601 deletions

2
.dockerignore Normal file
View File

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

View File

@ -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
View 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
}

View 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
View File

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

View File

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

View File

@ -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
}
}

View File

@ -1,17 +1,30 @@
sudo: false
language: node_js language: node_js
node_js: node_js:
- "4" - "10"
- "5"
- "6" env:
before_script: - NODE_ENV=integration LOG_LEVEL=warn MONGO_URI=127.0.0.1:27017/test_db MONGO_USER=travis MONGO_PASS=test
- npm install -g grunt-cli
- sleep 15
- mongo test_db --eval 'db.addUser("travis", "test");'
notifications:
email:
- build@mailvelope.com
services: services:
- mongodb - mongodb
env:
- 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
View 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
View 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" ]

View File

@ -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
View File

@ -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 #### Accepted `search` parameters
* Email addresses * Email addresses
@ -52,7 +57,7 @@ The HKP apis are not documented here. Please refer to the [HKP specification](ht
#### Accepted `options` parameters #### Accepted `options` parameters
* mr * mr
## REST api ## REST API
### Lookup a key ### Lookup a key
@ -74,12 +79,6 @@ GET /api/v1/key?fingerprint=e3317db04d3958fd5f662c37b8e4105cc9dedc77
GET /api/v1/key?email=user@example.com GET /api/v1/key?email=user@example.com
``` ```
#### By email address (shorthand link for sharing)
```
GET /user/user@example.com
```
#### Payload (JSON): #### Payload (JSON):
```json ```json
@ -125,48 +124,48 @@ POST /api/v1/key
```json ```json
{ {
"publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----", "publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK----- ... -----END PGP PUBLIC KEY BLOCK-----"
"primaryEmail": "user@example.com"
} }
``` ```
* **publicKeyArmored**: The ascii armored public PGP key to be uploaded * **publicKeyArmored**: The ascii armored public PGP key to be uploaded
* **primaryEmail (optional)**: The ascii armored block is parsed to check for user ids, so this parameter is purely optional. Normally a verification email is sent to every user id found in the pgp key. To prevent this behaviour, user agents can specify the user's primary email address to send out only one email.
E.g. to upload a key from shell:
```bash
curl https://keys.mailvelope.com/api/v1/key --data "{\"publicKeyArmored\":\"$( \
gpg --armor --export-options export-minimal --export $GPGKEYID | sed ':a;N;$!ba;s/\n/\\n/g' \
)\"}"
```
### Verify uploaded key ### 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 ### Request key removal
#### Via delete request
``` ```
DELETE /api/v1/key?keyId=b8e4105cc9dedc77 OR ?email=user@example.com DELETE /api/v1/key?keyId=b8e4105cc9dedc77 OR ?email=user@example.com
``` ```
#### 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
``` The server is written is in JavaScript ES7 and runs on [Node.js](https://nodejs.org/) v8+.
GET /api/v1/verifyRemove?keyId=b8e4105cc9dedc77&nonce=6a314915c09368224b11df0feedbc53c
``` It uses [MongoDB](https://www.mongodb.com/) v3.2+ as its database.
# Getting started
## Installation
# Development ### Node.js (Mac OS)
The server is written is in JavaScript ES6 and runs on [Node.js](https://nodejs.org/) v4+. It uses [MongoDB](https://www.mongodb.com/) v2.4+ as its database.
## Install Node.js (Mac OS)
This is how to install node on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [Node.js download page](https://nodejs.org/en/download/). This is how to install node on Mac OS using [homebrew](http://brew.sh/). For other operating systems, please refer to the [Node.js download page](https://nodejs.org/en/download/).
@ -175,7 +174,7 @@ brew update
brew install node 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/). 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 brew services start mongodb
``` ```
Now you can use the `mongo` CLI client to create a new test database. **The username and password used here match the ones in the `config/development.js` file. Be sure to change them for production use**: Now you can use the `mongo` CLI client to create a new test database. The username and password used here match the ones in the `config/development.js` file. **Be sure to change them for production use**:
```shell ```shell
mongo mongo
@ -199,31 +198,46 @@ use keyserver-test
db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite", db:"keyserver-test" }] }) db.createUser({ user:"keyserver-user", pwd:"trfepCpjhVrqgpXFWsEF", roles:[{ role:"readWrite", db:"keyserver-test" }] })
``` ```
## Setup SMTP user ### Dependencies
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
```shell ```shell
npm install && npm test npm install
``` ```
## Start local server ## Configuration
```shell 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)!
npm start
```
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_URI=127.0.0.1:27017/test_db
* MONGO_USER=db_user * MONGO_USER=db_user
* MONGO_PASS=db_password * 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_TLS=true
* SMTP_STARTTLS=true * SMTP_STARTTLS=true
* SMTP_PGP=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_USER=smtp_user
* SMTP_PASS=smtp_pass * SMTP_PASS=smtp_pass
* SENDER_NAME="OpenPGP Key Server" * SENDER_NAME="OpenPGP Key Server"
* SENDER_EMAIL=noreply@example.com * 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)) * PUBLIC_KEY_PURGE_TIME=**30**
* HTTPS_KEY_PIN=base64_encoded_sha256 (optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning)) (number of days after which uploaded keys are deleted if they have not been verified)
* HTTPS_KEY_PIN_BACKUP=base64_encoded_sha256 (optional, see [HPKP](https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning))
### 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
```

View File

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

View File

@ -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'
}
}
};

View File

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

View File

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

26
docker-compose.yml Normal file
View 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
View 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

View File

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

10
locales/de.json Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,47 @@
{ {
"name": "mailvelope-keyserver", "name": "mailvelope-keyserver",
"version": "1.0.0", "version": "3.0.0",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/mailvelope/keyserver.git" "url": "https://github.com/mailvelope/keyserver.git"
}, },
"engines": { "engines": {
"node": ">=4" "node": ">=10",
"npm": ">=6"
}, },
"scripts": { "scripts": {
"start": ": ${NODE_ENV=development} && node index.js", "start": "node index.js",
"test": ": ${NODE_ENV=development} && grunt test" "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": { "dependencies": {
"addressparser": "^1.0.1", "co-body": "6.1.0",
"co": "^4.6.0", "config": "3.3.6",
"co-body": "^4.2.0", "koa": "2.13.1",
"config": "^1.20.4", "koa-ejs": "4.3.0",
"koa": "^1.2.0", "koa-locales": "1.12.0",
"koa-router": "^5.4.0", "koa-router": "10.0.0",
"koa-static": "^2.0.0", "koa-static": "5.0.0",
"mongodb": "^2.1.20", "mongodb": "3.6.6",
"nodemailer": "^2.4.2", "nodemailer": "6.6.0",
"nodemailer-openpgp": "^1.0.2", "openpgp": "4.5.5",
"npmlog": "^2.0.4", "winston": "3.3.3",
"openpgp": "^2.3.0" "winston-papertrail": "1.0.5"
}, },
"devDependencies": { "devDependencies": {
"chai": "^3.5.0", "bootstrap": "^3.4.1",
"co-mocha": "^1.1.2", "chai": "^4.3.4",
"grunt": "^1.0.1", "chai-as-promised": "^7.1.1",
"grunt-contrib-jshint": "^1.0.0", "eslint": "^7.26.0",
"grunt-jscs": "^2.8.0", "jquery": "^3.6.0",
"grunt-mocha-test": "^0.12.7", "mocha": "^8.4.0",
"mocha": "^2.5.3", "sinon": "^10.0.0",
"sinon": "^1.17.4", "supertest": "^6.1.3"
"supertest": "^1.2.0"
} }
} }

View File

@ -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

View File

@ -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
View 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
View 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);
}
};

View File

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

52
src/dao/papertrail.js Normal file
View 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()
)
}));
}
};

View File

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

21
src/email/templates.js Normal file
View 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};

View File

@ -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
View 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;
}
})();

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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>&copy; 2016 Mailvelope GmbH</p>
</footer>
</div> <!-- /container -->
</body>
</html>

View File

@ -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>&copy; 2016 Mailvelope GmbH</p>
</footer>
</div> <!-- /container -->
</body>
</html>

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
View 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
View 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>&copy; 2021 Mailvelope GmbH</p>
</footer>

46
src/view/index.html Normal file
View 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
View 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
View 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
View 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>

View 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 -->

View 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
View 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
View File

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

261
test/fixtures/key3.asc vendored Normal file
View 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
View 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
View 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-----

View File

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

View File

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

View File

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

View File

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

View File

@ -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
View 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;

View File

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

View File

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

View File

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