From 2fb8df213d35bf8e371545ecfd31abca5b9a9a11 Mon Sep 17 00:00:00 2001 From: Valentin Andruschenko Date: Wed, 2 May 2018 23:31:38 +0300 Subject: [PATCH] Issue #49: support asynchronous Encryption Key Store --- src/modules/Encryption.js | 14 +- src/modules/EventuateClient.js | 141 ++++++++++++--------- test/AggregateRepositoryEncryption-spec.js | 13 +- test/Encryption-spec.js | 47 +++++-- test/EventuateClientEncryption-spec.js | 11 +- test/staticAPI-spec.js | 35 ++--- test/subscribeEncrypted-spec.js | 13 +- 7 files changed, 177 insertions(+), 97 deletions(-) diff --git a/src/modules/Encryption.js b/src/modules/Encryption.js index 3f59fa5..df50f2a 100644 --- a/src/modules/Encryption.js +++ b/src/modules/Encryption.js @@ -9,13 +9,19 @@ export default class Encryption { } encrypt(encryptionKeyId, eventData) { - const cipher = this.cipher(this.findKey(encryptionKeyId), eventData); - return `${this.prefix}${JSON.stringify({encryptionKeyId, data: cipher})}`; + return this.findKey(encryptionKeyId) + .then(key => { + const cipher = this.cipher(key, eventData); + return `${this.prefix}${JSON.stringify({encryptionKeyId, data: cipher})}`; + }); } decrypt(encryptedEventData) { const { encryptionKeyId, data } = JSON.parse(encryptedEventData.split(this.prefix)[1]); - return this.decipher(this.findKey(encryptionKeyId), data); + return this.findKey(encryptionKeyId) + .then(key => { + return this.decipher(key, data); + }); } cipher(key, text) { @@ -29,7 +35,7 @@ export default class Encryption { } findKey(id) { - return this.encryptionKeyStore[id]; + return this.encryptionKeyStore.get(id); } isEncrypted(eventDataStr) { diff --git a/src/modules/EventuateClient.js b/src/modules/EventuateClient.js index e2f14e8..e815041 100644 --- a/src/modules/EventuateClient.js +++ b/src/modules/EventuateClient.js @@ -103,16 +103,19 @@ export default class EventuateClient { options = rest; let events = this.prepareEvents(_events); - // Encrypt event data if needed - events = this.encryptEvents(encryptionKeyId, events); - const jsonData = { entityTypeName, events }; - this.addBodyOptions(jsonData, options); + // Encrypt event data if needed + this.encryptEvents(encryptionKeyId, events) + .then(events => { + console.log('events:', events); + const jsonData = { entityTypeName, events }; + this.addBodyOptions(jsonData, options); - const urlPath = path.join(this.baseUrlPath, this.spaceName); - const requestOptions = { path: urlPath, method: 'POST', apiKey: this.apiKey, jsonData, client: this }; + const urlPath = path.join(this.baseUrlPath, this.spaceName); + const requestOptions = { path: urlPath, method: 'POST', apiKey: this.apiKey, jsonData, client: this }; - this.attemptOperation({ handler: this.httpRequest, arg: requestOptions, retryConditionFn, context: this }) + return this.attemptOperation({ handler: this.httpRequest, arg: requestOptions, retryConditionFn, context: this }); + }) .then(({ res: httpResponse, body: jsonBody }) => { const { entityId, entityVersion, eventIds } = jsonBody; @@ -163,12 +166,10 @@ export default class EventuateClient { .then(({ res: httpResponse, body: jsonBody }) => { let { events } = jsonBody; - if (this.encryption) { - events = this.decryptEvens(events); - } - - events = this.eventDataToObject(events); - result.success(events); + return this.decryptEvens(events); + }) + .then(events => { + result.success(this.eventDataToObject(events)); }) .catch(err => { result.failure(err); @@ -195,21 +196,19 @@ export default class EventuateClient { options = rest; let events = this.prepareEvents(_events); - // Encrypt event data if needed - events = this.encryptEvents(encryptionKeyId, events); - const jsonData = { - entityId, - entityVersion, - events - }; - this.addBodyOptions(jsonData, options); + // Encrypt event data if needed + this.encryptEvents(encryptionKeyId, events) + .then(events => { - const urlPath = path.join(this.baseUrlPath, this.spaceName, entityTypeName, entityId); + const jsonData = { entityId, entityVersion, events }; + this.addBodyOptions(jsonData, options); - const requestOptions = { path: urlPath, method: 'POST', apiKey: this.apiKey, jsonData, client: this }; + const urlPath = path.join(this.baseUrlPath, this.spaceName, entityTypeName, entityId); + const requestOptions = { path: urlPath, method: 'POST', apiKey: this.apiKey, jsonData, client: this }; - this.attemptOperation({ handler: this.httpRequest, arg: requestOptions, retryConditionFn, context: this }) + return this.attemptOperation({ handler: this.httpRequest, arg: requestOptions, retryConditionFn, context: this }) + }) .then(({ res: httpResponse, body: jsonBody }) => { const { entityId, entityVersion, eventIds} = jsonBody; @@ -371,16 +370,16 @@ export default class EventuateClient { body.forEach(eventStr => { - const { error, event } = this.makeEvent(eventStr, headers.ack); - - if (error) { - throw new Error(error); - } - - eventHandler(event) - .then(acknowledge) - .catch(err => { - logger.error('eventHandler error', err); + this.makeEvent(eventStr, headers.ack) + .then(event => { + eventHandler(event) + .then(acknowledge) + .catch(err => { + logger.error('eventHandler error', err); + }); + }) + .catch(error => { + throw new Error(error); }); }); } @@ -587,29 +586,31 @@ export default class EventuateClient { } makeEvent(eventStr, ack) { + try { const parsedEvent = JSON.parse(eventStr); const { id: eventId, eventType, entityId, entityType, swimlane, eventToken } = parsedEvent; let { eventData: eventDataStr } = parsedEvent; + } catch (err) { + return Promise.reject(err); + } - eventDataStr = this.decrypt(eventDataStr); - const eventData = JSON.parse(eventDataStr); - - const event = { - eventId, - eventType, - entityId, - swimlane, - eventData, - eventToken, - ack, - entityType: entityType.split('/').pop(), - }; + return this.decrypt(eventDataStr) + .then(decryptedEventDataStr => { + const eventData = JSON.parse(decryptedEventDataStr); + + return { + eventId, + eventType, + entityId, + swimlane, + eventData, + eventToken, + ack, + entityType: entityType.split('/').pop(), + }; + }); - return { event }; - } catch (error) { - return { error }; - } } serialiseObject(obj) { @@ -719,39 +720,53 @@ export default class EventuateClient { } encryptEvents(encryptionKeyId, events) { - return events.map(({ eventData, ...rest }) => { - eventData = this.encrypt(encryptionKeyId, eventData); - return { eventData, ...rest }; + const promises = events.map(({ eventData }) => { + return this.encrypt(encryptionKeyId, eventData); }); + return Promise.all(promises) + .then(encryptedEventDataArr => { + return events.map(({ eventData, ...rest }, index) => { + return { + eventData: encryptedEventDataArr[index], + ...rest + }; + }) + }); } decryptEvens(events) { - return events.map(({ eventData, ...rest }) => { - eventData = this.decrypt(eventData); - return { eventData, ...rest }; - }); + const promises = events.map(({ eventData }) => { + return this.decrypt(eventData); + }); + return Promise.all(promises) + .then(decryptedEventDataArr => { + return events.map(({ eventData, ...rest }, index) => { + return { + eventData: decryptedEventDataArr[index], + ...rest + }; + }) + }); } encrypt(encryptionKeyId, eventData) { if (encryptionKeyId && this.encryption) { - eventData = this.encryption.encrypt(encryptionKeyId, eventData); + return this.encryption.encrypt(encryptionKeyId, eventData); } - return eventData; + return Promise.resolve(eventData); } decrypt(eventDataStr) { if (this.encryption && this.encryption.isEncrypted(eventDataStr)) { return this.encryption.decrypt(eventDataStr); } - - return eventDataStr; + return Promise.resolve(eventDataStr); } } function statusCodeError(statusCode, message) { if (statusCode !== 200) { - return new EventuateServerError({ error: `Server returned status code ${statusCode}`, statusCode, diff --git a/test/AggregateRepositoryEncryption-spec.js b/test/AggregateRepositoryEncryption-spec.js index de21f70..b824524 100644 --- a/test/AggregateRepositoryEncryption-spec.js +++ b/test/AggregateRepositoryEncryption-spec.js @@ -12,7 +12,18 @@ const EntityClass = require('./lib/EncryptedEntityClass'); const { CreatedEntityCommand, UpdateEntityCommand, FailureCommand } = EntityClass; const encryptionKeyId = 'id'; const keySecret = 'secret'; -const encryptionKeyStore = { [encryptionKeyId]: keySecret }; + +class EncryptionStore { + constructor(keys) { + this.keys = keys; + } + + get(encryptionKeyId) { + return Promise.resolve(this.keys[encryptionKeyId]); + } +} + +const encryptionKeyStore = new EncryptionStore({ [encryptionKeyId]: keySecret }); const encryption = new Encryption(encryptionKeyStore); const eventuateClient = createEventuateClient(encryption); diff --git a/test/Encryption-spec.js b/test/Encryption-spec.js index 3d70f3f..7c1d91b 100644 --- a/test/Encryption-spec.js +++ b/test/Encryption-spec.js @@ -4,10 +4,20 @@ const Encryption = require('../dist/modules/Encryption'); const keyId = 'id'; const keySecret = 'secret'; -const encryptionKeyStore = { [keyId]: keySecret }; +class EncryptionStore { + constructor(keys) { + this.keys = keys; + } + + get(encryptionKeyId) { + return Promise.resolve(this.keys[encryptionKeyId]); + } +} +const encryptionKeyStore = new EncryptionStore({ [keyId]: keySecret }); const encryptedPrefix = '__ENCRYPTED__'; const encryption = new Encryption(encryptionKeyStore); + describe('Encryption', () => { it('should check structure', () => { console.log('encryption:', encryption); @@ -21,6 +31,7 @@ describe('Encryption', () => { expect(encryption.cipher).to.be.a('function'); expect(encryption.decipher).to.be.a('function'); expect(encryption.isEncrypted).to.be.a('function'); + expect(encryption.findKey).to.be.a('function'); }); it('isEncrypted() should return true', () => { @@ -28,27 +39,43 @@ describe('Encryption', () => { expect(encryption.isEncrypted(str)).to.be.true; }); - it('isEncrypted() should return true', () => { + it('isEncrypted() should return false', () => { const str = 'abcde'; expect(encryption.isEncrypted(str)).to.be.false; }); + it('should find encryption key', done => { + encryption.findKey(keyId) + .then(key => { + expect(key).to.equal(keySecret); + done(); + }) + .catch(done); + }); + it('should cipher and decipher', () => { const text = 'secret text'; - const cipher = encryption.cipher(encryptionKeyStore[keyId], text); - const decipher = encryption.decipher(encryptionKeyStore[keyId], cipher); + const cipher = encryption.cipher(keySecret, text); + const decipher = encryption.decipher(keySecret, cipher); expect(decipher).to.equal(text); }); - it('should encrypt and decrypt', () => { + it('should encrypt and decrypt', done => { const eventData = { a: '1', b: 2 }; const eventDataString = JSON.stringify(eventData); - const encryptedEventData = encryption.encrypt(keyId, eventDataString); - const cipher = encryption.cipher(encryptionKeyStore[keyId], eventDataString); - expect(encryptedEventData).to.equal(`${encryptedPrefix}${JSON.stringify({ encryptionKeyId: keyId, data: cipher })}`); + encryption.encrypt(keyId, eventDataString) + .then(encryptedEventData => { + const cipher = encryption.cipher(keySecret, eventDataString); + expect(encryptedEventData).to.equal(`${encryptedPrefix}${JSON.stringify({ encryptionKeyId: keyId, data: cipher })}`); + + return encryption.decrypt(encryptedEventData); + }) + .then(decryptedEventData => { + expect(decryptedEventData).to.equal(eventDataString); + done(); + }) + .catch(done); - const decryptedEventData = encryption.decrypt(encryptedEventData); - expect(decryptedEventData).to.equal(eventDataString); }); }); diff --git a/test/EventuateClientEncryption-spec.js b/test/EventuateClientEncryption-spec.js index a549237..0814f5b 100644 --- a/test/EventuateClientEncryption-spec.js +++ b/test/EventuateClientEncryption-spec.js @@ -6,7 +6,16 @@ const Encryption = require('../dist/modules/Encryption'); const encryptionKeyId = 'id'; const keySecret = 'secret'; -const encryptionKeyStore = { [encryptionKeyId]: keySecret }; +class EncryptionStore { + constructor(keys) { + this.keys = keys; + } + + get(encryptionKeyId) { + return Promise.resolve(this.keys[encryptionKeyId]); + } +} +const encryptionKeyStore = new EncryptionStore({ [encryptionKeyId]: keySecret }); const encryption = new Encryption(encryptionKeyStore); const eventuateClient = helpers.createEventuateClient(encryption); diff --git a/test/staticAPI-spec.js b/test/staticAPI-spec.js index 655ea8f..a718569 100644 --- a/test/staticAPI-spec.js +++ b/test/staticAPI-spec.js @@ -1,8 +1,8 @@ 'use strict'; const expect = require('chai').expect; const helpers = require('./lib/helpers'); -const escapeStr = require('../dist/modules/specialChars').escapeStr; -const retryNTimes = require('../dist/modules/utils').retryNTimes; +const { escapeStr } = require('../dist/modules/specialChars'); +const { retryNTimes } = require('../dist/modules/utils'); const timeout = 15000; const eventuateClient = helpers.createEventuateClient(); @@ -16,19 +16,23 @@ describe('Test static API ', () => { expect(eventuateClient.makeEvent).to.be.a('Function'); }); - it('should return error for empty string', () => { - const result = eventuateClient.makeEvent(''); - expect(result).to.have.property('error'); - expect(result.error).to.be.instanceof(Error); + it('should return error for empty string', done => { + eventuateClient.makeEvent('') + .catch(error => { + expect(error).to.be.instanceof(Error); + done(); + }); }); it('should return error for event with empty eventData', () => { const eventStr = '{"id":"00000151e8f00022-0242ac1100320002","entityId":"00000151e8f00021-0242ac1100160000","entityType":"d6bfa47c283f4fcfb23c49b2df8c10ed/default/net.chrisrichardson.eventstore.example.MyEntity1451312021100","eventData":"","eventType":"net.chrisrichardson.eventstore.example.MyEntityWasCreated"}'; - const result = eventuateClient.makeEvent(eventStr); - expect(result).to.have.property('error'); - expect(result.error).to.be.instanceof(Error); + eventuateClient.makeEvent('') + .catch(error => { + expect(error).to.be.instanceof(Error); + done(); + }); }); it('should parse event', done => { @@ -50,15 +54,12 @@ describe('Test static API ', () => { eventId: '00000151e8f6932d-0242ac1100320002', eventType: 'net.chrisrichardson.eventstore.example.MyEntityWasCreated' } }; - const result = eventuateClient.makeEvent(eventStr, escapeStr(JSON.stringify(ack))); - - if (result.error) { - return done(result.error); - } + eventuateClient.makeEvent(eventStr, escapeStr(JSON.stringify(ack))) + .then(event => { + helpers.expectEvent(event, done); + }) + .catch(done); - expect(result).to.have.property('event'); - const event = result.event; - helpers.expectEvent(event, done); }); }); diff --git a/test/subscribeEncrypted-spec.js b/test/subscribeEncrypted-spec.js index bceec7b..1390e58 100644 --- a/test/subscribeEncrypted-spec.js +++ b/test/subscribeEncrypted-spec.js @@ -5,7 +5,18 @@ const Encryption = require('../dist/modules/Encryption'); const encryptionKeyId = 'id'; const keySecret = 'secret'; -const encryptionKeyStore = { [encryptionKeyId]: keySecret }; + +class EncryptionStore { + constructor(keys) { + this.keys = keys; + } + + get(encryptionKeyId) { + return Promise.resolve(this.keys[encryptionKeyId]); + } +} + +const encryptionKeyStore = new EncryptionStore({ [encryptionKeyId]: keySecret }); const encryption = new Encryption(encryptionKeyStore); const eventuateClient = helpers.createEventuateClient(encryption);