Skip to content

Commit

Permalink
Support secret handler configuration from Keystore (#963)
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez authored Jul 17, 2023
1 parent 995879b commit e480c0c
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 49 deletions.
11 changes: 6 additions & 5 deletions documentation/src/main/docs/config/secret-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ the following dependency:
!!! example

```properties title="application.properties"
smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=somearbitrarycrazystringthatdoesnotmatter
smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key=DDne5obnfH1RSeTg71xSZg

my.secret=${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}
my.secret=${aes-gcm-nopadding::DLTb_9zxThxeT5iAQqswEl5Dn1ju4FdM9hIyVip35t5V}
```

The `${aes-gcm-nopadding::...}` `SecretKeyHandler` requires
Expand All @@ -60,9 +60,10 @@ the following dependency:

##### Configuration

| Configuration Property | Type | Default |
|--- |--- |--- |
| `smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key`<br>The encription key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. | String | |
| Configuration Property | Type | Default |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|-----------|
| `smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key`<br>The encryption key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. | String | |
| `"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode"`<br>Decode the encryption key in Base64. | boolean | false |

### Jasypt

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,13 @@ interface KeyStore {
@WithDefault("PKCS12")
String type();

String password();

Optional<String> handler();

Map<String, Alias> aliases();

interface Alias {
Optional<String> name();

Optional<String> password();

Optional<String> handler();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,47 +13,116 @@
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Set;

import org.eclipse.microprofile.config.spi.ConfigSource;

import io.smallrye.config.AbstractLocationConfigSourceFactory;
import io.smallrye.config.ConfigMessages;
import io.smallrye.config.ConfigSourceContext;
import io.smallrye.config.ConfigSourceFactory.ConfigurableConfigSourceFactory;
import io.smallrye.config.ConfigurableConfigSource;
import io.smallrye.config.ConfigSourceFactory;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.PropertiesConfigSource;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;
import io.smallrye.config.source.keystore.KeyStoreConfig.KeyStore.Alias;

public class KeyStoreConfigSourceFactory implements ConfigurableConfigSourceFactory<KeyStoreConfig> {
public class KeyStoreConfigSourceFactory implements ConfigSourceFactory {
@Override
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context, KeyStoreConfig keyStoreConfig) {
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
KeyStoreConfig keyStoreConfig = getKeyStoreConfig(context);

// A keystore may contain the encryption key for a handler, so we load keystore that do not have handlers
Map<String, KeyStoreConfig.KeyStore> prioritized = new HashMap<>();
Map<String, KeyStoreConfig.KeyStore> late = new HashMap<>();
for (final Map.Entry<String, KeyStoreConfig.KeyStore> keyStoreEntry : keyStoreConfig.keystores().entrySet()) {
if (keyStoreEntry.getValue().handler().isEmpty()) {
prioritized.put(keyStoreEntry.getKey(), keyStoreEntry.getValue());
} else {
late.put(keyStoreEntry.getKey(), keyStoreEntry.getValue());
}
}

List<ConfigSource> keyStoreSources = new ArrayList<>();
for (Map.Entry<String, KeyStoreConfig.KeyStore> keyStoreEntry : keyStoreConfig.keystores().entrySet()) {
KeyStoreConfig.KeyStore keyStore = keyStoreEntry.getValue();
keyStoreSources.add(new ConfigurableConfigSource(new AbstractLocationConfigSourceFactory() {
@Override
protected String[] getFileExtensions() {
return new String[0];
}
for (Map.Entry<String, KeyStoreConfig.KeyStore> keyStoreEntry : prioritized.entrySet()) {
for (final ConfigSource configSource : loadKeyStoreSources(context, keyStoreEntry.getKey(),
keyStoreEntry.getValue())) {
keyStoreSources.add(configSource);
}
}

@Override
protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException {
return new UrlKeyStoreConfigSource(url, ordinal).loadKeyStore(keyStore);
}
ConfigSourceContext keyStoreContext = new ConfigSourceContext() {
final SmallRyeConfig contextConfig = new SmallRyeConfigBuilder()
.withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context))
.withSources(keyStoreSources).build();

@Override
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
return loadConfigSources(keyStore.path(), 100);
}
}));
@Override
public ConfigValue getValue(final String name) {
return contextConfig.getConfigValue(name);
}

@Override
public List<String> getProfiles() {
throw new UnsupportedOperationException();
}

@Override
public Iterator<String> iterateNames() {
return contextConfig.getPropertyNames().iterator();
}
};

for (Map.Entry<String, KeyStoreConfig.KeyStore> keyStoreEntry : late.entrySet()) {
for (final ConfigSource configSource : loadKeyStoreSources(keyStoreContext, keyStoreEntry.getKey(),
keyStoreEntry.getValue())) {
keyStoreSources.add(configSource);
}
}

return keyStoreSources;
}

private static KeyStoreConfig getKeyStoreConfig(final ConfigSourceContext context) {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(new ConfigSourceContext.ConfigSourceContextConfigSource(context))
.withMapping(KeyStoreConfig.class)
.withMappingIgnore("smallrye.config.source.keystore.*.password")
.build();
return config.getConfigMapping(KeyStoreConfig.class);
}

private static Iterable<ConfigSource> loadKeyStoreSources(final ConfigSourceContext context, final String name,
final KeyStoreConfig.KeyStore keyStore) {
return new AbstractLocationConfigSourceFactory() {
@Override
protected String[] getFileExtensions() {
return new String[0];
}

@Override
protected ConfigSource loadConfigSource(final URL url, final int ordinal) throws IOException {
// Avoid caching the keystore password
String passwordName = "smallrye.config.source.keystore." + name + ".password";
ConfigValue password = context.getValue(passwordName);
if (password == null || password.getValue() == null) {
throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(name));
}
return new UrlKeyStoreConfigSource(url, ordinal).loadKeyStore(keyStore, password.getValue().toCharArray());
}

@Override
public Iterable<ConfigSource> getConfigSources(final ConfigSourceContext context) {
return loadConfigSources(keyStore.path(), 100);
}

}.getConfigSources(context);
}

private static class UrlKeyStoreConfigSource implements ConfigSource {
private final URL url;
private final int ordinal;
Expand All @@ -63,10 +132,10 @@ private static class UrlKeyStoreConfigSource implements ConfigSource {
this.ordinal = ordinal;
}

ConfigSource loadKeyStore(KeyStoreConfig.KeyStore keyStoreConfig) throws IOException {
ConfigSource loadKeyStore(KeyStoreConfig.KeyStore keyStoreConfig, char[] password) throws IOException {
try {
KeyStore keyStore = KeyStore.getInstance(keyStoreConfig.type());
keyStore.load(url.openStream(), keyStoreConfig.password().toCharArray());
keyStore.load(url.openStream(), password);

Map<String, String> properties = new HashMap<>();
Enumeration<String> aliases = keyStore.aliases();
Expand All @@ -78,20 +147,14 @@ public Optional<String> name() {
return Optional.of(alias);
}

@Override
public Optional<String> password() {
return Optional.of(keyStoreConfig.password());
}

@Override
public Optional<String> handler() {
return keyStoreConfig.handler();
}
});

if (keyStore.isKeyEntry(alias)) {
Key key = keyStore.getKey(alias,
aliasConfig.password().orElse(keyStoreConfig.password()).toCharArray());
Key key = keyStore.getKey(alias, password);
String encoded;
Optional<String> handler = aliasConfig.handler();
if (handler.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ void multipleHandlers() {
Map<String, String> properties = Map.of(
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key",
"c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI",
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true",
"aes-gcm-nopadding.secret", "${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}",
"smallrye.config.secret-handler.jasypt.password", "jasypt",
"smallrye.config.secret-handler.jasypt.algorithm", "PBEWithHMACSHA512AndAES_256",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public SecretKeysHandler getSecretKeysHandler(final ConfigSourceContext context)
throw new NoSuchElementException(ConfigMessages.msg.propertyNotFound(ENCRYPTION_KEY));
}

boolean decode = true;
boolean decode = false;
ConfigValue plain = context.getValue("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode");
if (plain != null && plain.getValue() != null) {
decode = Converters.getImplicitConverter(Boolean.class).convert(plain.getValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@
class AESGCMNoPaddingSecretKeysHandlerTest {
@Test
void handler() {

SmallRyeConfig config = new SmallRyeConfigBuilder()
.addDefaultInterceptors()
.addDiscoveredSecretKeysHandlers()
.withDefaultValues(Map.of(
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key",
"c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI",
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true",
"my.secret", "${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}",
"my.expression", "${not.found:default}",
"another.expression", "${my.expression}"))
Expand All @@ -43,16 +43,15 @@ void keystore() {
.addDiscoveredSources()
.addDiscoveredSecretKeysHandlers()
.withDefaultValues(Map.of(
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key",
"somearbitrarycrazystringthatdoesnotmatter",
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "false",
"smallrye.config.source.keystore.test.path", "keystore",
"smallrye.config.source.keystore.test.password", "secret",
"smallrye.config.source.keystore.test.handler", "aes-gcm-nopadding"))
"smallrye.config.source.keystore.\"properties\".path", "properties",
"smallrye.config.source.keystore.\"properties\".password", "arealpassword",
"smallrye.config.source.keystore.\"properties\".handler", "aes-gcm-nopadding",
"smallrye.config.source.keystore.\"key\".path", "key",
"smallrye.config.source.keystore.\"key\".password", "anotherpassword"))
.build();

ConfigValue secret = config.getConfigValue("my.secret");
assertEquals("decoded", secret.getValue());
assertEquals("1234", secret.getValue());
}

@Test
Expand Down Expand Up @@ -86,8 +85,10 @@ void configurableSource() {
"smallrye.config.source.keystore.test.password", "secret",
"smallrye.config.source.keystore.test.handler", "aes-gcm-nopadding"))
.withSources((ConfigSourceFactory) context -> List.of(
new PropertiesConfigSource(Map.of("smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key",
"c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI"), "", 0)))
new PropertiesConfigSource(Map.of(
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key",
"c29tZWFyYml0cmFyeWNyYXp5c3RyaW5ndGhhdGRvZXNub3RtYXR0ZXI",
"smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key-decode", "true"), "", 0)))
.build();

ConfigValue secret = config.getConfigValue("my.secret");
Expand Down
Binary file added utils/crypto/src/test/resources/key
Binary file not shown.
Binary file added utils/crypto/src/test/resources/properties
Binary file not shown.

0 comments on commit e480c0c

Please sign in to comment.