auth-server-helper/lib/KeyStore.ts
Timo Hocker e80e3f9a94
Some checks failed
continuous-integration/drone/push Build is failing
improve debug, redis storage structure
2022-08-15 17:33:25 +02:00

198 lines
5.6 KiB
TypeScript

/*
* 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 <timo@scode.ovh>, 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 } from './Redis';
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;
private _sync_redis = false;
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');
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<void> {
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)
}
};
if (this._sync_redis)
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<string> {
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<string> {
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') {
if (this._sync_redis)
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 = {};
this._sync_redis = false;
redis.disconnect ();
}
public sync_redis (url: string): void {
redis.connect (url);
this._sync_redis = true;
}
}
const ks: KeyStore = (new KeyStore);
export default ks;
export { KeyStore };