From e8883e86d91ad3df9ce7cf39a58984918fb57953 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn-Andre=20Skaar?=
<31540110+bjornandre@users.noreply.github.com>
Date: Wed, 13 Mar 2024 10:02:23 +0100
Subject: [PATCH] Add new function that combines both mapping and encryption.
(#19)
Add MapAndEncryptFunc which combines both mapping and encryption
---
.../dlp/pseudo/func/AbstractPseudoFunc.java | 2 +-
.../ssb/dapla/dlp/pseudo/func/PseudoFunc.java | 4 +-
.../dlp/pseudo/func/TransformDirection.java | 5 +
.../func/composite/MapAndEncryptFunc.java | 96 +++++++++++++++++++
.../composite/MapAndEncryptFuncConfig.java | 18 ++++
.../dapla/dlp/pseudo/func/map/MapFunc.java | 4 +-
.../ssb/dapla/dlp/pseudo/func/map/Mapper.java | 4 +-
.../func/map/MappingNotFoundException.java | 7 ++
.../func/composite/MapAndEncryptFuncTest.java | 66 +++++++++++++
.../dlp/pseudo/func/map/MapFuncTest.java | 29 ++++++
.../dapla/dlp/pseudo/func/map/TestMapper.java | 41 ++++++++
.../no.ssb.dapla.dlp.pseudo.func.map.Mapper | 1 +
12 files changed, 271 insertions(+), 6 deletions(-)
create mode 100644 src/main/java/no/ssb/dapla/dlp/pseudo/func/TransformDirection.java
create mode 100644 src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFunc.java
create mode 100644 src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncConfig.java
create mode 100644 src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MappingNotFoundException.java
create mode 100644 src/test/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncTest.java
create mode 100644 src/test/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncTest.java
create mode 100644 src/test/java/no/ssb/dapla/dlp/pseudo/func/map/TestMapper.java
create mode 100644 src/test/resources/META-INF/services/no.ssb.dapla.dlp.pseudo.func.map.Mapper
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/AbstractPseudoFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/AbstractPseudoFunc.java
index f5540e4..26d4d0c 100644
--- a/src/main/java/no/ssb/dapla/dlp/pseudo/func/AbstractPseudoFunc.java
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/AbstractPseudoFunc.java
@@ -11,7 +11,7 @@ public abstract class AbstractPseudoFunc implements PseudoFunc {
private final String funcDecl;
@Override
- public void init(PseudoFuncInput input) {
+ public void init(PseudoFuncInput input, TransformDirection direction) {
// Do nothing, can be overridden by subclasses
}
}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/PseudoFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/PseudoFunc.java
index c13a0bf..91f8002 100644
--- a/src/main/java/no/ssb/dapla/dlp/pseudo/func/PseudoFunc.java
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/PseudoFunc.java
@@ -9,9 +9,9 @@ public interface PseudoFunc {
String getAlgorithm();
/**
- * Preprocessing of input. This will be called before apply
+ * Preprocessing of input. This will be called before apply or restore
*/
- void init(PseudoFuncInput input);
+ void init(PseudoFuncInput input, TransformDirection direction);
/**
* Pseudonymize
*/
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/TransformDirection.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/TransformDirection.java
new file mode 100644
index 0000000..c59ba63
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/TransformDirection.java
@@ -0,0 +1,5 @@
+package no.ssb.dapla.dlp.pseudo.func;
+
+public enum TransformDirection {
+ APPLY, RESTORE
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFunc.java
new file mode 100644
index 0000000..0adb0e4
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFunc.java
@@ -0,0 +1,96 @@
+package no.ssb.dapla.dlp.pseudo.func.composite;
+
+import no.ssb.dapla.dlp.pseudo.func.AbstractPseudoFunc;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFunc;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncFactory;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+import no.ssb.dapla.dlp.pseudo.func.TransformDirection;
+
+import java.util.function.Function;
+
+import static no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFuncConfig.Param.*;
+
+/**
+ * This is a composite {@code PseudoFunc} that combines two {@code PseudoFunc}s into a single operation.
+ *
+ * It assumes that one is a mapping function (e.g. MapFunc) and the other is an encryption function
+ * (e.g FpeFunc, TinkFpeFunc, TinkDaeadFunc).
+ *
+ * The {@code MapAndEncryptFunc} must be configured with a
+ * {@link no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFuncConfig.Param#ENCRYPTION_FUNC_IMPL} and a
+ * {@link no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFuncConfig.Param#MAP_FUNC_IMPL}.
+ *
+ * The {@code PseudoFuncConfig} must also contain all the necessary configs for each of the underlying
+ * {@code PseudoFunc}s. For example MapFuncConfig and TinkFpeFuncConfig.
+ */
+public class MapAndEncryptFunc extends AbstractPseudoFunc {
+
+ final PseudoFunc encryptionFunc;
+ final PseudoFunc mapFunc;
+
+ public MapAndEncryptFunc(PseudoFuncConfig genericConfig) {
+ super(genericConfig.getFuncDecl());
+ genericConfig.add(PseudoFuncConfig.Param.FUNC_IMPL,
+ genericConfig.getRequired(ENCRYPTION_FUNC_IMPL, String.class));
+ var encryptionFuncConfig = genericConfig.asMap();
+ genericConfig.add(PseudoFuncConfig.Param.FUNC_IMPL,
+ genericConfig.getRequired(MAP_FUNC_IMPL, String.class));
+ var mapFuncConfig = genericConfig.asMap();
+
+ this.encryptionFunc = PseudoFuncFactory.create(new PseudoFuncConfig(encryptionFuncConfig));
+ this.mapFunc = PseudoFuncFactory.create(new PseudoFuncConfig(mapFuncConfig));
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return encryptionFunc.getAlgorithm();
+ }
+
+ @Override
+ public void init(PseudoFuncInput input, TransformDirection direction) {
+ if (direction == TransformDirection.APPLY) {
+ // Prepare map from original value to mapped value
+ mapFunc.init(input, direction);
+ } else {
+ // Decrypt and then prepare to map back to original value
+ mapFunc.init(PseudoFuncInput.of(
+ encryptionFunc.restore(input).getValue()),
+ direction
+ );
+ }
+ }
+
+ @Override
+ public PseudoFuncOutput apply(PseudoFuncInput input) {
+ // Map original value to mapped value and then encrypt
+ return transform(input, mapFunc::apply, encryptionFunc::apply);
+ }
+ @Override
+ public PseudoFuncOutput restore(PseudoFuncInput input) {
+ // Decrypt and then map back to original value
+ return transform(input, encryptionFunc::restore, mapFunc::restore);
+ }
+
+ /**
+ * Apply both functions {@code inner} and {@code outer} and merge both
+ * outputs.
+ *
+ * @param input the original value
+ * @param inner the inner function to apply
+ * @param outer the outer function to apply
+ * @return the result object
+ */
+ private PseudoFuncOutput transform(PseudoFuncInput input,
+ Function inner,
+ Function outer) {
+ final PseudoFuncOutput innerOutput = inner.apply(input);
+ final PseudoFuncOutput outerOutput = outer.apply(
+ PseudoFuncInput.of(innerOutput.getValue()));
+ innerOutput.getWarnings().forEach(outerOutput::addWarning);
+ innerOutput.getMetadata().forEach(outerOutput::addMetadata);
+ return outerOutput;
+
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncConfig.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncConfig.java
new file mode 100644
index 0000000..89780b5
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncConfig.java
@@ -0,0 +1,18 @@
+package no.ssb.dapla.dlp.pseudo.func.composite;
+
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.UtilityClass;
+
+@Value
+@Builder
+public class MapAndEncryptFuncConfig {
+ String encryptionFunc;
+ String mapFunc;
+
+ @UtilityClass
+ public static class Param {
+ public static final String ENCRYPTION_FUNC_IMPL = "encryptionFunc";
+ public static final String MAP_FUNC_IMPL = "mapFunc";
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFunc.java
index 888d5c7..46e294d 100644
--- a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFunc.java
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFunc.java
@@ -5,6 +5,7 @@
import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+import no.ssb.dapla.dlp.pseudo.func.TransformDirection;
import java.util.ServiceLoader;
@@ -32,7 +33,8 @@ public static Mapper loadMapper() {
}
@Override
- public void init(PseudoFuncInput input) {
+ public void init(PseudoFuncInput input, TransformDirection direction) {
+ // Init method is the same regardless of the direction
mapper.init(input);
}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/Mapper.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/Mapper.java
index f3169ea..6be3c8f 100644
--- a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/Mapper.java
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/Mapper.java
@@ -9,8 +9,8 @@ public interface Mapper {
void init(PseudoFuncInput data);
void setConfig(Map config);
- PseudoFuncOutput map(PseudoFuncInput data);
+ PseudoFuncOutput map(PseudoFuncInput data) throws MappingNotFoundException;
- PseudoFuncOutput restore(PseudoFuncInput mapped);
+ PseudoFuncOutput restore(PseudoFuncInput mapped) throws MappingNotFoundException;
}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MappingNotFoundException.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MappingNotFoundException.java
new file mode 100644
index 0000000..1cd49a5
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MappingNotFoundException.java
@@ -0,0 +1,7 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+public class MappingNotFoundException extends RuntimeException {
+ public MappingNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/test/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncTest.java b/src/test/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncTest.java
new file mode 100644
index 0000000..a336e85
--- /dev/null
+++ b/src/test/java/no/ssb/dapla/dlp/pseudo/func/composite/MapAndEncryptFuncTest.java
@@ -0,0 +1,66 @@
+package no.ssb.dapla.dlp.pseudo.func.composite;
+
+import com.google.common.collect.ImmutableMap;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFunc;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncFactory;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+import no.ssb.dapla.dlp.pseudo.func.fpe.FpeFunc;
+import no.ssb.dapla.dlp.pseudo.func.fpe.FpeFuncConfig;
+import no.ssb.dapla.dlp.pseudo.func.map.MapFunc;
+import no.ssb.dapla.dlp.pseudo.func.map.MappingNotFoundException;
+import no.ssb.dapla.dlp.pseudo.func.map.TestMapper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class MapAndEncryptFuncTest {
+ private static final String BASE64_ENCODED_KEY = "w0/G6A5e/KHtTo31FD6mhhS1Tkga43l79IBK24gM4F8=";
+
+ final PseudoFuncConfig config = new PseudoFuncConfig(ImmutableMap.of(
+ PseudoFuncConfig.Param.FUNC_DECL, "map-fpe-test",
+ PseudoFuncConfig.Param.FUNC_IMPL, MapAndEncryptFunc.class.getName(),
+ MapAndEncryptFuncConfig.Param.MAP_FUNC_IMPL, MapFunc.class.getName(),
+ MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, FpeFunc.class.getName(),
+ FpeFuncConfig.Param.ALPHABET, "alphanumeric+whitespace",
+ FpeFuncConfig.Param.KEY_ID, "keyId1",
+ FpeFuncConfig.Param.KEY_DATA, BASE64_ENCODED_KEY
+
+ ));
+ /*
+ * This test should transform OriginalValue -> MappedValue -> FPE encrypted MappedValue
+ * (and back)
+ */
+ @Test
+ public void transformAndRestore() {
+ String expectedVal = "ygd1M9at1nK"; // FPE encrypted MappedValue
+ PseudoFunc func = PseudoFuncFactory.create(config);
+
+ PseudoFuncOutput pseudonymized = func.apply(PseudoFuncInput.of(TestMapper.ORIGINAL));
+ assertThat(pseudonymized.getValue()).isEqualTo(expectedVal);
+
+ PseudoFuncOutput depseudonymized = func.restore(PseudoFuncInput.of(pseudonymized.getValue()));
+ assertThat(depseudonymized.getValue()).isEqualTo(TestMapper.ORIGINAL);
+ }
+
+ @Test
+ public void call_apply_with_mapping_failure() {
+ PseudoFunc func = PseudoFuncFactory.create(config);
+
+ assertThrows(MappingNotFoundException.class,() ->
+ func.apply(PseudoFuncInput.of("unknown"))
+ );
+ }
+
+ @Test
+ public void call_restore_with_mapping_failure() {
+ PseudoFunc func = PseudoFuncFactory.create(config);
+
+ assertThrows(MappingNotFoundException.class,() ->
+ func.restore(PseudoFuncInput.of("unknown"))
+ );
+ }
+
+}
diff --git a/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncTest.java b/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncTest.java
new file mode 100644
index 0000000..e06c6dd
--- /dev/null
+++ b/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncTest.java
@@ -0,0 +1,29 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+import com.google.common.collect.ImmutableMap;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFunc;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncFactory;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.*;
+
+public class MapFuncTest {
+ @Test
+ public void transformAndRestore() {
+ final PseudoFuncConfig config = new PseudoFuncConfig(ImmutableMap.of(
+ PseudoFuncConfig.Param.FUNC_DECL, "map-test",
+ PseudoFuncConfig.Param.FUNC_IMPL, MapFunc.class.getName()
+ ));
+ PseudoFunc func = PseudoFuncFactory.create(config);
+
+ PseudoFuncOutput mapOutput = func.apply(PseudoFuncInput.of(TestMapper.ORIGINAL));
+ assertThat(mapOutput.getValue()).isEqualTo(TestMapper.MAPPED);
+
+ PseudoFuncOutput depseudonymized = func.restore(PseudoFuncInput.of(mapOutput.getValue()));
+ assertThat(depseudonymized.getValue()).isEqualTo(TestMapper.ORIGINAL);
+ }
+
+}
diff --git a/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/TestMapper.java b/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/TestMapper.java
new file mode 100644
index 0000000..dbcd8fb
--- /dev/null
+++ b/src/test/java/no/ssb/dapla/dlp/pseudo/func/map/TestMapper.java
@@ -0,0 +1,41 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+
+import java.util.Map;
+
+/**
+ * This class is loaded using the Java Service Provider API.
+ */
+public class TestMapper implements Mapper {
+
+ public static String ORIGINAL = "OriginalValue";
+ public static String MAPPED = "MappedValue";
+
+ @Override
+ public void init(PseudoFuncInput data) {
+ }
+
+ @Override
+ public void setConfig(Map config) {
+ }
+
+ @Override
+ public PseudoFuncOutput map(PseudoFuncInput input) throws MappingNotFoundException {
+ if (ORIGINAL.equals(input.value())) {
+ return PseudoFuncOutput.of(MAPPED);
+ } else {
+ throw new MappingNotFoundException(String.format("Could not map value %s", input.value()));
+ }
+ }
+
+ @Override
+ public PseudoFuncOutput restore(PseudoFuncInput mapped) throws MappingNotFoundException {
+ if (MAPPED.equals(mapped.value())) {
+ return PseudoFuncOutput.of(ORIGINAL);
+ } else {
+ throw new MappingNotFoundException(String.format("Could not map value %s", mapped.value()));
+ }
+ }
+}
diff --git a/src/test/resources/META-INF/services/no.ssb.dapla.dlp.pseudo.func.map.Mapper b/src/test/resources/META-INF/services/no.ssb.dapla.dlp.pseudo.func.map.Mapper
new file mode 100644
index 0000000..b93b579
--- /dev/null
+++ b/src/test/resources/META-INF/services/no.ssb.dapla.dlp.pseudo.func.map.Mapper
@@ -0,0 +1 @@
+no.ssb.dapla.dlp.pseudo.func.map.TestMapper
\ No newline at end of file