Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 47x 47x 47x 10x 10x 10x 10x 10x 10x 10x 10x 40x 40x 40x 40x 40x 40x 40x 40x 40x 40x 10x 10x 22x 22x 22x 22x 22x 22x 22x 2x 2x 22x 1x 1x 22x 1x 1x 18x 22x 4x 4x 4x 22x 10x 10x 10x 10x 10x 15x 15x 15x 15x 10x 10x 10x 12x 11x 11x 11x 12x 10x 10x 15x 15x 15x 15x 15x 15x 15x 40x 40x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 19x 21x 21x 21x 21x 21x 21x 21x 15x 15x 15x 16x 16x 13x 13x 13x 13x 16x 1x 1x 1x 12x 12x 12x 12x 11x 11x 11x 11x 15x 15x 15x 7x 7x 1x 1x 6x 6x 5x 5x 5x 5x 7x 2x 2x 2x 7x 1x 1x 2x 2x 2x 2x 15x 15x 10x | /*eslint-env node */
import { promisify } from 'util';
import sodium_plus from 'sodium-plus';
const { SodiumPlus } = sodium_plus;
import { hash_id, ErrorWithStatus } from './common.js';
import { cred as schemas } from './dist/schemas.js';
export default async function (fastify, options) {
const {
keystore,
webAuthn,
valid_ids,
session_data_timeout,
users,
default_user,
registration_options,
login_options
} = Object.assign({
session_data_timeout: 60000,
users: {},
default_user: {
id: 'anonymous',
name: 'Anonymous',
displayName: 'Anonymous'
}
}, options.cred_options);
const get_uris = promisify(keystore.get_uris.bind(keystore));
const add_pub_key = promisify(keystore.add_pub_key.bind(keystore));
const remove_pub_key = promisify(keystore.remove_pub_key.bind(keystore));
const get_pub_key_by_uri = promisify((uri, cb) => {
keystore.get_pub_key_by_uri(uri, (err, user, issuer_id) => {
cb(err, { user, issuer_id });
});
});
const deploy = promisify(keystore.deploy.bind(keystore));
// Use shared-key authenticated encryption for challenges
const sodium = await SodiumPlus.auto();
const session_data_key = await sodium.crypto_secretbox_keygen();
async function make_secret_session_data(id, type, session_data) {
const nonce = await sodium.randombytes_buf(
sodium.CRYPTO_SECRETBOX_NONCEBYTES);
return {
ciphertext: (await sodium.crypto_secretbox(
JSON.stringify([ id, type, session_data, Date.now() ]),
nonce,
session_data_key)).toString('base64'),
nonce: nonce.toString('base64')
};
}
async function verify_secret_session_data(expected_id, expected_type, secret_session_data) {
try {
const [ id, type, session_data, timestamp ] = JSON.parse(
await sodium.crypto_secretbox_open(
Buffer.from(secret_session_data.ciphertext, 'base64'),
Buffer.from(secret_session_data.nonce, 'base64'),
session_data_key));
if (id !== expected_id) {
throw new Error('wrong ID');
}
if (type !== expected_type) {
throw new Error('wrong type');
}
if ((timestamp + session_data_timeout) <= Date.now('dummy' /* for test */)) {
throw new Error('session timed out');
}
return session_data;
} catch (ex) {
ex.statusCode = 400;
throw ex;
}
}
// Store hash of the IDs so path can't be determined from database
const valid_hashes = new Set();
const valid_hashmap = new Map();
for (const [id, prefixed_id] of valid_ids) {
const hash = await hash_id(sodium, prefixed_id);
valid_hashes.add(hash);
valid_hashmap.set(id, hash);
}
// Delete pub keys that aren't passed as argument
for (const hash of await get_uris()) {
if (!valid_hashes.has(hash)) {
fastify.log.info(`removing pub key for hash: ${hash}`);
await remove_pub_key(hash);
}
}
for (const [id, hash] of valid_hashmap) { // eslint-disable-line require-atomic-updates
fastify.log.info(`setting up routes for id: ${id}, hash: ${hash}`);
const empty_user = Object.assign({}, users[id] || default_user, {
credentials: []
});
fastify.get(`/${id}/`, { schema: schemas.get }, async (request, reply) => {
const { user, issuer_id } = await get_pub_key_by_uri(hash);
if (user === null) {
reply.code(404);
const {
options,
sessionData
} = await webAuthn.beginRegistration(empty_user, ...registration_options);
return {
options,
session_data: await make_secret_session_data(
id, 'registration', sessionData)
};
}
const { options, sessionData } = await webAuthn.beginLogin(user, ...login_options);
return {
issuer_id,
options,
session_data: await make_secret_session_data(
id, 'login', sessionData)
};
});
fastify.put(`/${id}/`, { schema: schemas.put }, async (request, reply) => {
const session_data = await verify_secret_session_data(
id, 'registration', request.body.session_data);
let credential;
try {
credential = await webAuthn.finishRegistration(
empty_user, session_data, request.body.ccr);
} catch (ex) {
ex.statusCode = 400;
throw ex;
}
const user = Object.assign({}, empty_user, {
credentials: [credential]
});
const issuer_id = await add_pub_key(hash, user);
await deploy();
const { options } = await webAuthn.beginLogin(user, ...login_options);
reply.code(201);
return { issuer_id, options };
});
fastify.post(`/${id}/`, { schema: schemas.post }, async (request, reply) => {
const { user } = await get_pub_key_by_uri(hash);
if (user === null) {
throw new ErrorWithStatus('no user', 404);
}
const session_data = await verify_secret_session_data(
id, 'login', request.body.session_data);
let credential;
try {
credential = await webAuthn.finishLogin(
user, session_data, request.body.car);
} catch (ex) {
ex.statusCode = 400;
throw ex;
}
if (credential.authenticator.cloneWarning) {
throw new ErrorWithStatus('credential appears to be cloned', 403);
}
// Note we don't update signCount because the credential is expected to be used
// to sign assertions which are given out as perks, which (a) may be duplicated
// and (b) may be used in any order.
reply.code(204);
});
}
}
|