diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99b6f5c..aeabef6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
* `read...Credentials()` methods for specific database mounts (#92)
### Features
+* Support Vault transit API (#89)
* Support PEM certificate string from `VAULT_CACERT` environment variable (#93)
### Dependencies
diff --git a/pom.xml b/pom.xml
index c1c5be4..b6e1ae7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
de.stklcode.jvault
jvault-connector
- 1.4.1-SNAPSHOT
+ 1.5.0-SNAPSHOT
jar
diff --git a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java
index 788af95..30b0bab 100644
--- a/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java
+++ b/src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java
@@ -68,6 +68,11 @@ public class HTTPVaultConnector implements VaultConnector {
private static final String PATH_UNDELETE = "/undelete/";
private static final String PATH_DESTROY = "/destroy/";
+ private static final String PATH_TRANSIT = "transit";
+ private static final String PATH_TRANSIT_ENCRYPT = PATH_TRANSIT + "/encrypt/";
+ private static final String PATH_TRANSIT_DECRYPT = PATH_TRANSIT + "/decrypt/";
+ private static final String PATH_TRANSIT_HASH = PATH_TRANSIT + "/hash/";
+
private final RequestHelper request;
private boolean authorized = false; // Authorization status.
@@ -646,6 +651,47 @@ public class HTTPVaultConnector implements VaultConnector {
return true;
}
+ @Override
+ public final TransitResponse transitEncrypt(final String keyName, final String plaintext)
+ throws VaultConnectorException {
+ requireAuth();
+
+ Map payload = mapOf(
+ "plaintext", plaintext
+ );
+
+ return request.post(PATH_TRANSIT_ENCRYPT + keyName, payload, token, TransitResponse.class);
+ }
+
+ @Override
+ public final TransitResponse transitDecrypt(final String keyName, final String ciphertext)
+ throws VaultConnectorException {
+ requireAuth();
+
+ Map payload = mapOf(
+ "ciphertext", ciphertext
+ );
+
+ return request.post(PATH_TRANSIT_DECRYPT + keyName, payload, token, TransitResponse.class);
+ }
+
+ @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(
+ "input", input,
+ "format", format
+ );
+
+ return request.post(PATH_TRANSIT_HASH + algorithm, payload, token, TransitResponse.class);
+ }
+
/**
* Check for required authorization.
*
diff --git a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java
index 8ad17ea..3a673a4 100644
--- a/src/main/java/de/stklcode/jvault/connector/VaultConnector.java
+++ b/src/main/java/de/stklcode/jvault/connector/VaultConnector.java
@@ -21,10 +21,7 @@ import de.stklcode.jvault.connector.model.*;
import de.stklcode.jvault.connector.model.response.*;
import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
/**
* Vault Connector interface.
@@ -674,6 +671,82 @@ public interface VaultConnector extends AutoCloseable, Serializable {
*/
boolean deleteTokenRole(final String name) throws VaultConnectorException;
+ /**
+ * Encrypt plaintext via transit engine from Vault.
+ *
+ * @param keyName Transit key name
+ * @param plaintext Text to encrypt (Base64 encoded)
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ TransitResponse transitEncrypt(final String keyName, final String plaintext) throws VaultConnectorException;
+
+ /**
+ * Encrypt plaintext via transit engine from Vault.
+ *
+ * @param keyName Transit key name
+ * @param plaintext Binary data to encrypt
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ default TransitResponse transitEncrypt(final String keyName, final byte[] plaintext)
+ throws VaultConnectorException {
+ return transitEncrypt(keyName, Base64.getEncoder().encodeToString(plaintext));
+ }
+
+ /**
+ * Decrypt ciphertext via transit engine from Vault.
+ *
+ * @param keyName Transit key name
+ * @param ciphertext Text to decrypt
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ TransitResponse transitDecrypt(final String keyName, final String ciphertext) throws VaultConnectorException;
+
+ /**
+ * Hash data in hex format via transit engine from Vault.
+ *
+ * @param algorithm Specifies the hash algorithm to use
+ * @param input Data to hash
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ 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 (Base64 encoded)
+ * @param format Specifies the output encoding (hex/base64)
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ TransitResponse transitHash(final String algorithm, final String input, final String format)
+ throws VaultConnectorException;
+
+ /**
+ * Hash data via transit engine from Vault.
+ *
+ * @param algorithm Specifies the hash algorithm to use
+ * @param input Data to hash
+ * @return Transit response
+ * @throws VaultConnectorException on error
+ * @since 1.5.0
+ */
+ default TransitResponse transitHash(final String algorithm, final byte[] input, final String format)
+ throws VaultConnectorException {
+ return transitHash(algorithm, Base64.getEncoder().encodeToString(input), format);
+ }
+
/**
* Read credentials for MySQL backend at default mount point.
*
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
new file mode 100644
index 0000000..d7869f7
--- /dev/null
+++ b/src/main/java/de/stklcode/jvault/connector/model/response/TransitResponse.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util.Map;
+import java.util.Objects;
+
+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 = 6873804240772242771L;
+
+ 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
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ } else if (o == null || getClass() != o.getClass() || !super.equals(o)) {
+ return false;
+ }
+ TransitResponse that = (TransitResponse) o;
+ 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(), 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 e1ca4d7..6a07c40 100644
--- a/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java
+++ b/src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java
@@ -989,6 +989,75 @@ 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", "dGVzdCBtZQ=="),
+ "Failed to encrypt via transit"
+ );
+ assertNotNull(transitResponse.getCiphertext());
+ assertTrue(transitResponse.getCiphertext().startsWith("vault:v1:"));
+
+ transitResponse = assertDoesNotThrow(
+ () -> connector.transitEncrypt("my-key", "test me".getBytes(UTF_8)),
+ "Failed to encrypt binary data 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:1mhLVkBAR2nrFtIkJF/qg57DWfRj0FWgR6tvkGO8XOnL6sw="),
+ "Failed to decrypt via transit"
+ );
+
+ assertEquals("dGVzdCBtZQ==", transitResponse.getPlaintext());
+ }
+
+ @Test
+ @DisplayName("Transit hash")
+ void transitHashText() {
+ assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT));
+ assumeTrue(connector.isAuthorized());
+
+ TransitResponse transitResponse = assertDoesNotThrow(
+ () -> connector.transitHash("sha2-512", "dGVzdCBtZQ=="),
+ "Failed to hash via transit"
+ );
+
+ assertEquals("7677af0ee4effaa9f35e9b1e82d182f79516ab8321786baa23002de7c06851059492dd37d5fc3791f17d81d4b58198d24a6fd8bbd62c42c1c30b371da500f193", transitResponse.getSum());
+
+ TransitResponse transitResponseBase64 = assertDoesNotThrow(
+ () -> connector.transitHash("sha2-256", "dGVzdCBtZQ==", "base64"),
+ "Failed to hash via transit with base64 output"
+ );
+
+ assertEquals("5DfYkW7cvGLkfy36cXhqmZcygEy9HpnFNB4WWXKOl1M=", transitResponseBase64.getSum());
+
+ transitResponseBase64 = assertDoesNotThrow(
+ () -> connector.transitHash("sha2-256", "test me".getBytes(UTF_8), "base64"),
+ "Failed to hash binary data via transit"
+ );
+
+ assertEquals("5DfYkW7cvGLkfy36cXhqmZcygEy9HpnFNB4WWXKOl1M=", transitResponseBase64.getSum());
+ }
+ }
+
@Nested
@DisplayName("Misc Tests")
class MiscTests {
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=="}
diff --git a/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/archive/_my-key b/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/archive/_my-key
new file mode 100644
index 0000000..d58d0fa
--- /dev/null
+++ b/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/archive/_my-key
@@ -0,0 +1 @@
+{"Value":"AAAAAQIwsBgPcs7Hxl7UnB8OkTq9+5HERx4Fzn8jAzR8uIhoQZfpNG/15m2LEEsDJw1o+lxc6u+h7XcOB1QRqrKz8k7CFR15A+qsxD0UclHZdnk+MmMwXL9SSvYugp7XPiXmpG/uTZVf1QtigXQuehEEJeFfJVI7aFACu2AHEGVt+5b6yDQEgTUruo0seazRXuVU+J6NFCDU3ZvkR8e0al6OcMail59KamzjSCYiqDF4TeUuOQ6Zyr7zIG7gSaas1sog2JIlvrh+wkHW6I+OyD9SJSTinGEGRXl0tq15qoBJ4srfXdWODmKtExEupArbiDR5PyvSI3KlIHKIFduslJZKkJwiV3dBscdva4Rqb98FffMVYuM4G4s+VpvDDX+WVsqWF1ssoHRAFWCNAJGsenVDTxblzAF/4rGkJ7LC9yjLGsBtMOCkCZAKDQ3C9VFu+LGhbMRA5p2RKxNKWGem4Cyp5AqMmx62UzDAgMLGgm89A0g9s1/3FnCoLPdVmlWc6cg4QahN30qJCInJeAmH7T9NwIjv6/QxyJxyDVtdMtcfnMNxx15Q29lbyWTcbaI1iabHpc16iS/gwAPQeBTSbcvc4OxqwC5PDFThFq6bwZXaekYz4ghC13j9Ht79GVKH9cPZb5M="}
diff --git a/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/policy/_my-key b/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/policy/_my-key
new file mode 100644
index 0000000..5a07db5
--- /dev/null
+++ b/src/test/resources/data_dir/logical/c49ee9eb-94d9-928c-c958-95d092130c30/policy/_my-key
@@ -0,0 +1 @@
+{"Value":"AAAAAQKfotDJ0SihB+f5i4PxZ6lq1kxtH4QMprGI7Hj8HimEwXsW0Gbj/1B7YM4DQt4t5JFD4gKwFVOwlJDyusaJj92ar0QLSreCWyJTKUadqZplMFyB/bAdK42QdH+ZS8Z9KHUNchbRpNhnvOFIahoDG8dZ+nbCIXblJONCaey4/ri3H+GQbk/jfre1VByh7zVIN0ISew5PzZRCTkbO1CvcQZhrRGoUGiPmLywKVbHEMsvimVuZY5py6OfL+70QmElBmN9O45bTgX9XPbfSmyQIcGrElO1foi0WwZLPsb/fZcAIgrhC768jOnzeimChoX4zc0DPxuyV1YPvsD1yAlsnsFuJW6CP7TGkszbJU+rwDpg0TgqKtvFr1Lgkxyfcxg0h++1BSiEgoJU7b2IgIWP7reJVjc1tbAsoR1tOBCSAAhvqWZVpn2oht5rfe9aN370bV3Jcu17hFWyhB+VhzbCCPRcofPXX3f2U2dcQ0X7bU4nMiq1v6NQP6u/D5GAPj4Jc0519tPW4KQrd9SNqR20ct6OvqxjMFWV4GZXVcsL4+3xup87Yib6EDb0+hhe6XEpC6isYgD3D5OTTOWphHlsglGkGFi9lUc+h8zNPM4FHwha6uVTLmaqaLGbLziwT9WXF1ATacwtNW0t3kZlFUBvMwSwWzoPqO1+jxs+id4ie/VI6ZTOeowi4ceK2eWJ1/t9MB/gjvadpgE+FYt5QG6dFav4ujQN5Ne/yY78PGF5tp0CT4koox0rMUuxyD1xOIXkm0NBpJm1y9/J06yqLpMKqS40/jGcSQaycRngXDb+6H9rj7mheiO4qxcFGqViqECCUjDG3PnLP/fy9py5kFq7mf0pq7L0Jq/lLWC+iKJF9UaZmCaz8DwlQ9zC03XOFqABPNe8gMFlb8zU09VKBbY+g5gukOonjcBeoFOTRqQxuaWwwwB2lj8XnZScOyIcVJGkH"}