/* * Copyright (C) Sapphirecode - All Rights Reserved * This file is part of Auth-Server-Helper which is released under MIT. * See file 'LICENSE' for full license details. * Created by Timo Hocker , December 2020 */ import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper'; import { to_b58 } from '@sapphirecode/encoding-helper'; import { debug } from './debug'; import { KeyStoreData, KeyStoreExport } from './Key'; import { redis_key_store } from './RedisData/RedisKeyStore'; const logger = debug ('keystore'); const renew_interval = 3600; 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 * 1000); this._instance = to_b58 (random_hex (16), 'hex'); logger.extend ('constructor') ( 'created keystore instance %s', this._instance ); } private get_index (iat: number, instance = this._instance): string { return instance + Math.floor (iat / renew_interval) .toFixed (0); } private async create_key (index: string, valid_for: number): Promise { const log = logger.extend ('create_key'); log ('generating new key'); const time = (new Date) .getTime (); const pair = await generate_keypair (); const result = { private_key: { key: pair.private_key, valid_until: time + (renew_interval * 1000) }, public_key: { key: pair.public_key, valid_until: time + (valid_for * 1000) } }; await redis_key_store.set ({ ...result.public_key, index }); this._keys[index] = result; } private garbage_collect (): void { const log = logger.extend ('garbage_collect'); const time = (new Date) .getTime (); const keys = Object.keys (this._keys); for (const index of keys) { const entry = this._keys[index]; if (typeof entry.private_key !== 'undefined' && entry.private_key.valid_until < time ) { log ('deleting expired private key'); delete entry.private_key; } if (entry.public_key.valid_until < time) { log ('deleting expired key pair'); delete this._keys[index]; } } } public async get_sign_key ( iat: number, valid_for: number, instance?: string ): Promise { const log = logger.extend ('get_sign_key'); log ( 'querying key from %s for timestamp %d, valid for %d', instance, iat, valid_for ); if (valid_for <= 0) throw new Error ('cannot create infinitely valid key'); if ((iat + 1) * 1000 < (new Date) .getTime ()) throw new Error ('cannot access already expired keys'); const index = this.get_index (iat, instance); const valid_until = (new Date) .getTime () + (valid_for * 1000); if (typeof this._keys[index] !== 'undefined') { log ('loading existing key'); const key = this._keys[index]; if (typeof key.private_key === 'undefined') throw new Error ('cannot access already expired keys'); if (key.public_key.valid_until < valid_until) { log ('updating key valid timespan to match new value'); key.public_key.valid_until = valid_until; } return key.private_key?.key as string; } log ('key does not exist, creating a new one'); await this.create_key (index, valid_for); return this._keys[index].private_key?.key as string; } public async get_key (iat: number, instance?: string): Promise { const log = logger.extend ('get_key'); log ('querying public key from %s for timestamp %d', instance, iat); const index = this.get_index (iat, instance); let key = null; if (typeof this._keys[index] === 'undefined') key = await redis_key_store.get (index); else key = this._keys[index].public_key; if (key === null) throw new Error ('key could not be found'); return key.key; } public export_verification_data (): KeyStoreExport { const log = logger.extend ('export_verification_data'); log ('exporting public keys'); log ('cleaning up before export'); this.garbage_collect (); const out: KeyStoreExport = []; for (const index of Object.keys (this._keys)) { log ('exporting key %s', index); out.push ({ ...this._keys[index].public_key, index }); } return out; } public import_verification_data (data: KeyStoreExport): void { const log = logger.extend ('import_verification_data'); log ('importing %d public keys', data.length); for (const key of data) { log ('importing key %s', key.index); if (typeof this._keys[key.index] !== 'undefined') throw new Error ('cannot import to the same instance'); this._keys[key.index] = { public_key: { key: key.key, valid_until: key.valid_until } }; } log ('running garbage collector'); this.garbage_collect (); } public reset_instance (): void { logger.extend ('reset_instance') ('resetting keystore'); this._instance = to_b58 (random_hex (16), 'hex'); this._keys = {}; redis_key_store.disconnect (); } public sync_redis (url: string): void { redis_key_store.connect (url); } } const ks: KeyStore = (new KeyStore); export default ks; export { KeyStore };