Skip to content

Commit 0127cf3

Browse files
committed
feat: introduce methods for transit API interaction (#89)
Support hashing and de-/encryption using Vault's transit API.
1 parent 90f8bb7 commit 0127cf3

File tree

10 files changed

+426
-6
lines changed

10 files changed

+426
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* `read...Credentials()` methods for specific database mounts (#92)
55

66
### Features
7+
* Support Vault transit API (#89)
78
* Support PEM certificate string from `VAULT_CACERT` environment variable (#93)
89

910
### Dependencies

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
<groupId>de.stklcode.jvault</groupId>
66
<artifactId>jvault-connector</artifactId>
7-
<version>1.4.1-SNAPSHOT</version>
7+
<version>1.5.0-SNAPSHOT</version>
88

99
<packaging>jar</packaging>
1010

src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,11 @@ public class HTTPVaultConnector implements VaultConnector {
6868
private static final String PATH_UNDELETE = "/undelete/";
6969
private static final String PATH_DESTROY = "/destroy/";
7070

71+
private static final String PATH_TRANSIT = "transit";
72+
private static final String PATH_TRANSIT_ENCRYPT = PATH_TRANSIT + "/encrypt/";
73+
private static final String PATH_TRANSIT_DECRYPT = PATH_TRANSIT + "/decrypt/";
74+
private static final String PATH_TRANSIT_HASH = PATH_TRANSIT + "/hash/";
75+
7176
private final RequestHelper request;
7277

7378
private boolean authorized = false; // Authorization status.
@@ -646,6 +651,47 @@ public boolean deleteTokenRole(final String name) throws VaultConnectorException
646651
return true;
647652
}
648653

654+
@Override
655+
public final TransitResponse transitEncrypt(final String keyName, final String plaintext)
656+
throws VaultConnectorException {
657+
requireAuth();
658+
659+
Map<String, Object> payload = mapOf(
660+
"plaintext", plaintext
661+
);
662+
663+
return request.post(PATH_TRANSIT_ENCRYPT + keyName, payload, token, TransitResponse.class);
664+
}
665+
666+
@Override
667+
public final TransitResponse transitDecrypt(final String keyName, final String ciphertext)
668+
throws VaultConnectorException {
669+
requireAuth();
670+
671+
Map<String, Object> payload = mapOf(
672+
"ciphertext", ciphertext
673+
);
674+
675+
return request.post(PATH_TRANSIT_DECRYPT + keyName, payload, token, TransitResponse.class);
676+
}
677+
678+
@Override
679+
public final TransitResponse transitHash(final String algorithm, final String input, final String format)
680+
throws VaultConnectorException {
681+
if (format != null && !"hex".equals(format) && !"base64".equals(format)) {
682+
throw new IllegalArgumentException("Unsupported format " + format);
683+
}
684+
685+
requireAuth();
686+
687+
Map<String, Object> payload = mapOf(
688+
"input", input,
689+
"format", format
690+
);
691+
692+
return request.post(PATH_TRANSIT_HASH + algorithm, payload, token, TransitResponse.class);
693+
}
694+
649695
/**
650696
* Check for required authorization.
651697
*

src/main/java/de/stklcode/jvault/connector/VaultConnector.java

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@
2121
import de.stklcode.jvault.connector.model.response.*;
2222

2323
import java.io.Serializable;
24-
import java.util.ArrayList;
25-
import java.util.Collections;
26-
import java.util.List;
27-
import java.util.Map;
24+
import java.util.*;
2825

2926
/**
3027
* Vault Connector interface.
@@ -674,6 +671,82 @@ default boolean createOrUpdateTokenRole(final TokenRole role) throws VaultConnec
674671
*/
675672
boolean deleteTokenRole(final String name) throws VaultConnectorException;
676673

674+
/**
675+
* Encrypt plaintext via transit engine from Vault.
676+
*
677+
* @param keyName Transit key name
678+
* @param plaintext Text to encrypt (Base64 encoded)
679+
* @return Transit response
680+
* @throws VaultConnectorException on error
681+
* @since 1.5.0
682+
*/
683+
TransitResponse transitEncrypt(final String keyName, final String plaintext) throws VaultConnectorException;
684+
685+
/**
686+
* Encrypt plaintext via transit engine from Vault.
687+
*
688+
* @param keyName Transit key name
689+
* @param plaintext Binary data to encrypt
690+
* @return Transit response
691+
* @throws VaultConnectorException on error
692+
* @since 1.5.0
693+
*/
694+
default TransitResponse transitEncrypt(final String keyName, final byte[] plaintext)
695+
throws VaultConnectorException {
696+
return transitEncrypt(keyName, Base64.getEncoder().encodeToString(plaintext));
697+
}
698+
699+
/**
700+
* Decrypt ciphertext via transit engine from Vault.
701+
*
702+
* @param keyName Transit key name
703+
* @param ciphertext Text to decrypt
704+
* @return Transit response
705+
* @throws VaultConnectorException on error
706+
* @since 1.5.0
707+
*/
708+
TransitResponse transitDecrypt(final String keyName, final String ciphertext) throws VaultConnectorException;
709+
710+
/**
711+
* Hash data in hex format via transit engine from Vault.
712+
*
713+
* @param algorithm Specifies the hash algorithm to use
714+
* @param input Data to hash
715+
* @return Transit response
716+
* @throws VaultConnectorException on error
717+
* @since 1.5.0
718+
*/
719+
default TransitResponse transitHash(final String algorithm, final String input) throws VaultConnectorException {
720+
return transitHash(algorithm, input, "hex");
721+
}
722+
723+
/**
724+
* Hash data via transit engine from Vault.
725+
*
726+
* @param algorithm Specifies the hash algorithm to use
727+
* @param input Data to hash (Base64 encoded)
728+
* @param format Specifies the output encoding (hex/base64)
729+
* @return Transit response
730+
* @throws VaultConnectorException on error
731+
* @since 1.5.0
732+
*/
733+
TransitResponse transitHash(final String algorithm, final String input, final String format)
734+
throws VaultConnectorException;
735+
736+
/**
737+
* Hash data via transit engine from Vault.
738+
*
739+
* @param algorithm Specifies the hash algorithm to use
740+
* @param input Data to hash
741+
* @return Transit response
742+
* @throws VaultConnectorException on error
743+
* @since 1.5.0
744+
*/
745+
default TransitResponse transitHash(final String algorithm, final byte[] input, final String format)
746+
throws VaultConnectorException {
747+
return transitHash(algorithm, Base64.getEncoder().encodeToString(input), format);
748+
}
749+
677750
/**
678751
* Read credentials for MySQL backend at default mount point.
679752
*
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2016-2025 Stefan Kalscheuer
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package de.stklcode.jvault.connector.model.response;
18+
19+
import java.util.Map;
20+
import java.util.Objects;
21+
22+
import com.fasterxml.jackson.annotation.JsonSetter;
23+
24+
/**
25+
* Response entity for transit operations.
26+
*
27+
* @author Stefan Kalscheuer
28+
* @since 1.5.0
29+
*/
30+
public class TransitResponse extends VaultDataResponse {
31+
32+
private static final long serialVersionUID = 6873804240772242771L;
33+
34+
private String ciphertext;
35+
private String plaintext;
36+
private String sum;
37+
38+
@JsonSetter("data")
39+
private void setData(Map<String, String> data) {
40+
ciphertext = data.get("ciphertext");
41+
plaintext = data.get("plaintext");
42+
sum = data.get("sum");
43+
}
44+
45+
/**
46+
* Get ciphertext.
47+
* Populated after encryption.
48+
*
49+
* @return Ciphertext
50+
*/
51+
public String getCiphertext() {
52+
return ciphertext;
53+
}
54+
55+
/**
56+
* Get plaintext.
57+
* Base64 encoded, populated after decryption.
58+
*
59+
* @return Plaintext
60+
*/
61+
public String getPlaintext() {
62+
return plaintext;
63+
}
64+
65+
/**
66+
* Get hash sum.
67+
* Hex or Base64 string. Populated after hashing.
68+
*
69+
* @return Hash sum
70+
*/
71+
public String getSum() {
72+
return sum;
73+
}
74+
75+
@Override
76+
public boolean equals(Object o) {
77+
if (this == o) {
78+
return true;
79+
} else if (o == null || getClass() != o.getClass() || !super.equals(o)) {
80+
return false;
81+
}
82+
TransitResponse that = (TransitResponse) o;
83+
return Objects.equals(ciphertext, that.ciphertext) &&
84+
Objects.equals(plaintext, that.plaintext) &&
85+
Objects.equals(sum, that.sum);
86+
}
87+
88+
@Override
89+
public int hashCode() {
90+
return Objects.hash(super.hashCode(), ciphertext, plaintext, sum);
91+
}
92+
}

src/test/java/de/stklcode/jvault/connector/HTTPVaultConnectorIT.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,75 @@ void tokenRolesTest() {
989989
}
990990
}
991991

992+
@Nested
993+
@DisplayName("Transit Tests")
994+
class TransitTests {
995+
996+
@Test
997+
@DisplayName("Transit encryption")
998+
void transitEncryptTest() {
999+
assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT));
1000+
assumeTrue(connector.isAuthorized());
1001+
1002+
TransitResponse transitResponse = assertDoesNotThrow(
1003+
() -> connector.transitEncrypt("my-key", "dGVzdCBtZQ=="),
1004+
"Failed to encrypt via transit"
1005+
);
1006+
assertNotNull(transitResponse.getCiphertext());
1007+
assertTrue(transitResponse.getCiphertext().startsWith("vault:v1:"));
1008+
1009+
transitResponse = assertDoesNotThrow(
1010+
() -> connector.transitEncrypt("my-key", "test me".getBytes(UTF_8)),
1011+
"Failed to encrypt binary data via transit"
1012+
);
1013+
assertNotNull(transitResponse.getCiphertext());
1014+
assertTrue(transitResponse.getCiphertext().startsWith("vault:v1:"));
1015+
1016+
}
1017+
1018+
@Test
1019+
@DisplayName("Transit decryption")
1020+
void transitDecryptTest() {
1021+
assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT));
1022+
assumeTrue(connector.isAuthorized());
1023+
1024+
TransitResponse transitResponse = assertDoesNotThrow(
1025+
() -> connector.transitDecrypt("my-key", "vault:v1:1mhLVkBAR2nrFtIkJF/qg57DWfRj0FWgR6tvkGO8XOnL6sw="),
1026+
"Failed to decrypt via transit"
1027+
);
1028+
1029+
assertEquals("dGVzdCBtZQ==", transitResponse.getPlaintext());
1030+
}
1031+
1032+
@Test
1033+
@DisplayName("Transit hash")
1034+
void transitHashText() {
1035+
assertDoesNotThrow(() -> connector.authToken(TOKEN_ROOT));
1036+
assumeTrue(connector.isAuthorized());
1037+
1038+
TransitResponse transitResponse = assertDoesNotThrow(
1039+
() -> connector.transitHash("sha2-512", "dGVzdCBtZQ=="),
1040+
"Failed to hash via transit"
1041+
);
1042+
1043+
assertEquals("7677af0ee4effaa9f35e9b1e82d182f79516ab8321786baa23002de7c06851059492dd37d5fc3791f17d81d4b58198d24a6fd8bbd62c42c1c30b371da500f193", transitResponse.getSum());
1044+
1045+
TransitResponse transitResponseBase64 = assertDoesNotThrow(
1046+
() -> connector.transitHash("sha2-256", "dGVzdCBtZQ==", "base64"),
1047+
"Failed to hash via transit with base64 output"
1048+
);
1049+
1050+
assertEquals("5DfYkW7cvGLkfy36cXhqmZcygEy9HpnFNB4WWXKOl1M=", transitResponseBase64.getSum());
1051+
1052+
transitResponseBase64 = assertDoesNotThrow(
1053+
() -> connector.transitHash("sha2-256", "test me".getBytes(UTF_8), "base64"),
1054+
"Failed to hash binary data via transit"
1055+
);
1056+
1057+
assertEquals("5DfYkW7cvGLkfy36cXhqmZcygEy9HpnFNB4WWXKOl1M=", transitResponseBase64.getSum());
1058+
}
1059+
}
1060+
9921061
@Nested
9931062
@DisplayName("Misc Tests")
9941063
class MiscTests {

0 commit comments

Comments
 (0)