UNPKG

@getanthill/datastore

Version:

Event-Sourced Datastore

442 lines (359 loc) 13.4 kB
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), ); }); }); });