This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c3876cf57482e186b47a3f300237c0436638cb13 Author: Tran Tien Duc <[email protected]> AuthorDate: Thu Jun 6 10:21:20 2019 +0700 JAMES-2146 a new component dedicated to load java security keys from configuration And later, use it in new KeyStore StartUpCheck --- .../org/apache/james/jmap/JMAPCommonModule.java | 2 + .../apache/james/jmap/crypto/AsymmetricKeys.java | 42 ++++++++++ .../james/jmap/crypto/JamesSignatureHandler.java | 2 +- ...ignatureHandler.java => SecurityKeyLoader.java} | 61 ++------------ .../james/jmap/crypto/ClassLoaderFileSystem.java | 45 ++++++++++ .../james/jmap/crypto/SecurityKeyLoaderTest.java | 96 ++++++++++++++++++++++ 6 files changed, 194 insertions(+), 54 deletions(-) diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java index c562d60..9a44d3a 100644 --- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java +++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPCommonModule.java @@ -27,6 +27,7 @@ import org.apache.james.jmap.api.SimpleTokenManager; import org.apache.james.jmap.api.access.AccessTokenRepository; import org.apache.james.jmap.crypto.AccessTokenManagerImpl; import org.apache.james.jmap.crypto.JamesSignatureHandler; +import org.apache.james.jmap.crypto.SecurityKeyLoader; import org.apache.james.jmap.crypto.SignatureHandler; import org.apache.james.jmap.crypto.SignedTokenFactory; import org.apache.james.jmap.crypto.SignedTokenManager; @@ -64,6 +65,7 @@ public class JMAPCommonModule extends AbstractModule { bind(MessagePreviewGenerator.class).in(Scopes.SINGLETON); bind(MessageContentExtractor.class).in(Scopes.SINGLETON); bind(HeadersAuthenticationExtractor.class).in(Scopes.SINGLETON); + bind(SecurityKeyLoader.class).in(Scopes.SINGLETON); bind(SignatureHandler.class).to(JamesSignatureHandler.class); bind(ZonedDateTimeProvider.class).to(DefaultZonedDateTimeProvider.class); diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/AsymmetricKeys.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/AsymmetricKeys.java new file mode 100644 index 0000000..301fd4e --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/AsymmetricKeys.java @@ -0,0 +1,42 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.jmap.crypto; + +import java.security.PrivateKey; +import java.security.PublicKey; + +class AsymmetricKeys { + + private final PrivateKey privateKey; + private final PublicKey publicKey; + + AsymmetricKeys(PrivateKey privateKey, PublicKey publicKey) { + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + PrivateKey getPrivateKey() { + return privateKey; + } + + PublicKey getPublicKey() { + return publicKey; + } +} diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java index 37d3456..82ee210 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java @@ -50,7 +50,7 @@ public class JamesSignatureHandler implements SignatureHandler { public static final String ALIAS = "james"; public static final String ALGORITHM = "SHA1withRSA"; public static final String JKS = "JKS"; - + private final FileSystem fileSystem; private final JMAPConfiguration jmapConfiguration; diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SecurityKeyLoader.java similarity index 53% copy from server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java copy to server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SecurityKeyLoader.java index 37d3456..0bb359c 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/JamesSignatureHandler.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/crypto/SecurityKeyLoader.java @@ -20,52 +20,37 @@ package org.apache.james.jmap.crypto; import java.io.InputStream; -import java.security.InvalidKeyException; import java.security.Key; import java.security.KeyStore; import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; -import java.security.Signature; -import java.security.SignatureException; import java.security.cert.Certificate; -import java.util.Base64; import java.util.Optional; import javax.inject.Inject; import org.apache.james.filesystem.api.FileSystem; import org.apache.james.jmap.JMAPConfiguration; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -public class JamesSignatureHandler implements SignatureHandler { +public class SecurityKeyLoader { - private static final Logger LOGGER = LoggerFactory.getLogger(JamesSignatureHandler.class); + private static final String ALIAS = "james"; + private static final String JKS = "JKS"; - public static final String ALIAS = "james"; - public static final String ALGORITHM = "SHA1withRSA"; - public static final String JKS = "JKS"; - private final FileSystem fileSystem; private final JMAPConfiguration jmapConfiguration; - private PrivateKey privateKey; - private PublicKey publicKey; - - + @VisibleForTesting @Inject - @VisibleForTesting JamesSignatureHandler(FileSystem fileSystem, JMAPConfiguration jmapConfiguration) { + SecurityKeyLoader(FileSystem fileSystem, JMAPConfiguration jmapConfiguration) { this.fileSystem = fileSystem; this.jmapConfiguration = jmapConfiguration; } - @Override - public void init() throws Exception { + public AsymmetricKeys load() throws Exception { KeyStore keystore = KeyStore.getInstance(JKS); InputStream fis = fileSystem.getResource(jmapConfiguration.getKeystore()); char[] secret = jmapConfiguration.getSecret().toCharArray(); @@ -74,41 +59,11 @@ public class JamesSignatureHandler implements SignatureHandler { .ofNullable(keystore.getCertificate(ALIAS)) .orElseThrow(() -> new KeyStoreException("Alias '" + ALIAS + "' keystore can't be found")); - publicKey = aliasCertificate.getPublicKey(); + PublicKey publicKey = aliasCertificate.getPublicKey(); Key key = keystore.getKey(ALIAS, secret); if (! (key instanceof PrivateKey)) { throw new KeyStoreException("Provided key is not a PrivateKey"); } - privateKey = (PrivateKey) key; - } - - @Override - public String sign(String source) { - Preconditions.checkNotNull(source); - try { - Signature javaSignature = Signature.getInstance(ALGORITHM); - javaSignature.initSign(privateKey); - javaSignature.update(source.getBytes()); - return Base64.getEncoder().encodeToString(javaSignature.sign()); - } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean verify(String source, String signature) { - Preconditions.checkNotNull(source); - Preconditions.checkNotNull(signature); - try { - Signature javaSignature = Signature.getInstance(ALGORITHM); - javaSignature.initVerify(publicKey); - javaSignature.update(source.getBytes()); - return javaSignature.verify(Base64.getDecoder().decode(signature)); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { - throw new RuntimeException(e); - } catch (SignatureException e) { - LOGGER.warn("Attempt to use a malformed signature '{}' for source '{}'", signature, source, e); - return false; - } + return new AsymmetricKeys((PrivateKey) key, publicKey); } } diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/ClassLoaderFileSystem.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/ClassLoaderFileSystem.java new file mode 100644 index 0000000..43687e4 --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/ClassLoaderFileSystem.java @@ -0,0 +1,45 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.jmap.crypto; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.james.filesystem.api.FileSystem; + +class ClassLoaderFileSystem implements FileSystem { + + @Override + public InputStream getResource(String url) throws IOException { + return ClassLoader.getSystemResourceAsStream(url); + } + + @Override + public File getFile(String fileURL) throws FileNotFoundException { + return null; + } + + @Override + public File getBasedir() throws FileNotFoundException { + return null; + } +} diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SecurityKeyLoaderTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SecurityKeyLoaderTest.java new file mode 100644 index 0000000..63c1acb --- /dev/null +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/crypto/SecurityKeyLoaderTest.java @@ -0,0 +1,96 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.jmap.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.util.Optional; + +import org.apache.james.jmap.JMAPConfiguration; +import org.junit.jupiter.api.Test; + +class SecurityKeyLoaderTest { + + private static final String JWT_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtlChO/nlVP27MpdkG0Bh\n" + + "16XrMRf6M4NeyGa7j5+1UKm42IKUf3lM28oe82MqIIRyvskPc11NuzSor8HmvH8H\n" + + "lhDs5DyJtx2qp35AT0zCqfwlaDnlDc/QDlZv1CoRZGpQk1Inyh6SbZwYpxxwh0fi\n" + + "+d/4RpE3LBVo8wgOaXPylOlHxsDizfkL8QwXItyakBfMO6jWQRrj7/9WDhGf4Hi+\n" + + "GQur1tPGZDl9mvCoRHjFrD5M/yypIPlfMGWFVEvV5jClNMLAQ9bYFuOc7H1fEWw6\n" + + "U1LZUUbJW9/CH45YXz82CYqkrfbnQxqRb2iVbVjs/sHopHd1NTiCfUtwvcYJiBVj\n" + + "kwIDAQAB\n" + + "-----END PUBLIC KEY-----"; + + @Test + void loadShouldThrowWhenWrongKeystore() throws Exception { + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY)) + .keystore("badAliasKeystore") + .secret("password") + .build(); + + SecurityKeyLoader loader = new SecurityKeyLoader( + new ClassLoaderFileSystem(), + jmapConfiguration); + + assertThatThrownBy(loader::load) + .isInstanceOf(KeyStoreException.class) + .hasMessage("Alias 'james' keystore can't be found"); + } + + @Test + void loadShouldThrowWhenWrongPassword() throws Exception { + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY)) + .keystore("keystore") + .secret("WrongPassword") + .build(); + + SecurityKeyLoader loader = new SecurityKeyLoader( + new ClassLoaderFileSystem(), + jmapConfiguration); + + assertThatThrownBy(loader::load) + .isInstanceOf(IOException.class) + .hasMessage("Keystore was tampered with, or password was incorrect"); + } + + @Test + void loadShouldReturnSecurityKeysWhenCorrectPassword() throws Exception { + JMAPConfiguration jmapConfiguration = JMAPConfiguration.builder() + .enable() + .jwtPublicKeyPem(Optional.of(JWT_PUBLIC_KEY)) + .keystore("keystore") + .secret("james72laBalle") + .build(); + + SecurityKeyLoader loader = new SecurityKeyLoader( + new ClassLoaderFileSystem(), + jmapConfiguration); + + assertThat(loader.load()) + .isNotNull(); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
