From fd4f891b3e74e0cff450263312cae86280f849df Mon Sep 17 00:00:00 2001 From: Timo Hocker Date: Wed, 6 Jan 2021 22:43:03 +0100 Subject: [PATCH] asymmetric keys import/export --- lib/KeyStore.ts | 103 +++++++++++++++++++++++++++------------ lib/index.ts | 5 +- test/Helper.ts | 26 ++++++++++ test/spec/AuthHandler.ts | 27 +++++----- test/spec/Authority.ts | 9 ++-- test/spec/Blacklist.ts | 10 ++-- test/spec/Gateway.ts | 6 +-- test/spec/KeyStore.ts | 12 ++++- 8 files changed, 142 insertions(+), 56 deletions(-) diff --git a/lib/KeyStore.ts b/lib/KeyStore.ts index bb5824e..63bd5d8 100644 --- a/lib/KeyStore.ts +++ b/lib/KeyStore.ts @@ -7,34 +7,66 @@ import { generate_keypair } from '@sapphirecode/crypto-helper'; -interface Keypair { - private_key: string; - public_key: string; -} +const renew_interval = 60; interface Key { - key: Keypair; + key: string; valid_until: number; - timeout: NodeJS.Timeout; } +interface KeyPair { + private_key?: Key; + public_key: Key; +} + +type KeyStoreData = Record; + function get_index (iat: number): string { - return Math.floor (iat / 60) + return Math.floor (iat / renew_interval) .toFixed (0); } -class KeyStore { - private _keys: Record = {}; +async function create_key (valid_for: number) { + const time = (new Date) + .getTime (); + const pair = await generate_keypair (); + return { + private_key: { + key: pair.private_key, + valid_until: time + (renew_interval * 1000) + }, + public_key: { + key: pair.public_key, + valid_until: time + (valid_for * 1000) + } + }; +} - private set_timeout (index: string, valid_for: number): NodeJS.Timeout { - return setTimeout (() => { - delete this._keys[index]; - }, (valid_for + 5) * 1000); +function garbage_collect (set: KeyStoreData): void { + const time = (new Date) + .getTime (); + for (const index of Object.keys (set)) { + const entry = set[index]; + if (typeof entry.private_key !== 'undefined' + && entry.private_key.valid_until < time + ) + delete entry.private_key; + if (entry.public_key.valid_until < time) + delete set[index]; + } +} + +class KeyStore { + private _keys: KeyStoreData = {}; + private _interval: NodeJS.Timeout; + + public constructor () { + this._interval = setInterval (() => { + garbage_collect (this._keys); + }, renew_interval); } public async get_sign_key (iat: number, valid_for: number): Promise { - const index = get_index (iat); - if (valid_for <= 0) throw new Error ('cannot create infinitely valid key'); @@ -42,26 +74,21 @@ class KeyStore { .getTime ()) throw new Error ('cannot access already expired keys'); + const index = get_index (iat); + const valid_until = (new Date) .getTime () + (valid_for * 1000); if (typeof this._keys[index] !== 'undefined') { const key = this._keys[index]; - if (key.valid_until < valid_until) { - clearTimeout (key.timeout); - key.timeout = this.set_timeout (index, valid_for); - key.valid_until = valid_until; - } - return key.key.private_key; + if (key.public_key.valid_until < valid_until) + key.public_key.valid_until = valid_until; + + return key.private_key?.key as string; } - this._keys[index] = { - key: await generate_keypair (), - timeout: this.set_timeout (index, valid_for), - valid_until - }; - - return this._keys[index].key.private_key; + this._keys[index] = await create_key (valid_until); + return this._keys[index].private_key?.key as string; } public get_key (iat: number): string { @@ -71,10 +98,26 @@ class KeyStore { throw new Error ('key could not be found'); const key = this._keys[index]; - return key.key.public_key; + return key.public_key.key; + } + + public export_verification_data (): KeyStoreData { + garbage_collect (this._keys); + const out: KeyStoreData = {}; + for (const index of Object.keys (this._keys)) + out[index] = { public_key: this._keys[index].public_key }; + + return out; + } + + public import_verification_data (data: KeyStoreData): void { + const import_set = { ...data }; + garbage_collect (import_set); + + // TODO: import } } const ks: KeyStore = (new KeyStore); export default ks; -export { KeyStore }; +export { KeyStore, KeyStoreData, Key, KeyPair }; diff --git a/lib/index.ts b/lib/index.ts index 44603b6..0b869e4 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -28,7 +28,7 @@ import create_gateway, { Gateway, AnyFunc } from './Gateway'; -import { KeyStore } from './KeyStore'; +import { KeyStore, KeyStoreData } from './KeyStore'; export { create_gateway, @@ -52,5 +52,6 @@ export { GatewayClass, Gateway, AnyFunc, - KeyStore + KeyStore, + KeyStoreData }; diff --git a/test/Helper.ts b/test/Helper.ts index 8d30cde..9d0d9b6 100644 --- a/test/Helper.ts +++ b/test/Helper.ts @@ -5,7 +5,9 @@ * Created by Timo Hocker , January 2021 */ +/* eslint-disable no-console */ import http from 'http'; +import ks from '../lib/KeyStore'; export class Response extends http.IncomingMessage { body?: string; @@ -41,3 +43,27 @@ export function modify_signature (signature: string): string { dec[1] = ''; return dec.join ('.'); } + +/* eslint-disable dot-notation */ +export function assert_keystore_state (): void { + if (Object.keys (ks['_keys']).length !== 0) { + console.warn ('keystore gc not running!'); + ks['_keys'] = {}; + } +} +/* eslint-enable dot-notation */ + +export function flush_routine (install_clock = true):void { + if (install_clock) { + jasmine.clock () + .install (); + } + jasmine.clock () + .mockDate (new Date); + jasmine.clock () + .tick (30 * 24 * 60 * 60 * 1000); + if (install_clock) { + jasmine.clock () + .uninstall (); + } +} diff --git a/test/spec/AuthHandler.ts b/test/spec/AuthHandler.ts index 71aa91e..751e2cc 100644 --- a/test/spec/AuthHandler.ts +++ b/test/spec/AuthHandler.ts @@ -8,7 +8,10 @@ import http, { IncomingMessage, ServerResponse } from 'http'; import { to_b64 } from '@sapphirecode/encoding-helper'; import auth from '../../lib/Authority'; -import { get, modify_signature, Response } from '../Helper'; +import { + assert_keystore_state, flush_routine, + get, modify_signature, Response +} from '../Helper'; import { create_auth_handler } from '../../lib/index'; const expires_seconds = 600; @@ -47,7 +50,11 @@ function check_token (token: string, type: string):void { // eslint-disable-next-line max-lines-per-function describe ('auth handler', () => { let server: http.Server|null = null; + // eslint-disable-next-line max-lines-per-function beforeAll (() => { + flush_routine (); + assert_keystore_state (); + const ah = create_auth_handler ((req) => { if (!req.is_basic && !req.is_bearer) { req.invalid ('unknown authorization type'); @@ -100,6 +107,14 @@ describe ('auth handler', () => { .mockDate (new Date); }); + afterAll (() => { + if (server === null) + throw new Error ('server is null'); + server.close (); + jasmine.clock () + .uninstall (); + }); + it ('auth test sequence', async () => { // get initial access and refresh tokens const resp1 = await get ({ authorization: 'Basic foo:bar' }); @@ -253,14 +268,4 @@ describe ('auth handler', () => { error_description: 'unknown authorization type' }); }); - - afterAll (() => { - if (server === null) - throw new Error ('server is null'); - server.close (); - jasmine.clock () - .tick (24 * 60 * 60 * 1000); - jasmine.clock () - .uninstall (); - }); }); diff --git a/test/spec/Authority.ts b/test/spec/Authority.ts index 987d50f..a0e130a 100644 --- a/test/spec/Authority.ts +++ b/test/spec/Authority.ts @@ -7,7 +7,10 @@ import auth from '../../lib/Authority'; import bl from '../../lib/Blacklist'; -import { modify_signature } from '../Helper'; +import { + assert_keystore_state, + flush_routine, modify_signature +} from '../Helper'; // eslint-disable-next-line max-lines-per-function describe ('authority', () => { @@ -19,8 +22,8 @@ describe ('authority', () => { }); afterEach (() => { - jasmine.clock () - .tick (24 * 60 * 60 * 1000); + flush_routine (false); + assert_keystore_state (); jasmine.clock () .uninstall (); }); diff --git a/test/spec/Blacklist.ts b/test/spec/Blacklist.ts index fd4ba97..142c99f 100644 --- a/test/spec/Blacklist.ts +++ b/test/spec/Blacklist.ts @@ -16,6 +16,11 @@ describe ('blacklist', () => { .mockDate (new Date); }); + afterAll (() => { + jasmine.clock () + .uninstall (); + }); + it ('should validate any string', () => { expect (blacklist.is_valid ('foo')) .toBeTrue (); @@ -58,9 +63,4 @@ describe ('blacklist', () => { expect (blacklist.is_valid ('baz')) .toBeFalse (); }); - - afterAll (() => { - jasmine.clock () - .uninstall (); - }); }); diff --git a/test/spec/Gateway.ts b/test/spec/Gateway.ts index d099c03..902572c 100644 --- a/test/spec/Gateway.ts +++ b/test/spec/Gateway.ts @@ -9,13 +9,15 @@ import http from 'http'; import { create_gateway } from '../../lib/index'; import authority from '../../lib/Authority'; import blacklist from '../../lib/Blacklist'; -import { get } from '../Helper'; +import { assert_keystore_state, flush_routine, get } from '../Helper'; // eslint-disable-next-line max-lines-per-function describe ('gateway', () => { let server: http.Server|null = null; beforeAll (() => { + flush_routine (); + assert_keystore_state (); jasmine.clock () .install (); jasmine.clock () @@ -42,8 +44,6 @@ describe ('gateway', () => { throw new Error ('server is null'); server.close (); - jasmine.clock () - .tick (24 * 60 * 60 * 1000); jasmine.clock () .uninstall (); }); diff --git a/test/spec/KeyStore.ts b/test/spec/KeyStore.ts index 7b3c114..b4ac6d0 100644 --- a/test/spec/KeyStore.ts +++ b/test/spec/KeyStore.ts @@ -6,12 +6,15 @@ */ import ks from '../../lib/KeyStore'; +import { assert_keystore_state, flush_routine } from '../Helper'; const frame = 60; /* eslint-disable-next-line max-lines-per-function */ describe ('key store', () => { beforeAll (() => { + flush_routine (); + assert_keystore_state (); jasmine.clock () .install (); const base_date = (new Date); @@ -140,11 +143,16 @@ describe ('key store', () => { .toEqual (sign); }); + it ('should not allow invalid expiry times', async () => { + await expectAsync (ks.get_sign_key (0, 0)) + .toBeRejectedWithError ('cannot create infinitely valid key'); + await expectAsync (ks.get_sign_key (0, -1)) + .toBeRejectedWithError ('cannot create infinitely valid key'); + }); + // TODO: required use case: insert keys for verification of old tokens afterAll (() => { - jasmine.clock () - .tick (24 * 60 * 60 * 1000); jasmine.clock () .uninstall (); });