From 4a4c104f3d41b26874e38cfc349fccc1961fe10b Mon Sep 17 00:00:00 2001 From: Stefan Kalscheuer Date: Tue, 25 Feb 2025 19:06:23 +0100 Subject: [PATCH] refactor TransitResponse and extend unit tests --- .../jvault/connector/HTTPVaultConnector.java | 9 +- .../jvault/connector/VaultConnector.java | 32 ++-- .../model/response/TransitResponse.java | 78 ++++++++-- .../connector/HTTPVaultConnectorIT.java | 57 +++++++- .../model/response/TransitResponseTest.java | 137 ++++++++++++++++++ src/test/resources/data_dir/core/_mounts | 2 +- 6 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 src/test/java/de/stklcode/jvault/connector/model/response/TransitResponseTest.java diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java index 2f1c299..b05cd68 100644 --- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java @@ -673,13 +673,12 @@ public class HTTPVaultConnector implements VaultConnector { return request.post(PATH_TRANSIT_DECRYPT + keyName, payload, token, TransitResponse.class); } - @Override - public final TransitResponse transitHash(final String algorithm, final String input) throws VaultConnectorException { - return transitHash(algorithm, input, "hex"); - } - @Override public final TransitResponse transitHash(final String algorithm, final String input, final String format) throws VaultConnectorException { + if (format != null && !"hex".equals(format) && !"base64".equals(format)) { + throw new IllegalArgumentException("Unsupported format " + format); + } + requireAuth(); Map payload = mapOf( diff --git a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java index 394a565..120b025 100644 --- a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java +++ b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java @@ -675,47 +675,49 @@ public interface VaultConnector extends AutoCloseable, Serializable { boolean deleteTokenRole(final String name) throws VaultConnectorException; /** - * encrypt plaintext via transit engine from Vault. + * Encrypt plaintext via transit engine from Vault. * - * @param name Transit key name - * @param plaintext Text to encrypt + * @param keyName Transit key name + * @param plaintext Text to encrypt (Base64 encoded) * @return Transit response * @throws VaultConnectorException on error - * @since 1.4.1 + * @since 1.5.0 */ - TransitResponse transitEncrypt(final String name, final String plaintext) throws VaultConnectorException; + TransitResponse transitEncrypt(final String keyName, final String plaintext) throws VaultConnectorException; /** - * decrypt ciphertext via transit engine from Vault. + * Decrypt ciphertext via transit engine from Vault. * - * @param name Transit key name + * @param keyName Transit key name * @param ciphertext Text to decrypt * @return Transit response * @throws VaultConnectorException on error - * @since 1.4.1 + * @since 1.5.0 */ - TransitResponse transitDecrypt(final String name, final String ciphertext) throws VaultConnectorException; + TransitResponse transitDecrypt(final String keyName, final String ciphertext) throws VaultConnectorException; /** - * hash data in hex format via transit engine from Vault. + * Hash data in hex format via transit engine from Vault. * * @param algorithm Specifies the hash algorithm to use - * @param input Data to hash + * @param input Data to hash * @return Transit response * @throws VaultConnectorException on error - * @since 1.4.1 + * @since 1.5.0 */ - TransitResponse transitHash(final String algorithm, final String input) throws VaultConnectorException; + default TransitResponse transitHash(final String algorithm, final String input) throws VaultConnectorException { + return transitHash(algorithm, input, "hex"); + } /** * hash data via transit engine from Vault. * * @param algorithm Specifies the hash algorithm to use - * @param input Data to hash + * @param input Data to hash (Base64 encoded) * @param format Specifies the output encoding (hex/base64) * @return Transit response * @throws VaultConnectorException on error - * @since 1.4.1 + * @since 1.5.0 */ TransitResponse transitHash(final String algorithm, final String input, final String format) throws VaultConnectorException; diff --git a/src/main/java/de/stklcode/jvault/connector/model/response/TransitResponse.java b/src/main/java/de/stklcode/jvault/connector/model/response/TransitResponse.java index ee9b7be..d7869f7 100644 --- a/src/main/java/de/stklcode/jvault/connector/model/response/TransitResponse.java +++ b/src/main/java/de/stklcode/jvault/connector/model/response/TransitResponse.java @@ -1,21 +1,75 @@ +/* + * Copyright 2016-2025 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; -import java.io.Serializable; -import java.util.Collections; import java.util.Map; import java.util.Objects; -import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +/** + * Response entity for transit operations. + * + * @author Stefan Kalscheuer + * @since 1.5.0 + */ public class TransitResponse extends VaultDataResponse { - private static final long serialVersionUID = -4823865538268326557L; - @JsonProperty("data") - private Map data; + private static final long serialVersionUID = 6873804240772242771L; - //@Override - public final Map getData() { - return Objects.requireNonNullElseGet(data, Collections::emptyMap); + private String ciphertext; + private String plaintext; + private String sum; + + @JsonSetter("data") + private void setData(Map data) { + ciphertext = data.get("ciphertext"); + plaintext = data.get("plaintext"); + sum = data.get("sum"); + } + + /** + * Get ciphertext. + * Populated after encryption. + * + * @return Ciphertext + */ + public String getCiphertext() { + return ciphertext; + } + + /** + * Get plaintext. + * Base64 encoded, populated after decryption. + * + * @return Plaintext + */ + public String getPlaintext() { + return plaintext; + } + + /** + * Get hash sum. + * Hex or Base64 string. Populated after hashing. + * + * @return Hash sum + */ + public String getSum() { + return sum; } @Override @@ -26,11 +80,13 @@ public class TransitResponse extends VaultDataResponse { return false; } TransitResponse that = (TransitResponse) o; - return Objects.equals(data, that.data); + return Objects.equals(ciphertext, that.ciphertext) && + Objects.equals(plaintext, that.plaintext) && + Objects.equals(sum, that.sum); } @Override public int hashCode() { - return Objects.hash(super.hashCode(), data); + return Objects.hash(super.hashCode(), ciphertext, plaintext, sum); } } diff --git a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java index 65ee6e8..8f1f77d 100644 --- a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java +++ b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java @@ -989,6 +989,61 @@ class HTTPVaultConnectorIT { } } + @Nested + @DisplayName("Transit Tests") + class TransitTests { + + @Test + @DisplayName("Transit encryption") + void transitEncryptTest() { + assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT)); + assumeTrue(connector.isAuthorized()); + + TransitResponse transitResponse = assertDoesNotThrow( + () -> connector.transitEncrypt("my-key", "dGVzdCBtZQo="), + "Failed to encrypt via transit" + ); + + assertNotNull(transitResponse.getCiphertext()); + assertTrue(transitResponse.getCiphertext().startsWith("vault:v1:")); + } + + @Test + @DisplayName("Transit decryption") + void transitDecryptTest() { + assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT)); + assumeTrue(connector.isAuthorized()); + + TransitResponse transitResponse = assertDoesNotThrow( + () -> connector.transitDecrypt("my-key", "vault:v1:oWRLHHjAgjPIrY9cYwIV3BRBO665qVYjSK4roVWFwbG8jgdN"), + "Failed to decrypt via transit" + ); + + assertEquals("dGVzdCBtZQo=", transitResponse.getPlaintext()); + } + + @Test + @DisplayName("Transit hash") + void transitHashText() { + assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT)); + assumeTrue(connector.isAuthorized()); + + TransitResponse transitResponse = assertDoesNotThrow( + () -> connector.transitHash("sha2-512", "dGVzdCBtZQo="), + "Failed to hash via transit" + ); + + assertEquals("b759bc369a1503c2871ae39c01def2664d8029f089b6827732fa03c4ef981958f14f8d1e5e800c83bcf4114fb4c0661eb07bf18bc4fed5aed679057f74c3844f", transitResponse.getSum()); + + TransitResponse transitResponseBase64 = assertDoesNotThrow( + () -> connector.transitHash("sha2-512", "dGVzdCBtZQo=", "base64"), + "Failed to hash via transit" + ); + + assertEquals("t1m8NpoVA8KHGuOcAd7yZk2AKfCJtoJ3MvoDxO+YGVjxT40eXoAMg7z0EU+0wGYesHvxi8T+1a7WeQV/dMOETw==", transitResponseBase64.getSum()); + } + } + @Nested @DisplayName("Misc Tests") class MiscTests { @@ -1160,7 +1215,7 @@ class HTTPVaultConnectorIT { // Start vault process. try { - vaultProcess = new ProcessBuilder("vault", "server", "-config", configFile.toString()) + vaultProcess = new ProcessBuilder("/media/develop/Java/jvault/jvaultconnector/bin/vault_1.2.0", "server", "-config", configFile.toString()) .directory(dir) .start(); } catch (IOException e) { diff --git a/src/test/java/de/stklcode/jvault/connector/model/response/TransitResponseTest.java b/src/test/java/de/stklcode/jvault/connector/model/response/TransitResponseTest.java new file mode 100644 index 0000000..f47efe7 --- /dev/null +++ b/src/test/java/de/stklcode/jvault/connector/model/response/TransitResponseTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016-2025 Stefan Kalscheuer + * + * 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 de.stklcode.jvault.connector.model.response; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.stklcode.jvault.connector.model.AbstractModelTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * JUnit Test for {@link TransitResponse} model. + * + * @author Stefan Kalscheuer + * @since 1.5.0 + */ +class TransitResponseTest extends AbstractModelTest { + private static final String CIPHERTEXT = "vault:v1:XjsPWPjqPrBi1N2Ms2s1QM798YyFWnO4TR4lsFA="; + private static final String PLAINTEXT = "dGhlIHF1aWNrIGJyb3duIGZveAo="; + private static final String SUM = "dGhlIHF1aWNrIGJyb3duIGZveAo="; + + TransitResponseTest() { + super(TransitResponse.class); + } + + @Override + protected TransitResponse createFull() { + try { + return objectMapper.readValue( + json( + "\"ciphertext\": \"" + CIPHERTEXT + "\", " + + "\"plaintext\": \"" + PLAINTEXT + "\", " + + "\"sum\": \"" + SUM + "\"" + ), + TransitResponse.class + ); + } catch (JsonProcessingException e) { + fail("Creation of full model failed", e); + return null; + } + } + + @Test + void encryptionTest() { + TransitResponse res = assertDoesNotThrow( + () -> objectMapper.readValue( + json("\"ciphertext\": \"" + CIPHERTEXT + "\""), + TransitResponse.class + ), + "TransitResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals("987c6daf-b0e2-4142-a970-1e61fdb249d7", res.getRequestId(), "Incorrect request id"); + assertEquals("", res.getLeaseId(), "Unexpected lease id"); + assertFalse(res.isRenewable(), "Unexpected renewable flag"); + assertEquals(0, res.getLeaseDuration(), "Unexpected lease duration"); + assertEquals(CIPHERTEXT, res.getCiphertext(), "Incorrect ciphertext"); + assertNull(res.getPlaintext(), "Unexpected plaintext"); + assertNull(res.getSum(), "Unexpected sum"); + assertNull(res.getWrapInfo(), "Unexpected wrap info"); + assertNull(res.getWarnings(), "Unexpected warnings"); + assertNull(res.getAuth(), "Unexpected auth"); + } + + @Test + void decryptionTest() { + TransitResponse res = assertDoesNotThrow( + () -> objectMapper.readValue( + json("\"plaintext\": \"" + PLAINTEXT + "\""), + TransitResponse.class + ), + "TransitResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals("987c6daf-b0e2-4142-a970-1e61fdb249d7", res.getRequestId(), "Incorrect request id"); + assertEquals("", res.getLeaseId(), "Unexpected lease id"); + assertFalse(res.isRenewable(), "Unexpected renewable flag"); + assertEquals(0, res.getLeaseDuration(), "Unexpected lease duration"); + assertNull(res.getCiphertext(), "Unexpected ciphertext"); + assertEquals(PLAINTEXT, res.getPlaintext(), "Incorrect plaintext"); + assertNull(res.getSum(), "Unexpected sum"); + assertNull(res.getWrapInfo(), "Unexpected wrap info"); + assertNull(res.getWarnings(), "Unexpected warnings"); + assertNull(res.getAuth(), "Unexpected auth"); + } + + @Test + void hashTest() { + TransitResponse res = assertDoesNotThrow( + () -> objectMapper.readValue( + json("\"sum\": \"" + SUM + "\""), + TransitResponse.class + ), + "TransitResponse deserialization failed" + ); + assertNotNull(res, "Parsed response is NULL"); + assertEquals("987c6daf-b0e2-4142-a970-1e61fdb249d7", res.getRequestId(), "Incorrect request id"); + assertEquals("", res.getLeaseId(), "Unexpected lease id"); + assertFalse(res.isRenewable(), "Unexpected renewable flag"); + assertEquals(0, res.getLeaseDuration(), "Unexpected lease duration"); + assertNull(res.getCiphertext(), "Unexpected ciphertext"); + assertNull(res.getPlaintext(), "Unexpected plaintext"); + assertEquals(SUM, res.getSum(), "Incorrect sum"); + assertNull(res.getWrapInfo(), "Unexpected wrap info"); + assertNull(res.getWarnings(), "Unexpected warnings"); + assertNull(res.getAuth(), "Unexpected auth"); + } + + private static String json(String data) { + return "{\n" + + " \"request_id\" : \"987c6daf-b0e2-4142-a970-1e61fdb249d7\",\n" + + " \"lease_id\" : \"\",\n" + + " \"renewable\" : false,\n" + + " \"lease_duration\" : 0,\n" + + " \"data\" : {\n" + + " " + data + "\n" + + " },\n" + + " \"wrap_info\" : null,\n" + + " \"warnings\" : null,\n" + + " \"auth\" : null\n" + + "}"; + } +} diff --git a/src/test/resources/data_dir/core/_mounts b/src/test/resources/data_dir/core/_mounts index a6d1bca..fe4d71c 100644 --- a/src/test/resources/data_dir/core/_mounts +++ b/src/test/resources/data_dir/core/_mounts @@ -1 +1 @@ -{"Value":"AAAAAQIOTEuNxtf2ZfGu6k9+65GFonDBiaetJnyLYFk1qfWPrE/VqUunbxuTv/QyK4pgC/q14sqypdxPe4Ygp/5mxzzuY6JXB0VKiDMXe9R5BltTG7++rLmKH/j+G7nEh71LER1/qVktU6x8dmDmTbpTgl5xzAB5DIXLMMKjjWda/7qJ3ivNI0pEOQ3JyT27SkqjqM49Dp1JIgKnjIEVQORqKcsSP+aqIncMjIo2iGXOGlDYAesp5tZ4hln3VCwXaIlrU8z6Mmntgcg7NeK/O2WTl86K644dbJUh6frGFDujrjOh/Pgp9n9u0BI3E9K9GD1Z1S1wEb1MCqzT/e9oHG7I7D8ku8PH11wGWGH6BmYlESYUG/KRVqu0XOQEfroLHZQiLE00yHBdStW/UJ9y5pmGpu1aiQ88Q8fpjF5xmRey99b4c6KUIpHjw5Af0XA9ZKsEJUyjS/dbMKPM6PBOW21LYefXH5b0poXMWgLW6LJV0zLuVMyVZJqANbzCVtheDPSsEjkHHwR/CLa2zs2Z6wJF3j1GDZxUFf4nbnuXQzz3M3kVPPtS6htlb0d8RN0/dkOrqUue+c4UjnXhiacXyVUnQQzUVu0sGak4vrQi0020aBzMXM2ZG7rNgvw+wcYFS4txO90kwJL6aOMXT2BeJiQ3wjjHb7M74/sSd0ffRTUlJSODSDotO7Q7Dfcnc1FLrIhXPRFPavTjFoL5HRy77xuGG7U7jTMoGra+rK54v0sxqKbS5WZi1hADQmAVIO8bPb+jA1qoejRFygW6sLgAdmEFQQJOhCV/BoKP3A=="} +{"Value":"AAAAAQJ7mykBIbHX5k81qdXEpvlLgRF1ZSlODETcB1JBZ7nj0Muskpvvl3jofN5XH1Td8ibJlrR0o5o/OUjZAz9t0Da+ZzCy4ga9G2SmgWkUAravTqfPO/ZxWh2hqTso2WPBXRM3/IeR0SAv/zh7m7JILxjKybJmnl9U6fkjPID/us0AscckZ9kgJ4g8jwaTzPfRp5U8jMebHYbABXZ65PeUOvNiDVcOvQDWPJrMxICz12xbeJ8mKs5MHpcNkLtPoRCQSpsh0YYTwkuF7NvpIciqIJ4/Yb7wYO+vp9AATbiIs3sSFBWxXEl0OAg/SAmOvaR27Y7/NHN//mg81jkMOHv62/Fxf11I8t1d63oyWFQhP0xR9eoq5hGNQ/30I2m1prhZtLRC2ieKASBDMxTzyNS5G5bsVXvhCsxn8tiC0Ma+ySOfxMQzBRfbx8rtoGmLFP+l/d6VMOPGFxmYqzLS5HvvpCryscCqLn7A8i6TMSrZiF7ZevyfEBpThqhJiYHzUxf07O5IAe6JBSGuNV9gLE+uJXaiYLedJwSfjRKwdQyzer730dU1IegW3KYTb/5hSW4eaETKkjc/alC536WlvAv+5FyckDBc3aVC+hHB7lKZG5YANkOUS3m5I8epemOmuKQ5pnXLOdDkQ14DyNCC79NwLltkp0d6sTNstQ44XAbs0HlLjs4EFwg5Hps9qHeXgTOXeAvwUerJgM20nKszlB+Oy+JzZm0xOK5xoJwy+z0/U3PtJ+7pwAipesIa2QEzqMt3EneuPuwEcv3bPUcowukq542sCEK0CuZjLqTUU9nNqiZan5f5pWuL0hYw4NFIkNfQyRlqgKpMaplDk/2fBqaV98yn0DWceEMWRY4NXpEMS+ysPDIeamX99auWqakb95AZ7tySpkRAkZYtq1nY5Nu0w7NyJrJZ1lhBHs2ZjW0tpXn2CL0MhMVArg=="}