blacklist with automatic garbage collector
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Timo Hocker 2022-09-09 15:49:53 +02:00
parent 31f739d4b8
commit 64d4f00629
No known key found for this signature in database
GPG Key ID: 3B86485AC71C835C
9 changed files with 153 additions and 51 deletions

View File

@ -6,13 +6,13 @@
*/
import {
create_salt,
sign_object,
verify_signature_get_info
} from '@sapphirecode/crypto-helper';
import keystore from './KeyStore';
import blacklist from './Blacklist';
import { debug } from './debug';
import { generate_token_id } from './token_id';
const logger = debug ('authority');
@ -102,13 +102,15 @@ class Authority {
const log = logger.extend ('sign');
log ('signing new %s', type);
const time = Date.now ();
const valid_until = time + (valid_for * 1e3);
const key = await keystore.get_sign_key (time / 1000, valid_for);
const attributes = {
id: create_salt (),
id: generate_token_id (new Date (valid_until)),
iat: time,
iss: keystore.instance_id,
type,
valid_for,
valid_until,
next_module: options?.next_module
};
const signature = sign_object (options?.data, key, attributes);

View File

@ -7,19 +7,31 @@
import { debug } from './debug';
import { redis_blacklist_store } from './RedisData/RedisBlacklistStore';
import { parse_token_id } from './token_id';
const logger = debug ('blacklist');
interface Signature {
hash: string;
token_id: string;
iat: number;
valid_until: Date;
}
interface ExportedSignature {
token_id: string;
iat: number;
}
class Blacklist {
private _signatures: Signature[];
private _interval: NodeJS.Timeout;
public constructor () {
this._signatures = [];
this._interval = setInterval (
this.garbage_collect.bind (this),
3600000
);
}
public async clear (
@ -34,10 +46,15 @@ class Blacklist {
}
}
public async add_signature (hash: string): Promise<void> {
logger.extend ('add_signature') ('blacklisting signature %s', hash);
this._signatures.push ({ iat: Date.now (), hash });
await redis_blacklist_store.add (hash);
public async add_signature (token_id: string): Promise<void> {
logger.extend ('add_signature') ('blacklisting signature %s', token_id);
const parsed = parse_token_id (token_id);
this._signatures.push ({
iat: Date.now (),
token_id,
valid_until: parsed.valid_until
});
await redis_blacklist_store.add (token_id, parsed.valid_until);
}
public async remove_signature (signature: number | string): Promise<void> {
@ -48,13 +65,20 @@ class Blacklist {
log ('received string, searching through signatures');
key = signature;
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].hash === signature)
if (this._signatures[i].token_id === signature) {
log ('removing sigature %s at %d', signature, i);
this._signatures.splice (i, 1);
}
}
}
else {
log ('received index, removing at index');
key = this._signatures[signature].hash;
log (
'received index, removing signature %s at index %s',
this._signatures[signature].token_id,
signature
);
key = this._signatures[signature].token_id;
this._signatures.splice (signature, 1);
}
await redis_blacklist_store.remove (key);
@ -64,7 +88,7 @@ class Blacklist {
const log = logger.extend ('is_valid');
log ('checking signature for blacklist entry %s', hash);
for (const sig of this._signatures) {
if (sig.hash === hash) {
if (sig.token_id === hash) {
log ('found matching blacklist entry');
return false;
}
@ -80,22 +104,44 @@ class Blacklist {
return true;
}
public export_blacklist (): Signature[] {
public export_blacklist (): ExportedSignature[] {
logger.extend ('export_blacklist') ('exporting blacklist');
return this._signatures;
return this._signatures.map ((v) => ({
iat: v.iat,
token_id: v.token_id
}));
}
public import_blacklist (data: Signature[]): void {
public import_blacklist (data: ExportedSignature[]): void {
logger.extend ('import_blacklist') (
'importing %d blacklist entries',
data.length
);
this._signatures.push (...data);
for (const token of data) {
const parsed = parse_token_id (token.token_id);
this._signatures.push ({
token_id: token.token_id,
iat: token.iat,
valid_until: parsed.valid_until
});
}
}
public sync_redis (url: string): void {
redis_blacklist_store.connect (url);
}
private async garbage_collect (): Promise<void> {
const log = logger.extend ('garbage_collect');
const time = new Date;
log ('removing signatures expired before', time);
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].valid_until < time) {
log ('signature %s expired', this._signatures[i].token_id);
await this.remove_signature (i);
}
}
}
}
const bl = (new Blacklist);

View File

@ -27,7 +27,7 @@ class KeyStore {
public constructor () {
this._interval = setInterval (() => {
this.garbage_collect ();
}, renew_interval);
}, renew_interval * 1000);
this._instance = to_b58 (random_hex (16), 'hex');
logger.extend ('constructor') (
'created keystore instance %s',

View File

@ -11,14 +11,18 @@ import { Redis } from '../Redis';
const logger = debug ('RedisBlacklistStore');
export class RedisBlacklistStore extends Redis {
public async add (key: string): Promise<void> {
public async add (key: string, valid_until: Date): Promise<void> {
const log = logger.extend ('set');
log ('trying to add key %s to redis blacklist', key);
if (!this.is_active) {
log ('redis is inactive, skipping');
return;
}
await this.redis.sadd ('blacklist', key);
await this.redis.setex (
`blacklist_${key}`,
(valid_until.getTime () - Date.now ()) / 1000,
1
);
log ('saved key');
}
@ -29,7 +33,7 @@ export class RedisBlacklistStore extends Redis {
log ('redis is inactive, skipping');
return;
}
await this.redis.srem ('blacklist', key);
await this.redis.del (`blacklist_${key}`);
log ('removed key');
}
@ -40,7 +44,7 @@ export class RedisBlacklistStore extends Redis {
log ('redis is inactive, skipping');
return false;
}
const res = await this.redis.sismember ('blacklist', key) === 1;
const res = await this.redis.exists (`blacklist_${key}`) === 1;
log ('found key %s', res);
return res;
}

21
lib/token_id.ts Normal file
View File

@ -0,0 +1,21 @@
import { create_salt } from '@sapphirecode/crypto-helper';
import { to_b58 } from '@sapphirecode/encoding-helper';
export function generate_token_id (valid_until: Date) {
const salt = create_salt ();
return `${to_b58 (salt, 'hex')};${valid_until.toISOString ()}`;
}
export function parse_token_id (id: string) {
// eslint-disable-next-line max-len
const regex = /^(?<hash>[A-HJ-NP-Za-km-z1-9]+);(?<date>\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d{3}Z)$/u;
const result = regex.exec (id);
if (result === null)
throw new Error (`invalid token id ${id}`);
if (typeof result.groups === 'undefined')
throw new Error ('invalid state');
return {
hash: result.groups.hash as string,
valid_until: new Date (result.groups.date as string)
};
}

View File

@ -1,6 +1,6 @@
{
"name": "@sapphirecode/auth-server-helper",
"version": "4.0.0",
"version": "4.0.1",
"main": "dist/lib/index.js",
"author": {
"name": "Timo Hocker",

View File

@ -6,10 +6,15 @@
*/
import blacklist, { Blacklist } from '../../lib/Blacklist';
import { generate_token_id, parse_token_id } from '../../lib/token_id';
import { clock_finalize, clock_setup } from '../Helper';
// eslint-disable-next-line max-lines-per-function
describe ('blacklist', () => {
const token1 = generate_token_id (new Date (Date.now () + 3600000));
const token2 = generate_token_id (new Date (Date.now () + 3600000));
const token3 = generate_token_id (new Date (Date.now () + 3600000));
beforeAll (() => {
clock_setup ();
});
@ -19,77 +24,88 @@ describe ('blacklist', () => {
});
it ('should validate any string', async () => {
expect (await blacklist.is_valid ('foo'))
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should blacklist strings', async () => {
await blacklist.add_signature ('foo');
await blacklist.add_signature ('bar');
expect (await blacklist.is_valid ('foo'))
await blacklist.add_signature (token1);
await blacklist.add_signature (token2);
expect (await blacklist.is_valid (token1))
.toBeFalse ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should remove one string', async () => {
await blacklist.remove_signature ('foo');
expect (await blacklist.is_valid ('foo'))
await blacklist.remove_signature (token1);
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should clear after time', async () => {
jasmine.clock ()
.tick (5000);
await blacklist.add_signature ('baz');
await blacklist.add_signature (token3);
await blacklist.clear (Date.now () - 100);
expect (await blacklist.is_valid ('foo'))
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeFalse ();
});
it ('should clear all', async () => {
await blacklist.add_signature ('foo');
await blacklist.add_signature ('bar');
await blacklist.add_signature ('baz');
expect (await blacklist.is_valid ('foo'))
await blacklist.add_signature (token1);
await blacklist.add_signature (token2);
await blacklist.add_signature (token3);
expect (await blacklist.is_valid (token1))
.toBeFalse ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeFalse ();
await blacklist.clear ();
expect (await blacklist.is_valid ('foo'))
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid ('bar'))
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid ('baz'))
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should export and import data', async () => {
await blacklist.add_signature ('baz');
const exp = blacklist.export_blacklist ();
const time = new Date;
const token = generate_token_id (time);
await blacklist.add_signature (token);
// eslint-disable-next-line dot-notation
expect (blacklist['_signatures'])
.toEqual (exp);
.toEqual ([
{
token_id: token,
iat: time.getTime (),
valid_until: time
}
]);
const exp = blacklist.export_blacklist ();
expect (exp)
.toEqual ([ { token_id: token, iat: time.getTime () } ]);
const bl2 = (new Blacklist);
bl2.import_blacklist (exp);
// eslint-disable-next-line dot-notation
expect (bl2['_signatures'])
.toEqual (exp);
// eslint-disable-next-line dot-notation
.toEqual (blacklist['_signatures']);
});
});

View File

@ -192,7 +192,7 @@ describe ('gateway', () => {
expect (resp.statusCode)
.toEqual (200);
const blacklisted = blacklist.export_blacklist ()
.map ((v) => v.hash);
.map ((v) => v.token_id);
expect (blacklisted)
.toContain (token.id);
expect (blacklisted)

13
test/spec/token_id.ts Normal file
View File

@ -0,0 +1,13 @@
import { generate_token_id, parse_token_id } from '../../lib/token_id';
describe ('token_id', () => {
it ('should always generate valid tokens', () => {
for (let i = 0; i < 1000; i++) {
const date = new Date;
const token_id = generate_token_id (new Date);
const parsed = parse_token_id (token_id);
expect (parsed.valid_until)
.toEqual (date);
}
});
});