This commit is contained in:
parent
adfeeaa52c
commit
fd4f891b3e
103
lib/KeyStore.ts
103
lib/KeyStore.ts
@ -7,34 +7,66 @@
|
|||||||
|
|
||||||
import { generate_keypair } from '@sapphirecode/crypto-helper';
|
import { generate_keypair } from '@sapphirecode/crypto-helper';
|
||||||
|
|
||||||
interface Keypair {
|
const renew_interval = 60;
|
||||||
private_key: string;
|
|
||||||
public_key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
key: Keypair;
|
key: string;
|
||||||
valid_until: number;
|
valid_until: number;
|
||||||
timeout: NodeJS.Timeout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KeyPair {
|
||||||
|
private_key?: Key;
|
||||||
|
public_key: Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
type KeyStoreData = Record<string, KeyPair>;
|
||||||
|
|
||||||
function get_index (iat: number): string {
|
function get_index (iat: number): string {
|
||||||
return Math.floor (iat / 60)
|
return Math.floor (iat / renew_interval)
|
||||||
.toFixed (0);
|
.toFixed (0);
|
||||||
}
|
}
|
||||||
|
|
||||||
class KeyStore {
|
async function create_key (valid_for: number) {
|
||||||
private _keys: Record<string, Key> = {};
|
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 {
|
function garbage_collect (set: KeyStoreData): void {
|
||||||
return setTimeout (() => {
|
const time = (new Date)
|
||||||
delete this._keys[index];
|
.getTime ();
|
||||||
}, (valid_for + 5) * 1000);
|
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<string> {
|
public async get_sign_key (iat: number, valid_for: number): Promise<string> {
|
||||||
const index = get_index (iat);
|
|
||||||
|
|
||||||
if (valid_for <= 0)
|
if (valid_for <= 0)
|
||||||
throw new Error ('cannot create infinitely valid key');
|
throw new Error ('cannot create infinitely valid key');
|
||||||
|
|
||||||
@ -42,26 +74,21 @@ class KeyStore {
|
|||||||
.getTime ())
|
.getTime ())
|
||||||
throw new Error ('cannot access already expired keys');
|
throw new Error ('cannot access already expired keys');
|
||||||
|
|
||||||
|
const index = get_index (iat);
|
||||||
|
|
||||||
const valid_until = (new Date)
|
const valid_until = (new Date)
|
||||||
.getTime () + (valid_for * 1000);
|
.getTime () + (valid_for * 1000);
|
||||||
|
|
||||||
if (typeof this._keys[index] !== 'undefined') {
|
if (typeof this._keys[index] !== 'undefined') {
|
||||||
const key = this._keys[index];
|
const key = this._keys[index];
|
||||||
if (key.valid_until < valid_until) {
|
if (key.public_key.valid_until < valid_until)
|
||||||
clearTimeout (key.timeout);
|
key.public_key.valid_until = valid_until;
|
||||||
key.timeout = this.set_timeout (index, valid_for);
|
|
||||||
key.valid_until = valid_until;
|
return key.private_key?.key as string;
|
||||||
}
|
|
||||||
return key.key.private_key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._keys[index] = {
|
this._keys[index] = await create_key (valid_until);
|
||||||
key: await generate_keypair (),
|
return this._keys[index].private_key?.key as string;
|
||||||
timeout: this.set_timeout (index, valid_for),
|
|
||||||
valid_until
|
|
||||||
};
|
|
||||||
|
|
||||||
return this._keys[index].key.private_key;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get_key (iat: number): string {
|
public get_key (iat: number): string {
|
||||||
@ -71,10 +98,26 @@ class KeyStore {
|
|||||||
throw new Error ('key could not be found');
|
throw new Error ('key could not be found');
|
||||||
|
|
||||||
const key = this._keys[index];
|
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);
|
const ks: KeyStore = (new KeyStore);
|
||||||
export default ks;
|
export default ks;
|
||||||
export { KeyStore };
|
export { KeyStore, KeyStoreData, Key, KeyPair };
|
||||||
|
@ -28,7 +28,7 @@ import create_gateway, {
|
|||||||
Gateway,
|
Gateway,
|
||||||
AnyFunc
|
AnyFunc
|
||||||
} from './Gateway';
|
} from './Gateway';
|
||||||
import { KeyStore } from './KeyStore';
|
import { KeyStore, KeyStoreData } from './KeyStore';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
create_gateway,
|
create_gateway,
|
||||||
@ -52,5 +52,6 @@ export {
|
|||||||
GatewayClass,
|
GatewayClass,
|
||||||
Gateway,
|
Gateway,
|
||||||
AnyFunc,
|
AnyFunc,
|
||||||
KeyStore
|
KeyStore,
|
||||||
|
KeyStoreData
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,9 @@
|
|||||||
* Created by Timo Hocker <timo@scode.ovh>, January 2021
|
* Created by Timo Hocker <timo@scode.ovh>, January 2021
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import ks from '../lib/KeyStore';
|
||||||
|
|
||||||
export class Response extends http.IncomingMessage {
|
export class Response extends http.IncomingMessage {
|
||||||
body?: string;
|
body?: string;
|
||||||
@ -41,3 +43,27 @@ export function modify_signature (signature: string): string {
|
|||||||
dec[1] = '';
|
dec[1] = '';
|
||||||
return dec.join ('.');
|
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 ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
import http, { IncomingMessage, ServerResponse } from 'http';
|
import http, { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { to_b64 } from '@sapphirecode/encoding-helper';
|
import { to_b64 } from '@sapphirecode/encoding-helper';
|
||||||
import auth from '../../lib/Authority';
|
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';
|
import { create_auth_handler } from '../../lib/index';
|
||||||
|
|
||||||
const expires_seconds = 600;
|
const expires_seconds = 600;
|
||||||
@ -47,7 +50,11 @@ function check_token (token: string, type: string):void {
|
|||||||
// eslint-disable-next-line max-lines-per-function
|
// eslint-disable-next-line max-lines-per-function
|
||||||
describe ('auth handler', () => {
|
describe ('auth handler', () => {
|
||||||
let server: http.Server|null = null;
|
let server: http.Server|null = null;
|
||||||
|
// eslint-disable-next-line max-lines-per-function
|
||||||
beforeAll (() => {
|
beforeAll (() => {
|
||||||
|
flush_routine ();
|
||||||
|
assert_keystore_state ();
|
||||||
|
|
||||||
const ah = create_auth_handler ((req) => {
|
const ah = create_auth_handler ((req) => {
|
||||||
if (!req.is_basic && !req.is_bearer) {
|
if (!req.is_basic && !req.is_bearer) {
|
||||||
req.invalid ('unknown authorization type');
|
req.invalid ('unknown authorization type');
|
||||||
@ -100,6 +107,14 @@ describe ('auth handler', () => {
|
|||||||
.mockDate (new Date);
|
.mockDate (new Date);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll (() => {
|
||||||
|
if (server === null)
|
||||||
|
throw new Error ('server is null');
|
||||||
|
server.close ();
|
||||||
|
jasmine.clock ()
|
||||||
|
.uninstall ();
|
||||||
|
});
|
||||||
|
|
||||||
it ('auth test sequence', async () => {
|
it ('auth test sequence', async () => {
|
||||||
// get initial access and refresh tokens
|
// get initial access and refresh tokens
|
||||||
const resp1 = await get ({ authorization: 'Basic foo:bar' });
|
const resp1 = await get ({ authorization: 'Basic foo:bar' });
|
||||||
@ -253,14 +268,4 @@ describe ('auth handler', () => {
|
|||||||
error_description: 'unknown authorization type'
|
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 ();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
import auth from '../../lib/Authority';
|
import auth from '../../lib/Authority';
|
||||||
import bl from '../../lib/Blacklist';
|
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
|
// eslint-disable-next-line max-lines-per-function
|
||||||
describe ('authority', () => {
|
describe ('authority', () => {
|
||||||
@ -19,8 +22,8 @@ describe ('authority', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach (() => {
|
afterEach (() => {
|
||||||
jasmine.clock ()
|
flush_routine (false);
|
||||||
.tick (24 * 60 * 60 * 1000);
|
assert_keystore_state ();
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.uninstall ();
|
.uninstall ();
|
||||||
});
|
});
|
||||||
|
@ -16,6 +16,11 @@ describe ('blacklist', () => {
|
|||||||
.mockDate (new Date);
|
.mockDate (new Date);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterAll (() => {
|
||||||
|
jasmine.clock ()
|
||||||
|
.uninstall ();
|
||||||
|
});
|
||||||
|
|
||||||
it ('should validate any string', () => {
|
it ('should validate any string', () => {
|
||||||
expect (blacklist.is_valid ('foo'))
|
expect (blacklist.is_valid ('foo'))
|
||||||
.toBeTrue ();
|
.toBeTrue ();
|
||||||
@ -58,9 +63,4 @@ describe ('blacklist', () => {
|
|||||||
expect (blacklist.is_valid ('baz'))
|
expect (blacklist.is_valid ('baz'))
|
||||||
.toBeFalse ();
|
.toBeFalse ();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll (() => {
|
|
||||||
jasmine.clock ()
|
|
||||||
.uninstall ();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@ -9,13 +9,15 @@ import http from 'http';
|
|||||||
import { create_gateway } from '../../lib/index';
|
import { create_gateway } from '../../lib/index';
|
||||||
import authority from '../../lib/Authority';
|
import authority from '../../lib/Authority';
|
||||||
import blacklist from '../../lib/Blacklist';
|
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
|
// eslint-disable-next-line max-lines-per-function
|
||||||
describe ('gateway', () => {
|
describe ('gateway', () => {
|
||||||
let server: http.Server|null = null;
|
let server: http.Server|null = null;
|
||||||
|
|
||||||
beforeAll (() => {
|
beforeAll (() => {
|
||||||
|
flush_routine ();
|
||||||
|
assert_keystore_state ();
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.install ();
|
.install ();
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
@ -42,8 +44,6 @@ describe ('gateway', () => {
|
|||||||
throw new Error ('server is null');
|
throw new Error ('server is null');
|
||||||
server.close ();
|
server.close ();
|
||||||
|
|
||||||
jasmine.clock ()
|
|
||||||
.tick (24 * 60 * 60 * 1000);
|
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.uninstall ();
|
.uninstall ();
|
||||||
});
|
});
|
||||||
|
@ -6,12 +6,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import ks from '../../lib/KeyStore';
|
import ks from '../../lib/KeyStore';
|
||||||
|
import { assert_keystore_state, flush_routine } from '../Helper';
|
||||||
|
|
||||||
const frame = 60;
|
const frame = 60;
|
||||||
|
|
||||||
/* eslint-disable-next-line max-lines-per-function */
|
/* eslint-disable-next-line max-lines-per-function */
|
||||||
describe ('key store', () => {
|
describe ('key store', () => {
|
||||||
beforeAll (() => {
|
beforeAll (() => {
|
||||||
|
flush_routine ();
|
||||||
|
assert_keystore_state ();
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.install ();
|
.install ();
|
||||||
const base_date = (new Date);
|
const base_date = (new Date);
|
||||||
@ -140,11 +143,16 @@ describe ('key store', () => {
|
|||||||
.toEqual (sign);
|
.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
|
// TODO: required use case: insert keys for verification of old tokens
|
||||||
|
|
||||||
afterAll (() => {
|
afterAll (() => {
|
||||||
jasmine.clock ()
|
|
||||||
.tick (24 * 60 * 60 * 1000);
|
|
||||||
jasmine.clock ()
|
jasmine.clock ()
|
||||||
.uninstall ();
|
.uninstall ();
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user