diff --git a/build.gradle b/build.gradle index 2dc5c64f..f78d638a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,12 +5,12 @@ apply plugin: 'checkstyle' group 'com.bettercloud' archivesBaseName = 'vault-java-driver' -version '5.1.0' +version '5.3.0' ext.isReleaseVersion = !version.endsWith('SNAPSHOT') // This project is actually limited to Java 8 compatibility. See below. -sourceCompatibility = 9 -targetCompatibility = 9 +sourceCompatibility = 11 +targetCompatibility = 11 repositories { mavenCentral() diff --git a/src/main/java/com/bettercloud/vault/Vault.java b/src/main/java/com/bettercloud/vault/Vault.java index 479bb09a..93023c9c 100644 --- a/src/main/java/com/bettercloud/vault/Vault.java +++ b/src/main/java/com/bettercloud/vault/Vault.java @@ -8,6 +8,7 @@ import com.bettercloud.vault.api.database.Database; import com.bettercloud.vault.api.mounts.Mounts; import com.bettercloud.vault.api.pki.Pki; +import com.bettercloud.vault.api.transit.Transit; import com.bettercloud.vault.json.Json; import com.bettercloud.vault.json.JsonObject; import com.bettercloud.vault.json.JsonValue; @@ -198,6 +199,10 @@ public Pki pki(final String mountPath) { public Database database(final String mountPath) { return new Database(vaultConfig, mountPath); } + + public Transit transit() { return new Transit(vaultConfig); } + public Transit transit(final String mountPath) { return new Transit(vaultConfig, mountPath); } + /** * Returns the implementing class for Vault's lease operations (e.g. revoke, revoke-prefix). * diff --git a/src/main/java/com/bettercloud/vault/api/database/Database.java b/src/main/java/com/bettercloud/vault/api/database/Database.java index cf842ca2..e22e89d6 100644 --- a/src/main/java/com/bettercloud/vault/api/database/Database.java +++ b/src/main/java/com/bettercloud/vault/api/database/Database.java @@ -118,6 +118,74 @@ public DatabaseResponse createOrUpdateRole(final String roleName, final Database } } + /** + *
Operation to create or update an static role using the Database Secret engine.
+ * Relies on an authentication token being present in the VaultConfig
instance.
This version of the method accepts a DatabaseStaticRoleOptions
parameter, containing optional settings
+ * for the role creation operation. Example usage:
+ *+ * + * @param roleName A name for the role to be created or updated + * @param options Optional settings for the role to be created or updated (e.g. db_name, ttl, etc) + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public DatabaseResponse createOrUpdateStaticRole(final String roleName, final DatabaseStaticRoleOptions options) throws VaultException { + int retryCount = 0; + while (true) { + try { + final String requestJson = staticRoleOptionsToJson(options); + + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/static-roles/%s", config.getAddress(), this.mountPath, roleName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate restResponse + if (restResponse.getStatus() != 204) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new DatabaseResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + /** *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final DatabaseStaticRoleOptions options = new DatabaseStaticRoleOptions() + * .dbName("test") + * .rotationPeriod("9h"); + * final DatabaseResponse response = vault.database().createOrUpdateStaticRole("testRole", options); + * + * assertEquals(204, response.getRestResponse().getStatus()); + * }+ *
Operation to retrieve an role using the Database backend. Relies on an authentication token being present in
* the VaultConfig
instance.
Operation to generate a new set of credentials using the Database backend. + * + *
A successful operation will return a 204 HTTP status. A VaultException
will be thrown if
+ * the role does not exist, or if any other problem occurs. Credential information will be populated in the
+ * credential
field of the DatabaseResponse
return value. Example usage:
+ *+ * + * @param roleName The role for which to retrieve credentials + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public DatabaseResponse staticCreds(final String roleName) throws VaultException { + int retryCount = 0; + while (true) { + try { + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/static-creds/%s", config.getAddress(), this.mountPath, roleName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .get(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + String body = restResponse.getBody() != null ? new String(restResponse.getBody()) : "(no body)"; + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus() + " " + body, restResponse.getStatus()); + } + + return new DatabaseResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + private String roleOptionsToJson(final DatabaseRoleOptions options) { final JsonObject jsonObject = Json.object(); @@ -386,6 +516,19 @@ private String roleOptionsToJson(final DatabaseRoleOptions options) { return jsonObject.toString(); } + private String staticRoleOptionsToJson(final DatabaseStaticRoleOptions options) { + final JsonObject jsonObject = Json.object(); + + if (options != null) { + addJsonFieldIfNotNull(jsonObject, "db_name", options.getDbName()); + addJsonFieldIfNotNull(jsonObject, "username", options.getUsername()); + addJsonFieldIfNotNull(jsonObject, "rotation_period", options.getRotationPeriod()); + addJsonFieldIfNotNull(jsonObject, "rotation_statements", joinList(options.getRotationStatements())); + } + + return jsonObject.toString(); + } + private String joinList(List{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final DatabaseResponse response = vault.database().creds("testRole"); + * assertEquals(204, response.getRestResponse().getStatus(); + * }+ *
Operation to generate a new set of credentials or sign the embedded CSR, in the PKI backend. If CSR is passed the + * sign function of the vault will be called if not, issue will be used. + * The issuing CA certificate is returned as well, so that only the root CA need be in a + * client's trust store.
+ * + *A successful operation will return a 204 HTTP status. A VaultException
will be thrown if
+ * the role does not exist, or if any other problem occurs. Credential information will be populated in the
+ * credential
field of the PkiResponse
return value. Example usage:
+ *+ * + * @param roleName The role on which the credentials will be based. + * @param commonName The requested CN for the certificate. If the CN is allowed by role policy, it will be issued. + * @param altNames (optional) Requested Subject Alternative Names, in a comma-delimited list. These can be host names or email addresses; they will be parsed into their respective fields. If any requested names do not match role policy, the entire request will be denied. + * @param ipSans (optional) Requested IP Subject Alternative Names, in a comma-delimited list. Only valid if the role allows IP SANs (which is the default). + * @param ttl (optional) Requested Time To Live. Cannot be greater than the role's max_ttl value. If not provided, the role's ttl value will be used. Note that the role values default to system values if not explicitly set. + * @param format (optional) Format for returned data. Can be pem, der, or pem_bundle; defaults to pem. If der, the output is base64 encoded. If pem_bundle, the certificate field will contain the private key, certificate, and issuing CA, concatenated. + * @param privateKeyFormat (optional) Format for the returned private key. Generally the default will be controlled by the "format" parameter as either base64-encoded DER or PEM-encoded DER. However, this can be set to "pkcs8" to have the returned private key contain base64-encoded pkcs8 or PEM-encode pkcs8 instead. Defaults to "der". + * @param csr (optional) PEM Encoded CSR + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ public PkiResponse issue( final String roleName, final String commonName, @@ -416,6 +457,7 @@ public PkiResponse issue( final List{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final PkiResponse response = vault.pki().deleteRole("testRole"); + * assertEquals(204, response.getRestResponse().getStatus(); + * }+ *
Possible format options for private key issued by the PKI backend
+ * + *See: {@link Pki#issue(String, String, List, List, String, CredentialFormat, PrivateKeyFormat, String)}
+ */ +public enum PrivateKeyFormat { + DER, + PEM, + PKCS8; + + public static PrivateKeyFormat fromString(final String text) { + if (text != null) { + for (final PrivateKeyFormat format : PrivateKeyFormat.values()) { + if (text.equalsIgnoreCase(format.toString())) { + return format; + } + } + } + return null; + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/CryptData.java b/src/main/java/com/bettercloud/vault/api/transit/CryptData.java new file mode 100644 index 00000000..fe1c83c0 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/CryptData.java @@ -0,0 +1,37 @@ +package com.bettercloud.vault.api.transit; + +public class CryptData { + + private String ciphertext; + + private String plaintext; + + private Integer keyVersion; + + public String getCiphertext() { + return ciphertext; + } + + public String getPlaintext() { + return plaintext; + } + + public Integer getKeyVersion() { + return keyVersion; + } + + public CryptData ciphertext(String ciphertext){ + this.ciphertext = ciphertext; + return this; + } + + public CryptData plaintext(String plaintext){ + this.plaintext = plaintext; + return this; + } + + public CryptData keyVersion(Integer keyVersion){ + this.keyVersion = keyVersion; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/DataKeyOptions.java b/src/main/java/com/bettercloud/vault/api/transit/DataKeyOptions.java new file mode 100644 index 00000000..9cf5acdd --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/DataKeyOptions.java @@ -0,0 +1,41 @@ +package com.bettercloud.vault.api.transit; + +public class DataKeyOptions { + + private String context; + + + private String nonce; + + private Integer bits; + + + + + public String getContext() { + return context; + } + + public String getNonce() { + return nonce; + } + + public Integer getBits() { + return bits; + } + + public DataKeyOptions context(String context) { + this.context = context; + return this; + } + + public DataKeyOptions nonce(String nonce) { + this.nonce = nonce; + return this; + } + + public DataKeyOptions bits(int bits) { + this.bits = bits; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/DecryptOptions.java b/src/main/java/com/bettercloud/vault/api/transit/DecryptOptions.java new file mode 100644 index 00000000..3e176730 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/DecryptOptions.java @@ -0,0 +1,39 @@ +package com.bettercloud.vault.api.transit; + +public class DecryptOptions { + + private String ciphertext; + + private String context; + + private String nonce; + + + public String getCiphertext() { + return ciphertext; + } + + public String getContext() { + return context; + } + + + public String getNonce() { + return nonce; + } + + public DecryptOptions ciphertext(String ciphertext) { + this.ciphertext = ciphertext; + return this; + } + + public DecryptOptions context(String context) { + this.context = context; + return this; + } + + public DecryptOptions nonce(String nonce) { + this.nonce = nonce; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/EncryptOptions.java b/src/main/java/com/bettercloud/vault/api/transit/EncryptOptions.java new file mode 100644 index 00000000..deacd83d --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/EncryptOptions.java @@ -0,0 +1,49 @@ +package com.bettercloud.vault.api.transit; + +public class EncryptOptions { + + private byte[] plaintext; + + private String context; + + private Integer keyVersion; + + private String nonce; + + + public byte[] getPlaintext() { + return plaintext; + } + + public String getContext() { + return context; + } + + public Integer getKeyVersion() { + return keyVersion; + } + + public String getNonce() { + return nonce; + } + + public EncryptOptions plaintext(byte[] plaintext) { + this.plaintext = plaintext; + return this; + } + + public EncryptOptions context(String context) { + this.context = context; + return this; + } + + public EncryptOptions keyVersion(int keyVersion) { + this.keyVersion = keyVersion; + return this; + } + + public EncryptOptions nonce(String nonce) { + this.nonce = nonce; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/KeyOptions.java b/src/main/java/com/bettercloud/vault/api/transit/KeyOptions.java new file mode 100644 index 00000000..8526dd32 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/KeyOptions.java @@ -0,0 +1,136 @@ +package com.bettercloud.vault.api.transit; + +public class KeyOptions { + + private Boolean convergentEncryption; + + private Boolean derived; + + private Boolean exportable; + + private Boolean allowPlaintextBackup; + + private String type; + + private Integer autoRotatePeriod; + + //read only + private Boolean deletionAllowed; + + private String name; + + private Integer minDecryptionVersion; + + private Integer minEncryptionVersion; + + private Boolean supportsEncryption; + + private Boolean supportsDecryption; + + private Boolean supportsDerivation; + + private Boolean supportsSigning; + + public KeyOptions() { + } + + // from response + public KeyOptions(Boolean deletionAllowed, String name, Integer minDecryptionVersion, + Integer minEncryptionVersion, Boolean supportsEncryption, Boolean supportsDecryption, + Boolean supportsDerivation, Boolean supportsSigning) { + this.deletionAllowed = deletionAllowed; + this.name = name; + this.minDecryptionVersion = minDecryptionVersion; + this.minEncryptionVersion = minEncryptionVersion; + this.supportsEncryption = supportsEncryption; + this.supportsDecryption = supportsDecryption; + this.supportsDerivation = supportsDerivation; + this.supportsSigning = supportsSigning; + } + + public Boolean getConvergentEncryption() { + return convergentEncryption; + } + + public Boolean getDerived() { + return derived; + } + + public Boolean getExportable() { + return exportable; + } + + public Boolean getAllowPlaintextBackup() { + return allowPlaintextBackup; + } + + public String getType() { + return type; + } + + public Integer getAutoRotatePeriod() { + return autoRotatePeriod; + } + + public Boolean getDeletionAllowed() { + return deletionAllowed; + } + + public String getName() { + return name; + } + + public Integer getMinDecryptionVersion() { + return minDecryptionVersion; + } + + public Integer getMinEncryptionVersion() { + return minEncryptionVersion; + } + + public Boolean getSupportsEncryption() { + return supportsEncryption; + } + + public Boolean getSupportsDecryption() { + return supportsDecryption; + } + + public Boolean getSupportsDerivation() { + return supportsDerivation; + } + + public Boolean getSupportsSigning() { + return supportsSigning; + } + + public KeyOptions convergentEncryption(Boolean convergentEncryption) { + this.convergentEncryption = convergentEncryption; + return this; + } + + public KeyOptions derived(Boolean derived) { + this.derived = derived; + return this; + } + + public KeyOptions exportable(Boolean exportable) { + this.exportable = exportable; + return this; + } + + public KeyOptions allowPlaintextBackup(Boolean allowPlaintextBackup) { + this.allowPlaintextBackup = allowPlaintextBackup; + return this; + } + + public KeyOptions type(String type) { + this.type = type; + return this; + } + + public KeyOptions autoRotatePeriod(Integer autoRotatePeriod) { + this.autoRotatePeriod = autoRotatePeriod; + return this; + } +} diff --git a/src/main/java/com/bettercloud/vault/api/transit/Transit.java b/src/main/java/com/bettercloud/vault/api/transit/Transit.java new file mode 100644 index 00000000..89d65c2c --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/transit/Transit.java @@ -0,0 +1,528 @@ +package com.bettercloud.vault.api.transit; + +import com.bettercloud.vault.VaultConfig; +import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.database.DatabaseStaticRoleOptions; +import com.bettercloud.vault.json.Json; +import com.bettercloud.vault.json.JsonObject; +import com.bettercloud.vault.response.TransitResponse; +import com.bettercloud.vault.rest.Rest; +import com.bettercloud.vault.rest.RestResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +/** + *The implementing class for operations on Vault's database backend.
+ * + *This class is not intended to be constructed directly. Rather, it is meant to used by way of Vault
+ * in a DSL-style builder pattern. See the Javadoc comments of each public
method for usage examples.
/v1/transit
).
+ *
+ * @param config A container for the configuration settings needed to initialize a Vault
driver instance
+ */
+ public Transit(final VaultConfig config) {
+ this(config, "transit");
+ }
+
+ /**
+ * Constructor for use when the Transit backend is mounted on some non-default custom path (e.g. /v1/tr123
).
+ *
+ * @param config A container for the configuration settings needed to initialize a Vault
driver instance
+ * @param mountPath The path on which your Vault Transit backend is mounted, without the /v1/
prefix (e.g. "root-ca"
)
+ */
+ public Transit(final VaultConfig config, final String mountPath) {
+ this.config = config;
+ this.mountPath = mountPath;
+ if (this.config.getNameSpace() != null && !this.config.getNameSpace().isEmpty()) {
+ this.nameSpace = this.config.getNameSpace();
+ }
+ }
+ public TransitResponse createKey(String keyName) throws VaultException {
+ return createKey(keyName, null);
+ }
+ /**
+ * Operation to create an key using the Transit backend. Relies on an authentication token being present in
+ * the VaultConfig
instance.
This version of the method accepts a KeyOptions
parameter, containing optional settings
+ * for the key creation operation. Example usage:
+ *+ * + * @param keyName A name for the key to be created + * @param options Optional settings for the key to be created + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse createKey(String keyName, KeyOptions options) throws VaultException { + int retryCount = 0; + while (true) { + try { + final String requestJson = keyOptionsToJson(options); + + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/keys/%s", config.getAddress(), this.mountPath, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate restResponse + if (restResponse.getStatus() != 204) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final KeyOptions options = new KeyOptions() + * .type("aes128-gcm96") + * .exportable(true); + * final TransitResponse response = vault.transit().createKey("testKey", options); + * + * assertEquals(204, response.getRestResponse().getStatus()); + * }+ *
Operation to retrieve an key using the Transit backend. Relies on an authentication token being present in
+ * the VaultConfig
instance.
The key information will be populated in the keyOptions
field of the TransitResponse
+ * return value. Example usage:
+ *+ * + * @param keyName The name of the key to retrieve + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse getKey(String keyName) throws VaultException { + int retryCount = 0; + while (true) { + // Make an HTTP request to Vault + try { + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/keys/%s", config.getAddress(), this.mountPath, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .get(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * final TransitResponse response = vault.pki().getRole("testRole"); + * + * final KeyOptions details = response.getKeyOptions(); + * }+ *
Operation to delete an key using the Transit backend. Relies on an authentication token being present in
+ * the VaultConfig
instance.
A successful operation will return a 204 HTTP status. A VaultException
will be thrown if
+ * the role does not exist, or if any other problem occurs. Example usage:
+ *+ * + * @param keyName The name of the key to delete + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse deleteKey(String keyName) throws VaultException { + int retryCount = 0; + while (true) { + // Make an HTTP request to Vault + try { + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/keys/%s", config.getAddress(), this.mountPath, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .delete(); + + // Validate response + if (restResponse.getStatus() != 204) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final TransitResponse response = vault.transit().deleteKey("testKey"); + * assertEquals(204, response.getRestResponse().getStatus(); + * }+ *
Operation to encrypt data using the Transit Secret engine.
+ * Relies on an authentication token being present in the VaultConfig
instance.
This version of the method accepts a EncryptOptions
parameter, containing optional settings
+ * for the encrypt data operation. Example usage:
+ *+ * + * @param keyName A name for the encrypt key to be used + * @param options Data and params to encrypt data + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse encryptData(final String keyName, final EncryptOptions options) throws VaultException { + int retryCount = 0; + while (true) { + // Make an HTTP request to Vault + try { + final String requestJson = encryptOptionsToJson(options); + + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/encrypt/%s", config.getAddress(), this.mountPath, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final TransitEncryptOptions options = new EncryptOptions() + * .plaintext("test"getBytes()); + * final TransitResponse response = vault.transit().encryptData("encryptKey1", options); + * + * assertEquals(204, response.getRestResponse().getStatus()); + * }+ *
Operation to decrypt data using the Transit Secret engine.
+ * Relies on an authentication token being present in the VaultConfig
instance.
This version of the method accepts a DecryptOptions
parameter, containing optional settings
+ * for the encrypt data operation. Example usage:
+ *+ * + * @param keyName A name for the encrypt key to be used + * @param options Data and params to encrypt data + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse decryptData(final String keyName, final DecryptOptions options) throws VaultException { + int retryCount = 0; + while (true) { + // Make an HTTP request to Vault + try { + final String requestJson = decryptOptionsToJson(options); + + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/decrypt/%s", config.getAddress(), this.mountPath, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + public TransitResponse dataKey(final String type, final String keyName) throws VaultException { + return dataKey(type, keyName, null); + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final TransitEncryptOptions options = new DecryptOptions() + * .ciphertext("test"getBytes()); + * final TransitResponse response = vault.transit().decryptData("encryptKey1", options); + * + * assertEquals(204, response.getRestResponse().getStatus()); + * }+ *
Operation to encrypt data using the Transit Secret engine.
+ * Relies on an authentication token being present in the VaultConfig
instance.
This version of the method accepts a EncryptOptions
parameter, containing optional settings
+ * for the encrypt data operation. Example usage:
+ *+ * + * @param type A type for response (plaintext, wrapped) + * @param keyName A name for the encrypt key to be used + * @param options Data and params to encrypt data + * @return A container for the information returned by Vault + * @throws VaultException If any error occurs or unexpected response is received from Vault + */ + public TransitResponse dataKey(final String type, final String keyName, final DataKeyOptions options) throws VaultException { + int retryCount = 0; + while (true) { + // Make an HTTP request to Vault + try { + final String requestJson = dataKeyOptionsToJson(options); + + final RestResponse restResponse = new Rest()//NOPMD + .url(String.format("%s/v1/%s/datakey/%s/%s", config.getAddress(), this.mountPath, type, keyName)) + .header("X-Vault-Token", config.getToken()) + .header("X-Vault-Namespace", this.nameSpace) + .body(requestJson.getBytes(StandardCharsets.UTF_8)) + .connectTimeoutSeconds(config.getOpenTimeout()) + .readTimeoutSeconds(config.getReadTimeout()) + .sslVerification(config.getSslConfig().isVerify()) + .sslContext(config.getSslConfig().getSslContext()) + .post(); + + // Validate response + if (restResponse.getStatus() != 200 && restResponse.getStatus() != 404) { + throw new VaultException("Vault responded with HTTP status code: " + restResponse.getStatus(), restResponse.getStatus()); + } + return new TransitResponse(restResponse, retryCount); + } catch (Exception e) { + // If there are retries to perform, then pause for the configured interval and then execute the loop again... + if (retryCount < config.getMaxRetries()) { + retryCount++; + try { + final int retryIntervalMilliseconds = config.getRetryIntervalMilliseconds(); + Thread.sleep(retryIntervalMilliseconds); + } catch (InterruptedException e1) { + e1.printStackTrace(); + } + } else if (e instanceof VaultException) { + // ... otherwise, give up. + throw (VaultException) e; + } else { + throw new VaultException(e); + } + } + } + } + + private String keyOptionsToJson(KeyOptions options){ + final JsonObject jsonObject = Json.object(); + + if (options != null) { + addJsonFieldIfNotNull(jsonObject, "convergent_encryption", options.getConvergentEncryption()); + addJsonFieldIfNotNull(jsonObject, "derived", options.getDerived()); + addJsonFieldIfNotNull(jsonObject, "exportable", options.getExportable()); + addJsonFieldIfNotNull(jsonObject, "allow_plaintext_backup", options.getAllowPlaintextBackup()); + addJsonFieldIfNotNull(jsonObject, "type", options.getType()); + addJsonFieldIfNotNull(jsonObject, "auto_rotate_period", options.getAutoRotatePeriod()); + } + return jsonObject.toString(); + } + + private String encryptOptionsToJson(final EncryptOptions options) { + final JsonObject jsonObject = Json.object(); + + if (options != null) { + addJsonFieldIfNotNull(jsonObject, "plaintext", Base64.getEncoder().encodeToString(options.getPlaintext())); + addJsonFieldIfNotNull(jsonObject, "context", options.getContext()); + addJsonFieldIfNotNull(jsonObject, "key_version", options.getKeyVersion()); + addJsonFieldIfNotNull(jsonObject, "nonce", options.getNonce()); + } + + return jsonObject.toString(); + } + + private String decryptOptionsToJson(final DecryptOptions options) { + final JsonObject jsonObject = Json.object(); + + if (options != null) { + addJsonFieldIfNotNull(jsonObject, "ciphertext", options.getCiphertext()); + addJsonFieldIfNotNull(jsonObject, "context", options.getContext()); + addJsonFieldIfNotNull(jsonObject, "nonce", options.getNonce()); + } + + return jsonObject.toString(); + } + + private String dataKeyOptionsToJson(final DataKeyOptions options) { + final JsonObject jsonObject = Json.object(); + + if (options != null) { + addJsonFieldIfNotNull(jsonObject, "context", options.getContext()); + addJsonFieldIfNotNull(jsonObject, "nonce", options.getNonce()); + addJsonFieldIfNotNull(jsonObject, "bits", options.getBits()); + } + + return jsonObject.toString(); + } + + private JsonObject addJsonFieldIfNotNull(final JsonObject jsonObject, final String name, final Object value) { + if (value == null) { + return jsonObject; + } + if (value instanceof String) { + jsonObject.add(name, (String) value); + } else if (value instanceof Boolean) { + jsonObject.add(name, (Boolean) value); + } else if (value instanceof Long) { + jsonObject.add(name, (Long) value); + } else if (value instanceof Integer) { + jsonObject.add(name, (Integer) value); + } else if (value instanceof byte[]){ + jsonObject.add(name, new String((byte[]) value)); + } + + return jsonObject; + } + + + +} diff --git a/src/main/java/com/bettercloud/vault/response/TransitResponse.java b/src/main/java/com/bettercloud/vault/response/TransitResponse.java new file mode 100644 index 00000000..ec8857da --- /dev/null +++ b/src/main/java/com/bettercloud/vault/response/TransitResponse.java @@ -0,0 +1,138 @@ +package com.bettercloud.vault.response; + +import com.bettercloud.vault.api.Logical.logicalOperations; +import com.bettercloud.vault.api.transit.CryptData; +import com.bettercloud.vault.api.transit.KeyOptions; +import com.bettercloud.vault.rest.RestResponse; +import java.util.Map; + +public class TransitResponse extends LogicalResponse { + + private KeyOptions keyOptions; + + private CryptData cryptData; + + + /** + * @param restResponse The raw HTTP response from Vault. + * @param retries The number of retry attempts that occurred during the API call (can be zero). + */ + public TransitResponse(RestResponse restResponse, int retries) { + this(restResponse, retries, logicalOperations.authentication); + } + + /** + * @param restResponse The raw HTTP response from Vault. + * @param retries The number of retry attempts that occurred during the API call (can be zero). + * @param operation The operation requested. + */ + public TransitResponse(RestResponse restResponse, int retries, logicalOperations operation) { + super(restResponse, retries, operation); + keyOptions = buildKeyOptionsFromData(this.getData()); + cryptData = buildEncryptDataFromData(this.getData()); + } + + public KeyOptions getKeyOptions() { + return keyOptions; + } + + public CryptData getCryptData() { + return cryptData; + } + + /** + *{@code + * final VaultConfig config = new VaultConfig.address(...).token(...).build(); + * final Vault vault = new Vault(config); + * + * final TransitEncryptOptions options = new EncryptOptions() + * .plaintext("test"getBytes()); + * final TransitResponse response = vault.transit().encryptData("encryptKey1", options); + * + * assertEquals(204, response.getRestResponse().getStatus()); + * }+ *
Generates a KeyOptions
object from the response data returned by Transit
+ * backend REST calls, for those calls which do return role data
+ * (e.g. getKey(String keyName)
).
If the response data does not contain key information, then this method will
+ * return null
.
"data"
object from a Vault JSON response, converted into Java key-value pairs.
+ * @return A container for role options
+ */
+ private KeyOptions buildKeyOptionsFromData(final MapGenerates a EncryptData
object from the response data returned by Transit
+ * backend REST calls, for those calls which do return role data
+ * (e.g. encryptData(String keyName, EncryptOptions options)
).
"data"
object from a Vault JSON response, converted into Java key-value pairs.
+ * @return A container for role options
+ */
+ private CryptData buildEncryptDataFromData(final MapUsed to determine whether a String value contains a "true" or "false" value. The problem
+ * with Boolean.parseBoolean()
is that it swallows null values and returns them
+ * as false
rather than null
.
null
+ * @return A true or false value if the input can be parsed as such, or else null
.
+ */
+ private Boolean parseBoolean(final String input) {
+ if (input == null) {
+ return null;
+ } else {
+ return Boolean.parseBoolean(input);
+ }
+ }
+
+ private Integer parseInt(final String input) {
+ if (input == null) {
+ return null;
+ } else {
+ return Integer.parseInt(input);
+ }
+ }
+
+}
diff --git a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java
index d9d41c60..6ef457a1 100644
--- a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java
+++ b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendDatabaseTests.java
@@ -3,6 +3,7 @@
import com.bettercloud.vault.Vault;
import com.bettercloud.vault.VaultException;
import com.bettercloud.vault.api.database.DatabaseRoleOptions;
+import com.bettercloud.vault.api.database.DatabaseStaticRoleOptions;
import com.bettercloud.vault.response.DatabaseResponse;
import com.bettercloud.vault.util.DbContainer;
import com.bettercloud.vault.util.VaultContainer;
@@ -97,6 +98,22 @@ public void testGetCredentials() throws VaultException {
assertTrue(credsResponse.getCredential().getUsername().contains("new-role"));
}
+ @Test
+ public void testStaticCredentials() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ List/v1/transit/*
REST endpoints.
+ */
+public class AuthBackendTransitTests {
+
+ @ClassRule
+ public static final VaultContainer container = new VaultContainer();
+
+ @BeforeClass
+ public static void setupClass() throws IOException, InterruptedException {
+ container.initAndUnsealVault();
+ container.setupBackendTransit();
+ }
+
+ // @Before
+ public void setup() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ final TransitResponse defaultResponse = vault.transit().deleteKey("testKey");
+ final RestResponse defaultRestResponse = defaultResponse.getRestResponse();
+ assertEquals(204, defaultRestResponse.getStatus());
+
+ final TransitResponse customResponse = vault.transit("other-transit").deleteKey("testKey");
+ final RestResponse customRestResponse = customResponse.getRestResponse();
+ assertEquals(204, customRestResponse.getStatus());
+ }
+
+ @Test
+ public void testCreateKey_Defaults() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ vault.transit().createKey("testKey");
+ final TransitResponse response = vault.transit().getKey("testKey");
+ assertTrue(compareKeyOptions(new KeyOptions(), response.getKeyOptions()));
+ }
+
+ @Test
+ public void testCreateRole_WithOptions() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ final KeyOptions options = new KeyOptions().type("rsa-4096");
+ vault.transit().createKey("testKey", options);
+ final TransitResponse response = vault.transit().getKey("testKey");
+ assertTrue(compareKeyOptions(options, response.getKeyOptions()));
+ }
+
+ @Test
+ public void encryptDecryptTest() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ testCreateKey_Defaults();
+
+ EncryptOptions options = new EncryptOptions().plaintext("123456789".getBytes());
+ final TransitResponse encryptResponse = vault.transit().encryptData("testKey", options);
+ DecryptOptions decryptOptions = new DecryptOptions().ciphertext(encryptResponse.getCryptData().getCiphertext());
+ final TransitResponse decryptResponse = vault.transit().decryptData("testKey", decryptOptions);
+ assertTrue(Arrays.equals(options.getPlaintext(),
+ Base64.getDecoder().decode(decryptResponse.getCryptData().getPlaintext())));
+ }
+
+ @Test
+ public void dataKeyTest() throws VaultException {
+ final Vault vault = container.getRootVault();
+
+ testCreateKey_Defaults();
+
+ final TransitResponse encryptResponse = vault.transit().dataKey("plaintext", "testKey");
+ DecryptOptions decryptOptions = new DecryptOptions().ciphertext(encryptResponse.getCryptData().getCiphertext());
+ final TransitResponse decryptResponse = vault.transit().decryptData("testKey", decryptOptions);
+ assertTrue(Arrays.equals(Base64.getDecoder().decode(encryptResponse.getCryptData().getPlaintext()),
+ Base64.getDecoder().decode(decryptResponse.getCryptData().getPlaintext())));
+ }
+
+// @Test
+// public void testDeleteKey() throws VaultException {
+// final Vault vault = container.getRootVault();
+//
+// testCreateKey_Defaults();
+// final TransitResponse deleteResponse = vault.transit().deleteKey("testKey");
+// assertEquals(204, deleteResponse.getRestResponse().getStatus());
+// final TransitResponse getResponse = vault.transit().getKey("testKey");
+// assertEquals(404, getResponse.getRestResponse().getStatus());
+// }
+
+ // @Test
+// public void testIssueCredential() throws VaultException, InterruptedException {
+// final Vault vault = container.getRootVault();
+//
+// // Create a role
+// final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole",
+// new RoleOptions()
+// .allowedDomains(new ArrayList