export/import keys
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Timo Hocker 2021-01-08 13:30:53 +01:00
parent 4c42a682d5
commit d6a40871c4
5 changed files with 84 additions and 19 deletions

View File

@ -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 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 ## License
MIT © Timo Hocker <timo@scode.ovh> MIT © Timo Hocker <timo@scode.ovh>

View File

@ -49,7 +49,7 @@ class Authority {
key, key,
(info) => { (info) => {
try { try {
return keystore.get_key (info.iat / 1000); return keystore.get_key (info.iat / 1000, info.iss);
} }
catch { catch {
return ''; return '';
@ -89,6 +89,7 @@ class Authority {
const attributes = { const attributes = {
id: create_salt (), id: create_salt (),
iat: time, iat: time,
iss: keystore.instance_id,
type, type,
valid_for, valid_for,
next_module: options?.next_module next_module: options?.next_module

View File

@ -5,9 +5,10 @@
* Created by Timo Hocker <timo@scode.ovh>, December 2020 * Created by Timo Hocker <timo@scode.ovh>, 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 { interface Key {
key: string; key: string;
@ -21,11 +22,6 @@ interface KeyPair {
type KeyStoreData = Record<string, KeyPair>; type KeyStoreData = Record<string, KeyPair>;
function get_index (iat: number): string {
return Math.floor (iat / renew_interval)
.toFixed (0);
}
async function create_key (valid_for: number) { async function create_key (valid_for: number) {
const time = (new Date) const time = (new Date)
.getTime (); .getTime ();
@ -45,11 +41,22 @@ async function create_key (valid_for: number) {
class KeyStore { class KeyStore {
private _keys: KeyStoreData = {}; private _keys: KeyStoreData = {};
private _interval: NodeJS.Timeout; private _interval: NodeJS.Timeout;
private _instance: string;
public get instance_id (): string {
return this._instance;
}
public constructor () { public constructor () {
this._interval = setInterval (() => { this._interval = setInterval (() => {
this.garbage_collect (); this.garbage_collect ();
}, renew_interval); }, 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 { 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<string> { public async get_sign_key (
iat: number,
valid_for: number,
instance?: string
): Promise<string> {
if (valid_for <= 0) if (valid_for <= 0)
throw new Error ('cannot create infinitely valid key'); throw new Error ('cannot create infinitely valid key');
@ -76,7 +87,7 @@ 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 index = this.get_index (iat, instance);
const valid_until = (new Date) const valid_until = (new Date)
.getTime () + (valid_for * 1000); .getTime () + (valid_for * 1000);
@ -86,6 +97,9 @@ class KeyStore {
if (key.public_key.valid_until < valid_until) if (key.public_key.valid_until < valid_until)
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; return key.private_key?.key as string;
} }
@ -93,8 +107,8 @@ class KeyStore {
return this._keys[index].private_key?.key as string; return this._keys[index].private_key?.key as string;
} }
public get_key (iat: number): string { public get_key (iat: number, instance?: string): string {
const index = get_index (iat); const index = this.get_index (iat, instance);
if (typeof this._keys[index] === 'undefined') if (typeof this._keys[index] === 'undefined')
throw new Error ('key could not be found'); throw new Error ('key could not be found');
@ -115,8 +129,12 @@ class KeyStore {
public import_verification_data (data: KeyStoreData): void { public import_verification_data (data: KeyStoreData): void {
const import_set = { ...data }; const import_set = { ...data };
this.garbage_collect (import_set); this.garbage_collect (import_set);
for (const key of Object.keys (import_set)) {
// TODO: import if (typeof this._keys[key] !== 'undefined')
throw new Error ('cannot import to the same instance');
this._keys[key] = import_set[key];
}
this.garbage_collect ();
} }
} }

View File

@ -7,7 +7,6 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import http from 'http'; import http from 'http';
import { type } from 'os';
import ks from '../lib/KeyStore'; import ks from '../lib/KeyStore';
export class Response extends http.IncomingMessage { export class Response extends http.IncomingMessage {
@ -64,7 +63,7 @@ export function clock_setup ():void {
assert_keystore_state (); assert_keystore_state ();
const date = (new Date); const date = (new Date);
date.setSeconds (2, 0); date.setHours (0, 0, 2, 0);
jasmine.clock () jasmine.clock ()
.install (); .install ();
jasmine.clock () jasmine.clock ()

View File

@ -5,10 +5,10 @@
* Created by Timo Hocker <timo@scode.ovh>, December 2020 * Created by Timo Hocker <timo@scode.ovh>, December 2020
*/ */
import ks from '../../lib/KeyStore'; import ks, { KeyStore } from '../../lib/KeyStore';
import { clock_finalize, clock_setup } from '../Helper'; import { clock_finalize, clock_setup } from '../Helper';
const frame = 60; const frame = 3600;
/* eslint-disable-next-line max-lines-per-function */ /* eslint-disable-next-line max-lines-per-function */
describe ('key store', () => { describe ('key store', () => {
@ -146,5 +146,40 @@ describe ('key store', () => {
.toBeRejectedWithError ('cannot create infinitely valid key'); .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');
});
}); });