diff --git a/config/encryption/pom.xml b/config/encryption/pom.xml
index 364249d30a0..5c77b1abf21 100644
--- a/config/encryption/pom.xml
+++ b/config/encryption/pom.xml
@@ -1,7 +1,7 @@
+
+
+ 4.0.0
+
+ io.helidon.security.providers
+ helidon-security-providers-project
+ 2.3.0-SNAPSHOT
+
+ helidon-security-providers-config-vault
+ Helidon Security Providers Config Vault
+
+
+ An implementation of Vault like features based on configuration only.
+ Provide secrets, and encryption/decryption.
+ All secret operations are based on master password.
+ For full production solutions, please use OCI Vault or similar integrations.
+
+
+
+
+ io.helidon.security
+ helidon-security
+
+
+ io.helidon.config
+ helidon-config-encryption
+
+
+ io.helidon.bundles
+ helidon-bundles-config
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.hamcrest
+ hamcrest-all
+ test
+
+
+
diff --git a/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java
new file mode 100644
index 00000000000..4f7491d4418
--- /dev/null
+++ b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProvider.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.providers.config.vault;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import io.helidon.common.reactive.Single;
+import io.helidon.config.Config;
+import io.helidon.config.encryption.ConfigEncryptionException;
+import io.helidon.config.encryption.ConfigProperties;
+import io.helidon.config.encryption.EncryptionUtil;
+import io.helidon.security.spi.EncryptionProvider;
+import io.helidon.security.spi.ProviderConfig;
+import io.helidon.security.spi.SecretsProvider;
+
+/**
+ * Security provider to retrieve secrets directly from configuration and to encrypt/decrypt data
+ * using config's security setup.
+ */
+public class ConfigVaultProvider implements SecretsProvider,
+ EncryptionProvider {
+
+ private static final String CIPHER_TEXT_PREFIX_V2 = "helidon:2:";
+
+ private final Optional aesEncryption;
+
+ private ConfigVaultProvider(Builder builder) {
+ this.aesEncryption = builder.aesEncryption();
+ }
+
+ /**
+ * Create a new builder to configure this provider.
+ *
+ * @return a new builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Creates the provider with default configuration, supporting encryption if it is configured
+ * using environment variables or system properties.
+ *
+ * @return new security provider
+ */
+ public static ConfigVaultProvider create() {
+ return builder().build();
+ }
+
+ /**
+ * Creates the provider from configuration, supporting encryption if its configuration is found.
+ *
+ * @param config configuration of this provider
+ * @return new security provider
+ */
+ public static ConfigVaultProvider create(Config config) {
+ return builder().config(config).build();
+ }
+
+ @Override
+ public Supplier>> secret(Config config) {
+ Supplier> supplier = config.get("value").asString().optionalSupplier();
+ return () -> Single.just(supplier.get());
+ }
+
+ @Override
+ public Supplier>> secret(SecretConfig providerConfig) {
+ return providerConfig.value();
+ }
+
+ @Override
+ public EncryptionSupport encryption(Config config) {
+ return encryption(EncryptionConfig.create(config));
+ }
+
+ @Override
+ public EncryptionSupport encryption(EncryptionConfig providerConfig) {
+ return providerConfig.aesEncryption()
+ .or(() -> aesEncryption)
+ .orElseThrow(() -> new SecurityException("Encryption is not configured"));
+ }
+
+ /**
+ * Configuration of encryption. Currently has no additional configuration options.
+ */
+ public static class EncryptionConfig implements ProviderConfig {
+ private final Optional password;
+
+ private EncryptionConfig(Optional password) {
+ this.password = password;
+ }
+
+ /**
+ * Create a new instance.
+ *
+ * @return new instance with default configuration
+ */
+ public static EncryptionConfig create() {
+ return new EncryptionConfig(Optional.empty());
+ }
+
+ /**
+ * Create a new instance with custom password.
+ * @param password password to use
+ * @return a new instance using the custom password
+ */
+ public static EncryptionConfig create(char[] password) {
+ return new EncryptionConfig(Optional.ofNullable(password));
+ }
+
+ /**
+ * Create a new instance from config.
+ *
+ * @param config config to read password from (if any)
+ * @return a new instance configured from config
+ */
+ public static EncryptionConfig create(Config config) {
+ return new EncryptionConfig(config.get("password").asString().map(String::toCharArray));
+ }
+
+ private static EncryptionSupport encryptionSupport(char[] password) {
+ Function> encrypt = bytes -> {
+ return Single.just(CIPHER_TEXT_PREFIX_V2 + EncryptionUtil.encryptAesBytes(password, bytes));
+ };
+ Function> decrypt = cipherText -> {
+ if (cipherText.startsWith(CIPHER_TEXT_PREFIX_V2)) {
+ String base64 = cipherText.substring(CIPHER_TEXT_PREFIX_V2.length());
+ return Single.just(EncryptionUtil.decryptAesBytes(password, base64));
+ } else {
+ return Single.error(new ConfigEncryptionException("Invalid cipher text"));
+ }
+ };
+ return EncryptionSupport.create(encrypt, decrypt);
+ }
+
+ Optional aesEncryption() {
+ return password.map(EncryptionConfig::encryptionSupport);
+ }
+ }
+
+ /**
+ * Configuration of a secret.
+ */
+ public static class SecretConfig implements ProviderConfig {
+ private final Supplier>> value;
+
+ private SecretConfig(Supplier>> value) {
+ this.value = value;
+ }
+
+ /**
+ * Create a new secret configuration with a supplier of a future ({@link io.helidon.common.reactive.Single}),
+ * such as when retrieving the secret from a remote service.
+ * The supplier must be thread safe.
+ *
+ * @param valueSupplier supplier of a value
+ * @return a new secret configuration
+ */
+ public static SecretConfig createSingleSupplier(Supplier>> valueSupplier) {
+ return new SecretConfig(valueSupplier);
+ }
+
+ /**
+ * Create a new secret configuration with a supplier of an {@link Optional}, such as when retrieving
+ * the secret from some local information that may change.
+ * The supplier must be thread safe.
+ *
+ * @param valueSupplier supplier of an optional value
+ * @return a new secret configuration
+ */
+ public static SecretConfig createOptionalSupplier(Supplier> valueSupplier) {
+ return new SecretConfig(() -> Single.just(valueSupplier.get()));
+ }
+
+ /**
+ * Create a new secret from a supplier, such as when computing the secret value.
+ * The supplier must be thread safe.
+ *
+ * @param valueSupplier supplier of a value
+ * @return a new secret configuration
+ */
+ public static SecretConfig create(Supplier valueSupplier) {
+ return new SecretConfig(() -> Single.just(Optional.of(valueSupplier.get())));
+ }
+
+ /**
+ * Create a new secret from a value.
+ *
+ * @param value the secret value
+ * @return a new secret configuration
+ */
+ public static SecretConfig create(String value) {
+ return new SecretConfig(() -> Single.just(Optional.of(value)));
+ }
+
+ Supplier>> value() {
+ return value;
+ }
+ }
+
+ /**
+ * Fluent API builder for {@link ConfigVaultProvider}.
+ */
+ public static class Builder implements io.helidon.common.Builder {
+ private Config config = Config.empty();
+ private Optional masterPassword = Optional.empty();
+
+ private Builder() {
+ }
+
+ private static Optional resolveMasterPassword() {
+ Map env = System.getenv();
+ if (env.containsKey(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE)) {
+ return Optional.of(env.get(ConfigProperties.MASTER_PASSWORD_ENV_VARIABLE).toCharArray());
+ }
+ Properties properties = System.getProperties();
+ if (properties.containsKey(ConfigProperties.MASTER_PASSWORD_CONFIG_KEY)) {
+ return Optional.of(properties.getProperty(ConfigProperties.MASTER_PASSWORD_CONFIG_KEY).toCharArray());
+ }
+ return Optional.empty();
+ }
+
+ @Override
+ public ConfigVaultProvider build() {
+ this.masterPassword = masterPassword
+ .or(() -> config.get("master-password").asString().map(String::toCharArray))
+ .or(Builder::resolveMasterPassword);
+
+ return new ConfigVaultProvider(this);
+ }
+
+ /**
+ * Update this builder from provided configuration.
+ *
+ * @param config configuration to use
+ * @return updated builder
+ */
+ public Builder config(Config config) {
+ this.config = config;
+ return this;
+ }
+
+ /**
+ * Configure master password used for encryption/decryption.
+ * If master password cannot be obtained from any source (this method, configuration, system property,
+ * environment variable), encryption and decryption will not be supported.
+ *
+ * @param masterPassword password to use
+ * @return updated builder
+ */
+ public Builder masterPassword(char[] masterPassword) {
+ this.masterPassword = Optional.of(masterPassword);
+ return this;
+ }
+
+ Optional aesEncryption() {
+ return masterPassword.map(EncryptionConfig::encryptionSupport);
+ }
+ }
+}
diff --git a/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProviderService.java b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProviderService.java
new file mode 100644
index 00000000000..f835472ce2d
--- /dev/null
+++ b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/ConfigVaultProviderService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.providers.config.vault;
+
+import javax.annotation.Priority;
+
+import io.helidon.config.Config;
+import io.helidon.security.spi.SecurityProvider;
+import io.helidon.security.spi.SecurityProviderService;
+
+/**
+ * Java Service Loader implementation of a {@link io.helidon.security.Security} provider service.
+ * Do not instantiate directly.
+ */
+@Priority(5000)
+public class ConfigVaultProviderService implements SecurityProviderService {
+ /**
+ * @deprecated do not use, this should only be invoked by Java Service Loader
+ * @see ConfigVaultProvider
+ */
+ @Deprecated
+ public ConfigVaultProviderService() {
+ }
+
+ @Override
+ public String providerConfigKey() {
+ return "config-vault";
+ }
+
+ @Override
+ public Class extends SecurityProvider> providerClass() {
+ return ConfigVaultProvider.class;
+ }
+
+ @Override
+ public SecurityProvider providerInstance(Config config) {
+ return ConfigVaultProvider.create(config);
+ }
+}
diff --git a/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/package-info.java b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/package-info.java
new file mode 100644
index 00000000000..47911a96f7e
--- /dev/null
+++ b/security/providers/config-vault/src/main/java/io/helidon/security/providers/config/vault/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Vault operation backed by configuration.
+ */
+package io.helidon.security.providers.config.vault;
diff --git a/security/providers/config-vault/src/main/java/module-info.java b/security/providers/config-vault/src/main/java/module-info.java
new file mode 100644
index 00000000000..3d17d87b510
--- /dev/null
+++ b/security/providers/config-vault/src/main/java/module-info.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Vault operation backed by configuration.
+ */
+module io.helidon.security.providers.config.vault {
+ requires io.helidon.security;
+ requires io.helidon.config.encryption;
+
+ exports io.helidon.security.providers.config.vault;
+
+ provides io.helidon.security.spi.SecurityProviderService
+ with io.helidon.security.providers.config.vault.ConfigVaultProviderService;
+}
\ No newline at end of file
diff --git a/security/providers/config-vault/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService b/security/providers/config-vault/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService
new file mode 100644
index 00000000000..ded54e11a08
--- /dev/null
+++ b/security/providers/config-vault/src/main/resources/META-INF/services/io.helidon.security.spi.SecurityProviderService
@@ -0,0 +1,17 @@
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+io.helidon.security.providers.config.vault.ConfigVaultProviderService
diff --git a/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java b/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java
new file mode 100644
index 00000000000..05ea4eb03e8
--- /dev/null
+++ b/security/providers/config-vault/src/test/java/io/helidon/security/providers/config/vault/ConfigVaultProviderTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.providers.config.vault;
+
+import java.nio.charset.StandardCharsets;
+
+import io.helidon.config.Config;
+import io.helidon.config.encryption.ConfigEncryptionException;
+import io.helidon.security.Security;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+class ConfigVaultProviderTest {
+ private static Security security;
+ private static Security builtSecurity;
+
+ @BeforeAll
+ static void initClass() {
+ Config config = Config.create();
+ ConfigVaultProvider provider = ConfigVaultProvider.builder()
+ .config(config.get("security.0.config-vault"))
+ .build();
+
+ security = Security.builder()
+ .config(config.get("security"))
+ .build();
+
+ builtSecurity = Security.builder()
+ .addSecret("password", provider, ConfigVaultProvider.SecretConfig.create("configured-password"))
+ .addEncryption("config-vault-configured",
+ provider,
+ ConfigVaultProvider.EncryptionConfig.create("configured-password".toCharArray()))
+ .build();
+ }
+
+ @Test
+ void testEncryptionFromConfig() {
+ String secretString = "my secret";
+ byte[] secret = secretString.getBytes(StandardCharsets.UTF_8);
+
+ String encryptedDefault = security.encrypt("config-vault-default", secret).await();
+ String encryptedOverride = security.encrypt("config-vault-override", secret).await();
+
+ assertThat(encryptedOverride, not(encryptedDefault));
+
+ byte[] decrypted = security.decrypt("config-vault-default", encryptedDefault).await();
+ assertThat(new String(decrypted), is(secretString));
+
+ decrypted = security.decrypt("config-vault-override", encryptedOverride).await();
+ assertThat(new String(decrypted), is(secretString));
+
+ // now make sure we used a different password
+ Assertions.assertThrows(ConfigEncryptionException.class,
+ () -> security.decrypt("config-vault-override", encryptedDefault).await());
+
+ Assertions.assertThrows(ConfigEncryptionException.class,
+ () -> security.decrypt("config-vault-default", encryptedOverride).await());
+ }
+
+ @Test
+ void testSecretFromConfig() {
+ String password = security.secret("password", "default-value").await();
+
+ assertThat(password, is("secret-password"));
+ }
+
+ @Test
+ void testSecretFromBuilt() {
+ String password = builtSecurity.secret("password", "default-value").await();
+
+ assertThat(password, is("configured-password"));
+ }
+}
\ No newline at end of file
diff --git a/security/providers/config-vault/src/test/resources/application.yaml b/security/providers/config-vault/src/test/resources/application.yaml
new file mode 100644
index 00000000000..2da453f3a2c
--- /dev/null
+++ b/security/providers/config-vault/src/test/resources/application.yaml
@@ -0,0 +1,36 @@
+#
+# Copyright (c) 2021 Oracle and/or its affiliates.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+app:
+ # this could be loaded from k8s secrets, defined in env vars, or in system properties
+ password: "secret-password"
+
+security:
+ providers:
+ - config-vault:
+ master-password: "very much secret"
+ secrets:
+ - name: "password"
+ provider: "config-vault"
+ config:
+ value: "${app.password}"
+ encryption:
+ - name: "config-vault-default"
+ provider: "config-vault"
+ - name: "config-vault-override"
+ provider: "config-vault"
+ config:
+ password: "override"
diff --git a/security/providers/pom.xml b/security/providers/pom.xml
index 13f4cc0f89b..2435dfe93e2 100644
--- a/security/providers/pom.xml
+++ b/security/providers/pom.xml
@@ -42,5 +42,6 @@
oidc
idcs-mapper
oidc-common
+ config-vault
diff --git a/security/security/src/main/java/io/helidon/security/Security.java b/security/security/src/main/java/io/helidon/security/Security.java
index c402855d78f..be35e564d25 100644
--- a/security/security/src/main/java/io/helidon/security/Security.java
+++ b/security/security/src/main/java/io/helidon/security/Security.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018, 2020 Oracle and/or its affiliates.
+ * Copyright (c) 2018, 2021 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,6 +42,7 @@
import java.util.stream.Collectors;
import io.helidon.common.configurable.ThreadPoolSupplier;
+import io.helidon.common.reactive.Single;
import io.helidon.common.serviceloader.HelidonServiceLoader;
import io.helidon.config.Config;
import io.helidon.config.ConfigValue;
@@ -49,8 +50,12 @@
import io.helidon.security.spi.AuditProvider;
import io.helidon.security.spi.AuthenticationProvider;
import io.helidon.security.spi.AuthorizationProvider;
+import io.helidon.security.spi.DigestProvider;
+import io.helidon.security.spi.EncryptionProvider;
import io.helidon.security.spi.OutboundSecurityProvider;
+import io.helidon.security.spi.ProviderConfig;
import io.helidon.security.spi.ProviderSelectionPolicy;
+import io.helidon.security.spi.SecretsProvider;
import io.helidon.security.spi.SecurityProvider;
import io.helidon.security.spi.SecurityProviderService;
import io.helidon.security.spi.SubjectMappingProvider;
@@ -107,6 +112,10 @@ public class Security {
private final Config securityConfig;
private final boolean enabled;
+ private final Map>>> secrets;
+ private final Map encryptions;
+ private final Map digests;
+
@SuppressWarnings("unchecked")
private Security(Builder builder) {
this.enabled = builder.enabled;
@@ -184,6 +193,11 @@ public List> getProviders(Class
}
}
});
+
+ // secrets and transit security
+ this.secrets = Map.copyOf(builder.secrets);
+ this.encryptions = Map.copyOf(builder.encryptions);
+ this.digests = Map.copyOf(builder.digests);
}
/**
@@ -345,6 +359,131 @@ public Config configFor(String child) {
return securityConfig.get(child);
}
+ /**
+ * Encrypt bytes.
+ * This method handles the bytes in memory, and as such is not suitable
+ * for processing of large amounts of data.
+ *
+ * @param configurationName name of the configuration of this encryption
+ * @param bytesToEncrypt bytes to encrypt
+ * @return future with cipher text
+ */
+ public Single encrypt(String configurationName, byte[] bytesToEncrypt) {
+ EncryptionProvider.EncryptionSupport encryption = encryptions.get(configurationName);
+ if (encryption == null) {
+ return Single.error(new SecurityException("There is no configured encryption named " + configurationName));
+ }
+
+ return encryption.encrypt(bytesToEncrypt);
+ }
+
+ /**
+ * Decrypt cipher text.
+ * This method handles the bytes in memory, and as such is not suitable
+ * for processing of large amounts of data.
+ *
+ * @param configurationName name of the configuration of this encryption
+ * @param cipherText cipher text to decrypt
+ * @return future with decrypted bytes
+ */
+ public Single decrypt(String configurationName, String cipherText) {
+ EncryptionProvider.EncryptionSupport encryption = encryptions.get(configurationName);
+ if (encryption == null) {
+ return Single.error(new SecurityException("There is no configured encryption named " + configurationName));
+ }
+
+ return encryption.decrypt(cipherText);
+ }
+
+ /**
+ * Create a digest for the provided bytes.
+ *
+ * @param configurationName name of the digest configuration
+ * @param bytesToDigest data to digest
+ * @param preHashed whether the data is already a hash
+ * @return future with digest (such as signature or HMAC)
+ */
+ public Single digest(String configurationName, byte[] bytesToDigest, boolean preHashed) {
+ DigestProvider.DigestSupport digest = digests.get(configurationName);
+ if (digest == null) {
+ return Single.error(new SecurityException("There is no configured digest named " + configurationName));
+ }
+ return digest.digest(bytesToDigest, preHashed);
+ }
+
+ /**
+ * Create a digest for the provided raw bytes.
+ *
+ * @param configurationName name of the digest configuration
+ * @param bytesToDigest data to digest
+ * @return future with digest (such as signature or HMAC)
+ */
+ public Single digest(String configurationName, byte[] bytesToDigest) {
+ return digest(configurationName, bytesToDigest, false);
+ }
+
+ /**
+ * Verify a digest.
+ *
+ * @param configurationName name of the digest configuration
+ * @param bytesToDigest data to verify a digest for
+ * @param digest digest as provided by a third party (or another component)
+ * @param preHashed whether the data is already a hash
+ * @return future with result of verification ({@code true} means the digest is valid)
+ */
+ public Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest, boolean preHashed) {
+ DigestProvider.DigestSupport digestSupport = digests.get(configurationName);
+ if (digest == null) {
+ return Single.error(new SecurityException("There is no configured digest named " + configurationName));
+ }
+ return digestSupport.verify(bytesToDigest, preHashed, digest);
+ }
+
+ /**
+ * Verify a digest.
+ *
+ * @param configurationName name of the digest configuration
+ * @param bytesToDigest raw data to verify a digest for
+ * @param digest digest as provided by a third party (or another component)
+ * @return future with result of verification ({@code true} means the digest is valid)
+ */
+ public Single verifyDigest(String configurationName, byte[] bytesToDigest, String digest) {
+ return verifyDigest(configurationName, bytesToDigest, digest, false);
+ }
+
+ /**
+ * Get a secret.
+ *
+ * @param configurationName name of the secret configuration
+ * @return future with the secret value, or error if the secret is not configured
+ */
+ public Single> secret(String configurationName) {
+ Supplier>> singleSupplier = secrets.get(configurationName);
+ if (singleSupplier == null) {
+ return Single.error(new SecurityException("Secret \"" + configurationName + "\" is not configured."));
+ }
+
+ return singleSupplier.get();
+ }
+
+ /**
+ * Get a secret.
+ *
+ * @param configurationName name of the secret configuration
+ * @param defaultValue default value to use if secret not configured
+ * @return future with the secret value
+ */
+ public Single secret(String configurationName, String defaultValue) {
+ Supplier>> singleSupplier = secrets.get(configurationName);
+ if (singleSupplier == null) {
+ LOGGER.finest(() -> "There is no configured secret named " + configurationName + ", using default value");
+ return Single.just(defaultValue);
+ }
+
+ return singleSupplier.get()
+ .map(it -> it.orElse(defaultValue));
+ }
+
Optional extends AuthenticationProvider> resolveAtnProvider(String providerName) {
return resolveProvider(AuthenticationProvider.class, providerName);
}
@@ -417,8 +556,15 @@ public static final class Builder implements io.helidon.common.Builder
private final List> atnProviders = new LinkedList<>();
private final List> atzProviders = new LinkedList<>();
private final List> outboundProviders = new LinkedList<>();
+ private final Map> secretsProviders = new HashMap<>();
+ private final Map> encryptionProviders = new HashMap<>();
+ private final Map> digestProviders = new HashMap<>();
private final Map allProviders = new IdentityHashMap<>();
+ private final Map>>> secrets = new HashMap<>();
+ private final Map encryptions = new HashMap<>();
+ private final Map digests = new HashMap<>();
+
private NamedProvider authnProvider;
private NamedProvider authzProvider;
private SubjectMappingProvider subjectMappingProvider;
@@ -431,7 +577,7 @@ public static final class Builder implements io.helidon.common.Builder
private Supplier executorService = ThreadPoolSupplier.create();
private boolean enabled = true;
- private Set providerNames = new HashSet<>();
+ private final Set providerNames = new HashSet<>();
private Builder() {
}
@@ -783,6 +929,60 @@ public Builder addOutboundSecurityProvider(OutboundSecurityProvider provider, St
return this;
}
+ /**
+ * Add a named secret provider.
+ *
+ * @param provider provider to use
+ * @param name name of the provider for reference from configuration
+ * @return updated builder instance
+ */
+ public Builder addSecretProvider(SecretsProvider> provider, String name) {
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(name);
+
+ this.secretsProviders.put(name, provider);
+ this.allProviders.put(provider, true);
+ this.providerNames.add(name);
+
+ return this;
+ }
+
+ /**
+ * Add a named encryption provider.
+ *
+ * @param provider provider to use
+ * @param name name of the provider for reference from configuration
+ * @return updated builder instance
+ */
+ public Builder addEncryptionProvider(EncryptionProvider> provider, String name) {
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(name);
+
+ this.encryptionProviders.put(name, provider);
+ this.allProviders.put(provider, true);
+ this.providerNames.add(name);
+
+ return this;
+ }
+
+ /**
+ * Add a named digest provider (providing signatures and possibly HMAC).
+ *
+ * @param provider provider to use
+ * @param name name of the provider for reference from configuration
+ * @return updated builder instance
+ */
+ public Builder addDigestProvider(DigestProvider> provider, String name) {
+ Objects.requireNonNull(provider);
+ Objects.requireNonNull(name);
+
+ this.digestProviders.put(name, provider);
+ this.allProviders.put(provider, true);
+ this.providerNames.add(name);
+
+ return this;
+ }
+
/**
* Add an audit provider to this security runtime.
* All configured audit providers are used.
@@ -879,6 +1079,68 @@ public Security build() {
return new Security(this);
}
+ /**
+ * Add a secret to security configuration.
+ *
+ * @param name name of the secret configuration
+ * @param secretProvider security provider handling this secret
+ * @param providerConfig security provider configuration for this secret
+ * @param type of the provider specific configuration object
+ * @return updated builder instance
+ *
+ * @see #secret(String)
+ * @see #secret(String, String)
+ */
+ public Builder addSecret(String name,
+ SecretsProvider secretProvider,
+ T providerConfig) {
+
+ secrets.put(name, secretProvider.secret(providerConfig));
+ return this;
+ }
+
+ /**
+ * Add an encryption to security configuration.
+ *
+ * @param name name of the encryption configuration
+ * @param encryptionProvider security provider handling this encryption
+ * @param providerConfig security provider configuration for this encryption
+ * @param type of the provider specific configuration object
+ * @return updated builder instance
+ *
+ * @see #encrypt(String, byte[])
+ * @see #decrypt(String, String)
+ */
+ public Builder addEncryption(String name,
+ EncryptionProvider encryptionProvider,
+ T providerConfig) {
+
+ encryptions.put(name, encryptionProvider.encryption(providerConfig));
+ return this;
+ }
+
+ /**
+ * Add a signature/HMAC to security configuration.
+ *
+ * @param name name of the digest configuration
+ * @param digestProvider security provider handling this digest
+ * @param providerConfig security provider configuration for this digest
+ * @param type of the provider specific configuration object
+ * @return updated builder instance
+ *
+ * @see #digest(String, byte[])
+ * @see #digest(String, byte[], boolean)
+ * @see #verifyDigest(String, byte[], String)
+ * @see #verifyDigest(String, byte[], String, boolean)
+ */
+ public Builder addDigest(String name,
+ DigestProvider digestProvider,
+ T providerConfig) {
+
+ digests.put(name, digestProvider.digest(providerConfig));
+ return this;
+ }
+
private void fromConfig(Config config) {
config.get("enabled").asBoolean().ifPresent(this::enabled);
@@ -931,8 +1193,8 @@ private void fromConfig(Config config) {
}
// now policy
- config = config.get("provider-policy");
- ProviderSelectionPolicyType pType = config.get("type")
+ Config providerPolicyConfig = config.get("provider-policy");
+ ProviderSelectionPolicyType pType = providerPolicyConfig.get("type")
.asString()
.map(ProviderSelectionPolicyType::valueOf)
.orElse(ProviderSelectionPolicyType.FIRST);
@@ -942,14 +1204,65 @@ private void fromConfig(Config config) {
providerSelectionPolicy = FirstProviderSelectionPolicy::new;
break;
case COMPOSITE:
- providerSelectionPolicy = CompositeProviderSelectionPolicy.create(config);
+ providerSelectionPolicy = CompositeProviderSelectionPolicy.create(providerPolicyConfig);
break;
case CLASS:
- providerSelectionPolicy = findProviderSelectionPolicy(config);
+ providerSelectionPolicy = findProviderSelectionPolicy(providerPolicyConfig);
break;
default:
throw new IllegalStateException("Invalid enum option: " + pType + ", probably version mis-match");
}
+
+ config.get("secrets")
+ .asList(Config.class)
+ .ifPresent(confList -> {
+ confList.forEach(sConf -> {
+ String name = sConf.get("name").asString().get();
+ String provider = sConf.get("provider").asString().get();
+ Config secretConfig = sConf.get("config");
+ SecretsProvider> secretsProvider = secretsProviders.get(provider);
+ if (secretsProvider == null) {
+ throw new SecurityException("Provider \"" + provider
+ + "\" used for secret \"" + name + "\" not found");
+ } else {
+ secrets.put(name, secretsProvider.secret(secretConfig));
+ }
+ });
+ });
+
+ config.get("encryption")
+ .asList(Config.class)
+ .ifPresent(confList -> {
+ confList.forEach(eConf -> {
+ String name = eConf.get("name").asString().get();
+ String provider = eConf.get("provider").asString().get();
+ Config encryptionConfig = eConf.get("config");
+ EncryptionProvider> encryptionProvider = encryptionProviders.get(provider);
+ if (encryptionProvider == null) {
+ throw new SecurityException("Provider \"" + provider
+ + "\" used for encryption \"" + name + "\" not found");
+ } else {
+ encryptions.put(name, encryptionProvider.encryption(encryptionConfig));
+ }
+ });
+ });
+
+ config.get("digest")
+ .asList(Config.class)
+ .ifPresent(confList -> {
+ confList.forEach(dConf -> {
+ String name = dConf.get("name").asString().get();
+ String provider = dConf.get("provider").asString().get();
+ Config digestConfig = dConf.get("config");
+ DigestProvider> digestProvider = digestProviders.get(provider);
+ if (digestProvider == null) {
+ throw new SecurityException("Provider \"" + provider
+ + "\" used for digest \"" + name + "\" not found");
+ } else {
+ digests.put(name, digestProvider.digest(digestConfig));
+ }
+ });
+ });
}
private void providerFromConfig(Map configKeyToService,
@@ -1013,6 +1326,15 @@ private void providerFromConfig(Map configKeyTo
if (isSubjectMapper && (provider instanceof SubjectMappingProvider)) {
subjectMappingProvider((SubjectMappingProvider) provider);
}
+ if (provider instanceof SecretsProvider) {
+ addSecretProvider((SecretsProvider>) provider, name);
+ }
+ if (provider instanceof EncryptionProvider) {
+ addEncryptionProvider((EncryptionProvider>) provider, name);
+ }
+ if (provider instanceof DigestProvider) {
+ addDigestProvider((DigestProvider>) provider, name);
+ }
}
private void executorSupplier(Supplier supplier) {
diff --git a/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java b/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java
new file mode 100644
index 00000000000..7f8aa8f6e40
--- /dev/null
+++ b/security/security/src/main/java/io/helidon/security/spi/DigestProvider.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.spi;
+
+import io.helidon.common.reactive.Single;
+import io.helidon.config.Config;
+
+/**
+ * Provider that can create digests of bytes, and then verify them.
+ * The digest may be a signature, HMAC or similar.
+ *
+ * @param type of the custom configuration object
+ * @see io.helidon.security.Security#digest(String, byte[])
+ * @see io.helidon.security.Security#verifyDigest(String, byte[], String)
+ */
+public interface DigestProvider extends SecurityProvider {
+ /**
+ * Create digest support from configuration.
+ *
+ * @param config config located on the node of the specific digest {@code config} node
+ * @return digest support to digest/verify
+ */
+ DigestSupport digest(Config config);
+
+ /**
+ * Create digest support from configuration object.
+ *
+ * @param providerConfig configuring a specific digest
+ * @return digest support to digest/verify
+ */
+ DigestSupport digest(T providerConfig);
+
+ /**
+ * Function to generate a digest from bytes.
+ */
+ @FunctionalInterface
+ interface DigestFunction {
+ /**
+ * Create digest.
+ *
+ * @param data data to digest
+ * @param preHashed whether the data is already a hash ({@code true}), or the raw data ({@code false})
+ * @return future with the digest string (signature, HMAC)
+ */
+ Single apply(byte[] data, Boolean preHashed);
+ }
+
+ /**
+ * Function to verify a digest string.
+ */
+ @FunctionalInterface
+ interface VerifyFunction {
+ /**
+ * Verify digest.
+ *
+ * @param data data that was digested
+ * @param preHashed whether the data is already a hash
+ * @param digest original digest of the data (signature, HMAC)
+ * @return future with the result of verification
+ */
+ Single apply(byte[] data, Boolean preHashed, String digest);
+ }
+
+ /**
+ * Digest support created for each named digest configuration, used by {@link io.helidon.security.Security}
+ * for {@link io.helidon.security.Security#digest(String, byte[])}
+ * and {@link io.helidon.security.Security#verifyDigest(String, byte[], String)} methods.
+ */
+ class DigestSupport {
+ private final DigestFunction digestFunction;
+ private final VerifyFunction verifyFunction;
+
+ /**
+ * Digest support based on the two functions.
+ *
+ * @param digestFunction digest function
+ * @param verifyFunction verify function
+ */
+ protected DigestSupport(DigestFunction digestFunction,
+ VerifyFunction verifyFunction) {
+ this.digestFunction = digestFunction;
+ this.verifyFunction = verifyFunction;
+ }
+
+ /**
+ * Create a new support based on digest and verify functions.
+ *
+ * @param digestFunction digest function
+ * @param verifyFunction verify function
+ * @return new digest support
+ */
+ public static DigestSupport create(DigestFunction digestFunction,
+ VerifyFunction verifyFunction) {
+ return new DigestSupport(digestFunction, verifyFunction);
+ }
+
+ /**
+ * Generates a signature or an HMAC.
+ * @param bytes bytes to sign
+ * @param preHashed whether the bytes are pre-hashed
+ * @return future with the digest (signature or HMAC)
+ */
+ public Single digest(byte[] bytes, boolean preHashed) {
+ return digestFunction.apply(bytes, preHashed);
+ }
+
+ /**
+ * Verifies a signature or an HMAC.
+ *
+ * @param bytes bytes to verify
+ * @param preHashed whether the bytes are pre-hashed
+ * @param digest digest obtained from a third-part
+ * @return future with {@code true} if the digest is valid, {@code false} if not valid, and an error if not
+ * a supported digest
+ */
+ public Single verify(byte[] bytes, boolean preHashed, String digest) {
+ return verifyFunction.apply(bytes, preHashed, digest);
+ }
+ }
+}
diff --git a/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java b/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java
new file mode 100644
index 00000000000..17d007abc96
--- /dev/null
+++ b/security/security/src/main/java/io/helidon/security/spi/EncryptionProvider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.spi;
+
+import java.util.function.Function;
+
+import io.helidon.common.reactive.Single;
+import io.helidon.config.Config;
+
+/**
+ * Provider that can encrypt and decrypt secrets.
+ *
+ * @param type of the custom configuration object
+ * @see io.helidon.security.Security#encrypt(String, byte[])
+ * @see io.helidon.security.Security#decrypt(String, String)
+ */
+public interface EncryptionProvider extends SecurityProvider {
+ /**
+ * Create encryption support from configuration.
+ *
+ * @param config config located on the node of the specific encryption {@code config} node
+ * @return encryption support to encrypt/decrypt
+ */
+ EncryptionSupport encryption(Config config);
+
+ /**
+ * Create encryption support from configuration object.
+ *
+ * @param providerConfig configuring a specific encryption
+ * @return encryption support to encrypt/decrypt
+ */
+ EncryptionSupport encryption(T providerConfig);
+
+ /**
+ * Encryption support created for each named encryption configuration.
+ */
+ class EncryptionSupport {
+ private final Function> encryptionFunction;
+ private final Function> decryptionFunction;
+
+ /**
+ * Encryption support based on the two functions.
+ *
+ * @param encryptionFunction encrypts the provided bytes into cipher text
+ * @param decryptionFunction decrypts cipher text into bytes
+ */
+ protected EncryptionSupport(Function> encryptionFunction,
+ Function> decryptionFunction) {
+ this.encryptionFunction = encryptionFunction;
+ this.decryptionFunction = decryptionFunction;
+ }
+
+ /**
+ * Create a new support based on encrypt and decrypt functions.
+ *
+ * @param encryptionFunction encrypts the provided bytes into cipher text
+ * @param decryptionFunction decrypts cipher text into bytes
+ * @return new encryption support
+ */
+ public static EncryptionSupport create(Function> encryptionFunction,
+ Function> decryptionFunction) {
+ return new EncryptionSupport(encryptionFunction, decryptionFunction);
+ }
+
+ /**
+ * Encrypt the bytes.
+ *
+ * @param bytes bytes to encrypt
+ * @return future with the encrypted cipher text
+ */
+ public Single encrypt(byte[] bytes) {
+ return encryptionFunction.apply(bytes);
+ }
+
+ /**
+ * Decrypt the bytes.
+ *
+ * @param encrypted cipher text
+ * @return future with the decrypted bytes
+ */
+ public Single decrypt(String encrypted) {
+ return decryptionFunction.apply(encrypted);
+ }
+ }
+}
diff --git a/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java b/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java
new file mode 100644
index 00000000000..d3928592f9c
--- /dev/null
+++ b/security/security/src/main/java/io/helidon/security/spi/SecretsProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 Oracle and/or its affiliates.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.helidon.security.spi;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+import io.helidon.common.reactive.Single;
+import io.helidon.config.Config;
+
+/**
+ * Provider that can retrieve secrets.
+ *
+ * @param type of the custom configuration object
+ */
+public interface SecretsProvider extends SecurityProvider {
+ /**
+ * Create secret supplier from configuration.
+ *
+ * @param config config located on the node of the specific secret {@code config} node
+ * @return supplier to retrieve the secret
+ */
+ Supplier>> secret(Config config);
+
+ /**
+ * Create secret supplier from configuration object.
+ *
+ * @param providerConfig configuration of a specific secret
+ * @return supplier to retrieve the secret
+ */
+ Supplier>> secret(T providerConfig);
+}