diff options
Diffstat (limited to 'security/ssl')
8 files changed, 543 insertions, 0 deletions
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 @@ +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.onap.dcaegen2.services.sdk.security</groupId> + <artifactId>dcaegen2-services-sdk-security</artifactId> + <version>1.1.0-SNAPSHOT</version> + </parent> + + <artifactId>ssl</artifactId> + <version>1.1.1-SNAPSHOT</version> + + <name>SSL</name> + <description>Common SSL-related Classes Library</description> + <packaging>jar</packaging> + + <dependencies> + <dependency> + <groupId>io.projectreactor.netty</groupId> + <artifactId>reactor-netty</artifactId> + </dependency> + <dependency> + <groupId>org.immutables</groupId> + <artifactId>value</artifactId> + </dependency> + <dependency> + <groupId>io.vavr</groupId> + <artifactId>vavr</artifactId> + </dependency> + <dependency> + <groupId>org.jetbrains</groupId> + <artifactId>annotations</artifactId> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project>
\ 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 <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> + * @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 <T> Try<T> use(Function1<char[], Try<T>> 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 <T> Try<T> useChecked(CheckedFunction1<char[], T> 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 <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> + * @since 1.1.1 + */ +public final class Passwords { + + private Passwords() { + } + + public static @NotNull Try<Password> fromFile(File file) { + return fromPath(file.toPath()); + } + + public static @NotNull Try<Password> 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<Password> 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 <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> + * @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<SslContext> createSecureContext(final SecurityKeys keys) { + final Try<KeyManagerFactory> keyManagerFactory = + keyManagerFactory(keys.keyStore(), keys.keyStorePassword()); + final Try<TrustManagerFactory> 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> 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> 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<SslContext> 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 <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> + */ +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<Object> 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 <a href="mailto:piotr.jaszczyk@nokia.com">Piotr Jaszczyk</a> + * @since January 2019 + */ +class PasswordsTest { + + @Test + void fromFile() { + // given + final File file = new File("./src/test/resources/password.txt"); + + // when + final Try<Password> 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<Password> 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<Password> 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<Password> result = Passwords.fromResource(resource); + + // then + assertSuccessful(result); + assertThat(extractPassword(result)).isEqualTo("ja baczewski\n2nd line"); + } + + private void assertSuccessful(Try<Password> result) { + assertThat(result.isSuccess()).describedAs("Try.success?").isTrue(); + } + + private String extractPassword(Try<Password> 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 |