@getanthill/datastore
Version:
Event-Sourced Datastore
442 lines (359 loc) • 13.4 kB
text/typescript
import type { Services } from '../typings';
import crypto from 'crypto';
import { MongoDbConnector } from '@getanthill/mongodb-connector';
import usersModel from '../../test/fixtures/users';
import { init } from '.';
import setup from '../../test/setup';
describe('models', () => {
let app;
let services: Services;
let mongodb: MongoDbConnector;
beforeAll(async () => {
app = await setup.build();
services = app.services;
mongodb = services.mongodb;
});
afterEach(async () => {
jest.restoreAllMocks();
await setup.cleanModels(services);
});
afterAll(async () => {
await setup.teardownDb(services.mongodb);
});
describe('#rotateEncryptionKey', () => {
let models;
beforeEach(async () => {
services.config.security.activeNumberEncryptionKeys = 1;
services.config.security.encryptionKeys = {
all: [crypto.randomBytes(16).toString('hex')],
users: [crypto.randomBytes(16).toString('hex')],
};
models = init({ models: [] }, services);
await models.initInternalModels();
});
afterEach(() => {
jest.restoreAllMocks();
});
it('stops if no more entity is found on the cursor', async () => {
const Model = {
getCorrelationField: jest.fn(),
decrypt: jest.fn(),
};
const collection = {
find: jest.fn().mockImplementation(() => ({
hasNext: jest.fn().mockResolvedValue(true),
next: jest.fn().mockResolvedValue(null),
})),
};
await models.rotateEncryptionKeyOnCollection(Model, collection, {}, []);
expect(Model.decrypt).toHaveBeenCalledTimes(0);
});
it('noops on models without any encryption field', async () => {
await models.createModel({
...usersModel,
encrypted_fields: [],
});
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
const rotateEncryptionKeyOnCollectionMock = jest.spyOn(
models,
'rotateEncryptionKeyOnCollection',
);
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
expect(rotateEncryptionKeyOnCollectionMock).toHaveBeenCalledTimes(0);
});
it('noops on models without any encryption field (undefined)', async () => {
const modifiedModel = { ...usersModel };
delete modifiedModel.encrypted_fields;
await models.createModel(modifiedModel);
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
const rotateEncryptionKeyOnCollectionMock = jest.spyOn(
models,
'rotateEncryptionKeyOnCollection',
);
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
expect(rotateEncryptionKeyOnCollectionMock).toHaveBeenCalledTimes(0);
});
it('reencrypts entities with latest available encryption key', async () => {
await models.createModel(usersModel);
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
const aliceReencrypted = models.factory('users', alice.correlationId);
await aliceReencrypted.getState();
expect(alice.state).not.toEqual(aliceReencrypted.state);
expect(await models.getModel('users').decrypt(alice.state)).toEqual(
await models.getModel('users').decrypt(aliceReencrypted.state),
);
});
it('reencrypts entities with latest available encryption key (multiple eligible keys)', async () => {
services.config.security.activeNumberEncryptionKeys = 2;
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.createModel(usersModel);
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
crypto.randomBytes(16).toString('hex'),
// Previous keys
services.config.security.encryptionKeys.users[0],
services.config.security.encryptionKeys.users[1],
services.config.security.encryptionKeys.users[2],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
await models.rotateEncryptionKey();
const aliceReencrypted = models.factory('users', alice.correlationId);
await aliceReencrypted.getState();
expect(alice.state).not.toEqual(aliceReencrypted.state);
expect(await models.getModel('users').decrypt(alice.state)).toEqual(
await models.getModel('users').decrypt(aliceReencrypted.state),
);
});
it('noops on entities encrypted with unknown key', async () => {
await models.createModel(usersModel);
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
crypto.randomBytes(16).toString('hex'),
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
const aliceReencrypted = models.factory('users', alice.correlationId);
await aliceReencrypted.getState();
expect(alice.state).toEqual(aliceReencrypted.state);
expect(await models.getModel('users').decrypt(alice.state)).toEqual(
await models.getModel('users').decrypt(aliceReencrypted.state),
);
});
it('reencrypts all events with latest available encryption key', async () => {
await models.createModel(usersModel);
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
await alice.update({
lastname: 'Doe',
});
await alice.update({
sensitive_data: 'sensitive_data_2',
});
const eventsBeforeReencryption = await alice.getEvents().toArray();
// Keys rotation
services.config.security.encryptionKeys = {
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
const eventsAfterReencryption = await alice.getEvents().toArray();
const decryptedEventsBeforeReencryption = await Promise.all(
eventsBeforeReencryption.map((event) =>
models.getModel('users').decrypt(event),
),
);
const decryptedEventsAfterReencryption = await Promise.all(
eventsAfterReencryption.map((event) =>
models.getModel('users').decrypt(event),
),
);
expect(eventsBeforeReencryption).not.toEqual(
eventsAfterReencryption.state,
);
expect(decryptedEventsBeforeReencryption).toEqual(
decryptedEventsAfterReencryption,
);
});
it('reencrypts entities from several models with latest available encryption key', async () => {
await models.createModel(usersModel);
await models.createModel({
...usersModel,
name: 'accounts',
encrypted_fields: ['firstname', 'lastname'],
});
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
const bernard = models.factory('accounts');
await bernard.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
// Keys rotation
services.config.security.encryptionKeys = {
all: services.config.security.encryptionKeys.all,
// Defining a new key specific to this model
accounts: [crypto.randomBytes(16).toString('hex')],
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey();
const aliceReencrypted = models.factory('users', alice.correlationId);
await aliceReencrypted.getState();
const bernardReencrypted = models.factory(
'accounts',
bernard.correlationId,
);
await bernardReencrypted.getState();
expect(alice.state).not.toEqual(aliceReencrypted.state);
expect(await models.getModel('users').decrypt(alice.state)).toEqual(
await models.getModel('users').decrypt(aliceReencrypted.state),
);
expect(bernard.state).not.toEqual(bernardReencrypted.state);
expect(await models.getModel('accounts').decrypt(bernard.state)).toEqual(
await models.getModel('accounts').decrypt(bernardReencrypted.state),
);
});
it('reencrypts only entities from selected models with latest available encryption key', async () => {
await models.createModel(usersModel);
await models.createModel({
...usersModel,
name: 'accounts',
encrypted_fields: ['firstname', 'lastname'],
});
await models.reload();
const alice = models.factory('users');
await alice.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
const bernard = models.factory('accounts');
await bernard.create({
firstname: 'alice',
lastname: 'doe',
sensitive_data: 'sensitive_data',
});
// Keys rotation
services.config.security.encryptionKeys = {
all: services.config.security.encryptionKeys.all,
// Defining a new key specific to this model
accounts: [crypto.randomBytes(16).toString('hex')],
users: [
crypto.randomBytes(16).toString('hex'),
// Previous key
services.config.security.encryptionKeys.users[0],
],
};
models = init({ models: [] }, services);
await models.reload();
// Re-encrypt data with latest available key:
await models.rotateEncryptionKey(['accounts']);
const aliceReencrypted = models.factory('users', alice.correlationId);
await aliceReencrypted.getState();
const bernardReencrypted = models.factory(
'accounts',
bernard.correlationId,
);
await bernardReencrypted.getState();
// Alice unchanged
expect(alice.state).toEqual(aliceReencrypted.state);
expect(await models.getModel('users').decrypt(alice.state)).toEqual(
await models.getModel('users').decrypt(aliceReencrypted.state),
);
// Bernard reencrypted
expect(bernard.state).not.toEqual(bernardReencrypted.state);
expect(await models.getModel('accounts').decrypt(bernard.state)).toEqual(
await models.getModel('accounts').decrypt(bernardReencrypted.state),
);
});
});
});