diff --git a/.github/kts/build.main.kts b/.github/kts/build.main.kts index b40879b6..4ca84e8c 100755 --- a/.github/kts/build.main.kts +++ b/.github/kts/build.main.kts @@ -9,13 +9,11 @@ @file:DependsOn("actions:checkout:v4") @file:DependsOn("actions:cache:v4") @file:DependsOn("actions:setup-java:v4") -@file:DependsOn("codecov:codecov-action:v5") import io.github.typesafegithub.workflows.actions.actions.Cache import io.github.typesafegithub.workflows.actions.actions.Checkout import io.github.typesafegithub.workflows.actions.actions.SetupJava -import io.github.typesafegithub.workflows.actions.codecov.CodecovAction import io.github.typesafegithub.workflows.domain.RunnerType import io.github.typesafegithub.workflows.domain.triggers.PullRequest import io.github.typesafegithub.workflows.domain.triggers.Push diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index d24f9cd1..00000000 --- a/codecov.yml +++ /dev/null @@ -1,40 +0,0 @@ -codecov: - notify: - require_ci_to_pass: yes - -coverage: - precision: 2 - round: down - range: "75...100" - - status: - project: - default: - # basic - target: auto - threshold: 2% - base: auto - flags: - - unit - paths: - - "src" - # advanced - branches: - - main - if_not_found: success - if_ci_failed: error - informational: false - only_pulls: false - -parsers: - gcov: - branch_detection: - conditional: yes - loop: yes - method: no - macro: no - -comment: - layout: "header, diff" - behavior: default - require_changes: no diff --git a/cryptoshred-cloud-aws/src/main/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepository.java b/cryptoshred-cloud-aws/src/main/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepository.java index d56c1e21..3695f9bb 100644 --- a/cryptoshred-cloud-aws/src/main/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepository.java +++ b/cryptoshred-cloud-aws/src/main/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepository.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2023 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package eu.prismacapacity.cryptoshred.cloud.aws; -import java.util.Optional; - import eu.prismacapacity.cryptoshred.core.CryptoAlgorithm; import eu.prismacapacity.cryptoshred.core.CryptoEngine; import eu.prismacapacity.cryptoshred.core.CryptoSubjectId; @@ -25,6 +23,7 @@ import eu.prismacapacity.cryptoshred.core.keys.CryptoKeyRepository; import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; import eu.prismacapacity.cryptoshred.core.metrics.CryptoMetrics; +import java.util.Optional; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.val; @@ -48,7 +47,7 @@ public class DynamoDBCryptoKeyRepository implements CryptoKeyRepository { @NonNull private final String tableName; @Override - public Optional findKeyFor( + public @NonNull Optional findKeyFor( @NonNull CryptoSubjectId subjectId, @NonNull CryptoAlgorithm algorithm, @NonNull CryptoKeySize size) { @@ -66,7 +65,7 @@ public Optional findKeyFor( } @Override - public CryptoKey getOrCreateKeyFor( + public @NonNull CryptoKey getOrCreateKeyFor( @NonNull CryptoSubjectId subjectId, @NonNull CryptoAlgorithm algorithm, @NonNull CryptoKeySize size) diff --git a/cryptoshred-cloud-aws/src/test/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepositoryIntegrationTest.java b/cryptoshred-cloud-aws/src/test/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepositoryIntegrationTest.java index a0758529..84cd840c 100644 --- a/cryptoshred-cloud-aws/src/test/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepositoryIntegrationTest.java +++ b/cryptoshred-cloud-aws/src/test/java/eu/prismacapacity/cryptoshred/cloud/aws/DynamoDBCryptoKeyRepositoryIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020-2023 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,23 +17,21 @@ import static org.junit.Assert.*; +import eu.prismacapacity.cryptoshred.cloud.aws.utils.TestIntegration; +import eu.prismacapacity.cryptoshred.core.*; +import eu.prismacapacity.cryptoshred.core.keys.CryptoKey; +import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; +import eu.prismacapacity.cryptoshred.core.metrics.CryptoMetrics; import java.net.URI; import java.util.HashMap; import java.util.UUID; - +import lombok.NonNull; +import lombok.val; import org.junit.Assert; import org.junit.jupiter.api.BeforeAll; import org.testcontainers.containers.localstack.LocalStackContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; - -import eu.prismacapacity.cryptoshred.cloud.aws.utils.TestIntegration; -import eu.prismacapacity.cryptoshred.core.*; -import eu.prismacapacity.cryptoshred.core.keys.CryptoKey; -import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; -import eu.prismacapacity.cryptoshred.core.metrics.CryptoMetrics; -import lombok.NonNull; -import lombok.val; import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.dynamodb.DynamoDbClient; @@ -43,7 +41,7 @@ class DynamoDBCryptoKeyRepositoryIntegrationTest { private static String TABLE_NAME = "foo"; - CryptoEngine engine = new JDKCryptoEngine(CryptoInitializationVector.of("mysecret")); + CryptoEngine engine = new JDKCryptoEngine("mysecret", false); CryptoMetrics metrics = new CryptoMetrics.NOP(); @Container diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/AbstractCryptoEngine.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/AbstractCryptoEngine.java new file mode 100644 index 00000000..d21d4381 --- /dev/null +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/AbstractCryptoEngine.java @@ -0,0 +1,77 @@ +/* + * Copyright © 2026 PRISMA European Capacity Platform GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.prismacapacity.cryptoshred.core; + +import java.security.SecureRandom; +import java.util.*; +import javax.crypto.spec.IvParameterSpec; +import lombok.NonNull; + +public abstract class AbstractCryptoEngine implements CryptoEngine { + protected final CryptoInitializationVector configuredInitVector; + + protected final boolean useRandomInitVector; + + protected final Map exactCipherNames = createExactCipherMapping(); + + protected static final SecureRandom RANDOM = new SecureRandom(); + + protected AbstractCryptoEngine(String configuredInitVectorOrNull, boolean useRandomInitVector) { + if (!useRandomInitVector && null == configuredInitVectorOrNull) { + throw new IllegalArgumentException( + "No init vector configured, and useRandomInitVector is false."); + } + + this.useRandomInitVector = useRandomInitVector; + + if (configuredInitVectorOrNull == null) { + this.configuredInitVector = null; + } else this.configuredInitVector = CryptoInitializationVector.of(configuredInitVectorOrNull); + } + + private static Map createExactCipherMapping() { + // initialize with known algorithms + HashMap map = new HashMap<>(); + map.put(CryptoAlgorithm.AES_CBC, "AES/CBC/PKCS5PADDING"); + return Collections.unmodifiableMap(map); + } + + @NonNull + protected final IvParameterSpec resolveInitVectorForDecryption( + IvParameterSpec initializationVectorProvidedOrNull) { + + if (initializationVectorProvidedOrNull != null) { + return initializationVectorProvidedOrNull; + } else { + // no IV stored with the container, so we use the configured one + if (configuredInitVector == null) + throw new IllegalStateException( + "No init vector configured, and none stored with the container."); + else return configuredInitVector.getIvParameterSpec(); + } + } + + public final @NonNull IvParameterSpec getInitVectorForEncryption() { + + if (useRandomInitVector) { + byte[] iv = new byte[16]; + RANDOM.nextBytes(iv); + return new IvParameterSpec(iv); + } else + // guaranteed by constructor check + return configuredInitVector.getIvParameterSpec(); + } +} diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoContainer.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoContainer.java index 087d7121..907b613d 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoContainer.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoContainer.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; import eu.prismacapacity.cryptoshred.core.metrics.CryptoMetrics; import java.util.Optional; +import javax.crypto.spec.IvParameterSpec; import lombok.AccessLevel; import lombok.Getter; import lombok.NonNull; @@ -52,6 +53,7 @@ private CryptoContainer( CryptoKeySize size, CryptoSubjectId id, byte[] encrypted, + byte[] iv, CryptoEngine engine, CryptoKeyRepository keyRepo, CryptoMetrics metrics, @@ -66,6 +68,7 @@ private CryptoContainer( this.metrics = metrics; this.mapper = om; this.encryptedBytes = encrypted; + this.initializationVector = iv == null ? null : new IvParameterSpec(iv); } // set only on deserialization @@ -80,12 +83,13 @@ static CryptoContainer fromDeserialization( CryptoKeySize keySize, CryptoSubjectId subjectId, byte[] encrypted, + byte[] iv, CryptoEngine engine, CryptoKeyRepository keyRepo, CryptoMetrics metrics, ObjectMapper om) { return new CryptoContainer( - targetType, algorithm, keySize, subjectId, encrypted, engine, keyRepo, metrics, om); + targetType, algorithm, keySize, subjectId, encrypted, iv, engine, keyRepo, metrics, om); } @Getter private Class type; @@ -100,6 +104,14 @@ static CryptoContainer fromDeserialization( @Getter(value = AccessLevel.PACKAGE) private byte[] encryptedBytes; + /** + * Will be set when encrypting. + * + *

For backward-compatibility - if it does not exist, the engine will use the default iv. + */ + @Getter(value = AccessLevel.PACKAGE) + private IvParameterSpec initializationVector = null; + // set after decryption or before encryption for short circuit retrieval private transient Optional cachedValue; @@ -119,7 +131,7 @@ private T decrypt() { if (key.isPresent()) { try { - byte[] decrypted = engine.decrypt(getAlgo(), key.get(), bytes); + byte[] decrypted = engine.decrypt(getAlgo(), key.get(), bytes, initializationVector); T t = mapper.readerFor(getType()).readValue(decrypted); if (metrics != null) metrics.notifyDecryptionSuccess(); return t; @@ -139,9 +151,11 @@ private T decrypt() { @SneakyThrows protected void encrypt(CryptoKeyRepository keyRepository, CryptoEngine engine, ObjectMapper om) { + + this.initializationVector = engine.getInitVectorForEncryption(); + CryptoKey key = keyRepository.getOrCreateKeyFor(subjectId, algo, size); - byte[] bytes; - bytes = om.writeValueAsBytes(value()); - this.encryptedBytes = engine.encrypt(bytes, algo, key); + byte[] bytes = om.writeValueAsBytes(value()); + this.encryptedBytes = engine.encrypt(bytes, algo, key, this.initializationVector); } } diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java index 594573da..36ba333c 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ import eu.prismacapacity.cryptoshred.core.keys.CryptoKey; import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; +import javax.crypto.spec.IvParameterSpec; import lombok.NonNull; /** @@ -26,14 +27,20 @@ */ public interface CryptoEngine { - @NonNull byte[] decrypt( - @NonNull CryptoAlgorithm algo, @NonNull CryptoKey cryptoKey, @NonNull byte[] bytes); + @NonNull CryptoAlgorithm algo, + @NonNull CryptoKey cryptoKey, + byte @NonNull [] bytes, + IvParameterSpec initializationVectorOrNull); - @NonNull byte[] encrypt( - @NonNull byte[] unencypted, @NonNull CryptoAlgorithm algorithm, @NonNull CryptoKey key); + byte @NonNull [] unencypted, + @NonNull CryptoAlgorithm algorithm, + @NonNull CryptoKey key, + @NonNull IvParameterSpec initializationVector); @NonNull CryptoKey generateKey(@NonNull CryptoAlgorithm algo, @NonNull CryptoKeySize size); + + IvParameterSpec getInitVectorForEncryption(); } diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoInitializationVector.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoInitializationVector.java index 13386e88..63a4c5fe 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoInitializationVector.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoInitializationVector.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,20 +17,30 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; -import lombok.NonNull; -import lombok.Value; +import javax.crypto.spec.IvParameterSpec; +import lombok.*; -@Value(staticConstructor = "of") -public class CryptoInitializationVector { - @NonNull String initVector; +@RequiredArgsConstructor +@SuppressWarnings("java:S3329") +public final class CryptoInitializationVector { - public byte[] getBytes() { + @Getter private final byte[] bytes; + + public static CryptoInitializationVector of(@NonNull String initVector) { + return new CryptoInitializationVector(toBytes(initVector)); + } + + static byte[] toBytes(@NonNull String initVector) { // make sure, we have 16 bytes there - StringBuffer sb = new StringBuffer(initVector); + StringBuilder sb = new StringBuilder(initVector); while (sb.length() < 16) sb.append(initVector); byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8); // take the first 16 bytes return Arrays.copyOf(bytes, 16); } + + public IvParameterSpec getIvParameterSpec() { + return new IvParameterSpec(bytes); + } } diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoModule.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoModule.java index 690f88f9..334922f5 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoModule.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoModule.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ import java.util.UUID; import lombok.NonNull; +@SuppressWarnings({"java:S1948", "java:S1068"}) public class CryptoModule extends SimpleModule { private final CryptoEngine engine; @@ -66,6 +67,8 @@ public CryptoModule( private static final String JSON_KEY_ENCRYPTED_BYTES = "enc"; + private static final String JSON_KEY_IV = "iv"; + private static final String JSON_KEY_SUBJECT_ID = "id"; private static final String JSON_KEY_KEY_SIZE = "ksize"; @@ -99,12 +102,16 @@ public CryptoContainer deserialize(JsonParser jp, DeserializationContext ctxt String algo = tree.get(JSON_KEY_ALGO).asText(); byte[] encrypted = tree.get(JSON_KEY_ENCRYPTED_BYTES).binaryValue(); + JsonNode jsonNode = tree.get(JSON_KEY_IV); + byte[] iv = jsonNode == null ? null : jsonNode.binaryValue(); + return CryptoContainer.fromDeserialization( targetType, CryptoAlgorithm.of(algo), CryptoKeySize.of(keySize), CryptoSubjectId.of(UUID.fromString(subjectId)), encrypted, + iv, engine, keyRepo, metrics, @@ -132,6 +139,7 @@ public void serialize(CryptoContainer value, JsonGenerator jgen, SerializerProvi jgen.writeNumberField(JSON_KEY_KEY_SIZE, value.getSize().asInt()); jgen.writeStringField(JSON_KEY_SUBJECT_ID, value.getSubjectId().getId().toString()); jgen.writeBinaryField(JSON_KEY_ENCRYPTED_BYTES, value.getEncryptedBytes()); + jgen.writeBinaryField(JSON_KEY_IV, value.getInitializationVector().getIV()); jgen.writeEndObject(); } } diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/JDKCryptoEngine.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/JDKCryptoEngine.java index 29f9518f..8f1a617d 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/JDKCryptoEngine.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/JDKCryptoEngine.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,38 +15,28 @@ */ package eu.prismacapacity.cryptoshred.core; -import eu.prismacapacity.cryptoshred.core.keys.CryptoKey; -import eu.prismacapacity.cryptoshred.core.keys.CryptoKeySize; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; +import eu.prismacapacity.cryptoshred.core.keys.*; +import java.security.*; +import java.util.*; import javax.crypto.*; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; +import javax.crypto.spec.*; import lombok.NonNull; -import lombok.RequiredArgsConstructor; -@RequiredArgsConstructor -public class JDKCryptoEngine implements CryptoEngine { +public class JDKCryptoEngine extends AbstractCryptoEngine { - private final Map exactCipherNames = createExactCipherMapping(); - - private static Map createExactCipherMapping() { - // initialize with known algorithms - HashMap map = new HashMap<>(); - map.put(CryptoAlgorithm.AES_CBC, "AES/CBC/PKCS5PADDING"); - return Collections.unmodifiableMap(map); + public JDKCryptoEngine(String configuredInitVectorOrNull, boolean useRandomInitVector) { + super(configuredInitVectorOrNull, useRandomInitVector); } - @NonNull private final CryptoInitializationVector initVector; - @Override public byte[] decrypt( - @NonNull CryptoAlgorithm algo, @NonNull CryptoKey cryptoKey, @NonNull byte[] bytes) { - IvParameterSpec iv = new IvParameterSpec(initVector.getBytes()); + @NonNull CryptoAlgorithm algo, + @NonNull CryptoKey cryptoKey, + byte @NonNull [] bytes, + IvParameterSpec initializationVectorOrNull) { + + IvParameterSpec iv = resolveInitVectorForDecryption(initializationVectorOrNull); + try { Cipher cipher = getCipher(algo); SecretKeySpec secret = new SecretKeySpec(cryptoKey.getBytes(), algo.getId()); @@ -62,13 +52,15 @@ public byte[] decrypt( @Override public byte[] encrypt( - @NonNull byte[] unencypted, @NonNull CryptoAlgorithm algorithm, @NonNull CryptoKey key) { + byte @NonNull [] unencypted, + @NonNull CryptoAlgorithm algorithm, + @NonNull CryptoKey key, + @NonNull IvParameterSpec initializationVectorForEncryption) { - IvParameterSpec iv = new IvParameterSpec(initVector.getBytes()); try { Cipher cipher = getCipher(algorithm); SecretKeySpec secret = new SecretKeySpec(key.getBytes(), algorithm.getId()); - cipher.init(Cipher.ENCRYPT_MODE, secret, iv); + cipher.init(Cipher.ENCRYPT_MODE, secret, initializationVectorForEncryption); return cipher.doFinal(unencypted); } catch (InvalidKeyException | InvalidAlgorithmParameterException @@ -87,6 +79,7 @@ private Cipher getCipher(@NonNull CryptoAlgorithm defaultAlgorithm) { } @Override + @NonNull public CryptoKey generateKey(@NonNull CryptoAlgorithm algo, @NonNull CryptoKeySize size) { try { KeyGenerator kgen = KeyGenerator.getInstance(algo.getId()); diff --git a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/keys/CryptoKey.java b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/keys/CryptoKey.java index 33d1c716..1a1ae74c 100644 --- a/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/keys/CryptoKey.java +++ b/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/keys/CryptoKey.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ @EqualsAndHashCode public class CryptoKey { @Getter @NonNull private final String base64; - @Getter @NonNull private final byte[] bytes; + @Getter private final byte @NonNull [] bytes; public static CryptoKey fromBase64(@NonNull String base64encoded) { return new CryptoKey(base64encoded, Base64.getDecoder().decode(base64encoded)); diff --git a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/Benchmark.java b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/Benchmark.java new file mode 100644 index 00000000..4c089d52 --- /dev/null +++ b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/Benchmark.java @@ -0,0 +1,147 @@ +/* + * Copyright © 2026 PRISMA European Capacity Platform GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package eu.prismacapacity.cryptoshred.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Random; +import java.util.UUID; +import lombok.SneakyThrows; +import lombok.Value; + +public class Benchmark { + static ObjectMapper om; + static InMemCryptoKeyRepository keyRepository; + + static { + CryptoEngine engine = new JDKCryptoEngine("mysecret", false); + keyRepository = new InMemCryptoKeyRepository(engine); + om = new ObjectMapper(); + om.registerModule(new CryptoModule(engine, keyRepository)); + + try { + om.writeValueAsString( + new Dto(new CryptoContainer<>("init", CryptoSubjectId.of(UUID.randomUUID())))); + + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @SneakyThrows + public static void main(String[] args) { + int dataSize = 100_000; + System.out.println("Preparing " + dataSize + " data points for encryption..."); + Dto[] unencrypted = generateTestData(dataSize); + + System.out.println("now encrypting..."); + encrypt(dataSize, unencrypted); + } + + private static void encrypt(int dataSize, Dto[] unencrypted) throws JsonProcessingException { + + long start = System.currentTimeMillis(); + long[][] individualTimes = new long[3][dataSize]; + int idx = 0; + int lenMultiplier = 0; + + int capacity = 50_000_000; + StringBuffer sb = new StringBuffer(capacity); + + for (Dto dto : unencrypted) { + long startIndividual = System.currentTimeMillis(); + sb.append(om.writeValueAsString(dto)); + long endIndividual = System.currentTimeMillis(); + + if (capacity - sb.length() < 200_000) { + sb.delete(0, sb.length()); + } + + individualTimes[lenMultiplier++][idx] = endIndividual - startIndividual; + + if (lenMultiplier == 3) { + lenMultiplier = 0; + } + } + + long end = System.currentTimeMillis(); + System.out.println( + "Encryption took " + (end - start) + "ms or " + ((end - start) / 1_000) + "s"); + System.out.println(); + + for (lenMultiplier = 0; lenMultiplier < 3; lenMultiplier++) { + long durations = 0; + long[] individualTime = individualTimes[lenMultiplier]; + for (int i = 0; i < dataSize; i++) { + durations += individualTime[i]; + } + String caption = getCaption(lenMultiplier); + int average = (int) (durations / dataSize); + System.out.println(caption + ": " + average + "ms"); + } + + // hope that compiler does not optimize too much away if we do this + idx = new Random().nextInt(sb.length() - 1); + System.out.println(sb.substring(idx, idx + 1)); + } + + private static String getCaption(int lenMultiplier) { + if (lenMultiplier == 0) { + return "small data size"; + } + if (lenMultiplier == 1) { + return "medium data size"; + } + + return "large data size"; + } + + private static Dto[] generateTestData(int dataSize) { + Dto[] dtos = new Dto[dataSize]; + + int lenMultiplier = 1; + for (int i = 0; i < dataSize; i++) { + CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); + String data = generateString((int) (100 * Math.pow(10, lenMultiplier++))); + Dto dto = new Dto(new CryptoContainer<>(data, id)); + dtos[i] = dto; + + if (lenMultiplier == 4) { + lenMultiplier = 1; + } + } + + return dtos; + } + + private static String generateString(int targetStringLength) { + int leftLimit = 48; // numeral '0' + int rightLimit = 122; // letter 'z' + Random random = new Random(); + + return random + .ints(leftLimit, rightLimit + 1) + .filter(i -> (i <= 57 || i >= 65) && (i <= 90 || i >= 97)) + .limit(targetStringLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + } + + @Value + static class Dto { + CryptoContainer crypto; + } +} diff --git a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/CryptoContainerTest.java b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/CryptoContainerTest.java index c30e4023..1b1925bc 100644 --- a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/CryptoContainerTest.java +++ b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/CryptoContainerTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2021 PRISMA European Capacity Platform GmbH + * Copyright © 2021-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,13 +43,24 @@ class CryptoContainerTest { CryptoContainer withoutMetrics() { return CryptoContainer.fromDeserialization( - Integer.class, algo, size, subjectId, new byte[32], engine, keyRepo, null, mapper); + Integer.class, + algo, + size, + subjectId, + new byte[32], + new byte[16], + engine, + keyRepo, + null, + mapper); } @Nested class WhenDecrypting { + @Nested class DecryptionTest { + @Test void ignoresNullMetrics() { // must not throw NPE @@ -63,7 +74,7 @@ class WithDecryptionFailure extends DecryptionTest { void setup() { when(keyRepo.findKeyFor(any(), any(), any())) .thenReturn(Optional.of(mock(CryptoKey.class))); - when(engine.decrypt(any(), any(), any())).thenThrow(IllegalStateException.class); + when(engine.decrypt(any(), any(), any(), any())).thenThrow(IllegalStateException.class); } } @@ -73,7 +84,7 @@ class WithDecryptionSuccess extends DecryptionTest { void setup() { when(keyRepo.findKeyFor(any(), any(), any())) .thenReturn(Optional.of(mock(CryptoKey.class))); - when(engine.decrypt(any(), any(), any())).thenReturn(new byte[32]); + when(engine.decrypt(any(), any(), any(), any())).thenReturn(new byte[32]); when(mapper.readerFor(any(Class.class))).thenReturn(mock(ObjectReader.class)); } } diff --git a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/InMemCryptoKeyRepository.java b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/InMemCryptoKeyRepository.java index 755c187a..254b7184 100644 --- a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/InMemCryptoKeyRepository.java +++ b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/InMemCryptoKeyRepository.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import lombok.NonNull; public class InMemCryptoKeyRepository implements CryptoKeyRepository { @@ -33,14 +34,18 @@ public InMemCryptoKeyRepository(CryptoEngine engine) { } @Override - public synchronized Optional findKeyFor( - CryptoSubjectId subjectId, CryptoAlgorithm algo, CryptoKeySize size) { + public synchronized @NonNull Optional findKeyFor( + @NonNull CryptoSubjectId subjectId, + @NonNull CryptoAlgorithm algo, + @NonNull CryptoKeySize size) { return Optional.ofNullable(keys.get(subjectId)); } @Override - public synchronized CryptoKey getOrCreateKeyFor( - CryptoSubjectId subjectId, CryptoAlgorithm algo, CryptoKeySize size) + public synchronized @NonNull CryptoKey getOrCreateKeyFor( + @NonNull CryptoSubjectId subjectId, + @NonNull CryptoAlgorithm algo, + @NonNull CryptoKeySize size) throws CryptoKeyNotFoundAfterCreatingException { Optional existingKey = findKeyFor(subjectId, algo, size); return existingKey.orElseGet( diff --git a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/RoundtripTest.java b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/RoundtripTest.java index 7277078c..1657ba26 100644 --- a/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/RoundtripTest.java +++ b/cryptoshred-core/src/test/java/eu/prismacapacity/cryptoshred/core/RoundtripTest.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,26 +18,56 @@ import static org.junit.jupiter.api.Assertions.*; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import eu.prismacapacity.cryptoshred.core.keys.CryptoKey; import java.util.UUID; import lombok.*; import org.junit.jupiter.api.*; -public class RoundtripTest { +class RoundtripTest { ObjectMapper om; + private InMemCryptoKeyRepository keyRepository; + @BeforeEach void setup() { - CryptoEngine engine = new JDKCryptoEngine(CryptoInitializationVector.of("mysecret")); - InMemCryptoKeyRepository keyRepository = new InMemCryptoKeyRepository(engine); + CryptoEngine engine = new JDKCryptoEngine("mysecret", false); + keyRepository = new InMemCryptoKeyRepository(engine); om = new ObjectMapper(); om.registerModule(new CryptoModule(engine, keyRepository)); } + @SneakyThrows @Test - void testHappyPath() throws Exception { + void testStaticIV() { + CryptoEngine engine = new JDKCryptoEngine("mysecret", false); + keyRepository = new InMemCryptoKeyRepository(engine); + om = new ObjectMapper(); + om.registerModule(new CryptoModule(engine, keyRepository)); + // arrange + CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); + + // act + Foo foo = new Foo(); + foo.name = new CryptoContainer<>("Peter", id); + String json1 = om.writeValueAsString(foo); + String json2 = om.writeValueAsString(foo); + assertEquals(json1, json2); + } + + @SneakyThrows + @Test + void testRandomIV() { + CryptoEngine engine = new JDKCryptoEngine("mysecret", true); + keyRepository = new InMemCryptoKeyRepository(engine); + om = new ObjectMapper(); + om.registerModule(new CryptoModule(engine, keyRepository)); + + // arrange CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); // act @@ -45,10 +75,82 @@ void testHappyPath() throws Exception { Foo foo = new Foo(); foo.name = new CryptoContainer<>("Peter", id); + String json1 = om.writeValueAsString(foo); + String json2 = om.writeValueAsString(foo); + + assertNotEquals(json1, json2); + } + + @SneakyThrows + @Test + void testFailureWithRandomIVUsed() { + + CryptoEngine engine = new JDKCryptoEngine("mysecret", true); + keyRepository = new InMemCryptoKeyRepository(engine); + om = new ObjectMapper(); + om.registerModule(new CryptoModule(engine, keyRepository)); + + CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); + Foo foo = new Foo(); + foo.name = new CryptoContainer<>("Peter", id); + String json = om.writeValueAsString(foo); + + // deserialize with random IV + Foo foo2 = om.readValue(json, Foo.class); + assertEquals(foo.bar, foo2.bar); + assertEquals(foo.name.get(), foo2.name.get()); + + // remove IV from json + JsonNode tree = om.readTree(json); + ObjectNode on = (ObjectNode) tree.get("name"); + on.remove("iv"); + + // must fail due to unmatching IV + Foo fooWithoutIV = om.readValue(tree.toString(), Foo.class); + assertFalse(fooWithoutIV.name.isPresent()); // disappeared due to decryption failure + } + + @SneakyThrows + @Test + void testDownwardsCompatibility() { + CryptoEngine engine = new JDKCryptoEngine("mysecret", false); + keyRepository = new InMemCryptoKeyRepository(engine); + om = new ObjectMapper(); + om.registerModule(new CryptoModule(engine, keyRepository)); + + CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); + + Foo foo = new Foo(); + foo.name = new CryptoContainer<>("Peter", id); + String json = om.writeValueAsString(foo); - // remove - System.out.println("serialized to json: " + json); + Foo foo2 = om.readValue(json, Foo.class); + assertEquals(foo.bar, foo2.bar); + assertEquals(foo.name.get(), foo2.name.get()); + + // remove IV from json + JsonNode tree = om.readTree(json); + ObjectNode on = (ObjectNode) tree.get("name"); + on.remove("iv"); + + // should fall back to configured IV + Foo foo3 = om.readValue(tree.toString(), Foo.class); + assertEquals(foo.bar, foo3.bar); + assertEquals(foo.name.get(), foo3.name.get()); + } + + @Test + void testHappyPath() throws Exception { + // arrange + CryptoSubjectId id = CryptoSubjectId.of(UUID.randomUUID()); + + // act + + Foo foo = new Foo(); + foo.name = new CryptoContainer<>("Peter", id); + + String json = om.writeValueAsString(foo); Foo foo2 = om.readValue(json, Foo.class); @@ -60,7 +162,6 @@ void testHappyPath() throws Exception { Bar b = new Bar(); b.pair = new CryptoContainer<>(new Pair<>("hubba", 77), id); json = om.writeValueAsString(b); - System.out.println(json); Bar b2 = om.readValue(json, Bar.class); assertEquals(b.pair.get(), b2.pair.get()); @@ -69,7 +170,6 @@ void testHappyPath() throws Exception { baz.name = new CryptoContainer<>("Peter", id); baz.pair = new CryptoContainer<>(new Pair<>("hubba", 77), id); json = om.writeValueAsString(baz); - System.out.println(json); Baz baz2 = om.readValue(json, Baz.class); assertEquals(baz.name.get(), baz2.name.get()); @@ -77,7 +177,6 @@ void testHappyPath() throws Exception { CryptoContainer c = new CryptoContainer<>("hubbi", id); json = om.writeValueAsString(c); - System.out.println(json); // if we need to deserialize a container without a surrounding bean (so without // type info), we need to pass a type-reference CryptoContainer c2 = @@ -86,6 +185,40 @@ void testHappyPath() throws Exception { assertEquals("hubbi", c2.get()); } + @Test + void testHappyPathLegacy() throws Exception { + // arrange + CryptoSubjectId id = + CryptoSubjectId.of(UUID.fromString("a1f6280b-eddf-454d-a2d1-15ed4c716e8e")); + CryptoKey k = CryptoKey.fromBase64("Mniow0ZBV2TEAUsoxH/hT+2e4yetncAjnpNCAHEbR+c="); + keyRepository.keys.put(id, k); + + // act + Foo fooNew = new Foo(); + fooNew.name = new CryptoContainer<>("Peter", id); + String jsonNew = om.writeValueAsString(fooNew); + // reload + fooNew = om.readValue(jsonNew, Foo.class); + + // serialised with default iv + String jsonLegacy = + "{\"bar\":7,\"name\":{\"algo\":\"AES\",\"ksize\":256,\"id\":\"a1f6280b-eddf-454d-a2d1-15ed4c716e8e\",\"enc\":\"SylOMKTrag8qCH84xVhfqQ==\"}}"; + + Foo fooLegacy = om.readValue(jsonLegacy, Foo.class); + + // not set on this old instance + assertNull(fooLegacy.getName().getInitializationVector()); + + // after encryption, iv must be set + assertTrue(jsonNew.contains("\"iv\"")); + // and must be included in the container + assertNotNull(fooNew.getName().getInitializationVector()); + + String fooName = fooNew.name.get(); + String foo2Name = fooLegacy.name.get(); + assertEquals(fooName, foo2Name); + } + @Test void testDeAndReSerialization() throws Exception { // arrange diff --git a/cryptoshred-spring-boot-autoconfigure/README.md b/cryptoshred-spring-boot-autoconfigure/README.md index 591ffc5c..da988500 100644 --- a/cryptoshred-spring-boot-autoconfigure/README.md +++ b/cryptoshred-spring-boot-autoconfigure/README.md @@ -9,9 +9,15 @@ Configuration The following configuration properties exists: -| **Property name** | **Description** | **Required** | **Default Value** | -|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----| -| `cryptoshred.cloud.aws.dynamo.tablename`| name of AWS DynamoDB table to store the subjectId/ key pairs. Only required when module [cryptoshred-cloud-aws](../cryptoshred-cloud-aws) is also used. | yes | - | -| `cryptoshred.initVector` | initialization vector for encryption/ decryption. Should be a random value. Only required for the default [JDKCryptoEngine](https://github.com/prisma-capacity/cryptoshred/blob/main/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/JDKCryptoEngine.java). Alternatively, you can provide your own [CryptoEngine](https://github.com/prisma-capacity/cryptoshred/blob/main/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java) | yes (if JDKCryptoEngine is used) | - | -| `cryptoshred.defaults.algorithm` | crypto algorithm to be used. Currently only *AES* is supported | no | AES | -| `cryptoshred.defaults.keySize` | | no | 256 | +| **Property name** | **Description** | **Required** | **Default Value** | +|------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|-------------------| +| `cryptoshred.cloud.aws.dynamo.tablename` | name of AWS DynamoDB table to store the subjectId/ key pairs. Only required when module [cryptoshred-cloud-aws](../cryptoshred-cloud-aws) is also used. | yes | - | +| `cryptoshred.initVector` | Initialization vector for encryption/decryption if useRandomInitVector is false, otherwise use for decryption only if the data was encrypted with an older version of this library (no IV is stored with the encrypted data). Alternatively, you can provide your own [CryptoEngine](https://github.com/prisma-capacity/cryptoshred/blob/main/cryptoshred-core/src/main/java/eu/prismacapacity/cryptoshred/core/CryptoEngine.java) | no | - | +| `cryptoshred.useRandomInitVector` | Always use secure random initialization vector for encryption, regardless of the initVector property. | no | false | +| `cryptoshred.defaults.algorithm` | crypto algorithm to be used. Currently only *AES* is supported | no | AES | +| `cryptoshred.defaults.keySize` | | no | 256 | + +Using random initVectors is a good practice, so for new projects setting useRandomInitVector=true and not providing an initVector is recommended. +In order to stay compatible with existing data, the static configured initVector is still used for decryption, if an initVector is not provided in the data. + +In a next major release, the library will default to the use of a random initVector. diff --git a/cryptoshred-spring-boot-autoconfigure/src/main/java/eu/prismacapacity/cryptoshred/spring/boot/autoconfiguration/CryptoShredConfiguration.java b/cryptoshred-spring-boot-autoconfigure/src/main/java/eu/prismacapacity/cryptoshred/spring/boot/autoconfiguration/CryptoShredConfiguration.java index a1bf61a5..4e8be840 100644 --- a/cryptoshred-spring-boot-autoconfigure/src/main/java/eu/prismacapacity/cryptoshred/spring/boot/autoconfiguration/CryptoShredConfiguration.java +++ b/cryptoshred-spring-boot-autoconfigure/src/main/java/eu/prismacapacity/cryptoshred/spring/boot/autoconfiguration/CryptoShredConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright © 2020 PRISMA European Capacity Platform GmbH + * Copyright © 2020-2026 PRISMA European Capacity Platform GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -47,14 +47,10 @@ public CryptoModule cryptoModule( @Bean @ConditionalOnMissingBean - public CryptoEngine cryptoEngine(@Value("${cryptoshred.initVector:#{null}}") String initVector) { - // then we'll need an initVector - if (initVector == null || initVector.length() < 1) { - throw new CryptoPropertyMissingException( - "cryptoshred.initVector (non-empty String) is required unless you define a" - + " CryptoEngine."); - } - return new JDKCryptoEngine(CryptoInitializationVector.of(initVector)); + public CryptoEngine cryptoEngine( + @Value("${cryptoshred.initVector:#{null}}") String initVectorOrNull, + @Value("${cryptoshred.useRandomInitVector:#{false}}") boolean useRandomInitVector) { + return new JDKCryptoEngine(initVectorOrNull, useRandomInitVector); } @Bean