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 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 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); +}