From 0c16142a1c51aa31abd25724076be84fccaf90c2 Mon Sep 17 00:00:00 2001 From: aanghel Date: Tue, 7 Sep 2021 17:38:56 +0300 Subject: [PATCH 1/3] add support for privateKeyFormat in pki --- .../com/bettercloud/vault/api/pki/Pki.java | 47 ++++++++++++++++++- .../vault/api/pki/PrivateKeyFormat.java | 30 ++++++++++++ .../vault/api/pki/PrivateKeyFormatTests.java | 22 +++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java create mode 100644 src/test/java/com/bettercloud/vault/api/pki/PrivateKeyFormatTests.java diff --git a/src/main/java/com/bettercloud/vault/api/pki/Pki.java b/src/main/java/com/bettercloud/vault/api/pki/Pki.java index 1074c2be..82c0315d 100644 --- a/src/main/java/com/bettercloud/vault/api/pki/Pki.java +++ b/src/main/java/com/bettercloud/vault/api/pki/Pki.java @@ -407,8 +407,49 @@ public PkiResponse issue( * @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, + final List altNames, + final List ipSans, + final String ttl, + final CredentialFormat format, + final String csr + ) throws VaultException { + return issue(roleName, commonName, altNames, ipSans, ttl, format, null, csr); + } - + /** + *

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:

+ * + *
+ *
{@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();
+     * }
+ *
+ * + * @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 ipSans, final String ttl, final CredentialFormat format, + final PrivateKeyFormat privateKeyFormat, final String csr ) throws VaultException { int retryCount = 0; @@ -451,6 +493,9 @@ public PkiResponse issue( if (format != null) { jsonObject.add("format", format.toString()); } + if (privateKeyFormat != null) { + jsonObject.add("private_key_format", privateKeyFormat.toString()); + } if (csr != null) { jsonObject.add("csr", csr); } diff --git a/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java new file mode 100644 index 00000000..61598c30 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java @@ -0,0 +1,30 @@ +package com.bettercloud.vault.api.pki; + +import java.util.List; + +/** + *

Possible format options for private key issued by the PKI backend

+ * + *

See: {@link Pki#issue(String, String, List, List, String, PrivateKeyFormat)}

+ */ +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/test/java/com/bettercloud/vault/api/pki/PrivateKeyFormatTests.java b/src/test/java/com/bettercloud/vault/api/pki/PrivateKeyFormatTests.java new file mode 100644 index 00000000..25af9969 --- /dev/null +++ b/src/test/java/com/bettercloud/vault/api/pki/PrivateKeyFormatTests.java @@ -0,0 +1,22 @@ +package com.bettercloud.vault.api.pki; + +import org.junit.Assert; +import org.junit.Test; + +public class PrivateKeyFormatTests { + + @Test + public void CredentialEnumTest() { + Assert.assertEquals(String.valueOf(PrivateKeyFormat.DER), ("der")); + Assert.assertEquals(String.valueOf(PrivateKeyFormat.PEM), ("pem")); + Assert.assertEquals(String.valueOf(PrivateKeyFormat.PKCS8), ("pkcs8")); + } + + @Test + public void CredentialFromStringTests() { + Assert.assertNull(PrivateKeyFormat.fromString(null)); + Assert.assertNotNull(PrivateKeyFormat.fromString(PrivateKeyFormat.DER.toString())); + Assert.assertNotNull(PrivateKeyFormat.fromString(PrivateKeyFormat.PEM.toString())); + Assert.assertNotNull(PrivateKeyFormat.fromString(PrivateKeyFormat.PKCS8.toString())); + } +} From 52b3bca32132661e2d8a18ada91f2c706152fb3a Mon Sep 17 00:00:00 2001 From: aanghel Date: Tue, 7 Sep 2021 17:47:43 +0300 Subject: [PATCH 2/3] fix java doc --- .../java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java index 61598c30..a6f5c226 100644 --- a/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java +++ b/src/main/java/com/bettercloud/vault/api/pki/PrivateKeyFormat.java @@ -5,7 +5,7 @@ /** *

Possible format options for private key issued by the PKI backend

* - *

See: {@link Pki#issue(String, String, List, List, String, PrivateKeyFormat)}

+ *

See: {@link Pki#issue(String, String, List, List, String, CredentialFormat, PrivateKeyFormat, String)}

*/ public enum PrivateKeyFormat { DER, From fd963d2c3ee34f3d659e26bcbaa9130e1e767fd7 Mon Sep 17 00:00:00 2001 From: aanghel Date: Tue, 28 Jun 2022 16:08:46 +0300 Subject: [PATCH 3/3] add support for transit secret engine --- build.gradle | 6 +- .../java/com/bettercloud/vault/Vault.java | 5 + .../vault/api/database/Database.java | 143 +++++ .../database/DatabaseStaticRoleOptions.java | 78 +++ .../vault/api/transit/CryptData.java | 37 ++ .../vault/api/transit/DataKeyOptions.java | 41 ++ .../vault/api/transit/DecryptOptions.java | 39 ++ .../vault/api/transit/EncryptOptions.java | 49 ++ .../vault/api/transit/KeyOptions.java | 136 +++++ .../vault/api/transit/Transit.java | 528 ++++++++++++++++++ .../vault/response/TransitResponse.java | 138 +++++ .../vault/api/AuthBackendDatabaseTests.java | 17 + .../vault/api/AuthBackendTokenTests.java | 1 - .../vault/api/AuthBackendTransitTests.java | 252 +++++++++ .../vault/util/VaultContainer.java | 16 +- src/test-integration/resources/startup.sh | 6 +- .../vault/vault/api/TransitApiTest.java | 6 +- 17 files changed, 1486 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/bettercloud/vault/api/database/DatabaseStaticRoleOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/CryptData.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/DataKeyOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/DecryptOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/EncryptOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/KeyOptions.java create mode 100644 src/main/java/com/bettercloud/vault/api/transit/Transit.java create mode 100644 src/main/java/com/bettercloud/vault/response/TransitResponse.java create mode 100644 src/test-integration/java/com/bettercloud/vault/api/AuthBackendTransitTests.java 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:

+ * + *
+ *
{@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());
+     * }
+ *
+ * + * @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); + } + } + } + } + /** *

Operation to retrieve an role using the Database backend. Relies on an authentication token being present in * the VaultConfig instance.

@@ -370,6 +438,68 @@ public DatabaseResponse creds(final String roleName) throws VaultException { } } + /** + *

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:

+ * + *
+ *
{@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();
+     * }
+ *
+ * + * @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 data) { String result = ""; diff --git a/src/main/java/com/bettercloud/vault/api/database/DatabaseStaticRoleOptions.java b/src/main/java/com/bettercloud/vault/api/database/DatabaseStaticRoleOptions.java new file mode 100644 index 00000000..edb30182 --- /dev/null +++ b/src/main/java/com/bettercloud/vault/api/database/DatabaseStaticRoleOptions.java @@ -0,0 +1,78 @@ +package com.bettercloud.vault.api.database; + +import java.util.ArrayList; +import java.util.List; + +public class DatabaseStaticRoleOptions { + private String name; + private String dbName; + private String username; + private String rotationPeriod; + private List rotationStatements = new ArrayList<>(); + + public String getName() { + return name; + } + + public String getDbName() { + return dbName; + } + + public String getUsername() { + return username; + } + + public String getRotationPeriod() { + return rotationPeriod; + } + + public List getRotationStatements() { + return rotationStatements; + } + + /** + * @param name {@code String} – Specifies the name of the role to create. This is specified as part of the URL. + * @return This object, with name populated, ready for other builder methods or immediate use. + */ + public DatabaseStaticRoleOptions name(final String name) { + this.name = name; + return this; + } + + /** + * @param dbName {@code String} - The name of the database connection to use for this role. + * @return This object, with dbName populated, ready for other builder methods or immediate use. + */ + public DatabaseStaticRoleOptions dbName(final String dbName) { + this.dbName = dbName; + return this; + } + + /** + * @param username {@code String} - The database usernameto use for this role. + * @return This object, with dbName populated, ready for other builder methods or immediate use. + */ + public DatabaseStaticRoleOptions username(final String username) { + this.username = username; + return this; + } + + /** + * @param rotationPeriod (string/int: 0) - Specifies the amount of time Vault should wait before rotating the password. The minimum is 5 seconds. + * @return This object, with defaultTtl populated, ready for other builder methods or immediate use. + */ + public DatabaseStaticRoleOptions rotationPeriod(final String rotationPeriod) { + this.rotationPeriod = rotationPeriod; + return this; + } + + /** + * @param rotationStatements {@code List} – Specifies the database statements to be executed to rotate the password for the configured database user. Not every plugin type will support this functionality. See the plugin's API page for more information on support and formatting for this parameter. + * @return This object, with creationStatements populated, ready for other builder methods or immediate use. + */ + public DatabaseStaticRoleOptions rotationStatements(final List rotationStatements) { + this.rotationStatements = rotationStatements; + return this; + } + +} 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.

+ */ +public class Transit { + + private final VaultConfig config; + private final String mountPath; + private String nameSpace; + + public Transit withNameSpace(final String nameSpace) { + this.nameSpace = nameSpace; + return this; + } + + /** + * Constructor for use when the Transit backend is mounted on the default path (i.e. /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:

+ * + *
+ *
{@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());
+     * }
+ *
+ * + * @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); + } + } + } + } + + /** + *

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:

+ * + *
+ *
{@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();
+     * }
+ *
+ * + * @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); + } + } + } + } + + /** + *

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:

+ * + *
+ *
{@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();
+     * }
+ *
+ * + * @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); + } + } + } + } + + /** + *

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:

+ * + *
+ *
{@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());
+     * }
+ *
+ * + * @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); + } + } + } + } + + /** + *

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:

+ * + *
+ *
{@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());
+     * }
+ *
+ * + * @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); + } + + /** + *

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:

+ * + *
+ *
{@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());
+     * }
+ *
+ * + * @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; + } + + /** + *

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.

+ * + * @param data The "data" object from a Vault JSON response, converted into Java key-value pairs. + * @return A container for role options + */ + private KeyOptions buildKeyOptionsFromData(final Map data) { + if (data == null) { + return null; + } + + final String type = data.get("type"); + final Boolean deletionAllowed = parseBoolean(data.get("deletion_allowed")); + final Boolean derived = parseBoolean(data.get("derived")); + final Boolean exportable = parseBoolean(data.get("exportable")); + final Boolean allowPlaintextBackup = parseBoolean(data.get("allow_plaintext_backup")); + final Integer minDecryptionVersion = parseInt(data.get("min_decryption_version")); + final Integer minEncryptionVersion = parseInt(data.get("min_encryption_version")); + final String name = data.get("name"); + final Boolean supportsEncryption = parseBoolean(data.get("supports_encryption")); + final Boolean supportsDecryption = parseBoolean(data.get("supports_decryption")); + final Boolean supportsDerivation = parseBoolean(data.get("supports_derivation")); + final Boolean supportsSigning = parseBoolean(data.get("supports_signing")); + + if ( type == null && deletionAllowed == null && derived == null && exportable == null + && allowPlaintextBackup == null && minDecryptionVersion == null + && minEncryptionVersion == null && name == null && supportsEncryption == null + && supportsDecryption == null && supportsDerivation == null + && supportsSigning == null ) { + return null; + } + return new KeyOptions(deletionAllowed, name, minDecryptionVersion, minEncryptionVersion, + supportsEncryption, supportsDecryption, supportsDerivation, supportsSigning) + .type(type) + .derived(derived) + .exportable(exportable) + .allowPlaintextBackup(allowPlaintextBackup); + } + + /** + *

Generates 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)).

+ * + * @param data The "data" object from a Vault JSON response, converted into Java key-value pairs. + * @return A container for role options + */ + private CryptData buildEncryptDataFromData(final Map data) { + if (data == null) { + return null; + } + + final String ciphertext = data.get("ciphertext"); + final String plaintext = data.get("plaintext"); + final Integer keyVersion = parseInt(data.get("key_version")); + + if ( ciphertext == null && plaintext == null && keyVersion == null) { + return null; + } + return new CryptData() + .ciphertext(ciphertext) + .plaintext(plaintext) + .keyVersion(keyVersion); + } + + + /** + *

Used 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.

+ * + * @param input A string, which can be 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 rotationStatements = new ArrayList<>(); + rotationStatements.add("ALTER USER \"{{name}}\" WITH PASSWORD '{{password}}';"); + + DatabaseResponse response = vault.database().createOrUpdateStaticRole("new-role", new DatabaseStaticRoleOptions().dbName("postgres").username("test").rotationStatements(rotationStatements)); + assertEquals(204, response.getRestResponse().getStatus()); + + DatabaseResponse credsResponse = vault.database().staticCreds("new-role"); + assertEquals(200, credsResponse.getRestResponse().getStatus()); + + assertTrue(credsResponse.getCredential().getUsername().contains("new-role")); + } + private boolean compareRoleOptions(DatabaseRoleOptions expected, DatabaseRoleOptions actual) { return expected.getCreationStatements().size() == actual.getCreationStatements().size() && expected.getRenewStatements().size() == actual.getRenewStatements().size() && diff --git a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java index a5f9e337..2a3ab548 100644 --- a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java +++ b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTokenTests.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; -import java.util.List; import java.util.UUID; import org.junit.BeforeClass; import org.junit.ClassRule; diff --git a/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTransitTests.java b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTransitTests.java new file mode 100644 index 00000000..df08db84 --- /dev/null +++ b/src/test-integration/java/com/bettercloud/vault/api/AuthBackendTransitTests.java @@ -0,0 +1,252 @@ +package com.bettercloud.vault.api; + +import com.bettercloud.vault.Vault; +import com.bettercloud.vault.VaultException; +import com.bettercloud.vault.api.transit.DecryptOptions; +import com.bettercloud.vault.api.transit.EncryptOptions; +import com.bettercloud.vault.api.transit.KeyOptions; +import com.bettercloud.vault.response.TransitResponse; +import com.bettercloud.vault.rest.RestResponse; +import com.bettercloud.vault.util.VaultContainer; +import java.io.IOException; +import java.util.Arrays; +import java.util.Base64; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.assertTrue; + +/** + * Integration tests for for operations on Vault's /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() {{ +// add("myvault.com"); +// }}) +// .allowSubdomains(true) +// .maxTtl("9h") +// ); +// assertEquals(204, createRoleResponse.getRestResponse().getStatus()); +// Thread.sleep(3000); +// +// // Issue cert +// final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM); +// assertNotNull(issueResponse.getCredential().getCertificate()); +// assertNotNull(issueResponse.getCredential().getPrivateKey()); +// assertNotNull(issueResponse.getCredential().getSerialNumber()); +// assertEquals("rsa", issueResponse.getCredential().getPrivateKeyType()); +// assertNotNull(issueResponse.getCredential().getIssuingCa()); +// } +// +// @Test +// public void testIssueCredentialWithCsr() throws VaultException, InterruptedException, NoSuchAlgorithmException { +// +// KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); +// kpg.initialize(2048); +// KeyPair kp = kpg.generateKeyPair(); +// PublicKey pub = kp.getPublic(); +// PrivateKey pvt = kp.getPrivate(); +// String csr = null; +// try { +// csr = generatePKCS10(kp, "", "", "", "", "", ""); +// } catch (Exception e) { +// e.printStackTrace(); +// } +// final Vault vault = container.getRootVault(); +// +// // Create a role +// final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole", +// new RoleOptions() +// .allowedDomains(new ArrayList() {{ +// add("myvault.com"); +// }}) +// .allowSubdomains(true) +// .maxTtl("9h") +// ); +// assertEquals(204, createRoleResponse.getRestResponse().getStatus()); +// Thread.sleep(3000); +// +// // Issue cert +// final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM, csr); +// assertNotNull(issueResponse.getCredential().getCertificate()); +// assertNull(issueResponse.getCredential().getPrivateKey()); +// assertNotNull(issueResponse.getCredential().getSerialNumber()); +// assertNotNull(issueResponse.getCredential().getIssuingCa()); +// } +// +// @Test +// public void testRevocation() throws VaultException, InterruptedException, NoSuchAlgorithmException { +// final Vault vault = container.getRootVault(); +// +// // Create a role +// final PkiResponse createRoleResponse = vault.pki().createOrUpdateRole("testRole", +// new RoleOptions() +// .allowedDomains(new ArrayList() {{ +// add("myvault.com"); +// }}) +// .allowSubdomains(true) +// .maxTtl("9h") +// ); +// assertEquals(204, createRoleResponse.getRestResponse().getStatus()); +// Thread.sleep(3000); +// // Issue cert +// final PkiResponse issueResponse = vault.pki().issue("testRole", "test.myvault.com", null, null, "1h", CredentialFormat.PEM); +// assertNotNull(issueResponse.getCredential().getSerialNumber()); +// vault.pki().revoke(issueResponse.getCredential().getSerialNumber()); +// } +// +// @Test +// public void testCustomMountPath() throws VaultException { +// final Vault vault = container.getRootVault(); +// +// vault.pki("other-pki").createOrUpdateRole("testRole"); +// final PkiResponse response = vault.pki("other-pki").getRole("testRole"); +// assertTrue(compareRoleOptions(new RoleOptions(), response.getRoleOptions())); +// } +// + private boolean compareKeyOptions(final KeyOptions expected, final KeyOptions actual) { + if (expected.getConvergentEncryption() != null && !expected.getConvergentEncryption() + .equals(actual.getConvergentEncryption())) { + return false; + } + if (expected.getDerived() != null && !expected.getDerived().equals(actual.getDerived())) { + return false; + } + if (expected.getExportable() != null && !expected.getExportable() + .equals(actual.getExportable())) { + return false; + } + if (expected.getAllowPlaintextBackup() != null && !expected.getAllowPlaintextBackup() + .equals(actual.getAllowPlaintextBackup())) { + return false; + } + if (expected.getType() != null && !expected.getType().equals(actual.getType())) { + return false; + } + if (expected.getAutoRotatePeriod() != null && !expected.getAutoRotatePeriod() + .equals(actual.getAutoRotatePeriod())) { + return false; + } + if (expected.getDeletionAllowed() != null && !expected.getDeletionAllowed() + .equals(actual.getDeletionAllowed())) { + return false; + } + if (expected.getName() != null && !expected.getName().equals(actual.getName())) { + return false; + } + if (expected.getMinDecryptionVersion() != null && !expected.getMinDecryptionVersion() + .equals(actual.getMinDecryptionVersion())) { + return false; + } + if (expected.getMinEncryptionVersion() != null && !expected.getMinEncryptionVersion() + .equals(actual.getMinEncryptionVersion())) { + return false; + } + if (expected.getSupportsEncryption() != null && !expected.getSupportsEncryption() + .equals(actual.getSupportsEncryption())) { + return false; + } + if (expected.getSupportsDecryption() != null && !expected.getSupportsDecryption() + .equals(actual.getSupportsDecryption())) { + return false; + } + if (expected.getSupportsDerivation() != null && !expected.getSupportsDerivation() + .equals(actual.getSupportsDerivation())) { + return false; + } + return expected.getSupportsSigning() == null || expected.getSupportsSigning() + .equals(actual.getSupportsSigning()); + } + +} diff --git a/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java b/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java index 6602e881..c7bcb058 100644 --- a/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java +++ b/src/test-integration/java/com/bettercloud/vault/util/VaultContainer.java @@ -29,7 +29,7 @@ public class VaultContainer extends GenericContainer implements private static final Logger LOGGER = LoggerFactory.getLogger(VaultContainer.class); - public static final String DEFAULT_IMAGE_AND_TAG = "vault:1.1.3"; + public static final String DEFAULT_IMAGE_AND_TAG = "vault:1.7.3"; private String rootToken; private String unsealKey; @@ -168,6 +168,20 @@ public void setupBackendPki() throws IOException, InterruptedException { "common_name=myvault.com", "ttl=99h"); } + /** + * Prepares the Vault server for testing of the Transit auth backend (i.e. mounts the backend and populates test data). + * + * @throws IOException + * @throws InterruptedException + */ + public void setupBackendTransit() throws IOException, InterruptedException { + runCommand("vault", "login", "-ca-cert=" + CONTAINER_CERT_PEMFILE, rootToken); + + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=transit", "transit"); + runCommand("vault", "secrets", "enable", "-ca-cert=" + CONTAINER_CERT_PEMFILE, "-path=other-transit", "transit"); + + } + /** * Prepares the Vault server for testing of the TLS Certificate auth backend (i.e. mounts the backend and registers * the certificate and private key for client auth). diff --git a/src/test-integration/resources/startup.sh b/src/test-integration/resources/startup.sh index 2010c713..51bc1ad8 100644 --- a/src/test-integration/resources/startup.sh +++ b/src/test-integration/resources/startup.sh @@ -15,13 +15,13 @@ cd /vault/config/ssl rm -Rf * cp ../libressl.conf . # Create a CA root certificate and key -openssl req -newkey rsa:2048 -days 3650 -x509 -nodes -out root-cert.pem -keyout root-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' +libressl req -newkey rsa:2048 -days 3650 -x509 -nodes -out root-cert.pem -keyout root-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' # Create a private key, and a certificate-signing request -openssl req -newkey rsa:1024 -nodes -out vault-csr.pem -keyout vault-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' +libressl req -newkey rsa:1024 -nodes -out vault-csr.pem -keyout vault-privkey.pem -subj '/C=US/ST=GA/L=Atlanta/O=BetterCloud/CN=localhost' # Create an X509 certificate for the Vault server echo 000a > serialfile touch certindex -openssl ca -batch -config libressl.conf -notext -in vault-csr.pem -out vault-cert.pem +libressl ca -batch -config libressl.conf -notext -in vault-csr.pem -out vault-cert.pem # Configure SSL at the OS level to trust the new certs cp root-cert.pem vault-cert.pem /usr/local/share/ca-certificates # Clean up temp files diff --git a/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java b/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java index 5f215dfb..05b9ff67 100644 --- a/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java +++ b/src/test/java/com/bettercloud/vault/vault/api/TransitApiTest.java @@ -7,14 +7,12 @@ import com.bettercloud.vault.response.LogicalResponse; import com.bettercloud.vault.vault.VaultTestUtils; import com.bettercloud.vault.vault.mock.MockVault; +import java.util.Collections; +import java.util.Optional; import org.eclipse.jetty.server.Server; import org.junit.After; -import org.junit.Before; import org.junit.Test; -import java.util.Collections; -import java.util.Optional; - import static org.junit.Assert.assertEquals; public class TransitApiTest {