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); + } + } +}