diff --git a/eventuate-client-java-common-impl/build.gradle b/eventuate-client-java-common-impl/build.gradle index 4608dd5..734d601 100644 --- a/eventuate-client-java-common-impl/build.gradle +++ b/eventuate-client-java-common-impl/build.gradle @@ -3,6 +3,7 @@ apply plugin: PrivateModulePlugin dependencies { compile project(":eventuate-client-java") + compile "org.springframework.security:spring-security-core:5.0.4.RELEASE" compile "com.fasterxml.jackson.core:jackson-databind:2.8.8" compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.8.8" } \ No newline at end of file diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudFindOptions.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudFindOptions.java index 1aab466..a8dbab5 100644 --- a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudFindOptions.java +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudFindOptions.java @@ -1,6 +1,7 @@ package io.eventuate.javaclient.commonimpl; import io.eventuate.EventContext; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKeyStore; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; @@ -8,27 +9,29 @@ public class AggregateCrudFindOptions { - private Optional triggeringEvent = Optional.empty(); + private Optional triggeringEvent; + private Optional encryptionKeyStore; public AggregateCrudFindOptions() { + triggeringEvent = Optional.empty(); + encryptionKeyStore = Optional.empty(); } public AggregateCrudFindOptions(Optional triggeringEvent) { + this(triggeringEvent, Optional.empty()); + } + + public AggregateCrudFindOptions(Optional triggeringEvent, Optional encryptionKeyStore) { this.triggeringEvent = triggeringEvent; + this.encryptionKeyStore = encryptionKeyStore; } public Optional getTriggeringEvent() { return triggeringEvent; } - @Override - public int hashCode() { - return HashCodeBuilder.reflectionHashCode(this); - } - - @Override - public boolean equals(Object obj) { - return EqualsBuilder.reflectionEquals(this, obj); + public Optional getEncryptionKeyStore() { + return encryptionKeyStore; } public AggregateCrudFindOptions withTriggeringEvent(EventContext eventContext) { @@ -40,4 +43,14 @@ public AggregateCrudFindOptions withTriggeringEvent(Optional event this.triggeringEvent = eventContext; return this; } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } } diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudSaveOptions.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudSaveOptions.java index a0a7e41..c6a3438 100644 --- a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudSaveOptions.java +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudSaveOptions.java @@ -1,6 +1,7 @@ package io.eventuate.javaclient.commonimpl; import io.eventuate.EventContext; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKey; import java.util.Optional; @@ -8,15 +9,22 @@ public class AggregateCrudSaveOptions { private final Optional entityId; private final Optional triggeringEvent; + private final Optional encryptionKey; public AggregateCrudSaveOptions() { - this.entityId = Optional.empty(); - this.triggeringEvent = Optional.empty(); + entityId = Optional.empty(); + triggeringEvent = Optional.empty(); + encryptionKey = Optional.empty(); } public AggregateCrudSaveOptions(Optional triggeringEvent, Optional entityId) { + this(triggeringEvent, entityId, Optional.empty()); + } + + public AggregateCrudSaveOptions(Optional triggeringEvent, Optional entityId, Optional encryptionKey) { this.triggeringEvent = triggeringEvent; this.entityId = entityId; + this.encryptionKey = encryptionKey; } public Optional getEntityId() { @@ -27,14 +35,15 @@ public Optional getTriggeringEvent() { return triggeringEvent; } + public Optional getEncryptionKey() { + return encryptionKey; + } - public AggregateCrudSaveOptions withEventContext(EventContext ectx) { - return new AggregateCrudSaveOptions(Optional.of(ectx), this.entityId); - + public AggregateCrudSaveOptions withEventContext(EventContext eventContext) { + return new AggregateCrudSaveOptions(Optional.of(eventContext), entityId, encryptionKey); } public AggregateCrudSaveOptions withId(String entityId) { - return new AggregateCrudSaveOptions(this.triggeringEvent, Optional.of(entityId)); + return new AggregateCrudSaveOptions(triggeringEvent, Optional.of(entityId), encryptionKey); } - } diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudUpdateOptions.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudUpdateOptions.java index 297b18b..162cc30 100644 --- a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudUpdateOptions.java +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/AggregateCrudUpdateOptions.java @@ -1,6 +1,7 @@ package io.eventuate.javaclient.commonimpl; import io.eventuate.EventContext; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKey; import org.apache.commons.lang.builder.ToStringBuilder; import java.util.Optional; @@ -9,15 +10,34 @@ public class AggregateCrudUpdateOptions { private final Optional triggeringEvent; private final Optional snapshot; + private final Optional encryptionKey; public AggregateCrudUpdateOptions() { - this.triggeringEvent = Optional.empty(); - this.snapshot = Optional.empty(); + triggeringEvent = Optional.empty(); + snapshot = Optional.empty(); + encryptionKey = Optional.empty(); } public AggregateCrudUpdateOptions(Optional triggeringEvent, Optional snapshot) { + this(triggeringEvent, snapshot, Optional.empty()); + } + + public AggregateCrudUpdateOptions(Optional triggeringEvent, Optional snapshot, Optional encryptionKey) { this.triggeringEvent = triggeringEvent; this.snapshot = snapshot; + this.encryptionKey = encryptionKey; + } + + public Optional getTriggeringEvent() { + return triggeringEvent; + } + + public Optional getSnapshot() { + return snapshot; + } + + public Optional getEncryptionKey() { + return encryptionKey; } public AggregateCrudUpdateOptions withSnapshot(SerializedSnapshot serializedSnapshot) { @@ -29,16 +49,8 @@ public String toString() { return ToStringBuilder.reflectionToString(this); } - public Optional getTriggeringEvent() { - return triggeringEvent; - } - - public Optional getSnapshot() { - return snapshot; - } - public AggregateCrudUpdateOptions withTriggeringEvent(EventContext eventContext) { - return new AggregateCrudUpdateOptions(Optional.of(eventContext), this.snapshot); + return new AggregateCrudUpdateOptions(Optional.of(eventContext), snapshot); } } diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptedEventData.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptedEventData.java new file mode 100644 index 0000000..acdfdb7 --- /dev/null +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptedEventData.java @@ -0,0 +1,47 @@ +package io.eventuate.javaclient.commonimpl.encryption; + +import io.eventuate.javaclient.commonimpl.JSonMapper; + +public class EncryptedEventData { + public static String ENCRYPTED_EVENT_DATA_STRING_PREFIX = "__ENCRYPTED__"; + + private String encryptionKeyId; + private String data; + + public EncryptedEventData() { + } + + public EncryptedEventData(String encryptionKeyId, String data) { + this.encryptionKeyId = encryptionKeyId; + this.data = data; + } + + public static boolean checkIfEventDataStringIsEncrypted(String eventData) { + return eventData.startsWith(ENCRYPTED_EVENT_DATA_STRING_PREFIX); + } + + public static EncryptedEventData fromEventDataString(String eventData) { + String json = eventData.substring(ENCRYPTED_EVENT_DATA_STRING_PREFIX.length()); + return JSonMapper.fromJson(json, EncryptedEventData.class); + } + + public String getEncryptionKeyId() { + return encryptionKeyId; + } + + public void setEncryptionKeyId(String encryptionKeyId) { + this.encryptionKeyId = encryptionKeyId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String asString() { + return ENCRYPTED_EVENT_DATA_STRING_PREFIX + JSonMapper.toJson(this); + } +} diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKey.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKey.java new file mode 100644 index 0000000..3ea2e6e --- /dev/null +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKey.java @@ -0,0 +1,33 @@ +package io.eventuate.javaclient.commonimpl.encryption; + +public class EncryptionKey { + private String id; + private String key; + private String salt; + + public EncryptionKey(String id, String key, String salt) { + this.id = id; + this.key = key; + this.salt = salt; + } + + public String getId() { + return id; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getSalt() { + return salt; + } + + public void setSalt(String salt) { + this.salt = salt; + } +} diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKeyStore.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKeyStore.java new file mode 100644 index 0000000..3e6eeb3 --- /dev/null +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EncryptionKeyStore.java @@ -0,0 +1,5 @@ +package io.eventuate.javaclient.commonimpl.encryption; + +public interface EncryptionKeyStore { + EncryptionKey findEncryptionKeyById(String keyId); +} diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EventDataEncryptor.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EventDataEncryptor.java new file mode 100644 index 0000000..11c2bf0 --- /dev/null +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/encryption/EventDataEncryptor.java @@ -0,0 +1,13 @@ +package io.eventuate.javaclient.commonimpl.encryption; + +import org.springframework.security.crypto.encrypt.Encryptors; + +public class EventDataEncryptor { + public static String encrypt(EncryptionKey encryptionKey, String eventData) { + return Encryptors.text(encryptionKey.getKey(), encryptionKey.getSalt()).encrypt(eventData); + } + + public static String decrypt(EncryptionKey encryptionKey, String eventData) { + return Encryptors.text(encryptionKey.getKey(), encryptionKey.getSalt()).decrypt(eventData); + } +} diff --git a/eventuate-client-java-jdbc-common/src/main/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImpl.java b/eventuate-client-java-jdbc-common/src/main/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImpl.java index 9ef3842..a252c3b 100644 --- a/eventuate-client-java-jdbc-common/src/main/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImpl.java +++ b/eventuate-client-java-jdbc-common/src/main/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImpl.java @@ -17,6 +17,10 @@ import io.eventuate.javaclient.commonimpl.LoadedEvents; import io.eventuate.javaclient.commonimpl.SerializedSnapshot; import io.eventuate.javaclient.commonimpl.SerializedSnapshotWithVersion; +import io.eventuate.javaclient.commonimpl.encryption.EncryptedEventData; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKey; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKeyStore; +import io.eventuate.javaclient.commonimpl.encryption.EventDataEncryptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DuplicateKeyException; @@ -31,8 +35,6 @@ public class EventuateJdbcAccessImpl implements EventuateJdbcAccess { - public static final String DEFAULT_DATABASE_SCHEMA = "eventuate"; - protected Logger logger = LoggerFactory.getLogger(getClass()); private JdbcTemplate jdbcTemplate; @@ -69,13 +71,15 @@ public SaveUpdateResult save(String aggregateType, List events throw new EntityAlreadyExistsException(); } - for (EventIdTypeAndData event : eventsWithIds) jdbcTemplate.update(String.format("INSERT INTO %s (event_id, event_type, event_data, entity_type, entity_id, triggering_event, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", eventTable), - event.getId().asString(), event.getEventType(), event.getEventData(), aggregateType, entityId, + event.getId().asString(), + event.getEventType(), + encryptEventDataIfNeeded(event.getEventData(), saveOptions.flatMap(AggregateCrudSaveOptions::getEncryptionKey)), + aggregateType, + entityId, saveOptions.flatMap(AggregateCrudSaveOptions::getTriggeringEvent).map(EventContext::getEventToken).orElse(null), - event.getMetadata().orElse(null) - ); + event.getMetadata().orElse(null)); return new SaveUpdateResult(new EntityIdVersionAndEventIds(entityId, entityVersion, eventsWithIds.stream().map(EventIdTypeAndData::getId).collect(Collectors.toList())), new PublishableEvents(aggregateType, entityId, eventsWithIds)); @@ -140,6 +144,9 @@ public > LoadedEvents find(String aggregateType, String e ); } + events.forEach(eventAndTrigger -> + eventAndTrigger.event.setEventData(decryptEventDataIfNeeded(eventAndTrigger.event.getEventData(), findOptions.flatMap(AggregateCrudFindOptions::getEncryptionKeyStore)))); + logger.debug("Loaded {} events", events); Optional matching = findOptions. flatMap(AggregateCrudFindOptions::getTriggeringEvent). @@ -229,7 +236,7 @@ public SaveUpdateResult update(EntityIdAndType entityIdAndType, Int128 entityVer jdbcTemplate.update(String.format("INSERT INTO %s (event_id, event_type, event_data, entity_type, entity_id, triggering_event, metadata) VALUES (?, ?, ?, ?, ?, ?, ?)", eventTable), event.getId().asString(), event.getEventType(), - event.getEventData(), + encryptEventDataIfNeeded(event.getEventData(), updateOptions.flatMap(AggregateCrudUpdateOptions::getEncryptionKey)), entityType, entityId, updateOptions.flatMap(AggregateCrudUpdateOptions::getTriggeringEvent).map(EventContext::getEventToken).orElse(null), @@ -253,6 +260,25 @@ protected String snapshotTriggeringEvents(Optional previousSnaps return null; } + private String encryptEventDataIfNeeded(String eventData, Optional encryptionKey) { + return encryptionKey + .map(key -> new EncryptedEventData(key.getId(), EventDataEncryptor.encrypt(key, eventData)).asString()) + .orElse(eventData); + } + + private String decryptEventDataIfNeeded(String eventData, Optional encryptionKeyStore) { + if (EncryptedEventData.checkIfEventDataStringIsEncrypted(eventData)) { + EncryptedEventData encryptedEventData = EncryptedEventData.fromEventDataString(eventData); + String data = encryptedEventData.getData(); + String keyId = encryptedEventData.getEncryptionKeyId(); + EncryptionKey key = encryptionKeyStore + .map(store -> store.findEncryptionKeyById(keyId)) + .orElseThrow(() -> new IllegalArgumentException("EncryptionKeyStore is not specified")); + return EventDataEncryptor.decrypt(key, data); + } + + return eventData; + } } diff --git a/eventuate-client-java-jdbc/src/test/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImplTest.java b/eventuate-client-java-jdbc/src/test/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImplTest.java index f95c522..1169ab6 100644 --- a/eventuate-client-java-jdbc/src/test/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImplTest.java +++ b/eventuate-client-java-jdbc/src/test/java/io/eventuate/javaclient/spring/jdbc/EventuateJdbcAccessImplTest.java @@ -1,10 +1,11 @@ package io.eventuate.javaclient.spring.jdbc; import io.eventuate.EntityIdAndType; -import io.eventuate.javaclient.commonimpl.AggregateCrudUpdateOptions; -import io.eventuate.javaclient.commonimpl.EventTypeAndData; -import io.eventuate.javaclient.commonimpl.LoadedEvents; -import io.eventuate.javaclient.commonimpl.SerializedSnapshot; +import io.eventuate.javaclient.commonimpl.*; +import io.eventuate.javaclient.commonimpl.encryption.EncryptedEventData; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKey; +import io.eventuate.javaclient.commonimpl.encryption.EncryptionKeyStore; +import io.eventuate.javaclient.commonimpl.encryption.EventDataEncryptor; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -41,9 +42,29 @@ public EventuateJdbcAccess eventuateJdbcAccess(JdbcTemplate jdbcTemplate, Eventu private static final String testEventType = "testEventType1"; private static final String testEventData = "testEventData1"; + private static final EncryptionKey testEncryptionKey1 = new EncryptionKey("1", "testPass1", "481e155a423f11e8842f0ed5f89f718b"); + private static final EncryptionKey testEncryptionKey2 = new EncryptionKey("2", "testPass2", "481e1a0a423f11e8842f0ed5f89f718b"); + + private static final EncryptionKeyStore testKeyStore = new EncryptionKeyStore() { + private Map idKeyMap = new HashMap<>(); + + { + idKeyMap.put("1", testEncryptionKey1); + idKeyMap.put("2", testEncryptionKey2); + } + + @Override + public EncryptionKey findEncryptionKeyById(String keyId) { + return idKeyMap.get(keyId); + } + }; + @Autowired private JdbcTemplate jdbcTemplate; + @Autowired + private EventuateSchema eventuateSchema; + @Autowired private EventuateJdbcAccess eventuateJdbcAccess; @@ -103,6 +124,63 @@ public void testUpdate() { Assert.assertTrue(loadedEvents.getSnapshot().isPresent()); } + @Test + public void testSaveFindUpdateEncrypted() { + EventTypeAndData eventTypeAndData = new EventTypeAndData(testEventType, testEventData, Optional.empty()); + + SaveUpdateResult saveUpdateResult = eventuateJdbcAccess.save(testAggregate, + Collections.singletonList(eventTypeAndData), + Optional.of(new AggregateCrudSaveOptions(Optional.empty(), Optional.empty(), Optional.of(testEncryptionKey1)))); + + LoadedEvents loadedEvents = eventuateJdbcAccess.find(testAggregate, + saveUpdateResult.getEntityIdVersionAndEventIds().getEntityId(), + Optional.of(new AggregateCrudFindOptions(Optional.empty(), Optional.of(testKeyStore)))); + + Assert.assertEquals(1, loadedEvents.getEvents().size()); + Assert.assertEquals(testEventData, loadedEvents.getEvents().get(0).getEventData()); + + List> dbData = readEventsDirectlyFromDb(saveUpdateResult.getPublishableEvents().getAggregateType(), + saveUpdateResult.getPublishableEvents().getEntityId()); + + Assert.assertEquals(1, dbData.size()); + + EncryptedEventData encryptedEventData = EncryptedEventData.fromEventDataString((String)dbData.get(0).get("event_data")); + + String hexRegExp = "^[0-9a-fA-F]+$"; + + Assert.assertFalse(testEventData.matches(hexRegExp)); + Assert.assertTrue(encryptedEventData.getData().matches(hexRegExp)); + + String newEventData = testEventData + "Updated"; + + saveUpdateResult = eventuateJdbcAccess.update(new EntityIdAndType(saveUpdateResult.getEntityIdVersionAndEventIds().getEntityId(), saveUpdateResult.getPublishableEvents().getAggregateType()), + saveUpdateResult.getEntityIdVersionAndEventIds().getEntityVersion(), + Collections.singletonList(new EventTypeAndData(testEventType, newEventData, Optional.empty())), + Optional.of(new AggregateCrudUpdateOptions(Optional.empty(), Optional.empty(), Optional.of(testEncryptionKey1)))); + + loadedEvents = eventuateJdbcAccess.find(testAggregate, + saveUpdateResult.getEntityIdVersionAndEventIds().getEntityId(), + Optional.of(new AggregateCrudFindOptions(Optional.empty(), Optional.of(testKeyStore)))); + + Assert.assertEquals(2, loadedEvents.getEvents().size()); + Assert.assertEquals(newEventData, loadedEvents.getEvents().get(1).getEventData()); + + dbData = readEventsDirectlyFromDb(saveUpdateResult.getPublishableEvents().getAggregateType(), + saveUpdateResult.getPublishableEvents().getEntityId()); + + Assert.assertEquals(2, dbData.size()); + + encryptedEventData = EncryptedEventData.fromEventDataString((String)dbData.get(1).get("event_data")); + + Assert.assertFalse(newEventData.matches(hexRegExp)); + Assert.assertTrue(encryptedEventData.getData().matches(hexRegExp)); + } + + private List> readEventsDirectlyFromDb(String aggregateType, String entityId) { + return jdbcTemplate.queryForList(String.format("select event_data from %s where entity_type = ? and entity_id = ? order by event_id asc", eventuateSchema.qualifyTable("events")), + aggregateType, entityId); + } + protected List loadSqlScriptAsListOfLines(String script) throws IOException { try(BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/eventuate-embedded-schema.sql")))) { return bufferedReader.lines().collect(Collectors.toList());