From 1442bffa7b80665049d2347f7ba2a03ca6c2bd70 Mon Sep 17 00:00:00 2001 From: Jakub Dudycz Date: Wed, 30 Jan 2019 15:58:36 +0100 Subject: Extract HV VES Client ssl-related classes - Create common ssl module - Extract ssl-related classes from HV VES Client module - Mark org.onap.dcaegen2.services.sdk.rest.services.ssl.SslFactory class as deprecated Change-Id: I31ef784e8822981ba541fb3f525f003218cd5c88 Signed-off-by: Jakub Dudycz Issue-ID: DCAEGEN2-1135 --- security/ssl/pom.xml | 48 +++++++++ .../services/sdk/security/ssl/Password.java | 72 ++++++++++++++ .../services/sdk/security/ssl/Passwords.java | 87 ++++++++++++++++ .../services/sdk/security/ssl/SecurityKeys.java | 37 +++++++ .../services/sdk/security/ssl/SslFactory.java | 89 +++++++++++++++++ .../services/sdk/security/ssl/PasswordTest.java | 109 +++++++++++++++++++++ .../services/sdk/security/ssl/PasswordsTest.java | 99 +++++++++++++++++++ security/ssl/src/test/resources/password.txt | 2 + 8 files changed, 543 insertions(+) create mode 100644 security/ssl/pom.xml create mode 100644 security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Password.java create mode 100644 security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Passwords.java create mode 100644 security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SecurityKeys.java create mode 100644 security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SslFactory.java create mode 100644 security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordTest.java create mode 100644 security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordsTest.java create mode 100644 security/ssl/src/test/resources/password.txt (limited to 'security/ssl') diff --git a/security/ssl/pom.xml b/security/ssl/pom.xml new file mode 100644 index 00000000..ecccd767 --- /dev/null +++ b/security/ssl/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + + org.onap.dcaegen2.services.sdk.security + dcaegen2-services-sdk-security + 1.1.0-SNAPSHOT + + + ssl + 1.1.1-SNAPSHOT + + SSL + Common SSL-related Classes Library + jar + + + + io.projectreactor.netty + reactor-netty + + + org.immutables + value + + + io.vavr + vavr + + + org.jetbrains + annotations + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.assertj + assertj-core + test + + + + \ No newline at end of file diff --git a/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Password.java b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Password.java new file mode 100644 index 00000000..35fc7bbe --- /dev/null +++ b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Password.java @@ -0,0 +1,72 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import io.vavr.CheckedFunction1; +import io.vavr.Function1; +import io.vavr.control.Try; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.jetbrains.annotations.NotNull; + +/** + * Simple password representation. + * + * A password can be used only once. After it the corresponding memory is zeroed. + * + * @author Piotr Jaszczyk + * @since 1.1.1 + */ +public class Password { + + private char[] value; + + public Password(@NotNull char[] value) { + this.value = value; + } + + /** + * Consume the password. + * + * After consumption following uses of this method will return Failure(GeneralSecurityException). + * + * @param user of the password + */ + public Try use(Function1> user) { + if (value == null) + return Try.failure(new GeneralSecurityException("Password had been already used so it is in cleared state")); + + try { + return user.apply(value); + } finally { + clear(); + } + } + + public Try useChecked(CheckedFunction1 user) { + return use(CheckedFunction1.liftTry(user)); + } + + public void clear() { + Arrays.fill(value, (char) 0); + value = null; + } +} diff --git a/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Passwords.java b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Passwords.java new file mode 100644 index 00000000..39828086 --- /dev/null +++ b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/Passwords.java @@ -0,0 +1,87 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import io.vavr.control.Try; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import org.jetbrains.annotations.NotNull; + +/** + * Utility functions for loading passwords. + * + * @author Piotr Jaszczyk + * @since 1.1.1 + */ +public final class Passwords { + + private Passwords() { + } + + public static @NotNull Try fromFile(File file) { + return fromPath(file.toPath()); + } + + public static @NotNull Try fromPath(Path path) { + return Try.of(() -> { + final byte[] bytes = Files.readAllBytes(path); + final CharBuffer password = decodeChars(bytes); + final char[] result = convertToCharArray(password); + return new Password(result); + }); + } + + public static @NotNull Try fromResource(String resource) { + return Try.of(() -> Paths.get(Passwords.class.getResource(resource).toURI())) + .flatMap(Passwords::fromPath); + } + + private static @NotNull CharBuffer decodeChars(byte[] bytes) { + try { + return Charset.defaultCharset().decode(ByteBuffer.wrap(bytes)); + } finally { + Arrays.fill(bytes, (byte) 0); + } + } + + private static char[] convertToCharArray(CharBuffer password) { + try { + final char[] result = new char[password.limit()]; + password.get(result); + return result; + } finally { + password.flip(); + clearBuffer(password); + } + } + + private static void clearBuffer(CharBuffer password) { + while (password.remaining() > 0) { + password.put((char) 0); + } + } +} diff --git a/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SecurityKeys.java b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SecurityKeys.java new file mode 100644 index 00000000..05c3c470 --- /dev/null +++ b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SecurityKeys.java @@ -0,0 +1,37 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import java.nio.file.Path; +import org.immutables.value.Value; + +/** + * @author Piotr Jaszczyk + * @since 1.1.1 + */ +@Value.Immutable +public interface SecurityKeys { + Path keyStore(); + Password keyStorePassword(); + + Path trustStore(); + Password trustStorePassword(); +} diff --git a/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SslFactory.java b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SslFactory.java new file mode 100644 index 00000000..15739eb6 --- /dev/null +++ b/security/ssl/src/main/java/org/onap/dcaegen2/services/sdk/security/ssl/SslFactory.java @@ -0,0 +1,89 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.vavr.control.Try; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.TrustManagerFactory; + +public class SslFactory { + + /** + * Function for creating secure ssl context. + * + * @param keys - Security keys to be used + * @return configured SSL context + */ + public Try createSecureContext(final SecurityKeys keys) { + final Try keyManagerFactory = + keyManagerFactory(keys.keyStore(), keys.keyStorePassword()); + final Try trustManagerFactory = + trustManagerFactory(keys.trustStore(), keys.trustStorePassword()); + + return Try.success(SslContextBuilder.forClient()) + .flatMap(ctx -> keyManagerFactory.map(ctx::keyManager)) + .flatMap(ctx -> trustManagerFactory.map(ctx::trustManager)) + .mapTry(SslContextBuilder::build); + } + + private Try keyManagerFactory(Path path, Password password) { + return password.useChecked(passwordChars -> { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(loadKeyStoreFromFile(path, passwordChars), passwordChars); + return kmf; + }); + } + + private Try trustManagerFactory(Path path, Password password) { + return password.useChecked(passwordChars -> { + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(loadKeyStoreFromFile(path, passwordChars)); + return tmf; + }); + } + + private KeyStore loadKeyStoreFromFile(Path path, char[] keyStorePassword) + throws GeneralSecurityException, IOException { + KeyStore ks = KeyStore.getInstance("pkcs12"); + ks.load(Files.newInputStream(path, StandardOpenOption.READ), keyStorePassword); + return ks; + } + + /** + * Function for creating insecure ssl context. + * + * @return configured insecure ssl context + */ + public Try createInsecureContext() { + return Try.success(SslContextBuilder.forClient()) + .map(ctx -> ctx.trustManager(InsecureTrustManagerFactory.INSTANCE)) + .mapTry(SslContextBuilder::build); + } +} diff --git a/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordTest.java b/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordTest.java new file mode 100644 index 00000000..ede227eb --- /dev/null +++ b/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordTest.java @@ -0,0 +1,109 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import io.vavr.collection.Array; +import io.vavr.control.Try; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** + * @author Piotr Jaszczyk + */ +class PasswordTest { + + @Test + void use_shouldInvokeConsumerWithStoredPassword() { + // given + final String password = "hej ho"; + final Password cut = new Password(password.toCharArray()); + + // when + String result = cut.useChecked(String::new).get(); + + // then + assertThat(result).isEqualTo(password); + } + + @Test + void use_shouldClearPasswordAfterUse() { + // given + final char[] passwordChars = "hej ho".toCharArray(); + final Password cut = new Password(passwordChars); + + // when + useThePassword(cut); + + // then + assertAllCharsAreNull(passwordChars); + } + + @Test + void use_shouldFail_whenItWasAlreadyCalled() { + // given + final Password cut = new Password("ala ma kota".toCharArray()); + + // when & then + useThePassword(cut).get(); + + assertThatExceptionOfType(GeneralSecurityException.class).isThrownBy(() -> + useThePassword(cut).get()); + } + + @Test + void use_shouldFail_whenItWasCleared() { + // given + final Password cut = new Password("ala ma kota".toCharArray()); + + // when & then + cut.clear(); + + assertThatExceptionOfType(GeneralSecurityException.class).isThrownBy(() -> + useThePassword(cut).get()); + } + + @Test + void clear_shouldClearThePassword() { + // given + final char[] passwordChars = "hej ho".toCharArray(); + final Password cut = new Password(passwordChars); + + // when + cut.clear(); + + // then + assertAllCharsAreNull(passwordChars); + } + + private Try useThePassword(Password cut) { + return cut.use((pass) -> Try.success(42)); + } + + private void assertAllCharsAreNull(char[] passwordChars) { + assertThat(Array.ofAll(passwordChars).forAll(ch -> ch == '\0')) + .describedAs("all characters in " + Arrays.toString(passwordChars) + " should be == '\\0'") + .isTrue(); + } +} \ No newline at end of file diff --git a/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordsTest.java b/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordsTest.java new file mode 100644 index 00000000..07c5afe8 --- /dev/null +++ b/security/ssl/src/test/java/org/onap/dcaegen2/services/sdk/security/ssl/PasswordsTest.java @@ -0,0 +1,99 @@ +/* + * ============LICENSE_START==================================== + * DCAEGEN2-SERVICES-SDK + * ========================================================= + * Copyright (C) 2019 Nokia. All rights reserved. + * ========================================================= + * 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. + * ============LICENSE_END===================================== + */ + +package org.onap.dcaegen2.services.sdk.security.ssl; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.vavr.control.Try; +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +/** + * @author Piotr Jaszczyk + * @since January 2019 + */ +class PasswordsTest { + + @Test + void fromFile() { + // given + final File file = new File("./src/test/resources/password.txt"); + + // when + final Try result = Passwords.fromFile(file); + + // then + assertSuccessful(result); + assertThat(extractPassword(result)).isEqualTo("ja baczewski\n2nd line"); + } + + @Test + void fromPath() throws URISyntaxException { + // given + final Path path = Paths.get(PasswordsTest.class.getResource("/password.txt").toURI()); + + // when + final Try result = Passwords.fromPath(path); + + // then + assertSuccessful(result); + assertThat(extractPassword(result)).isEqualTo("ja baczewski\n2nd line"); + } + + @Test + void fromPath_shouldFail_whenNotFound() { + // given + final Path path = Paths.get("/", UUID.randomUUID().toString()); + + // when + final Try result = Passwords.fromPath(path); + + // then + assertThat(result.isFailure()).describedAs("Try.failure?").isTrue(); + assertThat(result.getCause()).isInstanceOf(NoSuchFileException.class); + } + + @Test + void fromResource() { + // given + final String resource = "/password.txt"; + + // when + final Try result = Passwords.fromResource(resource); + + // then + assertSuccessful(result); + assertThat(extractPassword(result)).isEqualTo("ja baczewski\n2nd line"); + } + + private void assertSuccessful(Try result) { + assertThat(result.isSuccess()).describedAs("Try.success?").isTrue(); + } + + private String extractPassword(Try result) { + return result.flatMap(pass -> pass.useChecked(String::new)).get(); + } +} \ No newline at end of file diff --git a/security/ssl/src/test/resources/password.txt b/security/ssl/src/test/resources/password.txt new file mode 100644 index 00000000..93e4a005 --- /dev/null +++ b/security/ssl/src/test/resources/password.txt @@ -0,0 +1,2 @@ +ja baczewski +2nd line \ No newline at end of file -- cgit 1.2.3-korg