From 6e80ce542f2c02253a3d05308f32968a30adb4ef Mon Sep 17 00:00:00 2001 From: Alex Soto Date: Mon, 16 Mar 2020 08:00:43 +0100 Subject: [PATCH] fix(#7865): Adds support for Vault TOTP secrets --- .../quarkus/vault/VaultTOTPSecretEngine.java | 64 +++++++++ .../quarkus/vault/runtime/VaultManager.java | 6 + .../vault/runtime/VaultServiceProducer.java | 7 + .../vault/runtime/VaultTOTPManager.java | 88 ++++++++++++ .../runtime/client/OkHttpVaultClient.java | 78 ++++++++++- .../vault/runtime/client/VaultClient.java | 17 +++ .../dto/totp/VaultTOTPCreateKeyBody.java | 38 ++++++ .../dto/totp/VaultTOTPCreateKeyData.java | 10 ++ .../dto/totp/VaultTOTPCreateKeyResult.java | 9 ++ .../dto/totp/VaultTOTPGenerateCodeData.java | 9 ++ .../dto/totp/VaultTOTPGenerateCodeResult.java | 9 ++ .../dto/totp/VaultTOTPListKeysData.java | 11 ++ .../dto/totp/VaultTOTPListKeysResult.java | 6 + .../client/dto/totp/VaultTOTPReadKeyData.java | 17 +++ .../dto/totp/VaultTOTPReadKeyResult.java | 9 ++ .../dto/totp/VaultTOTPValidateCodeBody.java | 13 ++ .../dto/totp/VaultTOTPValidateCodeData.java | 9 ++ .../dto/totp/VaultTOTPValidateCodeResult.java | 9 ++ .../secrets/totp/CreateKeyParameters.java | 129 ++++++++++++++++++ .../vault/secrets/totp/KeyConfiguration.java | 71 ++++++++++ .../vault/secrets/totp/KeyDefinition.java | 61 +++++++++ integration-tests/vault/pom.xml | 5 + .../io/quarkus/vault/VaultTOTPITCase.java | 111 +++++++++++++++ .../application-vault-totp.properties | 5 + .../vault/test/VaultTestExtension.java | 4 + .../vault/src/main/resources/vault.policy | 4 + 26 files changed, 794 insertions(+), 5 deletions(-) create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java create mode 100644 extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java create mode 100644 integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java create mode 100644 integration-tests/vault/src/test/resources/application-vault-totp.properties diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java new file mode 100644 index 0000000000000..d592bded66222 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/VaultTOTPSecretEngine.java @@ -0,0 +1,64 @@ +package io.quarkus.vault; + +import java.util.List; +import java.util.Optional; + +import io.quarkus.vault.secrets.totp.CreateKeyParameters; +import io.quarkus.vault.secrets.totp.KeyConfiguration; +import io.quarkus.vault.secrets.totp.KeyDefinition; + +/** + * This service provides access to the TOTP secret engine. + * + * @see TOTP Secrets Engine + */ +public interface VaultTOTPSecretEngine { + + /** + * Creates or updates a key definition. + * + * @param name of the key. + * @param createKeyParameters required to create or update a key. + * @return Barcode and/or URL of the created OTP key. + */ + Optional createKey(String name, CreateKeyParameters createKeyParameters); + + /** + * Queries the key definition. + * + * @param name of the key. + * @return The key configuration. + */ + KeyConfiguration readKey(String name); + + /** + * Returns a list of available keys. Only the key names are returned, not any values. + * + * @return List of available keys. + */ + List listKeys(); + + /** + * Deletes the key definition. + * + * @param name of the key. + */ + void deleteKey(String name); + + /** + * Generates a new time-based one-time use password based on the named key. + * + * @param name of the key. + * @return The Code. + */ + String generateCode(String name); + + /** + * Validates a time-based one-time use password generated from the named key. + * + * @param name of the key. + * @param code to validate. + * @return True if valid, false otherwise. + */ + boolean validateCode(String name, String code); +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java index 9157e05a2ddab..4bef6bf541b62 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultManager.java @@ -15,6 +15,7 @@ public class VaultManager { private VaultDbManager vaultDbManager; private VaultTransitManager vaultTransitManager; private VaultCredentialsProvider vaultCredentialsProvider; + private VaultTOTPManager vaultTOTPManager; public static VaultManager getInstance() { return instance; @@ -42,6 +43,7 @@ public VaultManager(VaultRuntimeConfig serverConfig, VaultClient vaultClient) { this.vaultDbManager = new VaultDbManager(this.vaultAuthManager, this.vaultClient, serverConfig); this.vaultTransitManager = new VaultTransitManager(this.vaultAuthManager, this.vaultClient, serverConfig); this.vaultCredentialsProvider = new VaultCredentialsProvider(serverConfig, this.vaultKvManager, this.vaultDbManager); + this.vaultTOTPManager = new VaultTOTPManager(this.vaultAuthManager, this.vaultClient); } public VaultClient getVaultClient() { @@ -60,6 +62,10 @@ public VaultKvManager getVaultKvManager() { return vaultKvManager; } + public VaultTOTPManager getVaultTOTPManager() { + return vaultTOTPManager; + } + public VaultTransitManager getVaultTransitManager() { return vaultTransitManager; } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java index d714bf2e62f6b..f2bea2fe3749f 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultServiceProducer.java @@ -7,6 +7,7 @@ import io.quarkus.vault.CredentialsProvider; import io.quarkus.vault.VaultKVSecretEngine; +import io.quarkus.vault.VaultTOTPSecretEngine; import io.quarkus.vault.VaultTransitSecretEngine; import io.quarkus.vault.runtime.config.VaultRuntimeConfig; @@ -25,6 +26,12 @@ public VaultTransitSecretEngine createTransitSecretEngine() { return VaultManager.getInstance().getVaultTransitManager(); } + @Produces + @ApplicationScoped + public VaultTOTPSecretEngine createVaultTOTPSecretEngine() { + return VaultManager.getInstance().getVaultTOTPManager(); + } + @Produces @ApplicationScoped @Named("vault-credentials-provider") diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java new file mode 100644 index 0000000000000..a1f933879c91b --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/VaultTOTPManager.java @@ -0,0 +1,88 @@ +package io.quarkus.vault.runtime; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import io.quarkus.vault.VaultTOTPSecretEngine; +import io.quarkus.vault.runtime.client.VaultClient; +import io.quarkus.vault.runtime.client.VaultClientException; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult; +import io.quarkus.vault.secrets.totp.CreateKeyParameters; +import io.quarkus.vault.secrets.totp.KeyConfiguration; +import io.quarkus.vault.secrets.totp.KeyDefinition; + +public class VaultTOTPManager implements VaultTOTPSecretEngine { + + private VaultAuthManager vaultAuthManager; + private VaultClient vaultClient; + + public VaultTOTPManager(VaultAuthManager vaultAuthManager, VaultClient vaultClient) { + this.vaultAuthManager = vaultAuthManager; + this.vaultClient = vaultClient; + } + + @Override + public Optional createKey(String name, CreateKeyParameters createKeyParameters) { + VaultTOTPCreateKeyBody body = new VaultTOTPCreateKeyBody(); + + body.accountName = createKeyParameters.getAccountName(); + body.algorithm = createKeyParameters.getAlgorithm(); + body.digits = createKeyParameters.getDigits(); + body.exported = createKeyParameters.getExported(); + body.generate = createKeyParameters.getGenerate(); + body.issuer = createKeyParameters.getIssuer(); + body.key = createKeyParameters.getKey(); + body.keySize = createKeyParameters.getKeySize(); + body.period = createKeyParameters.getPeriod(); + body.qrSize = createKeyParameters.getQrSize(); + body.skew = createKeyParameters.getSkew(); + body.url = createKeyParameters.getUrl(); + + final VaultTOTPCreateKeyResult result = this.vaultClient + .createTOTPKey(getToken(), name, body); + + return result == null ? Optional.empty() : Optional.of(new KeyDefinition(result.data.barcode, result.data.url)); + } + + @Override + public KeyConfiguration readKey(String name) { + final VaultTOTPReadKeyResult result = this.vaultClient.readTOTPKey(getToken(), name); + return new KeyConfiguration(result.data.accountName, + result.data.algorithm, result.data.digits, + result.data.issuer, result.data.period); + } + + @Override + public List listKeys() { + try { + return this.vaultClient.listTOTPKeys(getToken()).data.keys; + } catch (VaultClientException e) { + if (e.getStatus() == 404) { + return Collections.emptyList(); + } + throw e; + } + } + + @Override + public void deleteKey(String name) { + this.vaultClient.deleteTOTPKey(getToken(), name); + } + + @Override + public String generateCode(String name) { + return this.vaultClient.generateTOTPCode(getToken(), name).data.code; + } + + @Override + public boolean validateCode(String name, String code) { + return this.vaultClient.validateTOTPCode(getToken(), name, code).data.valid; + } + + private String getToken() { + return vaultAuthManager.getClientToken(); + } +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java index b10cb2937a946..a5ea06f68fad2 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/OkHttpVaultClient.java @@ -10,6 +10,7 @@ import org.jboss.logging.Logger; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -31,6 +32,13 @@ import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesBody; import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup; import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPGenerateCodeResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPListKeysResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeBody; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeResult; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecryptBody; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitEncrypt; @@ -61,6 +69,7 @@ public OkHttpVaultClient(VaultRuntimeConfig serverConfig) { this.client = createHttpClient(serverConfig); this.url = serverConfig.url.get(); this.mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false); + this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); } @Override @@ -103,12 +112,12 @@ public void writeSecretV2(String token, String secretEnginePath, String path, Va @Override public void deleteSecretV1(String token, String secretEnginePath, String path) { - delete(secretEnginePath + "/" + path, token, null, null, 204); + delete(secretEnginePath + "/" + path, token, 204); } @Override public void deleteSecretV2(String token, String secretEnginePath, String path) { - delete(secretEnginePath + "/data/" + path, token, null, null, 204); + delete(secretEnginePath + "/data/" + path, token, 204); } @Override @@ -167,11 +176,61 @@ public VaultTransitEncrypt rewrap(String token, String keyName, VaultTransitRewr return post("transit/rewrap/" + keyName, token, body, VaultTransitEncrypt.class); } + @Override + public VaultTOTPCreateKeyResult createTOTPKey(String token, String keyName, + VaultTOTPCreateKeyBody body) { + String path = "totp/keys/" + keyName; + + // Depending on parameters it might produce an output or not + if (body.isProducingOutput()) { + return post(path, token, body, VaultTOTPCreateKeyResult.class, 200); + } else { + post(path, token, body, 204); + return null; + } + } + + @Override + public VaultTOTPReadKeyResult readTOTPKey(String token, String keyName) { + String path = "totp/keys/" + keyName; + return get(path, token, VaultTOTPReadKeyResult.class); + } + + @Override + public VaultTOTPListKeysResult listTOTPKeys(String token) { + return list("totp/keys", token, VaultTOTPListKeysResult.class); + } + + @Override + public void deleteTOTPKey(String token, String keyName) { + String path = "totp/keys/" + keyName; + delete(path, token, 204); + } + + @Override + public VaultTOTPGenerateCodeResult generateTOTPCode(String token, String keyName) { + String path = "totp/code/" + keyName; + return get(path, token, VaultTOTPGenerateCodeResult.class); + } + + @Override + public VaultTOTPValidateCodeResult validateTOTPCode(String token, String keyName, + String code) { + String path = "totp/code/" + keyName; + VaultTOTPValidateCodeBody body = new VaultTOTPValidateCodeBody(code); + return post(path, token, body, VaultTOTPValidateCodeResult.class); + } + // --- - protected T delete(String path, String token, Object body, Class resultClass, int expectedCode) { - Request request = builder(path, token).delete(requestBody(body)).build(); - return exec(request, resultClass, expectedCode); + protected T list(String path, String token, Class resultClass) { + Request request = builder(path, token).method("LIST", null).build(); + return exec(request, resultClass); + } + + protected T delete(String path, String token, int expectedCode) { + Request request = builder(path, token).delete().build(); + return exec(request, expectedCode); } protected T post(String path, String token, Object body, Class resultClass, int expectedCode) { @@ -184,6 +243,11 @@ protected T post(String path, String token, Object body, Class resultClas return exec(request, resultClass); } + protected T post(String path, String token, Object body, int expectedCode) { + Request request = builder(path, token).post(requestBody(body)).build(); + return exec(request, expectedCode); + } + protected T put(String path, String token, Object body, Class resultClass) { Request request = builder(path, token).put(requestBody(body)).build(); return exec(request, resultClass); @@ -198,6 +262,10 @@ private T exec(Request request, Class resultClass) { return exec(request, resultClass, 200); } + private T exec(Request request, int expectedCode) { + return exec(request, null, expectedCode); + } + private T exec(Request request, Class resultClass, int expectedCode) { try (Response response = client.newCall(request).execute()) { if (response.code() != expectedCode) { diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java index f7b20c363d927..6d1c51f694b1c 100644 --- a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/VaultClient.java @@ -13,6 +13,12 @@ import io.quarkus.vault.runtime.client.dto.kv.VaultKvSecretV2WriteBody; import io.quarkus.vault.runtime.client.dto.sys.VaultLeasesLookup; import io.quarkus.vault.runtime.client.dto.sys.VaultRenewLease; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyBody; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPCreateKeyResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPGenerateCodeResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPListKeysResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPReadKeyResult; +import io.quarkus.vault.runtime.client.dto.totp.VaultTOTPValidateCodeResult; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecrypt; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitDecryptBody; import io.quarkus.vault.runtime.client.dto.transit.VaultTransitEncrypt; @@ -68,4 +74,15 @@ VaultTransitVerify verify(String token, String keyName, String hashAlgorithm, VaultTransitEncrypt rewrap(String token, String keyName, VaultTransitRewrapBody body); + VaultTOTPCreateKeyResult createTOTPKey(String token, String keyName, VaultTOTPCreateKeyBody vaultTOTPCreateKeyBody); + + VaultTOTPReadKeyResult readTOTPKey(String token, String keyName); + + VaultTOTPListKeysResult listTOTPKeys(String token); + + void deleteTOTPKey(String token, String keyName); + + VaultTOTPGenerateCodeResult generateTOTPCode(String token, String keyName); + + VaultTOTPValidateCodeResult validateTOTPCode(String token, String keyName, String code); } diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java new file mode 100644 index 0000000000000..6ac0739a573c0 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyBody.java @@ -0,0 +1,38 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPCreateKeyBody implements VaultModel { + + public Boolean generate; + public Boolean exported; + + @JsonProperty("key_size") + public Integer keySize; + + public String url; + public String key; + public String issuer; + + @JsonProperty("account_name") + public String accountName; + + public String period; + public String algorithm; + public Integer digits; + public Integer skew; + + @JsonProperty("qr_size") + public Integer qrSize; + + public boolean isProducingOutput() { + // When exported is not set, by default is true + return is(exported, true) && is(generate, false); + } + + private boolean is(Boolean v, boolean defaultValue) { + return v == null ? defaultValue : v; + } +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java new file mode 100644 index 0000000000000..d2df951dbd2d7 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyData.java @@ -0,0 +1,10 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPCreateKeyData implements VaultModel { + + public String barcode; + public String url; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java new file mode 100644 index 0000000000000..5c052e9a44a24 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPCreateKeyResult.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPCreateKeyResult implements VaultModel { + + public VaultTOTPCreateKeyData data; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java new file mode 100644 index 0000000000000..0530d6cad5d91 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeData.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPGenerateCodeData implements VaultModel { + + public String code; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java new file mode 100644 index 0000000000000..d2f80a957216c --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPGenerateCodeResult.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPGenerateCodeResult implements VaultModel { + + public VaultTOTPGenerateCodeData data; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java new file mode 100644 index 0000000000000..3d0efb51c5bb9 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysData.java @@ -0,0 +1,11 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import java.util.List; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPListKeysData implements VaultModel { + + public List keys; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java new file mode 100644 index 0000000000000..eeef9c4279f2b --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPListKeysResult.java @@ -0,0 +1,6 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.AbstractVaultDTO; + +public class VaultTOTPListKeysResult extends AbstractVaultDTO { +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java new file mode 100644 index 0000000000000..3b34802de9812 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyData.java @@ -0,0 +1,17 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPReadKeyData implements VaultModel { + + @JsonProperty("account_name") + public String accountName; + + public String algorithm; + public int digits; + public String issuer; + public int period; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java new file mode 100644 index 0000000000000..6c7e11a577fcb --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPReadKeyResult.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPReadKeyResult implements VaultModel { + + public VaultTOTPReadKeyData data; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java new file mode 100644 index 0000000000000..f72ab71676c98 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeBody.java @@ -0,0 +1,13 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPValidateCodeBody implements VaultModel { + + public VaultTOTPValidateCodeBody(String code) { + this.code = code; + } + + public String code; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java new file mode 100644 index 0000000000000..f8d7014d7ae3f --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeData.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPValidateCodeData implements VaultModel { + + public boolean valid; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java new file mode 100644 index 0000000000000..91dd0e1c5ded1 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/runtime/client/dto/totp/VaultTOTPValidateCodeResult.java @@ -0,0 +1,9 @@ +package io.quarkus.vault.runtime.client.dto.totp; + +import io.quarkus.vault.runtime.client.dto.VaultModel; + +public class VaultTOTPValidateCodeResult implements VaultModel { + + public VaultTOTPValidateCodeData data; + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java new file mode 100644 index 0000000000000..39140d55cf2bd --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/CreateKeyParameters.java @@ -0,0 +1,129 @@ +package io.quarkus.vault.secrets.totp; + +public class CreateKeyParameters { + + private Boolean generate; + private Boolean exported; + + private Integer keySize; + + private String url; + private String key; + private String issuer; + + private String accountName; + + private String period; + private String algorithm; + private Integer digits; + private Integer skew; + + private Integer qrSize; + + public CreateKeyParameters(String url) { + this.url = url; + } + + public CreateKeyParameters(String key, String issuer, String accountName) { + this.key = key; + this.issuer = issuer; + this.accountName = accountName; + } + + /** + * Constructs an object with generate to true as no key is provided. + * + * @param issuer to set. + * @param accountName to set. + */ + public CreateKeyParameters(String issuer, String accountName) { + this.issuer = issuer; + this.accountName = accountName; + this.generate = true; + } + + public void setGenerate(Boolean generate) { + this.generate = generate; + } + + public void setExported(Boolean exported) { + this.exported = exported; + } + + public void setKeySize(int keySize) { + this.keySize = keySize; + } + + public void setUrl(String url) { + this.url = url; + } + + public void setPeriod(String period) { + this.period = period; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } + + public void setDigits(Integer digits) { + this.digits = digits; + } + + public void setSkew(Integer skew) { + this.skew = skew; + } + + public void setQrSize(Integer qrSize) { + this.qrSize = qrSize; + } + + public Boolean getGenerate() { + return generate; + } + + public Boolean getExported() { + return exported; + } + + public Integer getKeySize() { + return keySize; + } + + public String getUrl() { + return url; + } + + public String getKey() { + return key; + } + + public String getIssuer() { + return issuer; + } + + public String getAccountName() { + return accountName; + } + + public String getPeriod() { + return period; + } + + public String getAlgorithm() { + return algorithm; + } + + public Integer getDigits() { + return digits; + } + + public Integer getSkew() { + return skew; + } + + public Integer getQrSize() { + return qrSize; + } + +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java new file mode 100644 index 0000000000000..2d2ba69aea08f --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyConfiguration.java @@ -0,0 +1,71 @@ +package io.quarkus.vault.secrets.totp; + +import java.util.Objects; + +public class KeyConfiguration { + + private String accountName; + private String algorithm; + private int digits; + private String issuer; + private int period; + + public KeyConfiguration(String accountName, String algorithm, int digits, String issuer, int period) { + this.accountName = accountName; + this.algorithm = algorithm; + this.digits = digits; + this.issuer = issuer; + this.period = period; + } + + public String getAccountName() { + return accountName; + } + + public String getAlgorithm() { + return algorithm; + } + + public int getDigits() { + return digits; + } + + public String getIssuer() { + return issuer; + } + + public int getPeriod() { + return period; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + final KeyConfiguration that = (KeyConfiguration) o; + return digits == that.digits && + period == that.period && + accountName.equals(that.accountName) && + Objects.equals(algorithm, that.algorithm) && + issuer.equals(that.issuer); + } + + @Override + public int hashCode() { + return Objects.hash(accountName, issuer); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("KeyConfiguration{"); + sb.append("accountName='").append(accountName).append('\''); + sb.append(", algorithm='").append(algorithm).append('\''); + sb.append(", digits=").append(digits); + sb.append(", issuer='").append(issuer).append('\''); + sb.append(", period=").append(period); + sb.append('}'); + return sb.toString(); + } +} diff --git a/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java new file mode 100644 index 0000000000000..12e3f60bb0d05 --- /dev/null +++ b/extensions/vault/runtime/src/main/java/io/quarkus/vault/secrets/totp/KeyDefinition.java @@ -0,0 +1,61 @@ +package io.quarkus.vault.secrets.totp; + +import java.util.Objects; + +public class KeyDefinition { + + private String barcode; + private String url; + + public KeyDefinition() { + } + + public KeyDefinition(String barcode, String url) { + this.barcode = barcode; + this.url = url; + } + + /** + * QR code in base64-formatteed PNG bytes. + * + * @return Barcode. + */ + public String getBarcode() { + return barcode; + } + + /** + * URL in otpauth format (ie + * otpauth://totp/Google:test@gmail.com?algorithm=SHA1&digits=6&issuer=Google&period=30&secret=HTXT7KJFVNAJUPYWQRWMNVQE5AF5YZI2) + * + * @return URL in otpauth format. + */ + public String getUrl() { + return url; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + final KeyDefinition that = (KeyDefinition) o; + return Objects.equals(barcode, that.barcode) && + Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(barcode, url); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("KeyDefinition{"); + sb.append("barcode='").append(barcode).append('\''); + sb.append(", url='").append(url).append('\''); + sb.append('}'); + return sb.toString(); + } +} diff --git a/integration-tests/vault/pom.xml b/integration-tests/vault/pom.xml index 1e7ae406edbd8..e1d057a53c192 100644 --- a/integration-tests/vault/pom.xml +++ b/integration-tests/vault/pom.xml @@ -30,6 +30,11 @@ quarkus-vault-deployment test + + org.assertj + assertj-core + test + diff --git a/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java new file mode 100644 index 0000000000000..59d8e675a9ca7 --- /dev/null +++ b/integration-tests/vault/src/test/java/io/quarkus/vault/VaultTOTPITCase.java @@ -0,0 +1,111 @@ +package io.quarkus.vault; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import javax.inject.Inject; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.vault.secrets.totp.CreateKeyParameters; +import io.quarkus.vault.secrets.totp.KeyConfiguration; +import io.quarkus.vault.secrets.totp.KeyDefinition; +import io.quarkus.vault.test.VaultTestLifecycleManager; + +@DisabledOnOs(OS.WINDOWS) // https://github.com/quarkusio/quarkus/issues/3796 +@QuarkusTestResource(VaultTestLifecycleManager.class) +public class VaultTOTPITCase { + + private static final String TEST_OTP_URL = "otpauth://totp/Vault:test@google.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault"; + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addAsResource("application-vault-totp.properties", "application.properties")); + + @Inject + VaultTOTPSecretEngine vaultTOTPSecretEngine; + + @Test + public void createKey() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + "otpauth://totp/Vault:test@test.com?secret=Y64VEVMBTSXCYIWRSHRNDZW62MPGVU2G&issuer=Vault"); + final Optional myKey = vaultTOTPSecretEngine.createKey("my_key", createKeyParameters); + + assertThat(myKey).isNotPresent(); + } + + @Test + public void createGenerateKey() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters("Google", "test@gmail.com"); + final Optional myKey = vaultTOTPSecretEngine.createKey("my_key_2", createKeyParameters); + + assertThat(myKey) + .isPresent() + .get().hasNoNullFieldsOrProperties(); + } + + @Test + public void readKey() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + TEST_OTP_URL); + vaultTOTPSecretEngine.createKey("my_key_3", createKeyParameters); + + final KeyConfiguration myKey3 = vaultTOTPSecretEngine.readKey("my_key_3"); + assertThat(myKey3).returns("test@google.com", KeyConfiguration::getAccountName); + assertThat(myKey3).returns("Vault", KeyConfiguration::getIssuer); + } + + @Test + public void listKeys() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + TEST_OTP_URL); + vaultTOTPSecretEngine.createKey("my_key_4", createKeyParameters); + + final List listKeys = vaultTOTPSecretEngine.listKeys(); + assertThat(listKeys).contains("my_key_4"); + } + + @Test + public void deleteKey() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + TEST_OTP_URL); + vaultTOTPSecretEngine.createKey("my_key_5", createKeyParameters); + + vaultTOTPSecretEngine.deleteKey("my_key_5"); + final List listKeys = vaultTOTPSecretEngine.listKeys(); + assertThat(listKeys).doesNotContain("my_key_5"); + } + + @Test + public void generateCode() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + TEST_OTP_URL); + vaultTOTPSecretEngine.createKey("my_key_6", createKeyParameters); + + final String myKey6Code = vaultTOTPSecretEngine.generateCode("my_key_6"); + assertThat(myKey6Code).isNotEmpty(); + } + + @Test + public void validateCode() { + CreateKeyParameters createKeyParameters = new CreateKeyParameters( + TEST_OTP_URL); + createKeyParameters.setPeriod("30m"); + + vaultTOTPSecretEngine.createKey("my_key_7", createKeyParameters); + final String myKey7Code = vaultTOTPSecretEngine.generateCode("my_key_7"); + + boolean valid = vaultTOTPSecretEngine.validateCode("my_key_7", myKey7Code); + assertThat(valid).isTrue(); + } +} diff --git a/integration-tests/vault/src/test/resources/application-vault-totp.properties b/integration-tests/vault/src/test/resources/application-vault-totp.properties new file mode 100644 index 0000000000000..e071c39ff76c8 --- /dev/null +++ b/integration-tests/vault/src/test/resources/application-vault-totp.properties @@ -0,0 +1,5 @@ +quarkus.vault.url=https://localhost:8200 +quarkus.vault.authentication.userpass.username=bob +quarkus.vault.authentication.userpass.password=sinclair + +quarkus.vault.tls.skip-verify=true \ No newline at end of file diff --git a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java index a4d56803c8dc6..c8d1daa5ee7d0 100644 --- a/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java +++ b/test-framework/vault/src/main/java/io/quarkus/vault/test/VaultTestExtension.java @@ -297,6 +297,10 @@ private void initVault() throws InterruptedException, IOException { execVault(format("vault write transit/keys/%s type=ed25519 derived=true", SIGN_DERIVATION_KEY_NAME)); execVault("vault write transit/keys/jws type=ecdsa-p256"); + + // TOTP + + execVault("vault secrets enable totp"); } public static boolean useTls() { diff --git a/test-framework/vault/src/main/resources/vault.policy b/test-framework/vault/src/main/resources/vault.policy index 9a3d750b293cd..4199f361ce2a8 100644 --- a/test-framework/vault/src/main/resources/vault.policy +++ b/test-framework/vault/src/main/resources/vault.policy @@ -48,4 +48,8 @@ path "secret/crud" { path "secret-v2/data/crud" { capabilities = ["read", "create", "update", "delete"] +} + +path "totp/*" { + capabilities = ["read", "create", "update", "delete", "list"] } \ No newline at end of file