diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/EventuateAggregateStoreImpl.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/EventuateAggregateStoreImpl.java index 9e62a7c..b5f6d28 100644 --- a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/EventuateAggregateStoreImpl.java +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/EventuateAggregateStoreImpl.java @@ -3,6 +3,7 @@ import io.eventuate.*; import io.eventuate.encryption.EncryptedEventData; import io.eventuate.encryption.EventDataEncryptor; +import io.eventuate.encryption.NoEncryptionKeyProvidedException; import io.eventuate.javaclient.commonimpl.schemametadata.EmptyEventSchemaMetadataManager; import io.eventuate.javaclient.commonimpl.schemametadata.EventSchemaMetadataManager; @@ -124,8 +125,15 @@ public > CompletableFuture> find(Cl .map(event -> AggregateCrudMapping.toEventWithMetadata(event, json -> findOptions .flatMap(FindOptions::getEncryptionKey) - .map(k -> EncryptedEventData.isEventDataStringEncrypted(json) ? eventDataEncryptor.decrypt(k, json) : json) - .orElse(json))) + .map(k -> EncryptedEventData.isEventDataStringEncrypted(json) ? + eventDataEncryptor.decrypt(k, EncryptedEventData.fromEventDataString(json).getData()) : + json) + .orElseGet(() -> { + if (EncryptedEventData.isEventDataStringEncrypted(json)) { + throw new NoEncryptionKeyProvidedException(EncryptedEventData.fromEventDataString(json)); + } + return json; + }))) .collect(Collectors.toList()); List events = eventsWithIds.stream().map(EventWithMetadata::getEvent).collect(Collectors.toList()); diff --git a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/sync/EventuateAggregateStoreImpl.java b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/sync/EventuateAggregateStoreImpl.java index 1a6efa6..77f8ec5 100644 --- a/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/sync/EventuateAggregateStoreImpl.java +++ b/eventuate-client-java-common-impl/src/main/java/io/eventuate/javaclient/commonimpl/sync/EventuateAggregateStoreImpl.java @@ -17,6 +17,7 @@ import io.eventuate.SubscriberOptions; import io.eventuate.UpdateOptions; import io.eventuate.encryption.EncryptedEventData; +import io.eventuate.encryption.NoEncryptionKeyProvidedException; import io.eventuate.javaclient.commonimpl.*; import io.eventuate.encryption.EventDataEncryptor; import io.eventuate.sync.EventuateAggregateStore; @@ -107,10 +108,18 @@ public > EntityWithMetadata find(Class clasz, Strin .getEvents() .stream() .map(event -> - AggregateCrudMapping.toEventWithMetadata(event, json -> findOptions - .flatMap(FindOptions::getEncryptionKey) - .map(k -> EncryptedEventData.isEventDataStringEncrypted(json) ? eventDataEncryptor.decrypt(k, json) : json) - .orElse(json))) + AggregateCrudMapping.toEventWithMetadata(event, json -> + findOptions + .flatMap(FindOptions::getEncryptionKey) + .map(k -> EncryptedEventData.isEventDataStringEncrypted(json) ? + eventDataEncryptor.decrypt(k, EncryptedEventData.fromEventDataString(json).getData()) : + json) + .orElseGet(() -> { + if (EncryptedEventData.isEventDataStringEncrypted(json)) { + throw new NoEncryptionKeyProvidedException(EncryptedEventData.fromEventDataString(json)); + } + return json; + }))) .collect(Collectors.toList()); List events = eventsWithIds.stream().map(EventWithMetadata::getEvent).collect(Collectors.toList()); return new EntityWithMetadata( diff --git a/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/AbstractEventuateAggregateStoreEncryptionTest.java b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/AbstractEventuateAggregateStoreEncryptionTest.java new file mode 100644 index 0000000..e039026 --- /dev/null +++ b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/AbstractEventuateAggregateStoreEncryptionTest.java @@ -0,0 +1,102 @@ +package io.eventuate.javaclient.spring.autoconfiguration; + +import io.eventuate.*; +import io.eventuate.encryption.EncryptionKey; +import io.eventuate.encryption.EventDataEncryptor; +import io.eventuate.encryption.NoEncryptionKeyProvidedException; +import io.vertx.core.json.Json; +import org.junit.Assert; +import org.junit.Test; + +public abstract class AbstractEventuateAggregateStoreEncryptionTest { + + private static String hexRegExp = "^[0-9a-fA-F]+$"; + + private static final String idKey = "1"; + private static final String key = "Super strong password"; + private static final String salt = "47b4033642e911e8842f0ed5f89f718b"; + + private static final String testEventData = "Some secret data"; + private static final String testEventDataUpdated = "Some secret data (Updated)"; + + protected static final EncryptionKey encryptionKey = new EncryptionKey(idKey, key, salt); + + protected static final EventDataEncryptor eventDataEncryptor = new EventDataEncryptor(); + + @Test + public void testSaveEncrypted() throws Exception { + EntityIdAndVersion entityIdAndVersion = save(testEventData); + + NoEncryptionKeyProvidedException noEncryptionKeyProvidedException = null; + + try { + find(entityIdAndVersion, false); + } catch (NoEncryptionKeyProvidedException e) { + noEncryptionKeyProvidedException = e; + } + + Assert.assertNotNull(noEncryptionKeyProvidedException); + + String data = noEncryptionKeyProvidedException.getEncryptedEventData().getData(); + + Assert.assertNotNull(data); + Assert.assertFalse(data.trim().isEmpty()); + Assert.assertTrue(data.matches(hexRegExp)); + Assert.assertEquals(Json.encode(new SomeEvent(testEventData)), eventDataEncryptor.decrypt(encryptionKey, data)); + + Assert.assertFalse(testEventData.matches(hexRegExp)); + } + + @Test + public void testSaveAndFind() throws Exception { + EntityIdAndVersion entityIdAndVersion = save(testEventData); + + EntityWithMetadata entityEntityWithMetadata = find(entityIdAndVersion, true); + + Assert.assertEquals(1, entityEntityWithMetadata.getEvents().size()); + Assert.assertEquals(testEventData, ((SomeEvent)entityEntityWithMetadata.getEvents().get(0).getEvent()).getData()); + } + + @Test + public void testUpdateAndFind() throws Exception { + EntityIdAndVersion entityIdAndVersion = save(testEventData); + + entityIdAndVersion = update(entityIdAndVersion, testEventDataUpdated); + + EntityWithMetadata entityEntityWithMetadata = find(entityIdAndVersion, true); + + Assert.assertEquals(2, entityEntityWithMetadata.getEvents().size()); + Assert.assertEquals(testEventDataUpdated, + ((SomeEvent)entityEntityWithMetadata.getEvents().get(1).getEvent()).getData()); + } + + protected abstract EntityIdAndVersion save(String data) throws Exception; + protected abstract EntityIdAndVersion update(EntityIdAndVersion entityIdAndVersion, String data) throws Exception; + protected abstract EntityWithMetadata find(EntityIdAndVersion entityIdAndVersion, boolean encrypted) throws Exception; + + public static class SomeAggregate implements Aggregate { + @Override + public SomeAggregate applyEvent(Event event) { + return this; + } + } + + public static class SomeEvent implements Event { + private String data; + + public SomeEvent() { + } + + public SomeEvent(String data) { + this.data = data; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } +} diff --git a/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/EventuateAggregateStoreEncryptionTest.java b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/EventuateAggregateStoreEncryptionTest.java new file mode 100644 index 0000000..7f7d2c9 --- /dev/null +++ b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/EventuateAggregateStoreEncryptionTest.java @@ -0,0 +1,50 @@ +package io.eventuate.javaclient.spring.autoconfiguration; + +import io.eventuate.*; +import io.eventuate.encryption.NoEncryptionKeyProvidedException; +import io.eventuate.javaclient.saasclient.EventuateAggregateStoreBuilder; +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = AutoConfigurationIntegrationTestConfiguration.class) +@IntegrationTest +public class EventuateAggregateStoreEncryptionTest extends AbstractEventuateAggregateStoreEncryptionTest { + + private io.eventuate.EventuateAggregateStore aggregateStore = EventuateAggregateStoreBuilder.defaultFromEnv(); + + @Override + protected EntityIdAndVersion save(String data) throws Exception { + return aggregateStore.save(SomeAggregate.class, + Collections.singletonList(new SomeEvent(data)), + new SaveOptions().withEncryptionKey(encryptionKey)).get(); + } + + @Override + protected EntityIdAndVersion update(EntityIdAndVersion entityIdAndVersion, String data) throws Exception { + return aggregateStore.update(SomeAggregate.class, + entityIdAndVersion, + Collections.singletonList(new SomeEvent(data)), + new UpdateOptions().withEncryptionKey(encryptionKey)).get(); + } + + @Override + protected EntityWithMetadata find(EntityIdAndVersion entityIdAndVersion, + boolean encrypted) throws Exception { + try { + return aggregateStore.find(SomeAggregate.class, + entityIdAndVersion.getEntityId(), + encrypted ? Optional.of(new FindOptions().withEncryptionKey(encryptionKey)) : Optional.empty()).get(); + } catch (ExecutionException e) { + Assert.assertTrue(e.getCause() instanceof NoEncryptionKeyProvidedException); + throw (NoEncryptionKeyProvidedException) e.getCause(); + } + } +} diff --git a/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/SyncEventuateAggregateStoreEncryptionTest.java b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/SyncEventuateAggregateStoreEncryptionTest.java new file mode 100644 index 0000000..f4aa9ca --- /dev/null +++ b/eventuate-client-java-tests/src/test/java/io/eventuate/javaclient/spring/autoconfiguration/SyncEventuateAggregateStoreEncryptionTest.java @@ -0,0 +1,42 @@ +package io.eventuate.javaclient.spring.autoconfiguration; + +import io.eventuate.*; +import io.eventuate.javaclient.saasclient.EventuateAggregateStoreBuilder; +import io.eventuate.sync.EventuateAggregateStore; +import org.junit.runner.RunWith; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Collections; +import java.util.Optional; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = AutoConfigurationIntegrationTestConfiguration.class) +@IntegrationTest +public class SyncEventuateAggregateStoreEncryptionTest extends AbstractEventuateAggregateStoreEncryptionTest { + + private EventuateAggregateStore aggregateStore = EventuateAggregateStoreBuilder.standard().buildSync(); + + @Override + protected EntityIdAndVersion save(String data) { + return aggregateStore.save(SomeAggregate.class, + Collections.singletonList(new SomeEvent(data)), + new SaveOptions().withEncryptionKey(encryptionKey)); + } + + @Override + protected EntityIdAndVersion update(EntityIdAndVersion entityIdAndVersion, String data) { + return aggregateStore.update(SomeAggregate.class, + entityIdAndVersion, + Collections.singletonList(new SomeEvent(data)), + new UpdateOptions().withEncryptionKey(encryptionKey)); + } + + @Override + protected EntityWithMetadata find(EntityIdAndVersion entityIdAndVersion, boolean encrypted) { + return aggregateStore.find(SomeAggregate.class, + entityIdAndVersion.getEntityId(), + encrypted ? Optional.of(new FindOptions().withEncryptionKey(encryptionKey)) : Optional.empty()); + } +} diff --git a/eventuate-client-java/src/main/java/io/eventuate/UpdateOptions.java b/eventuate-client-java/src/main/java/io/eventuate/UpdateOptions.java index bdff37d..bdb59a2 100644 --- a/eventuate-client-java/src/main/java/io/eventuate/UpdateOptions.java +++ b/eventuate-client-java/src/main/java/io/eventuate/UpdateOptions.java @@ -80,6 +80,10 @@ public UpdateOptions withTriggeringEvent(EventContext eventContext) { return new UpdateOptions(Optional.ofNullable(eventContext), this.eventMetadata, this.snapshot, this.interceptor); } + public UpdateOptions withEncryptionKey(EncryptionKey encryptionKey) { + return new UpdateOptions(this.triggeringEvent, this.eventMetadata, this.snapshot, this.interceptor, Optional.of(encryptionKey)); + } + public UpdateOptions withEventMetadata(Map eventMetadata) { return new UpdateOptions(this.triggeringEvent, Optional.of(eventMetadata), this.snapshot, this.interceptor); } diff --git a/eventuate-client-java/src/main/java/io/eventuate/encryption/NoEncryptionKeyProvidedException.java b/eventuate-client-java/src/main/java/io/eventuate/encryption/NoEncryptionKeyProvidedException.java new file mode 100644 index 0000000..7871096 --- /dev/null +++ b/eventuate-client-java/src/main/java/io/eventuate/encryption/NoEncryptionKeyProvidedException.java @@ -0,0 +1,13 @@ +package io.eventuate.encryption; + +public class NoEncryptionKeyProvidedException extends RuntimeException { + private EncryptedEventData encryptedEventData; + + public NoEncryptionKeyProvidedException(EncryptedEventData encryptedEventData) { + this.encryptedEventData = encryptedEventData; + } + + public EncryptedEventData getEncryptedEventData() { + return encryptedEventData; + } +}