Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>de.stklcode.jvault</groupId>
<artifactId>jvault-connector</artifactId>
<version>1.4.1-SNAPSHOT</version>
<version>1.5.0-SNAPSHOT</version>

<packaging>jar</packaging>

Expand Down
46 changes: 46 additions & 0 deletions src/main/java/de/stklcode/jvault/connector/HTTPVaultConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -646,6 +651,47 @@ public boolean deleteTokenRole(final String name) throws VaultConnectorException
return true;
}

@Override
public final TransitResponse transitEncrypt(final String keyName, final String plaintext)
throws VaultConnectorException {
requireAuth();

Map<String, Object> 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<String, Object> 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<String, Object> payload = mapOf(
"input", input,
"format", format
);

return request.post(PATH_TRANSIT_HASH + algorithm, payload, token, TransitResponse.class);
}

/**
* Check for required authorization.
*
Expand Down
81 changes: 77 additions & 4 deletions src/main/java/de/stklcode/jvault/connector/VaultConnector.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@
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.
Expand Down Expand Up @@ -674,6 +671,82 @@ default boolean createOrUpdateTokenRole(final TokenRole role) throws VaultConnec
*/
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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,75 @@ void tokenRolesTest() {
}
}

@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 {
Expand Down
Loading