From 77d221da0416807d9d2a0043ecc743475e6a5e40 Mon Sep 17 00:00:00 2001 From: Ryan Baxter Date: Fri, 5 Apr 2024 16:04:04 -0400 Subject: [PATCH] Add the ability for FailsafeTextEncryptor to have a delegate. (#1349) Related to https://github.com/spring-cloud/spring-cloud-config/issues/2330 Co-authored-by: Ryan Baxter <524254+ryanjbaxter@users.noreply.github.com> --- .../bootstrap/encrypt/TextEncryptorUtils.java | 26 +++++++++++++++++ .../encrypt/EncryptionIntegrationTests.java | 22 +++++++++++++++ .../test/TestConfigDataLocationResolver.java | 28 ++++++++++++++++++- .../resources/application-failsafe.properties | 1 + 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 spring-cloud-context/src/test/resources/application-failsafe.properties diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/TextEncryptorUtils.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/TextEncryptorUtils.java index 5571e46db..71ace819b 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/TextEncryptorUtils.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/encrypt/TextEncryptorUtils.java @@ -177,14 +177,40 @@ public static boolean isLegacyBootstrap(Environment environment) { */ public static class FailsafeTextEncryptor implements TextEncryptor { + private TextEncryptor delegate; + + /** + * You can set a delegate that can be used to encrypt/decrypt values if later on + * after the initial initialization of the app we have the necessary values to + * create a proper {@link TextEncryptor}. Depending on where the encryption keys + * are set we might not have the right values to create a {@link TextEncryptor} + * (this can happen if the keys are in application.properties for example, but we + * create the text encryptor during Bootstrap). The delegate functionality allows + * us the option to set the delegate later on when we have the necessary values. + * @param delegate The TextEncryptor to use for encryption/decryption + */ + public void setDelegate(TextEncryptor delegate) { + this.delegate = delegate; + } + + public TextEncryptor getDelegate() { + return this.delegate; + } + @Override public String encrypt(String text) { + if (this.delegate != null) { + return this.delegate.encrypt(text); + } throw new UnsupportedOperationException( "No encryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); } @Override public String decrypt(String encryptedText) { + if (this.delegate != null) { + return this.delegate.decrypt(encryptedText); + } throw new UnsupportedOperationException( "No decryption for FailsafeTextEncryptor. Did you configure the keystore correctly?"); } diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java index fd3635b47..2d6c99385 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/bootstrap/encrypt/EncryptionIntegrationTests.java @@ -142,6 +142,28 @@ public void decryptAfterRefresh() { TestConfigDataLocationResolver.config.clear(); } + @Test + public void failsafeTextEncryptor() { + ConfigurableApplicationContext context = new SpringApplicationBuilder( + EncryptionIntegrationTests.TestConfiguration.class).web(WebApplicationType.NONE).properties().run(); + then(context.getBean(TextEncryptor.class)).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class); + } + + @Test + public void failsafeShouldHaveDelegate() { + TestConfigDataLocationResolver.config.put("foo.password", + "{cipher}bf29452295df354e6153c5b31b03ef23c70e55fba24299aa85c63438f1c43c95"); + ConfigurableApplicationContext context = new SpringApplicationBuilder(TestAutoConfiguration.class) + .web(WebApplicationType.NONE) + .properties("spring.config.import=testdatasource:,classpath:application-failsafe.properties", + "createfailsafedelegate=true") + .run(); + ConfigurableEnvironment env = context.getBean(ConfigurableEnvironment.class); + then(env.getProperty("foo.password")).isEqualTo("test"); + context.close(); + TestConfigDataLocationResolver.config.clear(); + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(PasswordProperties.class) protected static class TestConfiguration { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/context/test/TestConfigDataLocationResolver.java b/spring-cloud-context/src/test/java/org/springframework/cloud/context/test/TestConfigDataLocationResolver.java index 6738c2307..eeb8d4796 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/context/test/TestConfigDataLocationResolver.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/context/test/TestConfigDataLocationResolver.java @@ -29,8 +29,15 @@ import org.springframework.boot.context.config.ConfigDataLocationResolverContext; import org.springframework.boot.context.config.ConfigDataResourceNotFoundException; import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.cloud.bootstrap.encrypt.KeyProperties; +import org.springframework.cloud.bootstrap.encrypt.RsaProperties; +import org.springframework.cloud.bootstrap.encrypt.TextEncryptorUtils; +import org.springframework.core.Ordered; +import org.springframework.security.crypto.encrypt.TextEncryptor; -public class TestConfigDataLocationResolver implements ConfigDataLocationResolver { +import static org.assertj.core.api.Assertions.assertThat; + +public class TestConfigDataLocationResolver implements ConfigDataLocationResolver, Ordered { public static AtomicInteger count = new AtomicInteger(1); @@ -52,6 +59,20 @@ public List resolve(ConfigDataLocationResolverContext co context.getBootstrapContext().registerIfAbsent(aClass, supplier); } String myplaceholder = context.getBinder().bind("myplaceholder", Bindable.of(String.class)).orElse("notfound"); + boolean createFailsafeDelegate = context.getBinder().bind("createfailsafedelegate", Bindable.of(Boolean.class)) + .orElse(Boolean.FALSE); + if (createFailsafeDelegate) { + assertThat(context.getBootstrapContext().isRegistered(TextEncryptor.class)).isTrue(); + TextEncryptor textEncryptor = context.getBootstrapContext().get(TextEncryptor.class); + assertThat(textEncryptor).isInstanceOf(TextEncryptorUtils.FailsafeTextEncryptor.class); + KeyProperties keyProperties = context.getBinder().bindOrCreate(KeyProperties.PREFIX, + Bindable.of(KeyProperties.class)); + assertThat(TextEncryptorUtils.keysConfigured(keyProperties)).isTrue(); + RsaProperties rsaProperties = context.getBinder().bindOrCreate(RsaProperties.PREFIX, + Bindable.of(RsaProperties.class)); + ((TextEncryptorUtils.FailsafeTextEncryptor) textEncryptor) + .setDelegate(TextEncryptorUtils.createTextEncryptor(keyProperties, rsaProperties)); + } HashMap props = new HashMap<>(config); props.put(TestEnvPostProcessor.EPP_VALUE, count.get()); if (count.get() == 99 && myplaceholder.contains("${vcap")) { @@ -61,4 +82,9 @@ public List resolve(ConfigDataLocationResolverContext co return Collections.singletonList(new TestConfigDataResource(props)); } + @Override + public int getOrder() { + return -1; + } + } diff --git a/spring-cloud-context/src/test/resources/application-failsafe.properties b/spring-cloud-context/src/test/resources/application-failsafe.properties new file mode 100644 index 000000000..36e677340 --- /dev/null +++ b/spring-cloud-context/src/test/resources/application-failsafe.properties @@ -0,0 +1 @@ +encrypt.key=pie