From 65d3b0d71cc26d7f57809060448a37e926eb16e9 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Mon, 11 Jan 2021 13:42:41 -0700 Subject: [PATCH] Add ResourceKeyConverterAdapter Simplifies publishing RsaKeyConverters with @ConfigurationPropertiesBinding Issue gh-9316 --- .../RsaKeyConversionServicePostProcessor.java | 74 ++--------- .../ResourceKeyConverterAdapter.java | 102 +++++++++++++++ .../ResourceKeyConverterAdapterTests.java | 117 ++++++++++++++++++ .../security/converter/simple.priv | 28 +++++ 4 files changed, 258 insertions(+), 63 deletions(-) create mode 100644 core/src/main/java/org/springframework/security/converter/ResourceKeyConverterAdapter.java create mode 100644 core/src/test/java/org/springframework/security/converter/ResourceKeyConverterAdapterTests.java create mode 100644 core/src/test/resources/org/springframework/security/converter/simple.priv diff --git a/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java index 5174a62bc5..c9a4924ee9 100644 --- a/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java +++ b/config/src/main/java/org/springframework/security/config/crypto/RsaKeyConversionServicePostProcessor.java @@ -18,11 +18,6 @@ package org.springframework.security.config.crypto; import java.beans.PropertyEditor; import java.beans.PropertyEditorSupport; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.nio.charset.StandardCharsets; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; @@ -32,9 +27,8 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; +import org.springframework.security.converter.ResourceKeyConverterAdapter; import org.springframework.security.converter.RsaKeyConverters; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -50,11 +44,15 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc private static final String CONVERSION_SERVICE_BEAN_NAME = "conversionService"; - private ResourceLoader resourceLoader = new DefaultResourceLoader(); + private ResourceKeyConverterAdapter x509 = new ResourceKeyConverterAdapter<>(RsaKeyConverters.x509()); + + private ResourceKeyConverterAdapter pkcs8 = new ResourceKeyConverterAdapter<>( + RsaKeyConverters.pkcs8()); public void setResourceLoader(ResourceLoader resourceLoader) { Assert.notNull(resourceLoader, "resourceLoader cannot be null"); - this.resourceLoader = resourceLoader; + this.x509.setResourceLoader(resourceLoader); + this.pkcs8.setResourceLoader(resourceLoader); } @Override @@ -62,18 +60,16 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc if (hasUserDefinedConversionService(beanFactory)) { return; } - Converter pkcs8 = pkcs8(); - Converter x509 = x509(); ConversionService service = beanFactory.getConversionService(); if (service instanceof ConverterRegistry) { ConverterRegistry registry = (ConverterRegistry) service; - registry.addConverter(String.class, RSAPrivateKey.class, pkcs8); - registry.addConverter(String.class, RSAPublicKey.class, x509); + registry.addConverter(String.class, RSAPrivateKey.class, this.pkcs8); + registry.addConverter(String.class, RSAPublicKey.class, this.x509); } else { beanFactory.addPropertyEditorRegistrar((registry) -> { - registry.registerCustomEditor(RSAPublicKey.class, new ConverterPropertyEditorAdapter<>(x509)); - registry.registerCustomEditor(RSAPrivateKey.class, new ConverterPropertyEditorAdapter<>(pkcs8)); + registry.registerCustomEditor(RSAPublicKey.class, new ConverterPropertyEditorAdapter<>(this.x509)); + registry.registerCustomEditor(RSAPrivateKey.class, new ConverterPropertyEditorAdapter<>(this.pkcs8)); }); } } @@ -83,54 +79,6 @@ public class RsaKeyConversionServicePostProcessor implements BeanFactoryPostProc && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class); } - private Converter pkcs8() { - Converter pemInputStreamConverter = pemInputStreamConverter(); - Converter pkcs8KeyConverter = autoclose(RsaKeyConverters.pkcs8()); - return pair(pemInputStreamConverter, pkcs8KeyConverter); - } - - private Converter x509() { - Converter pemInputStreamConverter = pemInputStreamConverter(); - Converter x509KeyConverter = autoclose(RsaKeyConverters.x509()); - return pair(pemInputStreamConverter, x509KeyConverter); - } - - private Converter pemInputStreamConverter() { - return (source) -> source.startsWith("-----") ? toInputStream(source) - : toInputStream(this.resourceLoader.getResource(source)); - } - - private InputStream toInputStream(String raw) { - return new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)); - } - - private InputStream toInputStream(Resource resource) { - try { - return resource.getInputStream(); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - } - - private Converter autoclose(Converter inputStreamKeyConverter) { - return (inputStream) -> { - try (InputStream is = inputStream) { - return inputStreamKeyConverter.convert(is); - } - catch (IOException ex) { - throw new UncheckedIOException(ex); - } - }; - } - - private Converter pair(Converter one, Converter two) { - return (source) -> { - I intermediary = one.convert(source); - return two.convert(intermediary); - }; - } - private static class ConverterPropertyEditorAdapter extends PropertyEditorSupport { private final Converter converter; diff --git a/core/src/main/java/org/springframework/security/converter/ResourceKeyConverterAdapter.java b/core/src/main/java/org/springframework/security/converter/ResourceKeyConverterAdapter.java new file mode 100644 index 0000000000..704689b250 --- /dev/null +++ b/core/src/main/java/org/springframework/security/converter/ResourceKeyConverterAdapter.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.converter; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.security.Key; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; + +/** + * Adapts any {@link Key} {@link Converter} into once that will first extract that key + * from a resource. + * + * By default, keys can be read from the file system, the classpath, and from HTTP + * endpoints. This can be customized by providing a {@link ResourceLoader} + * + * @author Josh Cummings + * @since 5.5 + */ +public class ResourceKeyConverterAdapter implements Converter { + + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + private final Converter delegate; + + /** + * Construct a {@link ResourceKeyConverterAdapter} with the provided parameters + * @param delegate converts a stream of key material into a {@link Key} + */ + public ResourceKeyConverterAdapter(Converter delegate) { + this.delegate = pemInputStreamConverter().andThen(autoclose(delegate)); + } + + /** + * {@inheritDoc} + */ + @Override + public T convert(String source) { + return this.delegate.convert(source); + } + + /** + * Use this {@link ResourceLoader} to read the key material + * @param resourceLoader the {@link ResourceLoader} to use + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + Assert.notNull(resourceLoader, "resourceLoader cannot be null"); + this.resourceLoader = resourceLoader; + } + + private Converter pemInputStreamConverter() { + return (source) -> source.startsWith("-----") ? toInputStream(source) + : toInputStream(this.resourceLoader.getResource(source)); + } + + private InputStream toInputStream(String raw) { + return new ByteArrayInputStream(raw.getBytes(StandardCharsets.UTF_8)); + } + + private InputStream toInputStream(Resource resource) { + try { + return resource.getInputStream(); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private Converter autoclose(Converter inputStreamKeyConverter) { + return (inputStream) -> { + try (InputStream is = inputStream) { + return inputStreamKeyConverter.convert(is); + } + catch (IOException ex) { + throw new UncheckedIOException(ex); + } + }; + } + +} diff --git a/core/src/test/java/org/springframework/security/converter/ResourceKeyConverterAdapterTests.java b/core/src/test/java/org/springframework/security/converter/ResourceKeyConverterAdapterTests.java new file mode 100644 index 0000000000..202ff2109c --- /dev/null +++ b/core/src/test/java/org/springframework/security/converter/ResourceKeyConverterAdapterTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * 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 + * + * https://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 org.springframework.security.converter; + +import java.security.interfaces.RSAPrivateKey; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResourceKeyConverterAdapter} + */ +public class ResourceKeyConverterAdapterTests { + + // @formatter:off + private static final String PKCS8_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCMk7CKSTfu3QoV\n" + + "HoPVXxwZO+qweztd36cVWYqGOZinrOR2crWFu50AgR2CsdIH0+cqo7F4Vx7/3O8i\n" + + "RpYYZPe2VoO5sumzJt8P6fS80/TAKjhJDAqgZKRJTgGN8KxCM6p/aJli1ZeDBqiV\n" + + "v7vJJe+ZgJuPGRS+HMNa/wPxEkqqXsglcJcQV1ZEtfKXSHB7jizKpRL38185SyAC\n" + + "pwyjvBu6Cmm1URfhQo88mf239ONh4dZ2HoDfzN1q6Ssu4F4hgutxr9B0DVLDP5u+\n" + + "WFrm3nsJ76zf99uJ+ntMUHJ+bY+gOjSlVWIVBIZeAaEGKCNWRk/knjvjbijpvm3U\n" + + "acGlgdL3AgMBAAECggEACxxxS7zVyu91qI2s5eSKmAQAXMqgup6+2hUluc47nqUv\n" + + "uZz/c/6MPkn2Ryo+65d4IgqmMFjSfm68B/2ER5FTcvoLl1Xo2twrrVpUmcg3BClS\n" + + "IZPuExdhVNnxjYKEWwcyZrehyAoR261fDdcFxLRW588efIUC+rPTTRHzAc7sT+Ln\n" + + "t/uFeYNWJm3LaegOLoOmlMAhJ5puAWSN1F0FxtRf/RVgzbLA9QC975SKHJsfWCSr\n" + + "IZyPsdeaqomKaF65l8nfqlE0Ua2L35gIOGKjUwb7uUE8nI362RWMtYdoi3zDDyoY\n" + + "hSFbgjylCHDM0u6iSh6KfqOHtkYyJ8tUYgVWl787wQKBgQDYO3wL7xuDdD101Lyl\n" + + "AnaDdFB9fxp83FG1cWr+t7LYm9YxGfEUsKHAJXN6TIayDkOOoVwIl+Gz0T3Z06Bm\n" + + "eBGLrB9mrVA7+C7NJwu5gTMlzP6HxUR9zKJIQ/VB1NUGM77LSmvOFbHc9Q0+z8EH\n" + + "X5WO516a3Z7lNtZJcCoPOtu2rwKBgQCmbj41Fh+SSEUApCEKms5ETRpe7LXQlJgx\n" + + "yW7zcJNNuIb1C3vBLPxjiOTMgYKOeMg5rtHTGLT43URHLh9ArjawasjSAr4AM3J4\n" + + "xpoi/sKGDdiKOsuDWIGfzdYL8qyTHSdpZLQsCTMRiRYgAHZFPgNa7SLZRfZicGlr\n" + + "GHN1rJW6OQKBgEjiM/upyrJSWeypUDSmUeAZMpA6aWkwsfHgmtnkfUn5rQa74cDB\n" + + "kKO9e+D7LmOR3z+SL/1NhGwh2SE07dncGr3jdGodfO/ZxZyszozmeaECKcEFwwJM\n" + + "GV8WWPKplGwUwPiwywmZ0mvRxXcoe73KgBS88+xrSwWjqDL0tZiQlEJNAoGATkei\n" + + "GMQMG3jEg9Wu+NbxV6zQT3+U0MNjhl9RQU1c63x0dcNt9OFc4NAdlZcAulRTENaK\n" + + "OHjxffBM0hH+fySx8m53gFfr2BpaqDX5f6ZGBlly1SlsWZ4CchCVsc71nshipi7I\n" + + "k8HL9F5/OpQdDNprJ5RMBNfkWE65Nrcsb1e6oPkCgYAxwgdiSOtNg8PjDVDmAhwT\n" + + "Mxj0Dtwi2fAqQ76RVrrXpNp3uCOIAu4CfruIb5llcJ3uak0ZbnWri32AxSgk80y3\n" + + "EWiRX/WEDu5znejF+5O3pI02atWWcnxifEKGGlxwkcMbQdA67MlrJLFaSnnGpNXo\n" + + "yPfcul058SOqhafIZQMEKQ==\n" + + "-----END PRIVATE KEY-----"; + // @formatter:on + + private static final String PKCS8_PRIVATE_KEY_LOCATION = "classpath:org/springframework/security/converter/simple.priv"; + + private ResourceKeyConverterAdapter adapter; + + @Before + public void setup() { + this.adapter = new ResourceKeyConverterAdapter<>(RsaKeyConverters.pkcs8()); + } + + @Test + public void convertWhenUsingAdapterForRawKeyThenOk() { + RSAPrivateKey key = this.adapter.convert(PKCS8_PRIVATE_KEY); + assertThat(key.getModulus().bitLength()).isEqualTo(2048); + } + + @Test + public void convertWhenReferringToClasspathPublicKeyThenConverts() { + RSAPrivateKey key = this.adapter.convert(PKCS8_PRIVATE_KEY_LOCATION); + assertThat(key.getModulus().bitLength()).isEqualTo(2048); + } + + @Test + public void convertWhenReferringToClasspathPrivateKeyThenConverts() { + this.adapter.setResourceLoader(new CustomResourceLoader()); + RSAPrivateKey key = this.adapter.convert("custom:simple.priv"); + assertThat(key.getModulus().bitLength()).isEqualTo(2048); + } + + private static class CustomResourceLoader implements ResourceLoader { + + private final ResourceLoader delegate = new DefaultResourceLoader(); + + @Override + public Resource getResource(String location) { + if (location.startsWith("classpath:")) { + return this.delegate.getResource(location); + } + else if (location.startsWith("custom:")) { + String[] parts = location.split(":"); + return this.delegate.getResource("classpath:org/springframework/security/converter/" + parts[1]); + } + throw new IllegalArgumentException("unsupported resource"); + } + + @Override + public ClassLoader getClassLoader() { + return this.delegate.getClassLoader(); + } + + } + +} diff --git a/core/src/test/resources/org/springframework/security/converter/simple.priv b/core/src/test/resources/org/springframework/security/converter/simple.priv new file mode 100644 index 0000000000..7177ea578d --- /dev/null +++ b/core/src/test/resources/org/springframework/security/converter/simple.priv @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCMk7CKSTfu3QoV +HoPVXxwZO+qweztd36cVWYqGOZinrOR2crWFu50AgR2CsdIH0+cqo7F4Vx7/3O8i +RpYYZPe2VoO5sumzJt8P6fS80/TAKjhJDAqgZKRJTgGN8KxCM6p/aJli1ZeDBqiV +v7vJJe+ZgJuPGRS+HMNa/wPxEkqqXsglcJcQV1ZEtfKXSHB7jizKpRL38185SyAC +pwyjvBu6Cmm1URfhQo88mf239ONh4dZ2HoDfzN1q6Ssu4F4hgutxr9B0DVLDP5u+ +WFrm3nsJ76zf99uJ+ntMUHJ+bY+gOjSlVWIVBIZeAaEGKCNWRk/knjvjbijpvm3U +acGlgdL3AgMBAAECggEACxxxS7zVyu91qI2s5eSKmAQAXMqgup6+2hUluc47nqUv +uZz/c/6MPkn2Ryo+65d4IgqmMFjSfm68B/2ER5FTcvoLl1Xo2twrrVpUmcg3BClS +IZPuExdhVNnxjYKEWwcyZrehyAoR261fDdcFxLRW588efIUC+rPTTRHzAc7sT+Ln +t/uFeYNWJm3LaegOLoOmlMAhJ5puAWSN1F0FxtRf/RVgzbLA9QC975SKHJsfWCSr +IZyPsdeaqomKaF65l8nfqlE0Ua2L35gIOGKjUwb7uUE8nI362RWMtYdoi3zDDyoY +hSFbgjylCHDM0u6iSh6KfqOHtkYyJ8tUYgVWl787wQKBgQDYO3wL7xuDdD101Lyl +AnaDdFB9fxp83FG1cWr+t7LYm9YxGfEUsKHAJXN6TIayDkOOoVwIl+Gz0T3Z06Bm +eBGLrB9mrVA7+C7NJwu5gTMlzP6HxUR9zKJIQ/VB1NUGM77LSmvOFbHc9Q0+z8EH +X5WO516a3Z7lNtZJcCoPOtu2rwKBgQCmbj41Fh+SSEUApCEKms5ETRpe7LXQlJgx +yW7zcJNNuIb1C3vBLPxjiOTMgYKOeMg5rtHTGLT43URHLh9ArjawasjSAr4AM3J4 +xpoi/sKGDdiKOsuDWIGfzdYL8qyTHSdpZLQsCTMRiRYgAHZFPgNa7SLZRfZicGlr +GHN1rJW6OQKBgEjiM/upyrJSWeypUDSmUeAZMpA6aWkwsfHgmtnkfUn5rQa74cDB +kKO9e+D7LmOR3z+SL/1NhGwh2SE07dncGr3jdGodfO/ZxZyszozmeaECKcEFwwJM +GV8WWPKplGwUwPiwywmZ0mvRxXcoe73KgBS88+xrSwWjqDL0tZiQlEJNAoGATkei +GMQMG3jEg9Wu+NbxV6zQT3+U0MNjhl9RQU1c63x0dcNt9OFc4NAdlZcAulRTENaK +OHjxffBM0hH+fySx8m53gFfr2BpaqDX5f6ZGBlly1SlsWZ4CchCVsc71nshipi7I +k8HL9F5/OpQdDNprJ5RMBNfkWE65Nrcsb1e6oPkCgYAxwgdiSOtNg8PjDVDmAhwT +Mxj0Dtwi2fAqQ76RVrrXpNp3uCOIAu4CfruIb5llcJ3uak0ZbnWri32AxSgk80y3 +EWiRX/WEDu5znejF+5O3pI02atWWcnxifEKGGlxwkcMbQdA67MlrJLFaSnnGpNXo +yPfcul058SOqhafIZQMEKQ== +-----END PRIVATE KEY-----