diff --git a/.gitignore b/.gitignore index 320656b8..430475fd 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ generated_testSrc # Mac .DS_Store + +# Jqwik property based testing +.jqwik-database diff --git a/build.gradle b/build.gradle index 7ffd19b8..673d442b 100644 --- a/build.gradle +++ b/build.gradle @@ -50,7 +50,7 @@ subprojects { tasks.withType(Test) { useJUnitPlatform { - includeEngines 'junit-jupiter' + includeEngines 'jqwik', 'junit-jupiter' } maxParallelForks Runtime.getRuntime().availableProcessors() testLogging { diff --git a/encrypted-config-value-module/build.gradle b/encrypted-config-value-module/build.gradle index 2fc374d9..3a72a452 100644 --- a/encrypted-config-value-module/build.gradle +++ b/encrypted-config-value-module/build.gradle @@ -11,9 +11,12 @@ dependencies { implementation 'com.palantir.safe-logging:preconditions' implementation 'com.palantir.safe-logging:safe-logging' + testRuntimeOnly 'net.jqwik:jqwik' + testImplementation 'com.fasterxml.jackson.core:jackson-annotations' testImplementation 'com.google.code.findbugs:jsr305' testImplementation 'com.google.errorprone:error_prone_annotations' + testImplementation 'net.jqwik:jqwik-api' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/encrypted-config-value-module/src/test/java/com/palantir/config/crypto/DecryptingVariableSubstitutorTest.java b/encrypted-config-value-module/src/test/java/com/palantir/config/crypto/DecryptingVariableSubstitutorTest.java index d9bec3f2..b27320bb 100644 --- a/encrypted-config-value-module/src/test/java/com/palantir/config/crypto/DecryptingVariableSubstitutorTest.java +++ b/encrypted-config-value-module/src/test/java/com/palantir/config/crypto/DecryptingVariableSubstitutorTest.java @@ -22,16 +22,23 @@ import com.palantir.config.crypto.algorithm.Algorithm; import com.palantir.config.crypto.util.StringSubstitutionException; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import net.jqwik.api.Assume; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.CharRange; +import net.jqwik.api.constraints.StringLength; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -public class DecryptingVariableSubstitutorTest { +public final class DecryptingVariableSubstitutorTest { private static final Algorithm ALGORITHM = Algorithm.RSA; private static final KeyPair KEY_PAIR = ALGORITHM.newKeyPair(); + public static final String TEST_KEY_PATH = DecryptingVariableSubstitutorTest.class.getName() + "test-key"; private static String previousProperty; private final DecryptingVariableSubstitutor substitutor = new DecryptingVariableSubstitutor(); @@ -39,11 +46,7 @@ public class DecryptingVariableSubstitutorTest { @BeforeAll public static void beforeClass() throws IOException { previousProperty = System.getProperty(KeyFileUtils.KEY_PATH_PROPERTY); - - Path tempFilePath = Files.createTempDirectory("temp-key-directory").resolve("test.key"); - KeyFileUtils.keyPairToFile(KEY_PAIR, tempFilePath); - System.setProperty( - KeyFileUtils.KEY_PATH_PROPERTY, tempFilePath.toAbsolutePath().toString()); + ensureTestKeysExist(); } @AfterAll @@ -51,15 +54,16 @@ public static void afterClass() { if (previousProperty != null) { System.setProperty(KeyFileUtils.KEY_PATH_PROPERTY, previousProperty); } + System.clearProperty(TEST_KEY_PATH); } @Test - public final void constantsAreNotModified() { + public void constantsAreNotModified() { assertThat(substitutor.replace("abc")).isEqualTo("abc"); } @Test - public final void invalidEncryptedVariablesThrowStringSubstitutionException() { + public void invalidEncryptedVariablesThrowStringSubstitutionException() { try { substitutor.replace("${enc:invalid-contents}"); fail("fail"); @@ -70,20 +74,60 @@ public final void invalidEncryptedVariablesThrowStringSubstitutionException() { } @Test - public final void nonEncryptedVariablesAreNotModified() { + public void nonEncryptedVariablesAreNotModified() { assertThat(substitutor.replace("${abc}")).isEqualTo("${abc}"); } @Test - public final void variableIsDecrypted() throws Exception { + public void variableIsDecrypted() { assertThat(substitutor.replace("${" + encrypt("abc") + "}")).isEqualTo("abc"); } @Test - public final void variableIsDecryptedWithRegex() throws Exception { + public void variableIsDecryptedWithRegex() { assertThat(substitutor.replace("${" + encrypt("$5") + "}")).isEqualTo("$5"); } + @Test + public void decryptsMultiple() { + String abc = "${" + encrypt("abc") + "}"; + String def = "${" + encrypt("def") + "}"; + String hello = "${" + encrypt("enc:hello") + "}"; + String source = abc + ":" + def + '.' + hello; + assertThat(substitutor.replace(source)).isEqualTo("abc:def.enc:hello"); + } + + @Test + public void decryptsWithPlaceholders() { + String abc = "${" + encrypt("abc") + "}"; + String def = "${" + encrypt("${enc:test}") + "}"; + String source = abc + ":" + def; + assertThat(substitutor.replace(source)).isEqualTo("abc:${enc:test}"); + } + + @Property(tries = 10_000) + void propertyTestValues(@ForAll @CharRange(from = 0, to = 1024) @StringLength(max = 100) String plaintext) + throws IOException { + // RSA test key can only encrypt 190 bytes + Assume.that(plaintext.getBytes(StandardCharsets.UTF_8).length <= 190); + ensureTestKeysExist(); + assertThat(substitutor.replace("${" + encrypt(plaintext) + "}")).isEqualTo(plaintext); + } + + private static void ensureTestKeysExist() throws IOException { + String testKeyPath = System.getProperty(TEST_KEY_PATH); + if (testKeyPath != null && Files.isRegularFile(Path.of(testKeyPath))) { + return; + } + Path tempFilePath = Files.createTempDirectory("temp-key-directory") + .resolve(ALGORITHM.name() + "-test.key") + .toAbsolutePath(); + KeyFileUtils.keyPairToFile(KEY_PAIR, tempFilePath); + String path = tempFilePath.toString(); + System.setProperty(KeyFileUtils.KEY_PATH_PROPERTY, path); + System.setProperty(TEST_KEY_PATH, path); + } + private String encrypt(String value) { return ALGORITHM.newEncrypter().encrypt(KEY_PAIR.encryptionKey(), value).toString(); } diff --git a/encrypted-config-value/build.gradle b/encrypted-config-value/build.gradle index 5e9a9197..f2638a78 100644 --- a/encrypted-config-value/build.gradle +++ b/encrypted-config-value/build.gradle @@ -12,6 +12,9 @@ dependencies { implementation 'com.palantir.safe-logging:safe-logging' implementation 'com.palantir.safe-logging:preconditions' + testRuntimeOnly 'net.jqwik:jqwik' + + testImplementation 'net.jqwik:jqwik-api' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/encrypted-config-value/src/test/java/com/palantir/config/crypto/AlgorithmTest.java b/encrypted-config-value/src/test/java/com/palantir/config/crypto/AlgorithmTest.java index 2d7eb8fb..ebca24a0 100644 --- a/encrypted-config-value/src/test/java/com/palantir/config/crypto/AlgorithmTest.java +++ b/encrypted-config-value/src/test/java/com/palantir/config/crypto/AlgorithmTest.java @@ -19,14 +19,27 @@ import static org.assertj.core.api.Assertions.assertThat; import com.palantir.config.crypto.algorithm.Algorithm; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import net.jqwik.api.Assume; +import net.jqwik.api.EdgeCasesMode; +import net.jqwik.api.ForAll; +import net.jqwik.api.Property; +import net.jqwik.api.constraints.StringLength; import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; public final class AlgorithmTest { - private static final String plaintext = "Some top secret plaintext for testing things"; + static Stream args() { + return Stream.of(Algorithm.values()) + .flatMap(algorithm -> Stream.of( + Arguments.of(algorithm, "Some top secret plaintext for testing things"), + Arguments.of(algorithm, "test"))); + } @ParameterizedTest - @EnumSource(Algorithm.class) + @MethodSource("args") public void weGenerateRandomKeys(Algorithm algorithm) { KeyPair keyPair1 = algorithm.newKeyPair(); KeyPair keyPair2 = algorithm.newKeyPair(); @@ -35,21 +48,14 @@ public void weGenerateRandomKeys(Algorithm algorithm) { } @ParameterizedTest - @EnumSource(Algorithm.class) - public void weCanEncryptAndDecrypt(Algorithm algorithm) { - KeyPair keyPair = algorithm.newKeyPair(); - - EncryptedValue encryptedValue = algorithm.newEncrypter().encrypt(keyPair.encryptionKey(), plaintext); - - KeyWithType decryptionKey = keyPair.decryptionKey(); - String decrypted = encryptedValue.decrypt(decryptionKey); - - assertThat(decrypted).isEqualTo(plaintext); + @MethodSource("args") + public void weCanEncryptAndDecrypt(Algorithm algorithm, String plaintext) { + encryptAndDecrypt(algorithm, plaintext); } @ParameterizedTest - @EnumSource(Algorithm.class) - public void theSameStringEncryptsToDifferentCiphertexts(Algorithm algorithm) { + @MethodSource("args") + public void theSameStringEncryptsToDifferentCiphertexts(Algorithm algorithm, String plaintext) { KeyPair keyPair = algorithm.newKeyPair(); EncryptedValue encrypted1 = algorithm.newEncrypter().encrypt(keyPair.encryptionKey(), plaintext); @@ -68,4 +74,27 @@ public void theSameStringEncryptsToDifferentCiphertexts(Algorithm algorithm) { assertThat(decryptedString1).isEqualTo(plaintext); assertThat(decryptedString2).isEqualTo(plaintext); } + + @Property(tries = 10_000) + void aes(@ForAll @StringLength(max = 100_000) String plaintext) { + encryptAndDecrypt(Algorithm.AES, plaintext); + } + + @Property(tries = 100, edgeCases = EdgeCasesMode.MIXIN) + void rsa(@ForAll @StringLength(max = 64) String plaintext) { + // RSA test key can only encrypt 190 bytes + Assume.that(plaintext.getBytes(StandardCharsets.UTF_8).length <= 190); + encryptAndDecrypt(Algorithm.RSA, plaintext); + } + + private static void encryptAndDecrypt(Algorithm algorithm, String plaintext) { + KeyPair keyPair = algorithm.newKeyPair(); + + EncryptedValue encryptedValue = algorithm.newEncrypter().encrypt(keyPair.encryptionKey(), plaintext); + + KeyWithType decryptionKey = keyPair.decryptionKey(); + String decrypted = encryptedValue.decrypt(decryptionKey); + + assertThat(decrypted).isEqualTo(plaintext); + } } diff --git a/versions.lock b/versions.lock index c92fa9b6..3293f8aa 100644 --- a/versions.lock +++ b/versions.lock @@ -105,7 +105,12 @@ io.dropwizard:dropwizard-testing:1.3.29 (1 constraints: 0305f035) junit:junit:4.13.1 (3 constraints: ed4b45b4) net.bytebuddy:byte-buddy:1.12.16 (2 constraints: ef164566) net.bytebuddy:byte-buddy-agent:1.12.16 (1 constraints: 750baee9) -org.apiguardian:apiguardian-api:1.1.2 (5 constraints: 105480ac) +net.jqwik:jqwik:1.7.0 (1 constraints: 0a050536) +net.jqwik:jqwik-api:1.7.0 (5 constraints: 5c2915b3) +net.jqwik:jqwik-engine:1.7.0 (1 constraints: 9b07f76c) +net.jqwik:jqwik-time:1.7.0 (1 constraints: 9b07f76c) +net.jqwik:jqwik-web:1.7.0 (1 constraints: 9b07f76c) +org.apiguardian:apiguardian-api:1.1.2 (10 constraints: 4f819858) org.assertj:assertj-core:3.23.1 (2 constraints: 0614c776) org.glassfish.jersey.test-framework:jersey-test-framework-core:2.25.1 (1 constraints: aa230902) org.glassfish.jersey.test-framework.providers:jersey-test-framework-provider-inmemory:2.25.1 (1 constraints: c90ee65f) @@ -114,9 +119,9 @@ org.junit.jupiter:junit-jupiter:5.9.1 (1 constraints: 11052036) org.junit.jupiter:junit-jupiter-api:5.9.1 (5 constraints: 4f4322b4) org.junit.jupiter:junit-jupiter-engine:5.9.1 (1 constraints: 0c0ee13b) org.junit.jupiter:junit-jupiter-params:5.9.1 (2 constraints: 1c13903c) -org.junit.platform:junit-platform-commons:1.9.1 (2 constraints: dd200f4b) -org.junit.platform:junit-platform-engine:1.9.1 (1 constraints: ab1029b4) +org.junit.platform:junit-platform-commons:1.9.1 (4 constraints: 2134e7af) +org.junit.platform:junit-platform-engine:1.9.1 (2 constraints: eb1a8a4e) org.mockito:mockito-core:4.8.1 (2 constraints: d6130a66) org.mockito:mockito-junit-jupiter:4.8.1 (1 constraints: 0f051836) org.objenesis:objenesis:3.3 (3 constraints: 8e1d111c) -org.opentest4j:opentest4j:1.2.0 (2 constraints: cd205b49) +org.opentest4j:opentest4j:1.2.0 (6 constraints: 72469adf) diff --git a/versions.props b/versions.props index 451e16f8..ad4a5c5e 100644 --- a/versions.props +++ b/versions.props @@ -7,6 +7,7 @@ com.palantir.safe-logging:* = 3.2.0 io.dropwizard:* = 1.0.0 io.dropwizard:dropwizard-validation = 1.3.29 javax.ws.rs:javax.ws.rs-api = 2.0.1 +net.jqwik:* = 1.7.0 org.assertj:assertj-core = 3.23.1 org.immutables:* = 2.8.8 org.junit.jupiter:* = 5.9.1