diff --git a/README.md b/README.md index e69275f..2a0d8c7 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,18 @@ const {blacklist} = require('@sapphirecode/auth-server-helper'); blacklist.add_signature(token_id); // the token id is returned from any function that creates tokens ``` +### Exporting and importing public keys to validate tokens across server instances + +```js +const {keystore} = require('@sapphirecode/auth-server-helper'); + +const export = keystore.export_verification_data(); + +// second instance + +keystore.import_verification_data(export); +``` + ## License MIT © Timo Hocker diff --git a/lib/Authority.ts b/lib/Authority.ts index 363c069..54c063b 100644 --- a/lib/Authority.ts +++ b/lib/Authority.ts @@ -49,7 +49,7 @@ class Authority { key, (info) => { try { - return keystore.get_key (info.iat / 1000); + return keystore.get_key (info.iat / 1000, info.iss); } catch { return ''; @@ -89,6 +89,7 @@ class Authority { const attributes = { id: create_salt (), iat: time, + iss: keystore.instance_id, type, valid_for, next_module: options?.next_module diff --git a/lib/KeyStore.ts b/lib/KeyStore.ts index 147c42d..2032b94 100644 --- a/lib/KeyStore.ts +++ b/lib/KeyStore.ts @@ -5,9 +5,10 @@ * Created by Timo Hocker , December 2020 */ -import { generate_keypair } from '@sapphirecode/crypto-helper'; +import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper'; +import { to_b58 } from '@sapphirecode/encoding-helper'; -const renew_interval = 60; +const renew_interval = 3600; interface Key { key: string; @@ -21,11 +22,6 @@ interface KeyPair { type KeyStoreData = Record; -function get_index (iat: number): string { - return Math.floor (iat / renew_interval) - .toFixed (0); -} - async function create_key (valid_for: number) { const time = (new Date) .getTime (); @@ -45,11 +41,22 @@ async function create_key (valid_for: number) { class KeyStore { private _keys: KeyStoreData = {}; private _interval: NodeJS.Timeout; + private _instance: string; + + public get instance_id (): string { + return this._instance; + } public constructor () { this._interval = setInterval (() => { this.garbage_collect (); }, renew_interval); + this._instance = to_b58 (random_hex (16), 'hex'); + } + + private get_index (iat: number, instance = this._instance): string { + return instance + Math.floor (iat / renew_interval) + .toFixed (0); } private garbage_collect (set: KeyStoreData = this._keys): void { @@ -68,7 +75,11 @@ class KeyStore { } } - public async get_sign_key (iat: number, valid_for: number): Promise { + public async get_sign_key ( + iat: number, + valid_for: number, + instance?: string + ): Promise { if (valid_for <= 0) throw new Error ('cannot create infinitely valid key'); @@ -76,7 +87,7 @@ class KeyStore { .getTime ()) throw new Error ('cannot access already expired keys'); - const index = get_index (iat); + const index = this.get_index (iat, instance); const valid_until = (new Date) .getTime () + (valid_for * 1000); @@ -86,6 +97,9 @@ class KeyStore { if (key.public_key.valid_until < valid_until) key.public_key.valid_until = valid_until; + if (typeof key.private_key === 'undefined') + throw new Error ('cannot access already expired keys'); + return key.private_key?.key as string; } @@ -93,8 +107,8 @@ class KeyStore { return this._keys[index].private_key?.key as string; } - public get_key (iat: number): string { - const index = get_index (iat); + public get_key (iat: number, instance?: string): string { + const index = this.get_index (iat, instance); if (typeof this._keys[index] === 'undefined') throw new Error ('key could not be found'); @@ -115,8 +129,12 @@ class KeyStore { public import_verification_data (data: KeyStoreData): void { const import_set = { ...data }; this.garbage_collect (import_set); - - // TODO: import + for (const key of Object.keys (import_set)) { + if (typeof this._keys[key] !== 'undefined') + throw new Error ('cannot import to the same instance'); + this._keys[key] = import_set[key]; + } + this.garbage_collect (); } } diff --git a/test/Helper.ts b/test/Helper.ts index f60bcf2..5237f8a 100644 --- a/test/Helper.ts +++ b/test/Helper.ts @@ -7,7 +7,6 @@ /* eslint-disable no-console */ import http from 'http'; -import { type } from 'os'; import ks from '../lib/KeyStore'; export class Response extends http.IncomingMessage { @@ -64,7 +63,7 @@ export function clock_setup ():void { assert_keystore_state (); const date = (new Date); - date.setSeconds (2, 0); + date.setHours (0, 0, 2, 0); jasmine.clock () .install (); jasmine.clock () diff --git a/test/spec/KeyStore.ts b/test/spec/KeyStore.ts index 60759de..cbe4906 100644 --- a/test/spec/KeyStore.ts +++ b/test/spec/KeyStore.ts @@ -5,10 +5,10 @@ * Created by Timo Hocker , December 2020 */ -import ks from '../../lib/KeyStore'; +import ks, { KeyStore } from '../../lib/KeyStore'; import { clock_finalize, clock_setup } from '../Helper'; -const frame = 60; +const frame = 3600; /* eslint-disable-next-line max-lines-per-function */ describe ('key store', () => { @@ -146,5 +146,40 @@ describe ('key store', () => { .toBeRejectedWithError ('cannot create infinitely valid key'); }); - // TODO: required use case: insert keys for verification of old tokens + it ('should export and import all keys', async () => { + const iat = (new Date) + .getTime () / 1000; + + const sign = await ks.get_sign_key (iat, frame); + const ver = ks.get_key (iat); + const exp = ks.export_verification_data (); + // eslint-disable-next-line dot-notation + expect (Object.keys (ks['_keys'])) + .toEqual (Object.keys (exp)); + expect (Object.keys (exp) + .filter ((v) => typeof exp[v].private_key !== 'undefined').length) + .toEqual (0); + + const ks2 = (new KeyStore); + expect (ks2.instance_id).not.toEqual (ks.instance_id); + ks2.import_verification_data (exp); + // eslint-disable-next-line dot-notation + expect (ks2['_keys']) + .toEqual (exp); + + const sign2 = await ks2.get_sign_key (iat, frame); + const ver2 = ks2.get_key (iat); + expect (sign).not.toEqual (sign2); + expect (ver).not.toEqual (ver2); + await expectAsync (ks2.get_sign_key (iat, 60, ks.instance_id)) + .toBeRejectedWithError ('cannot access already expired keys'); + expect (ks2.get_key (iat, ks.instance_id)) + .toEqual (ver); + }); + + it ('should disallow importing to itself', () => { + const exp = ks.export_verification_data (); + expect (() => ks.import_verification_data (exp)) + .toThrowError ('cannot import to the same instance'); + }); });