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