diff --git a/pom.xml b/pom.xml
index 9c22b70..82b881a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,11 +5,11 @@
no.ssb.dapla.dlp.pseudo.func
dapla-dlp-pseudo-func
- 1.0.3-SNAPSHOT
+ 1.1.0-SNAPSHOT
dapla-dlp-pseudo-func
- 8
+ 11
${java.version}
${java.version}
UTF-8
@@ -25,6 +25,7 @@
1.2.10
1.18.22
1.7.36
+ 1.7.0
2.1.4
@@ -52,6 +53,28 @@
format-preserving-encryption
${format-preserving-encryption.version}
+
+ com.google.crypto.tink
+ tink
+ ${tink.version}
+
+
+
+
+
+
org.projectlombok
lombok
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFunc.java
new file mode 100644
index 0000000..d472ccb
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFunc.java
@@ -0,0 +1,69 @@
+package no.ssb.dapla.dlp.pseudo.func.daead;
+
+import com.google.crypto.tink.DeterministicAead;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import no.ssb.dapla.dlp.pseudo.func.*;
+import no.ssb.dapla.dlp.pseudo.func.util.FromString;
+
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.util.Base64;
+
+@Slf4j
+public class DaeadFunc extends AbstractPseudoFunc {
+
+ // FIXME: Replace this with something real
+ private static final byte[] DAEAD_STAMP_BYTES = "".getBytes(StandardCharsets.UTF_8);
+ private final DaeadFuncConfigService configService = new DaeadFuncConfigService();
+ private final DaeadFuncConfig config;
+
+ public DaeadFunc(@NonNull PseudoFuncConfig genericConfig) {
+ super(genericConfig.getFuncDecl());
+ this.config = configService.resolve(genericConfig);
+ }
+
+ private DeterministicAead daead() {
+ return config.getDaead();
+ }
+
+ @Override
+ public PseudoFuncOutput apply(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+ input.getValues().forEach(in -> {
+ String plain = String.valueOf(in);
+ try {
+ byte[] ciphertext = daead().encryptDeterministically(plain.getBytes(StandardCharsets.UTF_8), DAEAD_STAMP_BYTES);
+ output.add(Base64.getEncoder().encodeToString(ciphertext));
+ }
+ catch (GeneralSecurityException e) {
+ throw new DaeadPseudoFuncException("DAEAD apply error. func=" + getFuncDecl() + ", contentType=" + input.getParamMetadata(), e);
+ }
+ });
+
+ return output;
+ }
+
+ @Override
+ public PseudoFuncOutput restore(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+ input.getValues().forEach(in -> {
+ byte[] ciphertext = Base64.getDecoder().decode(String.valueOf(in));
+ try {
+ byte[] plaintext = daead().decryptDeterministically(ciphertext, DAEAD_STAMP_BYTES);
+ output.add(FromString.convert(new String(plaintext), in.getClass()));
+ }
+ catch (GeneralSecurityException e) {
+ throw new DaeadPseudoFuncException("DAEAD restore error. func=" + getFuncDecl() + ", contentType=" + input.getParamMetadata(), e);
+ }
+ });
+
+ return output;
+ }
+
+ public static class DaeadPseudoFuncException extends PseudoFuncException {
+ public DaeadPseudoFuncException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfig.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfig.java
new file mode 100644
index 0000000..ab2216f
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfig.java
@@ -0,0 +1,21 @@
+package no.ssb.dapla.dlp.pseudo.func.daead;
+
+import com.google.crypto.tink.DeterministicAead;
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.UtilityClass;
+
+@Value
+@Builder
+public class DaeadFuncConfig {
+ private final String dataEncryptionKeyId;
+ private final String base64EncodedWrappedDataEncryptionKey;
+ private final DeterministicAead daead;
+
+ @UtilityClass
+ public static class Param {
+ public static final String DEK_ID = "dataEncryptionKeyId";
+ public static final String WDEK = "wrappedDataEncryptionKey";
+ public static final String DAEAD = "deterministicAead";
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfigService.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfigService.java
new file mode 100644
index 0000000..12c6809
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncConfigService.java
@@ -0,0 +1,19 @@
+package no.ssb.dapla.dlp.pseudo.func.daead;
+
+import com.google.crypto.tink.DeterministicAead;
+import lombok.extern.slf4j.Slf4j;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+
+import static no.ssb.dapla.dlp.pseudo.func.daead.DaeadFuncConfig.Param.DAEAD;
+
+@Slf4j
+public class DaeadFuncConfigService {
+
+ public DaeadFuncConfig resolve(PseudoFuncConfig cfg) {
+
+ return DaeadFuncConfig.builder()
+ .daead(cfg.getRequired(DAEAD, DeterministicAead.class))
+ .build();
+ }
+
+}
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
new file mode 100644
index 0000000..49a722a
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFunc.java
@@ -0,0 +1,54 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+import lombok.extern.slf4j.Slf4j;
+import no.ssb.dapla.dlp.pseudo.func.AbstractPseudoFunc;
+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 java.util.ServiceLoader;
+
+@Slf4j
+public class MapFunc extends AbstractPseudoFunc {
+ private final MapFuncConfig config;
+ private final MapFuncConfigService mapFuncConfigService = new MapFuncConfigService();
+ private final Mapper mapper;
+
+ public MapFunc(PseudoFuncConfig genericConfig) {
+ super(genericConfig.getFuncDecl());
+ this.config = mapFuncConfigService.resolve(genericConfig);
+ // TODO: Filter Service Implementation by some annotation (to choose the implementation that is used)
+ this.mapper = ServiceLoader.load(Mapper.class)
+ .findFirst()
+ .orElseThrow(() -> new IllegalStateException(getClass().getSimpleName() + " requires a " + Mapper.class.getName() + " implementation to be present on the classpath"));
+ }
+
+ @Override
+ public PseudoFuncOutput apply(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+
+ for (Object inputValue : input.getValues()) {
+ String plain = String.valueOf(inputValue);
+ final Object pseudonymized = mapper.map(plain);
+ //output.add(FromString.convert(pseudonymized, inputValue.getClass()));
+ output.add(pseudonymized);
+ }
+
+ return output;
+ }
+
+ @Override
+ public PseudoFuncOutput restore(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+
+ for (Object inputValue : input.getValues()) {
+ String mapped = String.valueOf(inputValue);
+ final Object clear = mapper.restore(mapped);
+ //output.add(FromString.convert(clear, inputValue.getClass()));
+ output.add(clear);
+ }
+
+ return output;
+ }
+
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfig.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfig.java
new file mode 100644
index 0000000..a74bebd
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfig.java
@@ -0,0 +1,16 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.UtilityClass;
+
+@Value
+@Builder
+public class MapFuncConfig {
+ private final String context;
+
+ @UtilityClass
+ public static class Param {
+ public static final String CONTEXT = "context";
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfigService.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfigService.java
new file mode 100644
index 0000000..7d5c5ec
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/MapFuncConfigService.java
@@ -0,0 +1,16 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+
+public class MapFuncConfigService {
+
+ public MapFuncConfig resolve(PseudoFuncConfig genericConfig) {
+
+ String context = genericConfig.getRequired(MapFuncConfig.Param.CONTEXT, String.class);
+
+ return MapFuncConfig.builder()
+ .context(context)
+ .build();
+ }
+
+}
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
new file mode 100644
index 0000000..f1b4102
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/map/Mapper.java
@@ -0,0 +1,9 @@
+package no.ssb.dapla.dlp.pseudo.func.map;
+
+public interface Mapper {
+
+ Object map(Object data);
+
+ Object restore(Object mapped);
+
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFunc.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFunc.java
new file mode 100644
index 0000000..1aa58e6
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFunc.java
@@ -0,0 +1,44 @@
+package no.ssb.dapla.dlp.pseudo.func.redact;
+
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import no.ssb.dapla.dlp.pseudo.func.AbstractPseudoFunc;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncInput;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncOutput;
+
+@Slf4j
+public class RedactFunc extends AbstractPseudoFunc {
+ private final RedactFuncConfigService configService = new RedactFuncConfigService();
+ private final RedactFuncConfig config;
+
+ public RedactFunc(@NonNull PseudoFuncConfig genericConfig) {
+ super(genericConfig.getFuncDecl());
+ this.config = configService.resolve(genericConfig);
+ }
+
+ @Override
+ public PseudoFuncOutput apply(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+ input.getValues().forEach(in -> {
+ String plain = String.valueOf(in);
+ if (config.getRegex() != null) {
+ output.add(plain.replaceAll(config.getRegex(), config.getPlaceholder()));
+ }
+ else {
+ output.add(config.getPlaceholder());
+ }
+ });
+
+ return output;
+ }
+
+ @Override
+ public PseudoFuncOutput restore(PseudoFuncInput input) {
+ PseudoFuncOutput output = new PseudoFuncOutput();
+ input.getValues().forEach(in -> output.add(in));
+
+ return output;
+ }
+
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfig.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfig.java
new file mode 100644
index 0000000..7cd1832
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfig.java
@@ -0,0 +1,18 @@
+package no.ssb.dapla.dlp.pseudo.func.redact;
+
+import lombok.Builder;
+import lombok.Value;
+import lombok.experimental.UtilityClass;
+
+@Value
+@Builder
+public class RedactFuncConfig {
+ private final String placeholder;
+ private final String regex;
+
+ @UtilityClass
+ public static class Param {
+ public static final String PLACEHOLDER = "placeholder";
+ public static final String REGEX = "regex";
+ }
+}
diff --git a/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfigService.java b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfigService.java
new file mode 100644
index 0000000..a1319a4
--- /dev/null
+++ b/src/main/java/no/ssb/dapla/dlp/pseudo/func/redact/RedactFuncConfigService.java
@@ -0,0 +1,19 @@
+package no.ssb.dapla.dlp.pseudo.func.redact;
+
+import lombok.extern.slf4j.Slf4j;
+import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig;
+
+import static no.ssb.dapla.dlp.pseudo.func.redact.RedactFuncConfig.Param.PLACEHOLDER;
+import static no.ssb.dapla.dlp.pseudo.func.redact.RedactFuncConfig.Param.REGEX;
+
+@Slf4j
+public class RedactFuncConfigService {
+
+ public RedactFuncConfig resolve(PseudoFuncConfig cfg) {
+ return RedactFuncConfig.builder()
+ .placeholder(cfg.get(PLACEHOLDER, String.class).orElse("***"))
+ .regex(cfg.get(REGEX, String.class).orElse(null))
+ .build();
+ }
+
+}
diff --git a/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncTest.java b/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncTest.java
new file mode 100644
index 0000000..f22fe40
--- /dev/null
+++ b/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadFuncTest.java
@@ -0,0 +1,66 @@
+package no.ssb.dapla.dlp.pseudo.func.daead;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.crypto.tink.daead.DeterministicAeadConfig;
+import no.ssb.dapla.dlp.pseudo.func.*;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.security.GeneralSecurityException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class DaeadFuncTest {
+
+ @BeforeAll
+ static void initTink() throws GeneralSecurityException {
+ DeterministicAeadConfig.register();
+ }
+
+ @Test
+ void alphanumeric_daead_shouldTransformAndRestore() {
+ DaeadWrapper daeadWrapper = new DaeadWrapper();
+ String originalVal = "Ken sent me";
+ transformAndRestore(originalVal, new PseudoFuncConfig(ImmutableMap.of(
+ PseudoFuncConfig.Param.FUNC_DECL, String.format("tink-daead(%s)", daeadWrapper.getKeyId()),
+ PseudoFuncConfig.Param.FUNC_IMPL, DaeadFunc.class.getName(),
+ DaeadFuncConfig.Param.DAEAD, daeadWrapper.getDaead()
+ )));
+ }
+
+ @Test
+ void multipleAlphanumeric_daead_shouldTransformAndRestore() {
+ DaeadWrapper daeadWrapper = new DaeadWrapper();
+ List originalVal = ImmutableList.of("Ken sent me...", "Kilroy was here!");
+ transformAndRestore(originalVal, new PseudoFuncConfig(ImmutableMap.of(
+ PseudoFuncConfig.Param.FUNC_DECL, String.format("tink-daead(%s)", daeadWrapper.getKeyId()),
+ PseudoFuncConfig.Param.FUNC_IMPL, DaeadFunc.class.getName(),
+ DaeadFuncConfig.Param.DAEAD, daeadWrapper.getDaead()
+ )));
+ }
+
+ /*
+ TODO: Implement a typesafe daead func (e.g. by storing type metadata in the ciphertext?)
+ @Test
+ void longValue_fpe_shouldTransformAndRestore() {
+ DaeadWrapper daeadWrapper = new DaeadWrapper();
+ Long originalVal = 123456789L;
+ transformAndRestore(originalVal, new PseudoFuncConfig(ImmutableMap.of(
+ PseudoFuncConfig.Param.FUNC_DECL, String.format("tink-daead(%s)", daeadWrapper.getKeyId()),
+ PseudoFuncConfig.Param.FUNC_IMPL, DaeadFunc.class.getName(),
+ DaeadFuncConfig.Param.DAEAD, daeadWrapper.getDaead()
+ )));
+ }
+ */
+
+ private void transformAndRestore(Object originalVal, PseudoFuncConfig config) {
+ Iterable originalElements = (originalVal instanceof Iterable) ? (Iterable) originalVal : ImmutableList.of(originalVal);
+ PseudoFunc func = PseudoFuncFactory.create(config);
+ PseudoFuncOutput pseudonymized = func.apply(PseudoFuncInput.of(originalVal));
+ PseudoFuncOutput depseudonymized = func.restore(PseudoFuncInput.of(pseudonymized.getValues()));
+ assertThat(depseudonymized.getValues()).containsExactlyElementsOf(originalElements);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadWrapper.java b/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadWrapper.java
new file mode 100644
index 0000000..40b0b44
--- /dev/null
+++ b/src/test/java/no/ssb/dapla/dlp/pseudo/func/daead/DaeadWrapper.java
@@ -0,0 +1,58 @@
+package no.ssb.dapla.dlp.pseudo.func.daead;
+
+import com.google.crypto.tink.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+public class DaeadWrapper {
+
+ private final KeysetHandle keysetHandle;
+
+ private final DeterministicAead daead;
+
+ public DaeadWrapper() {
+ try {
+ this.keysetHandle = KeysetHandle.generateNew(KeyTemplates.get("AES256_SIV"));
+ this.daead = keysetHandle.getPrimitive(DeterministicAead.class);
+ }
+ catch (GeneralSecurityException e) {
+ throw new DaeadWrapperException("Error initializing DaeadWrapper for testing", e);
+ }
+ }
+
+ private static String toKeyJson(KeysetHandle keysetHandle) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ CleartextKeysetHandle.write(keysetHandle, JsonKeysetWriter.withOutputStream(baos));
+ return new String(baos.toByteArray());
+ }
+
+ public String getKeyJson() {
+ try {
+ return toKeyJson(keysetHandle);
+ }
+ catch (IOException e) {
+ throw new DaeadWrapperException("Error deducing keyJson", e);
+ }
+ }
+
+ public String getKeyId() {
+ try {
+ return String.valueOf(keysetHandle.primaryKey().getId());
+ }
+ catch (GeneralSecurityException e) {
+ throw new DaeadWrapperException("Error deducing keyId", e);
+ }
+ }
+
+ public DeterministicAead getDaead() {
+ return daead;
+ }
+
+ public static class DaeadWrapperException extends RuntimeException {
+ public DaeadWrapperException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+}