├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── database.js ├── helpers ├── formatFingerprint.js └── logger.js ├── html ├── faq.pug ├── index.pug ├── remove.pug ├── search.pug ├── static │ ├── icon.png │ └── logotype.png └── style.css ├── index.js ├── nodes.md ├── package.json ├── routes ├── fetch.js ├── publish.js ├── requestRemove.js ├── search.js └── verifyRemove.js ├── test.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test 38 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | *.pug 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:prettier/recommended" 5 | ], 6 | 7 | "rules": { 8 | "no-console": ["error", { "allow": ["warn", "error"] }], 9 | "jsx-quotes": [2, "prefer-double"], 10 | "object-curly-spacing": ["error", "always"] 11 | }, 12 | "env": { 13 | "es6": true, 14 | "node": true 15 | }, 16 | "parserOptions": { 17 | "sourceType": "module", 18 | "ecmaVersion": 2018, 19 | "ecmaFeatures": { 20 | "jsx": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .eslintcache 25 | 26 | *.log 27 | 28 | testdb/ 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tom Snelling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logotype](https://raw.githubusercontent.com/tdjsnelling/dat-keyserver/master/html/static/logotype.png) 2 | 3 | # dat-keyserver 4 | 5 | [![CircleCI](https://circleci.com/gh/tdjsnelling/dat-keyserver.svg?style=svg)](https://circleci.com/gh/tdjsnelling/dat-keyserver) 6 | 7 | A distributed PGP keyserver project based on the dat protocol. 8 | 9 | - [Introduction](#introduction) 10 | - [Pools](#pools) 11 | - [Removing keys](#removing-keys) 12 | - [Installation](#installation) 13 | - [Usage](#usage) 14 | - [Pools](#pools-1) 15 | - [Port](#port) 16 | - [Discovery](#discovery) 17 | - [Seeding](#seeding) 18 | - [Database location](#database-location) 19 | - [Nodes](#nodes) 20 | - [License](#license) 21 | 22 | ## Introduction 23 | 24 | This project provides an OpenPGP keyserver that is fast, easy to set up, and fully decentralized. A key submitted to any server will be propagated to all other servers within the same pool, meaning each server stores the full set of submitted keys at all times. If a server fails or is otherwise no longer running, the keys submitted to that server are not lost and will still be available at all other servers in the pool. 25 | 26 | #### Pools 27 | 28 | A pool is a group of servers that share their set of data. A server operator has the choice to join an exisiting pool or create a new one. There is a 'master pool' which most servers should join, but should a company/organisation/other group of individuals want to run their own pool with a specific set of keys, then they can do so without other unwanted keys ending up on their servers. 29 | 30 | #### Removing keys 31 | 32 | `dat-keyserver` provides an important feature that `sks-keyserver` does not - the ability to remove keys. If a user can prove that a key belongs to them (by signing a message with their private key) then they are able to remove their public key with no interaction needed from the server operator. Once a key is removed, it is removed from all servers in the pool. 33 | 34 | ## Installation 35 | 36 | Clone this repo and `npm install` to install dependencies (`yarn` is fine too). 37 | 38 | ## Usage 39 | 40 | To start a new pool with no data (you probably don't want to do this) then run: 41 | 42 | ``` 43 | npm start 44 | ``` 45 | 46 | If you want to keep `dat-keyserver` running in the background, then you can use something like [PM2](http://pm2.keymetrics.io/). 47 | 48 | #### Pools 49 | 50 | If you want to join an existing pool then pass the `-k` option: 51 | 52 | ``` 53 | npm start -- -k [POOL_KEY] 54 | ``` 55 | 56 | If you come across a pool you wish to join but don't know the key, then you can navigate to `/key` to find it. I currently have a public pool with key `9ceccb8abeaba2868fe22d14605790b0b84ac58aba3e48606a710f4d33c5a4f7`. 57 | 58 | #### Port 59 | 60 | By default, `dat-keyserver` runs on port 4000. To change this, pass the `-p` option: 61 | 62 | ``` 63 | npm start -- -p 8080 64 | ``` 65 | 66 | #### Discovery 67 | 68 | In order for your node to be able to discover others, you must have at least one of the [discovery ports](https://github.com/datproject/hyperdiscovery/blob/238c0ae274222fa1fbc536c965dac8af03fcdac3/index.js#L13) open and useable on your machine. At the time of writing, these are `3282`, `3000`, `3002`, `3004`, `2001`, `2003` & `2005`. 69 | 70 | #### Seeding 71 | 72 | If you would just like to run a 'seed' node, pass the `-s` option when you start the server. Your node will still hold and replicate data, and thus aid the network, but will not expose a web interface. 73 | 74 | #### Database location 75 | 76 | By default, `dat-keyserver` will create it's database in `~/.datkeyserver/`. If you want to change the location of the database, pass the `-d` option. For example: 77 | 78 | ``` 79 | npm start -- -d my-custom-pool/ 80 | ``` 81 | 82 | Use this if you want to your node to join a new pool, but don't want to lose data from a previous pool (data from different pools cannot be stored within the same directory). The directory will be created if it does not exist. 83 | 84 | ## Nodes 85 | 86 | For a list of existing nodes, see [nodes.md](nodes.md). If you run a node and want to add it to the list, please submit a pull request. 87 | 88 | ## License 89 | 90 | MIT 91 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | /* 2 | database.js 3 | 4 | Hyperdb storage and authorization logic. If a pool key was supplied, then 5 | initiate our hyperdb using that key. If not, then initiate using our local 6 | key. When a peer connects, check if the peer is authorized and if not then 7 | authorize them so any changed will propogate. 8 | */ 9 | 10 | const hyperdb = require('hyperdb') 11 | const hyperdiscovery = require('hyperdiscovery') 12 | const path = require('path') 13 | const args = require('minimist')(process.argv.slice(2)) 14 | const logger = require('./helpers/logger') 15 | 16 | const homeDir = require('os').homedir() 17 | let appDir = args.d ? args.d : path.resolve(homeDir, '.datkeyserver') 18 | 19 | if (process.env.NODE_ENV === 'test') { 20 | appDir = 'testdb' 21 | } 22 | 23 | let db, swarm 24 | 25 | if (args.k) { 26 | db = hyperdb(path.resolve(appDir, 'keys.db'), args.k, { 27 | valueEncoding: 'json' 28 | }) 29 | } else { 30 | db = hyperdb(path.resolve(appDir, 'keys.db'), { valueEncoding: 'json' }) 31 | } 32 | 33 | db.on('ready', () => { 34 | swarm = hyperdiscovery(db) 35 | logger.info(`database ${db.key.toString('hex')} ready`) 36 | 37 | if (!args.k) { 38 | logger.info(`your pool key is ${db.key.toString('hex')}`) 39 | } 40 | 41 | db.put('/t', { t: new Date().getTime() }) 42 | 43 | swarm.on('connection', (peer, type) => { 44 | logger.info( 45 | `a peer at ${type.host} connected. ${swarm.connections.length} total` 46 | ) 47 | 48 | db.authorized(peer.remoteUserData, (err, auth) => { 49 | if (err) { 50 | logger.error(`${err}`) 51 | } else { 52 | if (!auth) { 53 | db.authorize(peer.remoteUserData, err => { 54 | if (!err) { 55 | logger.info(`peer at ${type.host} was authorised`) 56 | } 57 | }) 58 | } 59 | } 60 | }) 61 | 62 | peer.on('close', () => { 63 | logger.info(`a peer at ${type.host} disconnected`) 64 | }) 65 | }) 66 | }) 67 | 68 | const getSwarm = () => swarm 69 | 70 | module.exports = { db, getSwarm } 71 | -------------------------------------------------------------------------------- /helpers/formatFingerprint.js: -------------------------------------------------------------------------------- 1 | /* 2 | formatFingerprint.js 3 | 4 | Small helper function to change fingerprints into a more machine-readable 5 | format. Removes spaces and converts to lower case. 6 | */ 7 | 8 | const formatFingerprint = fingerprint => 9 | fingerprint.replace(' ', '').toLowerCase() 10 | 11 | module.exports = formatFingerprint 12 | -------------------------------------------------------------------------------- /helpers/logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | logger.js 3 | 4 | Helper for global logging. Logs errors to error.log, all output to 5 | combined.log, and if we aren't in a production environment log everything to 6 | the console as well. 7 | */ 8 | 9 | const winston = require('winston') 10 | 11 | const logger = winston.createLogger({ 12 | level: 'info', 13 | format: winston.format.combine( 14 | winston.format.timestamp({ 15 | format: 'YYYY-MM-DD HH:mm:ss' 16 | }), 17 | winston.format.json() 18 | ), 19 | defaultMeta: { service: 'dat-keyserver' }, 20 | transports: [ 21 | new winston.transports.File({ filename: 'error.log', level: 'error' }), 22 | new winston.transports.File({ filename: 'combined.log' }) 23 | ] 24 | }) 25 | 26 | if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test') { 27 | logger.add( 28 | new winston.transports.Console({ 29 | format: winston.format.combine( 30 | winston.format.colorize(), 31 | winston.format.simple() 32 | ) 33 | }) 34 | ) 35 | } 36 | 37 | module.exports = logger 38 | -------------------------------------------------------------------------------- /html/faq.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | meta(name='viewport', content='width=device-width, initial-scale=1') 4 | link(rel='icon' href='/icon.png') 5 | title FAQ / dat-keyserver 6 | style 7 | include style.css 8 | body 9 | h1 10 | a(href='/') dat-keyserver 11 | span  (#{version}) 12 | em 13 | | a distributed PGP keyserver project based on the  14 | a(href='https://datproject.org', target='_blank', rel='nofollow norefferer') dat protocol 15 | hr 16 | ul 17 | li 18 | strong What? 19 | p dat-keyserver is a distributed PGP keyserver project based on the  20 | a(href='https://datproject.org', target='_blank', rel='nofollow norefferer') dat protocol 21 | | . 22 | li 23 | strong Updating keys 24 | p To update your public key, just publish it again and any existing key with the same fingerprint will be overwritten. 25 | li 26 | strong Removing keys 27 | p In order to comply with privacy regulations, a key can be removed as long as a user can prove that the key belongs to them. They can prove this by clearsigning a message with their private key, and then the server can verify this signed message against their public key. If the signed message cannnot be verified, then the public key does not belong to them. A signed message can be generated like so: 28 | code echo "Hello, world" | gpg --clearsign 29 | p Despite this, it is preferable that a user generates a  30 | a(href='http://www.pgp.net/pgpnet/pgp-faq/pgp-faq-key-revocation.html', target='_blank', rel='nofollow norefferer') revocation certificate 31 | |  and uploads that if their key is no longer in use, rather than just removing it. This will let other people know that the key exists but should not be used any more. 32 | 33 | li 34 | strong What are ‘pools’? 35 | p A pool is made up of multiple nodes. Any key uploaded to a node will be shared to all other nodes within the pool that that node belongs to. Keys are not shared between pools. 36 | p The ‘master’ pool, which is intended to be the largest public pool, has the key  37 | em 9ceccb8abeaba2868fe22d14605790b0b84ac58aba3e48606a710f4d33c5a4f7 38 | | . This is the pool that almost all nodes will want to join. 39 | p However, if organisations / companies / other groups want to have their own (private) pools, then they can do this without having unwanted keys being shared between the nodes in their pool. 40 | li 41 | strong How is this software licensed? 42 | p dat-keyserver is provided under the MIT license. View the source code on  43 | a(href='https://github.com/tdjsnelling/dat-keyserver', target='_blank', rel='nofollow norefferer') GitHub 44 | | . 45 | li 46 | strong I want to run a node! 47 | p See the GitHub repo for instructions on how to get the project running and join a pool 48 | li 49 | strong I have found a bug / have a feature suggestion 50 | p Please follow the link above to the GitHub repo and create an issue. 51 | -------------------------------------------------------------------------------- /html/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | meta(name='viewport', content='width=device-width, initial-scale=1') 4 | link(rel='icon' href='/icon.png') 5 | title dat-keyserver 6 | style 7 | include style.css 8 | body 9 | h1 10 | a(href='/') dat-keyserver 11 | span  (#{version}) 12 | em 13 | | a distributed PGP keyserver project based on the  14 | a(href='https://datproject.org', target='_blank', rel='nofollow norefferer') dat protocol 15 | hr 16 | form(action='/search') 17 | h2 Search 18 | p Search by user ID (name, comment, email) or key ID 19 | input(type='text', name='query', size='80', required='') 20 | br 21 | button Search 22 | hr 23 | form(action='/fetch') 24 | h2 Fetch 25 | p Fetch by fingerprint 26 | input(type='text', name='fingerprint', size='80', required='') 27 | br 28 | button Fetch 29 | hr 30 | form(action='/publish', method='POST') 31 | h2 Publish 32 | p Enter ASCII-armored public key 33 | textarea(name='key', cols='80', rows='30', required='') 34 | br 35 | button Submit 36 | hr 37 | form(action='/remove/request' method='POST') 38 | h2 Remove 39 | p Key fingerprint 40 | input(type='text', name='fingerprint', size='80', required='') 41 | br 42 | button Remove 43 | hr 44 | p 45 | | Pool key: #{key} / #{peers} peers /  46 | a(href='/faq') FAQ 47 | |  /  48 | a(href='https://github.com/tdjsnelling/dat-keyserver', target='_blank', rel='nofollow norefferer') Source 49 | 50 | 51 | -------------------------------------------------------------------------------- /html/remove.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | meta(name='viewport', content='width=device-width, initial-scale=1') 4 | link(rel='icon' href='/icon.png') 5 | title Remove key / dat-keyserver 6 | style 7 | include style.css 8 | body 9 | h1 10 | a(href='/') dat-keyserver 11 | span  (#{version}) 12 | em 13 | | a distributed PGP keyserver project based on the  14 | a(href='https://datproject.org', target='_blank', rel='nofollow norefferer') dat protocol 15 | hr 16 | h2 Remove a key 17 | p Clearsign the following message: 18 | code #{message} 19 | p and enter the output below. 20 | form(action='/remove/verify' method='POST') 21 | input(name='fingerprint', size='80', value=fingerprint, required='' style='display:none') 22 | textarea(name='message', cols='80', rows='30', required='') 23 | br 24 | button Submit 25 | -------------------------------------------------------------------------------- /html/search.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | meta(name='viewport', content='width=device-width, initial-scale=1') 4 | link(rel='icon' href='/icon.png') 5 | title Search results for “#{query}” / dat-keyserver 6 | style 7 | include style.css 8 | body 9 | h1 10 | a(href='/') dat-keyserver 11 | span  (#{version}) 12 | h2 Search results for “#{query}” 13 | hr 14 | if results.length 15 | ul.searchResults 16 | each item in results 17 | li 18 | p 19 | strong pub 20 | | #{item.algorithm.bits}/ 21 | a(href=`/fetch?fingerprint=${item.fingerprint.slice(-16)}`)= item.fingerprint.slice(-8).toUpperCase() 22 | span (cr. 23 | | #{item.created.split('T')[0]} 24 | span , exp. 25 | | #{item.expiry ? item.expiry.split('T')[0] : 'n/a'}) #{item.fingerprint.toUpperCase()} 26 | ul 27 | each user in item.users 28 | li 29 | p 30 | strong uid  31 | span.uid #{user.userId} 32 | ul 33 | each sig in user.signatures 34 | li sig #{sig.keyId.slice(-8).toUpperCase()} 35 | span (cr. 36 | | #{sig.created.split('T')[0]} 37 | span , exp. 38 | | #{sig.expiry ? sig.expiry.split('T')[0] : 'n/a'})  39 | if sig.keyId.slice(-8) === item.fingerprint.slice(-8) 40 | | [selfsig] 41 | else 42 | a(href=`/search?query=${sig.keyId.slice(-16)}`)= sig.keyId.slice(-16).toUpperCase() 43 | ul 44 | each subkey in item.subkeys 45 | li 46 | p 47 | strong sub 48 | | #{subkey.algorithm.bits}/#{subkey.fingerprint.slice(-8).toUpperCase()} (cr. #{subkey.created.split('T')[0]}) 49 | ul 50 | each sig in subkey.signatures 51 | li sig #{sig.keyId.slice(-8).toUpperCase()} 52 | span (cr. 53 | | #{sig.created.split('T')[0]} 54 | span , exp. 55 | | #{sig.expiry ? sig.expiry.split('T')[0] : 'n/a'}) 56 | else 57 | p No results for query “#{query}”. 58 | -------------------------------------------------------------------------------- /html/static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdjsnelling/dat-keyserver/80b93bf51391e346eebfb6926da34940335f9121/html/static/icon.png -------------------------------------------------------------------------------- /html/static/logotype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tdjsnelling/dat-keyserver/80b93bf51391e346eebfb6926da34940335f9121/html/static/logotype.png -------------------------------------------------------------------------------- /html/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Courier', monospace; 3 | font-size: 14px; 4 | margin: 16px; 5 | } 6 | h1 span { 7 | color: #999; 8 | font-size: 0.8em; 9 | } 10 | h2 { 11 | font-size: 18px; 12 | } 13 | input, 14 | textarea { 15 | margin-bottom: 1em; 16 | max-width: 100%; 17 | } 18 | hr { 19 | background: 0; 20 | border: 0; 21 | border-top: 1px solid #999; 22 | margin: 16px 0; 23 | } 24 | ul.searchResults { 25 | padding-left: 16px; 26 | } 27 | ul.searchResults > li { 28 | margin-bottom: 32px; 29 | } 30 | ul.searchResults p { 31 | margin: 0.5em 0; 32 | } 33 | span.uid { 34 | color: coral; 35 | } 36 | code { 37 | background: #dfdfdf; 38 | display: inline-block; 39 | padding: 8px; 40 | } 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | index.js 3 | 4 | Entry point for the project. Here we set up our Express routes and expose 5 | a port for the server to run on. 6 | */ 7 | 8 | const express = require('express') 9 | const app = express() 10 | const bodyParser = require('body-parser') 11 | const args = require('minimist')(process.argv.slice(2)) 12 | const pkg = require('./package.json') 13 | const logger = require('./helpers/logger') 14 | const { db, getSwarm } = require('./database') 15 | 16 | const publish = require('./routes/publish') 17 | const fetch = require('./routes/fetch') 18 | const search = require('./routes/search') 19 | const requestRemove = require('./routes/requestRemove') 20 | const verifyRemove = require('./routes/verifyRemove') 21 | 22 | if (args.h) { 23 | // eslint-disable-next-line 24 | console.log( 25 | `dat-keyserver: a distributed PGP keyserver project based on the dat protocol 26 | 27 | Options: 28 | -k : the key of the pool you want to join 29 | -p : the port to run the HTTP server on (4000) 30 | -s : run in seeding mode (false) 31 | -d : path to the folder in which you want to store the database (~/.datkeyserver) 32 | 33 | See README.md for more information.` 34 | ) 35 | process.exit(0) 36 | } 37 | 38 | // Express setup 39 | app.set('view engine', 'pug') 40 | app.set('views', './html') 41 | app.use(express.static('html/static')) 42 | app.use(bodyParser.urlencoded({ extended: true })) 43 | 44 | // Render index page 45 | app.get('/', (req, res) => { 46 | res.render('index', { 47 | version: pkg.version, 48 | key: db.key.toString('hex'), 49 | peers: getSwarm().connections.length 50 | }) 51 | }) 52 | 53 | // Render FAQ page 54 | app.get('/faq', (req, res) => { 55 | res.render('faq', { version: pkg.version }) 56 | }) 57 | 58 | // HTTP route to get pool key 59 | app.get('/key', (req, res) => { 60 | res.send(`
${db.key.toString('hex')}
`) 61 | }) 62 | 63 | // HTTP route to publish a new public key 64 | app.post('/publish', publish) 65 | 66 | // HTTP route to fetch a pubilc key 67 | app.get('/fetch', fetch) 68 | 69 | // HTTP route to search keys for a specific query 70 | app.get('/search', search) 71 | 72 | // HTTP route to request removal of a key 73 | app.post('/remove/request', requestRemove) 74 | 75 | // HTTP route to verify the removal of a key 76 | app.post('/remove/verify', verifyRemove) 77 | 78 | if (!args.s) { 79 | // Start the HTTP server 80 | const port = args.p || 4000 81 | app.listen(port, () => logger.info(`dat-keyserver started on port ${port}`)) 82 | } 83 | 84 | module.exports = app 85 | -------------------------------------------------------------------------------- /nodes.md: -------------------------------------------------------------------------------- 1 | # List of existing dat-keyserver nodes 2 | 3 | | URL | Key | 4 | | --- | --- | 5 | | [https://keys.tdjs.tech](https://keys.tdjs.tech) | `9ceccb8abeaba2868fe22d14605790b0b84ac58aba3e48606a710f4d33c5a4f7` | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dat-keyserver", 3 | "version": "1.6.0", 4 | "description": "a distributed PGP keyserver project based on the dat protocol", 5 | "main": "index.js", 6 | "repository": "https://github.com/tdjsnelling/dat-keyserver", 7 | "author": "Tom Snelling (@tdjsnelling)", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "nodemon index.js", 11 | "test": "mocha --timeout 10000" 12 | }, 13 | "dependencies": { 14 | "body-parser": "^1.18.3", 15 | "express": "^4.16.4", 16 | "hyperdb": "^3.5.0", 17 | "hyperdiscovery": "^8.0.0", 18 | "minimist": "^1.2.0", 19 | "openpgp": "^4.4.10", 20 | "pug": "^2.0.3", 21 | "winston": "^3.2.1" 22 | }, 23 | "devDependencies": { 24 | "chai": "^4.2.0", 25 | "chai-http": "^4.3.0", 26 | "cheerio": "^1.0.0-rc.3", 27 | "eslint": "^5.16.0", 28 | "eslint-config-prettier": "^4.1.0", 29 | "eslint-plugin-prettier": "^3.0.1", 30 | "mocha": "^6.2.0", 31 | "nodemon": "^1.18.11", 32 | "prettier": "^1.16.4", 33 | "rimraf": "^3.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /routes/fetch.js: -------------------------------------------------------------------------------- 1 | /* 2 | fetch.js 3 | 4 | GET route to return then public key of given fingerprint 5 | 6 | Lookup a fingerprint in our hyperdb and return the associated public key. If 7 | not then return a Not Found response. 8 | */ 9 | 10 | const { db } = require('../database') 11 | const logger = require('../helpers/logger') 12 | const formatFingerprint = require('../helpers/formatFingerprint') 13 | 14 | const fetch = (req, res) => { 15 | if (req.query.fingerprint) { 16 | db.get( 17 | `/${formatFingerprint(req.query.fingerprint).slice(-16)}`, 18 | (err, nodes) => { 19 | if (!err) { 20 | if (nodes[0]) { 21 | logger.info( 22 | `fetched key ${formatFingerprint(req.query.fingerprint)}` 23 | ) 24 | res.send(`
\n${nodes[0].value.key}\n
`) 25 | } else { 26 | logger.info( 27 | `key ${formatFingerprint(req.query.fingerprint)} not found` 28 | ) 29 | res.sendStatus(404) 30 | } 31 | } else { 32 | logger.error(`${err}`) 33 | res.sendStatus(500) 34 | } 35 | } 36 | ) 37 | } else { 38 | res.sendStatus(400) 39 | } 40 | } 41 | 42 | module.exports = fetch 43 | -------------------------------------------------------------------------------- /routes/publish.js: -------------------------------------------------------------------------------- 1 | /* 2 | publish.js 3 | 4 | POST route to publish a new public key. 5 | 6 | Here we recieve the public key, parse out all of the useful information, 7 | and store it in the hyperdb. 8 | */ 9 | 10 | const openpgp = require('openpgp') 11 | const { db } = require('../database') 12 | const logger = require('../helpers/logger') 13 | 14 | const publish = async (req, res) => { 15 | if (req.body.key) { 16 | const result = await openpgp.key.readArmored(req.body.key) 17 | 18 | if (result.err) { 19 | res.sendStatus(500) 20 | return 21 | } 22 | 23 | let entry = {} 24 | entry.key = req.body.key 25 | entry.fingerprint = result.keys[0].getFingerprint() 26 | entry.created = result.keys[0].getCreationTime() 27 | entry.userIds = result.keys[0].getUserIds() 28 | entry.algorithm = result.keys[0].getAlgorithmInfo() 29 | entry.expiry = await result.keys[0].getExpirationTime() 30 | entry.users = [] 31 | entry.subkeys = [] 32 | 33 | result.keys[0].users.map(user => { 34 | const userObj = { 35 | userId: user.userId ? user.userId.userid : '[contents omitted]', 36 | signatures: [] 37 | } 38 | 39 | user.selfCertifications.map(selfCert => { 40 | userObj.signatures.push({ 41 | keyId: selfCert.issuerKeyId.toHex(), 42 | created: selfCert.created, 43 | expiry: selfCert.signatureExpirationTime 44 | }) 45 | }) 46 | 47 | user.otherCertifications.map(otherCert => { 48 | userObj.signatures.push({ 49 | keyId: otherCert.issuerKeyId.toHex(), 50 | created: otherCert.created, 51 | expiry: otherCert.signatureExpirationTime 52 | }) 53 | }) 54 | 55 | entry.users.push(userObj) 56 | }) 57 | 58 | result.keys[0].getSubkeys().map(subkey => { 59 | const subkeyObj = { 60 | created: subkey.getCreationTime(), 61 | algorithm: subkey.getAlgorithmInfo(), 62 | fingerprint: subkey.getFingerprint(), 63 | signatures: [] 64 | } 65 | 66 | subkey.bindingSignatures.map(sbind => { 67 | subkeyObj.signatures.push({ 68 | keyId: sbind.issuerKeyId.toHex(), 69 | created: sbind.created, 70 | expiry: sbind.signatureExpirationTime 71 | }) 72 | }) 73 | 74 | entry.subkeys.push(subkeyObj) 75 | }) 76 | 77 | db.put(`/${entry.fingerprint.slice(-16)}`, entry, err => { 78 | if (!err) { 79 | logger.info(`published key ${entry.fingerprint}`) 80 | res.send(`
Success! Published key ${entry.fingerprint}
`) 81 | } else { 82 | logger.error(`${err}`) 83 | res.sendStatus(500) 84 | } 85 | }) 86 | } else { 87 | // request body didn't include a public key 88 | res.sendStatus(400) 89 | } 90 | } 91 | 92 | module.exports = publish 93 | -------------------------------------------------------------------------------- /routes/requestRemove.js: -------------------------------------------------------------------------------- 1 | /* 2 | requestRemove.js 3 | 4 | POST route to start the process of removing a key and proving that the key 5 | owner is the person requesting the removal. 6 | 7 | We generate a message that includes a token. The token is the SHA256 hash of 8 | the public key that has been requested to be removed. We then render the next 9 | step of the removal process, which includes our message. 10 | */ 11 | 12 | const crypto = require('crypto') 13 | const { db } = require('../database') 14 | const pkg = require('../package.json') 15 | 16 | const requestRemove = (req, res) => { 17 | if (req.body.fingerprint) { 18 | db.get(`/${req.body.fingerprint.slice(-16)}`, (err, nodes) => { 19 | if (!err) { 20 | if (nodes[0]) { 21 | const key = nodes[0].value.key 22 | const hash = crypto 23 | .createHash('sha256') 24 | .update(key) 25 | .digest('hex') 26 | const message = `I am requesting the removal of my public key from dat-keyserver. token=${hash}` 27 | 28 | res.render('remove', { 29 | version: pkg.version, 30 | fingerprint: req.body.fingerprint, 31 | message: message 32 | }) 33 | } else { 34 | res.sendStatus(404) 35 | } 36 | } else { 37 | res.sendStatus(500) 38 | } 39 | }) 40 | } else { 41 | res.sendStatus(400) 42 | } 43 | } 44 | 45 | module.exports = requestRemove 46 | -------------------------------------------------------------------------------- /routes/search.js: -------------------------------------------------------------------------------- 1 | /* 2 | search.js 3 | 4 | GET route to return a list of keys based on a search query 5 | 6 | We have to fetch _all_ entries from the hyperdb and filter them by our search 7 | query. First we try and match our query to user ID strings, which include 8 | name, comment and email address. We also filter by key fingerprint, and only 9 | include matches if they are not already in our list of results. 10 | */ 11 | 12 | const { db } = require('../database') 13 | const logger = require('../helpers/logger') 14 | const formatFingerprint = require('../helpers/formatFingerprint') 15 | const pkg = require('../package.json') 16 | 17 | const search = (req, res) => { 18 | if (req.query.query) { 19 | let results = [] 20 | 21 | db.list('/', (err, list) => { 22 | if (!err) { 23 | for (let i in list) { 24 | const userIds = list[i][0].value.userIds 25 | 26 | for (let j in userIds) { 27 | if ( 28 | userIds[j].toLowerCase().includes(req.query.query.toLowerCase()) 29 | ) { 30 | if (!results.includes(list[i][0].value)) { 31 | results.push(list[i][0].value) 32 | } 33 | } 34 | } 35 | 36 | if (list[i][0].value.fingerprint) { 37 | if ( 38 | list[i][0].value.fingerprint 39 | .toLowerCase() 40 | .includes(formatFingerprint(req.query.query)) && 41 | !results.includes(list[i][0].value) 42 | ) { 43 | results.push(list[i][0].value) 44 | } 45 | } 46 | } 47 | 48 | logger.info( 49 | `search for ${req.query.query} returned ${results.length} results` 50 | ) 51 | res.render('search', { 52 | version: pkg.version, 53 | query: req.query.query, 54 | results: results 55 | }) 56 | } else { 57 | logger.error(`${err}`) 58 | } 59 | }) 60 | } else { 61 | res.sendStatus(400) 62 | } 63 | } 64 | 65 | module.exports = search 66 | -------------------------------------------------------------------------------- /routes/verifyRemove.js: -------------------------------------------------------------------------------- 1 | /* 2 | verifyRemove.js 3 | 4 | POST route to remove a key given a fingerprint and a signed message as 5 | conformation 6 | 7 | First, we fetch the public key of the given fingerprint. Then we use it to 8 | verify if the signed message came from the owner of the paired private key. 9 | If so, then we delete the key from the hyperdb. If not, we send an 10 | Unauthorized response. 11 | */ 12 | 13 | const openpgp = require('openpgp') 14 | const crypto = require('crypto') 15 | const { db } = require('../database') 16 | const logger = require('../helpers/logger') 17 | const formatFingerprint = require('../helpers/formatFingerprint') 18 | 19 | const remove = (req, res) => { 20 | if (req.body.fingerprint && req.body.message) { 21 | db.get( 22 | `/${formatFingerprint(req.body.fingerprint).slice(-16)}`, 23 | async (err, nodes) => { 24 | if (!err) { 25 | if (nodes[0]) { 26 | let message 27 | try { 28 | message = await openpgp.cleartext.readArmored(req.body.message) 29 | } catch (e) { 30 | res.sendStatus(500) 31 | return 32 | } 33 | 34 | const key = await openpgp.key.readArmored(nodes[0].value.key) 35 | 36 | const correctMessageContent = message.text.startsWith( 37 | 'I am requesting the removal of my public key from dat-keyserver. token=' 38 | ) 39 | 40 | const submittedToken = message.text.slice(-64) 41 | const actualToken = crypto 42 | .createHash('sha256') 43 | .update(nodes[0].value.key) 44 | .digest('hex') 45 | 46 | const options = { 47 | message: message, 48 | publicKeys: key.keys 49 | } 50 | const verified = await openpgp.verify(options) 51 | const signatureValid = verified.signatures[0].valid 52 | 53 | if ( 54 | correctMessageContent && 55 | submittedToken === actualToken && 56 | signatureValid 57 | ) { 58 | db.del( 59 | `/${formatFingerprint(req.body.fingerprint).slice(-16)}`, 60 | err => { 61 | if (!err) { 62 | logger.info( 63 | `key ${formatFingerprint( 64 | req.body.fingerprint 65 | )} was removed successfully` 66 | ) 67 | res.send( 68 | `
Key ${formatFingerprint(
 69 |                         req.body.fingerprint
 70 |                       )} was removed successfully`
 71 |                     )
 72 |                   } else {
 73 |                     logger.error(
 74 |                       `there was an error removing key ${formatFingerprint(
 75 |                         req.body.fingerprint
 76 |                       )}`
 77 |                     )
 78 |                     res.sendStatus(500)
 79 |                   }
 80 |                 }
 81 |               )
 82 |             } else {
 83 |               logger.error(
 84 |                 `user was not authorised to remove key ${formatFingerprint(
 85 |                   req.body.fingerprint
 86 |                 )}`
 87 |               )
 88 |               res.sendStatus(401)
 89 |             }
 90 |           } else {
 91 |             logger.info(
 92 |               `key ${formatFingerprint(req.body.fingerprint)} not found`
 93 |             )
 94 |             res.sendStatus(404)
 95 |           }
 96 |         } else {
 97 |           logger.error(`${err}`)
 98 |           res.sendStatus(500)
 99 |         }
100 |       }
101 |     )
102 |   } else {
103 |     res.sendStatus(400)
104 |   }
105 | }
106 | 
107 | module.exports = remove
108 | 


--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
  1 | /* eslint-disable no-unused-vars */
  2 | /* eslint-disable no-undef */
  3 | 
  4 | process.env.NODE_ENV = 'test'
  5 | 
  6 | const chai = require('chai')
  7 | const chaiHttp = require('chai-http')
  8 | const rimraf = require('rimraf')
  9 | const cheerio = require('cheerio')
 10 | const assert = require('assert')
 11 | const crypto = require('crypto')
 12 | const should = chai.should()
 13 | let server
 14 | 
 15 | const pubKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
 16 | 
 17 | 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
 50 | ltn5FVFmGLTbNp3iw2pNR9jA3phXoc4=
 51 | =r3dV
 52 | -----END PGP PUBLIC KEY BLOCK-----`
 53 | 
 54 | const signedMessage = {
 55 |   valid: `-----BEGIN PGP SIGNED MESSAGE-----
 56 | Hash: SHA256
 57 | 
 58 | I am requesting the removal of my public key from dat-keyserver. token=be354767a89dd09b4d1e739c0b9e5a4d0fd43526ce9105d63683503db47f4df9
 59 | -----BEGIN PGP SIGNATURE-----
 60 | 
 61 | iQEzBAEBCAAdFiEEzPgVre6I8V30V/z2RiN4G7RVLCUFAl1uxrsACgkQRiN4G7RV
 62 | LCXyvwgAn4C5YhMqBuACKlcxSVpQWz2PSsWjS0x/kGOt8jaNDBko9bUuuT4S7VsA
 63 | 6LCiRU2xCAs+zv1/hnXEjiEi+wJpliwNK8b7s+ku+yzJPgHwiqhT58hMYIdVwpv+
 64 | Dz3nIlywCdtObUJuLCAE1/vW8Jnfvp0CpJXhRH8tdKPGGiNqpSsiZ+3w0kYcc9AS
 65 | 8a15rHOVFk6eESjGXWofWuQRaTwob4poQVCZPbY2p/FK2iBLQYszlJn0aKftgGqg
 66 | CMIF79wNEa+uADm/WMcuIilW1jL/YuDbgpVwjnetrzNU71nR921tS8RA8zrToBQY
 67 | E9iOXSqfiFEfL40A+z3Ul+tffusNrw==
 68 | =gaQ6
 69 | -----END PGP SIGNATURE-----`,
 70 |   invalid: `-----BEGIN PGP SIGNED MESSAGE-----
 71 | Hash: SHA256
 72 | 
 73 | I am requesting the removal of my public key from dat-keyserver. token=1234123412341234123412341234123412341234123412341234123412341234
 74 | -----BEGIN PGP SIGNATURE-----
 75 | 
 76 | iQEzBAEBCAAdFiEEzPgVre6I8V30V/z2RiN4G7RVLCUFAl1uxrsACgkQRiN4G7RV
 77 | LCXyvwgAn4C5YhMqBuACKlcxSVpQWz2PSsWjS0x/kGOt8jaNDBko9bUuuT4S7VsA
 78 | 6LCiRU2xCAs+zv1/hnXEjiEi+wJpliwNK8b7s+ku+yzJPgHwiqhT58hMYIdVwpv+
 79 | Dz3nIlywCdtObUJuLCAE1/vW8Jnfvp0CpJXhRH8tdKPGGiNqpSsiZ+3w0kYcc9AS
 80 | 8a15rHOVFk6eESjGXWofWuQRaTwob4poQVCZPbY2p/FK2iBLQYszlJn0aKftgGqg
 81 | CMIF79wNEa+uADm/WMcuIilW1jL/YuDbgpVwjnetrzNU71nR921tS8RA8zrToBQY
 82 | E9iOXSqfiFEfL40A+z3Ul+tffusNrw==
 83 | =gaQ6
 84 | -----END PGP SIGNATURE-----`,
 85 |   validButWrongToken: `-----BEGIN PGP SIGNED MESSAGE-----
 86 | Hash: SHA256
 87 | 
 88 | I am requesting the removal of my public key from dat-keyserver. token=1234123412341234123412341234123412341234123412341234123412341234
 89 | -----BEGIN PGP SIGNATURE-----
 90 | 
 91 | iQEzBAEBCAAdFiEEzPgVre6I8V30V/z2RiN4G7RVLCUFAl1uy+kACgkQRiN4G7RV
 92 | LCXQKQf/f2yUkW1W9u7tAnxiFW5bDN/DzunTb51fN2Jnpnjj5IrZI5pEtq51yaq5
 93 | TPU5h757bD3mnVGuAwSDI2iTgGx/H2uBqDC+wSanf5mwaxwaxVwrOqHJPT0io5om
 94 | sFx0MzmodzdgHk5ORVMe9mw7SE5YT6v5m3P3JIXHrnneXq7vAFSOgA6KDxuHJMxc
 95 | WwXifsFBE5iUMXQv4fJoR/cs+Hl0RD3KWqstU0SI4bd5VfaZDeLKTwr/yEy4s3sD
 96 | QpexJLBmz0nuB3VzThSShRjJHKa+g6dl6o4X1cDKgQMUPzgmBPZMxPxO9MKXarep
 97 | /n+7YaqW7lg1f15h9mPokFLasWMktg==
 98 | =o0VQ
 99 | -----END PGP SIGNATURE-----`
100 | }
101 | 
102 | const token = crypto
103 |   .createHash('sha256')
104 |   .update(pubKey)
105 |   .digest('hex')
106 | 
107 | chai.use(chaiHttp)
108 | 
109 | describe('dat-keyserver', () => {
110 |   before(done => {
111 |     // delete test database and wait for new database to initialise before tests
112 |     rimraf.sync('testdb')
113 |     server = require('./index')
114 |     setTimeout(() => {
115 |       done()
116 |     }, 3000)
117 |   })
118 | 
119 |   describe('GET /', () => {
120 |     it('should render the index page', done => {
121 |       chai
122 |         .request(server)
123 |         .get('/')
124 |         .end((err, res) => {
125 |           res.should.have.status(200)
126 |           done()
127 |         })
128 |     })
129 | 
130 |     it('should render the FAQ page', done => {
131 |       chai
132 |         .request(server)
133 |         .get('/faq')
134 |         .end((err, res) => {
135 |           res.should.have.status(200)
136 |           done()
137 |         })
138 |     })
139 | 
140 |     it('should render the key page', done => {
141 |       chai
142 |         .request(server)
143 |         .get('/key')
144 |         .end((err, res) => {
145 |           res.should.have.status(200)
146 |           done()
147 |         })
148 |     })
149 |   })
150 | 
151 |   describe('POST /publish', () => {
152 |     it('should publish a new public key', done => {
153 |       chai
154 |         .request(server)
155 |         .post('/publish')
156 |         .set('content-type', 'application/x-www-form-urlencoded')
157 |         .send({ key: pubKey })
158 |         .end((err, res) => {
159 |           res.should.have.status(200)
160 |           res.text.should.be.eql(
161 |             '
Success! Published key ccf815adee88f15df457fcf64623781bb4552c25
' 162 | ) 163 | done() 164 | }) 165 | }) 166 | 167 | it('should fail to publish and invalid key', done => { 168 | chai 169 | .request(server) 170 | .post('/publish') 171 | .set('content-type', 'application/x-www-form-urlencoded') 172 | .send({ key: 'malformed key' }) 173 | .end((err, res) => { 174 | res.should.have.status(500) 175 | done() 176 | }) 177 | }) 178 | 179 | it('should return 400 if no key is provided', done => { 180 | chai 181 | .request(server) 182 | .post('/publish') 183 | .set('content-type', 'application/x-www-form-urlencoded') 184 | .end((err, res) => { 185 | res.should.have.status(400) 186 | done() 187 | }) 188 | }) 189 | }) 190 | 191 | describe('GET /fetch', () => { 192 | it('should fetch a public key by key ID', done => { 193 | chai 194 | .request(server) 195 | .get('/fetch?fingerprint=4623781bb4552c25') 196 | .end((err, res) => { 197 | res.text.should.be.eql(`
\n${pubKey}\n
`) 198 | res.should.have.status(200) 199 | done() 200 | }) 201 | }) 202 | 203 | it('should fetch a public key by fingerprint', done => { 204 | chai 205 | .request(server) 206 | .get('/fetch?fingerprint=ccf815adee88f15df457fcf64623781bb4552c25') 207 | .end((err, res) => { 208 | res.text.should.be.eql(`
\n${pubKey}\n
`) 209 | res.should.have.status(200) 210 | done() 211 | }) 212 | }) 213 | 214 | it('should return 404 if no key is found', done => { 215 | chai 216 | .request(server) 217 | .get('/fetch?fingerprint=1234567890abcdef') 218 | .end((err, res) => { 219 | res.should.have.status(404) 220 | done() 221 | }) 222 | }) 223 | 224 | it('should return 400 if no fingerprint is provided', done => { 225 | chai 226 | .request(server) 227 | .get('/fetch') 228 | .end((err, res) => { 229 | res.should.have.status(400) 230 | done() 231 | }) 232 | }) 233 | }) 234 | 235 | describe('GET /search', () => { 236 | it('should return a correct search result', done => { 237 | chai 238 | .request(server) 239 | .get('/search?query=tdjsnelling') 240 | .end((err, res) => { 241 | res.should.have.status(200) 242 | 243 | const $ = cheerio.load(res.text) 244 | assert($('.searchResults li').length, 1) 245 | assert.strictEqual( 246 | $($('.searchResults li p').get(0)).text(), 247 | 'pub 2048/B4552C25 (cr. 2016-09-21, exp. n/a) CCF815ADEE88F15DF457FCF64623781BB4552C25' 248 | ) 249 | 250 | done() 251 | }) 252 | }) 253 | 254 | it('should return a "no results" message if there are no results', done => { 255 | chai 256 | .request(server) 257 | .get('/search?query=this+will+return+no+results') 258 | .end((err, res) => { 259 | res.should.have.status(200) 260 | 261 | const $ = cheerio.load(res.text) 262 | assert.strictEqual($('.searchResults').length, 0) 263 | assert( 264 | res.text.includes( 265 | 'No results for query “this will return no results”' 266 | ) 267 | ) 268 | 269 | done() 270 | }) 271 | }) 272 | 273 | it('should return 400 if no query is provided', done => { 274 | chai 275 | .request(server) 276 | .get('/search') 277 | .end((err, res) => { 278 | res.should.have.status(400) 279 | done() 280 | }) 281 | }) 282 | }) 283 | 284 | describe('POST /remove/request', () => { 285 | it('should provide a correct message to be signed', done => { 286 | chai 287 | .request(server) 288 | .post('/remove/request') 289 | .set('content-type', 'application/x-www-form-urlencoded') 290 | .send({ fingerprint: 'ccf815adee88f15df457fcf64623781bb4552c25' }) 291 | .end((err, res) => { 292 | res.should.have.status(200) 293 | 294 | const $ = cheerio.load(res.text) 295 | assert.strictEqual( 296 | $('code').text(), 297 | `I am requesting the removal of my public key from dat-keyserver. token=${token}` 298 | ) 299 | 300 | done() 301 | }) 302 | }) 303 | 304 | it('should return 404 if no key is found', done => { 305 | chai 306 | .request(server) 307 | .post('/remove/request') 308 | .set('content-type', 'application/x-www-form-urlencoded') 309 | .send({ fingerprint: '1234567890abcdef' }) 310 | .end((err, res) => { 311 | res.should.have.status(404) 312 | done() 313 | }) 314 | }) 315 | 316 | it('should return 400 if no fingerprint is provided', done => { 317 | chai 318 | .request(server) 319 | .post('/remove/request') 320 | .set('content-type', 'application/x-www-form-urlencoded') 321 | .end((err, res) => { 322 | res.should.have.status(400) 323 | done() 324 | }) 325 | }) 326 | }) 327 | 328 | describe('POST /remove/verify', () => { 329 | it('should return 500 if signed message is malformed', done => { 330 | chai 331 | .request(server) 332 | .post('/remove/verify') 333 | .set('content-type', 'application/x-www-form-urlencoded') 334 | .send({ 335 | fingerprint: 'ccf815adee88f15df457fcf64623781bb4552c25', 336 | message: 'malformed message' 337 | }) 338 | .end((err, res) => { 339 | res.should.have.status(500) 340 | done() 341 | }) 342 | }) 343 | 344 | it('should return 401 if signed message is invalid', done => { 345 | chai 346 | .request(server) 347 | .post('/remove/verify') 348 | .set('content-type', 'application/x-www-form-urlencoded') 349 | .send({ 350 | fingerprint: 'ccf815adee88f15df457fcf64623781bb4552c25', 351 | message: signedMessage.invalid 352 | }) 353 | .end((err, res) => { 354 | res.should.have.status(401) 355 | done() 356 | }) 357 | }) 358 | 359 | it('should returm 401 if signed message is valid but token incorrect', done => { 360 | chai 361 | .request(server) 362 | .post('/remove/verify') 363 | .set('content-type', 'application/x-www-form-urlencoded') 364 | .send({ 365 | fingerprint: 'ccf815adee88f15df457fcf64623781bb4552c25', 366 | message: signedMessage.validButWrongToken 367 | }) 368 | .end((err, res) => { 369 | res.should.have.status(401) 370 | done() 371 | }) 372 | }) 373 | 374 | it('should remove a key if provided with valid message and token', done => { 375 | chai 376 | .request(server) 377 | .post('/remove/verify') 378 | .set('content-type', 'application/x-www-form-urlencoded') 379 | .send({ 380 | fingerprint: 'ccf815adee88f15df457fcf64623781bb4552c25', 381 | message: signedMessage.valid 382 | }) 383 | .end((err, res) => { 384 | res.should.have.status(200) 385 | done() 386 | }) 387 | }) 388 | }) 389 | 390 | after(() => { 391 | // delete the test database and exit cleanly after tests are complete 392 | setTimeout(() => { 393 | rimraf.sync('testdb') 394 | process.exit(0) 395 | }, 1000) 396 | }) 397 | }) 398 | --------------------------------------------------------------------------------