diff --git a/src/main/java/no/ssb/dlp/pseudo/core/field/FieldPseudonymizer.java b/src/main/java/no/ssb/dlp/pseudo/core/field/FieldPseudonymizer.java index bc99212..f63ed96 100644 --- a/src/main/java/no/ssb/dlp/pseudo/core/field/FieldPseudonymizer.java +++ b/src/main/java/no/ssb/dlp/pseudo/core/field/FieldPseudonymizer.java @@ -2,6 +2,7 @@ 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 no.ssb.dlp.pseudo.core.PseudoException; import no.ssb.dlp.pseudo.core.PseudoKeyset; import no.ssb.dlp.pseudo.core.PseudoOperation; @@ -41,7 +42,7 @@ public Optional match(FieldDescriptor field) { public void init(FieldDescriptor field, String varValue) { Optional match = pseudoFuncs.findPseudoFunc(field); if (match.isPresent()) { - match.get().getFunc().init(PseudoFuncInput.of(varValue)); + match.get().getFunc().init(PseudoFuncInput.of(varValue), TransformDirection.APPLY); } } diff --git a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncConfigFactory.java b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncConfigFactory.java index cf0a398..f268937 100644 --- a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncConfigFactory.java +++ b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncConfigFactory.java @@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import no.ssb.crypto.tink.fpe.UnknownCharacterStrategy; import no.ssb.dapla.dlp.pseudo.func.PseudoFuncConfig; +import no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFunc; +import no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFuncConfig; import no.ssb.dapla.dlp.pseudo.func.fpe.Alphabets; import no.ssb.dapla.dlp.pseudo.func.fpe.FpeFunc; import no.ssb.dapla.dlp.pseudo.func.fpe.FpeFuncConfig; @@ -35,6 +37,8 @@ class PseudoFuncConfigFactory { tinkDaeadPseudoFuncConfigPreset(DAEAD), tinkFpePseudoFuncConfigPreset(FF31), sidMappingPseudoFuncConfigPreset(MAP_SID), + sidMappingAndTinkFpePseudoFuncConfigPreset(MAP_SID_FF31), + sidMappingAndTinkDaeadPseudoFuncConfigPreset(MAP_SID_DAEAD), redactPseudoFuncConfigPreset(REDACT), fpePseudoFuncConfigPreset(FPE + "-text", alphabetNameOf(ALPHANUMERIC, WHITESPACE, SYMBOLS)), fpePseudoFuncConfigPreset(FPE + "-text_no", alphabetNameOf(ALPHANUMERIC_NO, WHITESPACE, SYMBOLS)), @@ -53,6 +57,27 @@ private static PseudoFuncConfigPreset sidMappingPseudoFuncConfigPreset(String fu .build(); } + private static PseudoFuncConfigPreset sidMappingAndTinkFpePseudoFuncConfigPreset(String funcName) { + return PseudoFuncConfigPreset.builder(funcName, MapAndEncryptFunc.class) + .staticParam(MapAndEncryptFuncConfig.Param.MAP_FUNC_IMPL, MapFunc.class.getName()) + .staticParam(MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, TinkFpeFunc.class.getName()) + .requiredParam(String.class, TinkFpeFuncConfig.Param.KEY_ID) + .optionalParam(String.class, MapFuncConfig.Param.SNAPSHOT_DATE) + .optionalParam(UnknownCharacterStrategy.class, TinkFpeFuncConfig.Param.UNKNOWN_CHARACTER_STRATEGY, UnknownCharacterStrategy.FAIL) + .optionalParam(String.class, TinkFpeFuncConfig.Param.TWEAK) + .optionalParam(Character.class, TinkFpeFuncConfig.Param.REDACT_CHAR) + .build(); + } + + private static PseudoFuncConfigPreset sidMappingAndTinkDaeadPseudoFuncConfigPreset(String funcName) { + return PseudoFuncConfigPreset.builder(funcName, MapAndEncryptFunc.class) + .staticParam(MapAndEncryptFuncConfig.Param.MAP_FUNC_IMPL, MapFunc.class.getName()) + .staticParam(MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, TinkDaeadFunc.class.getName()) + .optionalParam(String.class, MapFuncConfig.Param.SNAPSHOT_DATE) + .requiredParam(String.class, TinkDaeadFuncConfig.Param.KEY_ID) + .build(); + } + private static PseudoFuncConfigPreset redactPseudoFuncConfigPreset(String funcName) { return PseudoFuncConfigPreset.builder(funcName, RedactFunc.class) .optionalParam(String.class, RedactFuncConfig.Param.PLACEHOLDER, "*") diff --git a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncNames.java b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncNames.java index 180b1db..3d410fe 100644 --- a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncNames.java +++ b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncNames.java @@ -9,7 +9,10 @@ private PseudoFuncNames() { public static final String FF31 = "ff31"; public static final String DAEAD = "daead"; + @Deprecated public static final String MAP_SID = "map-sid"; + public static final String MAP_SID_FF31 = "map-sid-ff31"; + public static final String MAP_SID_DAEAD = "map-sid-daead"; public static final String REDACT = "redact"; public static final String FPE = "fpe"; diff --git a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncs.java b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncs.java index 42ecf61..f790462 100644 --- a/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncs.java +++ b/src/main/java/no/ssb/dlp/pseudo/core/func/PseudoFuncs.java @@ -8,6 +8,8 @@ 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.composite.MapAndEncryptFunc; +import no.ssb.dapla.dlp.pseudo.func.composite.MapAndEncryptFuncConfig; 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.tink.daead.TinkDaeadFunc; @@ -56,18 +58,34 @@ static Map initPseudoFuncConfigs(Collection pseudoKeysetMap, + Map pseudoSecretsMap, + Collection pseudoSecrets) { + if (FpeFunc.class.getName().equals(funcConfig + .getRequired(MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, String.class))) { + enrichLegacyFpeFuncConfig(funcConfig, pseudoSecretsMap); + } else if (TinkDaeadFunc.class.getName().equals(funcConfig + .getRequired(MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, String.class))) { + enrichTinkDaeadFuncConfig(funcConfig, pseudoKeysetMap, pseudoSecrets); + } else if (TinkFpeFunc.class.getName().equals(funcConfig + .getRequired(MapAndEncryptFuncConfig.Param.ENCRYPTION_FUNC_IMPL, String.class))) { + enrichTinkFpeFuncConfig(funcConfig, pseudoKeysetMap, pseudoSecrets); + } + } + private static void enrichLegacyFpeFuncConfig(PseudoFuncConfig funcConfig, Map pseudoSecretsMap) { String secretId = funcConfig.getRequired(FpeFuncConfig.Param.KEY_ID, String.class); if (! pseudoSecretsMap.containsKey(secretId)) { diff --git a/src/test/java/no/ssb/dlp/pseudo/core/func/Ff31FuncTest.java b/src/test/java/no/ssb/dlp/pseudo/core/func/Ff31FuncTest.java index 6e3286e..4333e20 100644 --- a/src/test/java/no/ssb/dlp/pseudo/core/func/Ff31FuncTest.java +++ b/src/test/java/no/ssb/dlp/pseudo/core/func/Ff31FuncTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.security.GeneralSecurityException; import java.util.Map; import static org.assertj.core.api.Assertions.*; @@ -22,8 +23,12 @@ public class Ff31FuncTest { @BeforeAll - static void init() throws Exception { - FpeConfig.register(); + static void init() { + try { + FpeConfig.register(); + } catch (GeneralSecurityException e) { + //Ignore since it may happen in concurrent junit tests + } } private final static String KEYSET_JSON_FF31_256_ALPHANUMERIC = "{\"primaryKeyId\":832997605,\"key\":[{\"keyData\":{\"typeUrl\":\"type.googleapis.com/ssb.crypto.tink.FpeFfxKey\",\"value\":\"EiCCNkK81HHmUY4IjEzXDrGLOT5t+7PGQ1eIyrGqGa4S3BpCEAIaPjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6\",\"keyMaterialType\":\"SYMMETRIC\"},\"status\":\"ENABLED\",\"keyId\":832997605,\"outputPrefixType\":\"RAW\"}]}"; diff --git a/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndDaeadFuncTest.java b/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndDaeadFuncTest.java new file mode 100644 index 0000000..2e877d7 --- /dev/null +++ b/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndDaeadFuncTest.java @@ -0,0 +1,80 @@ +package no.ssb.dlp.pseudo.core.func; + +import com.google.crypto.tink.CleartextKeysetHandle; +import com.google.crypto.tink.DeterministicAead; +import com.google.crypto.tink.JsonKeysetReader; +import com.google.crypto.tink.KeysetHandle; +import com.google.crypto.tink.daead.DeterministicAeadConfig; +import no.ssb.crypto.tink.fpe.Fpe; +import no.ssb.crypto.tink.fpe.FpeConfig; +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.map.MapFunc; +import no.ssb.dapla.dlp.pseudo.func.map.Mapper; +import no.ssb.dapla.dlp.pseudo.func.tink.daead.TinkDaeadFuncConfig; +import no.ssb.dapla.dlp.pseudo.func.tink.fpe.TinkFpeFuncConfig; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.security.GeneralSecurityException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class MapAndDaeadFuncTest { + + @BeforeAll + static void init() { + try { + DeterministicAeadConfig.register(); + } catch (GeneralSecurityException e) { + //Ignore since it may happen in concurrent junit tests + } + } + + private final static String KEYSET_JSON_AES256_SIV = "{\"primaryKeyId\":1284924461,\"key\":[{\"keyData\":{\"typeUrl\":\"type.googleapis.com/google.crypto.tink.AesSivKey\",\"value\":\"EkCIjYUrKTTMAxEZST8xoyBXrfSLtTt+XmfBcE/PQxhr1Ob+YdD84bSMPQDaTGMqD241C4J7oQ+w3RFXaC8vKzbI\",\"keyMaterialType\":\"SYMMETRIC\"},\"status\":\"ENABLED\",\"keyId\":1284924461,\"outputPrefixType\":\"TINK\"}]}"; + + private final static Map KEYSETS = Map.of( + "1284924461", KEYSET_JSON_AES256_SIV + ); + + private DeterministicAead daeadPrimitive(String keyId) throws Exception { + if (! KEYSETS.containsKey(keyId)) { + throw new RuntimeException("Unknown keyId: " + keyId); + } + + KeysetHandle keysetHandle = CleartextKeysetHandle.read(JsonKeysetReader.withString(KEYSETS.get(keyId))); + return keysetHandle.getPrimitive(DeterministicAead.class); + } + + private PseudoFunc f(String funcDecl) throws Exception { + PseudoFuncConfig config = PseudoFuncConfigFactory.get(funcDecl); + String keyId = config.getRequired(TinkFpeFuncConfig.Param.KEY_ID, String.class); + config.add(TinkDaeadFuncConfig.Param.DAEAD, daeadPrimitive(keyId)); + return PseudoFuncFactory.create(config); + } + + private void transformAndRestore(String originalVal, String expectedVal, PseudoFunc func) { + PseudoFuncOutput pseudonymized = func.apply(PseudoFuncInput.of(originalVal)); + assertThat(pseudonymized.getValue()).isEqualTo(expectedVal); + PseudoFuncOutput depseudonymized = func.restore(PseudoFuncInput.of(pseudonymized.getValue())); + assertThat(depseudonymized.getValue()).isEqualTo(originalVal); + } + + @Test + void givenText_map_and_daead_shouldEncryptAndDecrypt() throws Exception { + final Mapper mockMapper = mock(Mapper.class); + try (var mapFunc = mockStatic(MapFunc.class)) { + mapFunc.when(() -> MapFunc.loadMapper()).thenReturn(mockMapper); + when(mockMapper.map(eq(PseudoFuncInput.of("Something")))).thenReturn(PseudoFuncOutput.of("Secret")); + when(mockMapper.restore(eq(PseudoFuncInput.of("Secret")))).thenReturn(PseudoFuncOutput.of("Something")); + String funcDeclStr = "map-sid-daead(keyId=1284924461)"; + transformAndRestore("Something", "AUyWZC2kWmY72/261fvqshAWQXfy+FY+F7PB", f(funcDeclStr)); + } + } + +} diff --git a/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndFf31FuncTest.java b/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndFf31FuncTest.java new file mode 100644 index 0000000..e406370 --- /dev/null +++ b/src/test/java/no/ssb/dlp/pseudo/core/func/MapAndFf31FuncTest.java @@ -0,0 +1,77 @@ +package no.ssb.dlp.pseudo.core.func; + +import com.google.crypto.tink.CleartextKeysetHandle; +import com.google.crypto.tink.JsonKeysetReader; +import com.google.crypto.tink.KeysetHandle; +import no.ssb.crypto.tink.fpe.Fpe; +import no.ssb.crypto.tink.fpe.FpeConfig; +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.map.MapFunc; +import no.ssb.dapla.dlp.pseudo.func.map.Mapper; +import no.ssb.dapla.dlp.pseudo.func.tink.fpe.TinkFpeFuncConfig; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.security.GeneralSecurityException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +public class MapAndFf31FuncTest { + + @BeforeAll + static void init() { + try { + FpeConfig.register(); + } catch (GeneralSecurityException e) { + //Ignore since it may happen in concurrent junit tests + } + } + + private final static String KEYSET_JSON_FF31_256_ALPHANUMERIC = "{\"primaryKeyId\":832997605,\"key\":[{\"keyData\":{\"typeUrl\":\"type.googleapis.com/ssb.crypto.tink.FpeFfxKey\",\"value\":\"EiCCNkK81HHmUY4IjEzXDrGLOT5t+7PGQ1eIyrGqGa4S3BpCEAIaPjAxMjM0NTY3ODlBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWmFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6\",\"keyMaterialType\":\"SYMMETRIC\"},\"status\":\"ENABLED\",\"keyId\":832997605,\"outputPrefixType\":\"RAW\"}]}"; + + private final static Map KEYSETS = Map.of( + "1234567890", KEYSET_JSON_FF31_256_ALPHANUMERIC + ); + + private Fpe fpePrimitive(String keyId) throws Exception { + if (! KEYSETS.containsKey(keyId)) { + throw new RuntimeException("Unknown keyId: " + keyId); + } + + KeysetHandle keysetHandle = CleartextKeysetHandle.read(JsonKeysetReader.withString(KEYSETS.get(keyId))); + return keysetHandle.getPrimitive(Fpe.class); + } + + private PseudoFunc f(String funcDecl) throws Exception { + PseudoFuncConfig config = PseudoFuncConfigFactory.get(funcDecl); + String keyId = config.getRequired(TinkFpeFuncConfig.Param.KEY_ID, String.class); + config.add(TinkFpeFuncConfig.Param.FPE, fpePrimitive(keyId)); + return PseudoFuncFactory.create(config); + } + + private void transformAndRestore(String originalVal, String expectedVal, PseudoFunc func) { + PseudoFuncOutput pseudonymized = func.apply(PseudoFuncInput.of(originalVal)); + assertThat(pseudonymized.getValue()).isEqualTo(expectedVal); + PseudoFuncOutput depseudonymized = func.restore(PseudoFuncInput.of(pseudonymized.getValue())); + assertThat(depseudonymized.getValue()).isEqualTo(originalVal); + } + + @Test + void givenText_map_and_ff31_shouldEncryptAndDecrypt() throws Exception { + final Mapper mockMapper = mock(Mapper.class); + try (var mapFunc = mockStatic(MapFunc.class)) { + mapFunc.when(() -> MapFunc.loadMapper()).thenReturn(mockMapper); + when(mockMapper.map(eq(PseudoFuncInput.of("Something")))).thenReturn(PseudoFuncOutput.of("Secret")); + when(mockMapper.restore(eq(PseudoFuncInput.of("Secret")))).thenReturn(PseudoFuncOutput.of("Something")); + String funcDeclStr = "map-sid-ff31(keyId=1234567890)"; + transformAndRestore("Something", "CQqlS3", f(funcDeclStr)); + } + } + +} diff --git a/src/test/java/no/ssb/dlp/pseudo/core/func/MapFuncTest.java b/src/test/java/no/ssb/dlp/pseudo/core/func/MapFuncTest.java index 994a16d..4cba0b5 100644 --- a/src/test/java/no/ssb/dlp/pseudo/core/func/MapFuncTest.java +++ b/src/test/java/no/ssb/dlp/pseudo/core/func/MapFuncTest.java @@ -4,6 +4,7 @@ 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.TransformDirection; import no.ssb.dapla.dlp.pseudo.func.map.MapFunc; import no.ssb.dapla.dlp.pseudo.func.map.Mapper; import org.junit.jupiter.api.Test; @@ -26,7 +27,7 @@ void mapFuncWithTimestamp() { mapFunc.when(() -> MapFunc.loadMapper()).thenReturn(mockMapper); String funcDeclStr = "map-sid(keyId=1284924461, snapshotDate=2023-05-21)"; PseudoFunc func = f(funcDeclStr); - func.init(PseudoFuncInput.of("50607080901")); + func.init(PseudoFuncInput.of("50607080901"), TransformDirection.APPLY); } // Check that the mockMapper has received the versionTimestamp ArgumentCaptor argumentsCaptured = ArgumentCaptor.forClass(Map.class);