2020-12-13 13:37:11 +01:00
|
|
|
/*
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
|
2021-01-08 13:30:53 +01:00
|
|
|
import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper';
|
|
|
|
import { to_b58 } from '@sapphirecode/encoding-helper';
|
2022-01-05 12:32:04 +01:00
|
|
|
import { debug } from './debug';
|
2022-08-08 15:52:56 +02:00
|
|
|
import { KeyStoreData, KeyStoreExport } from './Key';
|
|
|
|
import { redis } from './Redis';
|
2022-01-05 12:32:04 +01:00
|
|
|
|
|
|
|
const logger = debug ('keystore');
|
2021-01-06 16:06:03 +01:00
|
|
|
|
2021-01-08 13:30:53 +01:00
|
|
|
const renew_interval = 3600;
|
2020-12-06 21:06:40 +01:00
|
|
|
|
|
|
|
class KeyStore {
|
2021-01-06 22:43:03 +01:00
|
|
|
private _keys: KeyStoreData = {};
|
|
|
|
private _interval: NodeJS.Timeout;
|
2021-01-08 13:30:53 +01:00
|
|
|
private _instance: string;
|
2022-08-08 15:52:56 +02:00
|
|
|
private _sync_redis = false;
|
2021-01-08 13:30:53 +01:00
|
|
|
|
|
|
|
public get instance_id (): string {
|
|
|
|
return this._instance;
|
|
|
|
}
|
2021-01-06 11:15:56 +01:00
|
|
|
|
2021-01-06 22:43:03 +01:00
|
|
|
public constructor () {
|
|
|
|
this._interval = setInterval (() => {
|
2021-01-07 15:43:54 +01:00
|
|
|
this.garbage_collect ();
|
2021-01-06 22:43:03 +01:00
|
|
|
}, renew_interval);
|
2021-01-08 13:30:53 +01:00
|
|
|
this._instance = to_b58 (random_hex (16), 'hex');
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('created keystore instance %s', this._instance);
|
2021-01-08 13:30:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private get_index (iat: number, instance = this._instance): string {
|
|
|
|
return instance + Math.floor (iat / renew_interval)
|
|
|
|
.toFixed (0);
|
2021-01-06 11:15:56 +01:00
|
|
|
}
|
2020-12-06 21:06:40 +01:00
|
|
|
|
2022-08-08 15:52:56 +02:00
|
|
|
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.set_key ({ ...result.public_key, index });
|
|
|
|
this._keys[index] = result;
|
|
|
|
}
|
|
|
|
|
2021-01-14 21:31:21 +01:00
|
|
|
private garbage_collect (): void {
|
2021-01-07 15:43:54 +01:00
|
|
|
const time = (new Date)
|
|
|
|
.getTime ();
|
2021-01-14 21:31:21 +01:00
|
|
|
const keys = Object.keys (this._keys);
|
2021-01-07 15:43:54 +01:00
|
|
|
for (const index of keys) {
|
2021-01-14 21:31:21 +01:00
|
|
|
const entry = this._keys[index];
|
2021-01-07 15:43:54 +01:00
|
|
|
if (typeof entry.private_key !== 'undefined'
|
|
|
|
&& entry.private_key.valid_until < time
|
2022-01-05 12:32:04 +01:00
|
|
|
) {
|
|
|
|
logger ('deleting expired private key');
|
2021-01-07 15:43:54 +01:00
|
|
|
delete entry.private_key;
|
2022-01-05 12:32:04 +01:00
|
|
|
}
|
2021-01-07 15:43:54 +01:00
|
|
|
|
2022-01-05 12:32:04 +01:00
|
|
|
if (entry.public_key.valid_until < time) {
|
|
|
|
logger ('deleting expired key pair');
|
2021-01-14 21:31:21 +01:00
|
|
|
delete this._keys[index];
|
2022-01-05 12:32:04 +01:00
|
|
|
}
|
2021-01-07 15:43:54 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-08 13:30:53 +01:00
|
|
|
public async get_sign_key (
|
|
|
|
iat: number,
|
|
|
|
valid_for: number,
|
|
|
|
instance?: string
|
|
|
|
): Promise<string> {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger (
|
|
|
|
'querying key from %s for timestamp %d, valid for %d',
|
|
|
|
instance,
|
|
|
|
iat,
|
|
|
|
valid_for
|
|
|
|
);
|
2021-01-06 16:06:03 +01:00
|
|
|
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');
|
2020-12-06 21:06:40 +01:00
|
|
|
|
2021-01-08 13:30:53 +01:00
|
|
|
const index = this.get_index (iat, instance);
|
2021-01-06 22:43:03 +01:00
|
|
|
|
2021-01-06 11:15:56 +01:00
|
|
|
const valid_until = (new Date)
|
|
|
|
.getTime () + (valid_for * 1000);
|
|
|
|
|
|
|
|
if (typeof this._keys[index] !== 'undefined') {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('loading existing key');
|
2021-01-06 11:15:56 +01:00
|
|
|
const key = this._keys[index];
|
2020-12-06 21:06:40 +01:00
|
|
|
|
2021-01-08 13:30:53 +01:00
|
|
|
if (typeof key.private_key === 'undefined')
|
|
|
|
throw new Error ('cannot access already expired keys');
|
|
|
|
|
2022-01-05 12:32:04 +01:00
|
|
|
if (key.public_key.valid_until < valid_until) {
|
|
|
|
logger ('updating key valid timespan to match new value');
|
|
|
|
key.public_key.valid_until = valid_until;
|
|
|
|
}
|
|
|
|
|
2021-01-06 22:43:03 +01:00
|
|
|
return key.private_key?.key as string;
|
|
|
|
}
|
2021-01-06 16:06:03 +01:00
|
|
|
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('key does not exist, creating a new one');
|
2022-08-08 15:52:56 +02:00
|
|
|
await this.create_key (index, valid_for);
|
2021-01-06 22:43:03 +01:00
|
|
|
return this._keys[index].private_key?.key as string;
|
2021-01-06 16:06:03 +01:00
|
|
|
}
|
2020-12-06 21:29:11 +01:00
|
|
|
|
2022-08-08 15:52:56 +02:00
|
|
|
public async get_key (iat: number, instance?: string): Promise<string> {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('querying public key from %s for timestamp %d', instance, iat);
|
2021-01-08 13:30:53 +01:00
|
|
|
const index = this.get_index (iat, instance);
|
2021-01-06 11:15:56 +01:00
|
|
|
|
2022-08-08 15:52:56 +02:00
|
|
|
let key = null;
|
|
|
|
|
|
|
|
if (typeof this._keys[index] === 'undefined') {
|
|
|
|
if (this._sync_redis)
|
|
|
|
key = await redis.get_key (index);
|
|
|
|
}
|
|
|
|
else { key = this._keys[index].public_key; }
|
|
|
|
|
|
|
|
if (key === null)
|
2021-01-06 16:06:03 +01:00
|
|
|
throw new Error ('key could not be found');
|
2020-12-06 21:06:40 +01:00
|
|
|
|
2022-08-08 15:52:56 +02:00
|
|
|
return key.key;
|
2021-01-06 22:43:03 +01:00
|
|
|
}
|
|
|
|
|
2021-01-14 21:31:21 +01:00
|
|
|
public export_verification_data (): KeyStoreExport {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('exporting public keys');
|
2021-01-07 15:43:54 +01:00
|
|
|
this.garbage_collect ();
|
2021-01-14 21:31:21 +01:00
|
|
|
const out: KeyStoreExport = [];
|
2021-01-06 22:43:03 +01:00
|
|
|
for (const index of Object.keys (this._keys))
|
2021-01-14 21:31:21 +01:00
|
|
|
out.push ({ ...this._keys[index].public_key, index });
|
2021-01-06 22:43:03 +01:00
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
2021-01-14 21:31:21 +01:00
|
|
|
public import_verification_data (data: KeyStoreExport): void {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('importing %d public keys', data.length);
|
2021-01-14 21:31:21 +01:00
|
|
|
for (const key of data) {
|
|
|
|
if (typeof this._keys[key.index] !== 'undefined')
|
2021-01-08 13:30:53 +01:00
|
|
|
throw new Error ('cannot import to the same instance');
|
2021-01-14 21:31:21 +01:00
|
|
|
this._keys[key.index] = {
|
|
|
|
public_key: {
|
|
|
|
key: key.key,
|
|
|
|
valid_until: key.valid_until
|
|
|
|
}
|
|
|
|
};
|
2021-01-08 13:30:53 +01:00
|
|
|
}
|
|
|
|
this.garbage_collect ();
|
2020-12-06 21:06:40 +01:00
|
|
|
}
|
2021-01-15 14:45:05 +01:00
|
|
|
|
|
|
|
public reset_instance (): void {
|
2022-01-05 12:32:04 +01:00
|
|
|
logger ('resetting keystore');
|
2021-01-15 14:45:05 +01:00
|
|
|
this._instance = to_b58 (random_hex (16), 'hex');
|
|
|
|
this._keys = {};
|
2022-08-08 15:52:56 +02:00
|
|
|
this._sync_redis = false;
|
|
|
|
redis.disconnect ();
|
|
|
|
}
|
|
|
|
|
|
|
|
public sync_redis (url: string): void {
|
|
|
|
redis.connect (url);
|
|
|
|
this._sync_redis = true;
|
2021-01-15 14:45:05 +01:00
|
|
|
}
|
2020-12-06 21:06:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const ks: KeyStore = (new KeyStore);
|
|
|
|
export default ks;
|
2022-08-08 15:52:56 +02:00
|
|
|
export { KeyStore };
|