diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index b7fa3a8..d140ebd 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - build-on-linux-with-jdk-17: + build-on-linux-with-jdk-21: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -16,7 +16,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: '17' + java-version: '21' cache: 'maven' - name: Build and test diff --git a/README.MD b/README.MD index 3ceaee2..7aa29de 100644 --- a/README.MD +++ b/README.MD @@ -4,7 +4,7 @@ A repository containing different java tutorials **Minimum requirements:** -1. Java 17 +1. Java 21 2. Maven 3.5.0 3. Eclipse, Intellij IDEA (or any other text editor like VIM) 4. A terminal @@ -36,6 +36,7 @@ A repository containing different java tutorials - [Spring Boot Reactive Server with Common Name Validation based on Spring Security](spring-security-cn-validation-for-reactive-server) - [Spring Boot Server with Common Name Validation based on AOP with AspectJ Weaver](spring-cn-validation-with-aop) - [Bypassing and overruling SSL configuration of libraries](bypassing-overruling-ssl-configuration) +- [Prompting to trust an unknown certificate in a GUI and reloading the ssl configuration](trust-me) ## Serialization & Deserialization ☢️ - [Two-way object serialization while using one model with Jackson and Spring Boot](two-way-object-serialization) diff --git a/mock-statics-with-mockito/pom.xml b/mock-statics-with-mockito/pom.xml index 57501e4..c8759e6 100644 --- a/mock-statics-with-mockito/pom.xml +++ b/mock-statics-with-mockito/pom.xml @@ -43,6 +43,12 @@ ${version.assertj-core} test + + net.bytebuddy + byte-buddy + ${version.byte-buddy} + test + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 6a1f1bb..8905f8b 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ instant-server-ssl-reloading-with-quarkus instant-ssl-reloading-with-spring-tomcat bypassing-overruling-ssl-configuration + trust-me @@ -47,7 +48,7 @@ - 11 + 21 3.0.0-M7 3.0.0-M7 3.10.1 @@ -62,6 +63,7 @@ 8.3.6 2.9.3 3.3.2 + 21.0.3 10.1.26 1.9.22.1 2.17.2 @@ -86,8 +88,9 @@ 3.23.1 5.10.3 1.10.3 - 5.12.0 + 5.13.0 5.2.0 + 1.15.1 1.0.3 42.5.0 1.17.3 diff --git a/trust-me/README.md b/trust-me/README.md new file mode 100644 index 0000000..7d099a2 --- /dev/null +++ b/trust-me/README.md @@ -0,0 +1,32 @@ +# Trust Me 🔐 +A proof-of-concept GUI for prompting an user when a certificate is not trusted yet. The ssl configuration will be reloaded during runtime. + +This GUI app demonstrates the feature of [Trusting additional new certificates at runtime](https://github.com/Hakky54/sslcontext-kickstart?tab=readme-ov-file#trust-additional-new-certificates-at-runtime) from the library [sslcontext-kickstart](https://github.com/Hakky54/sslcontext-kickstart) +It might occur that your truststore has outdated certificates and is not easy to maintain or it just calls servers which has recently updated their certificates. +This option demonstrates how to integrate it in your GUI app, and it will prompt when the certificate is not trusted yet, which gives the option to the end-user to either trust or reject it. + +## Demo +![alt text](https://github.com/Hakky54/java-tutorials/blob/main/trust-me/images/demo.gif?raw=true) + +## Running locally + +### Minimum requirements +- JDK 21 +- Maven +- Terminal + +Although this project requires JDK 21, the [library](https://github.com/Hakky54/sslcontext-kickstart) itself is compatible with JDK 8 and therefor will work with that version. + +Run the following commands in your terminal: + +```bash +mvn clean package +mvn spring-boot:run +``` + +## Contributing + +There are plenty of ways to contribute to this project: + +* Give it a star +* Submit a PR diff --git a/trust-me/images/demo.gif b/trust-me/images/demo.gif new file mode 100644 index 0000000..4645dcb Binary files /dev/null and b/trust-me/images/demo.gif differ diff --git a/trust-me/pom.xml b/trust-me/pom.xml new file mode 100644 index 0000000..a89df29 --- /dev/null +++ b/trust-me/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + io.github.hakky54 + java-tutorials + 1.0.0-SNAPSHOT + + + trust-me + + + + io.github.hakky54 + sslcontext-kickstart + ${version.sslcontext-kickstart} + + + + org.openjfx + javafx-base + ${version.javafx} + + + org.openjfx + javafx-fxml + ${version.javafx} + + + org.openjfx + javafx-controls + ${version.javafx} + + + org.openjfx + javafx-graphics + ${version.javafx} + + + + org.springframework.boot + spring-boot-starter + ${version.spring} + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${version.maven-compiler-plugin} + + ${version.java} + ${version.java} + + + + + org.codehaus.mojo + exec-maven-plugin + ${version.exec-maven-plugin} + + + + java + + + + + nl.altindag.ssl.trustme.App + + + + + org.springframework.boot + spring-boot-maven-plugin + ${version.spring} + + trust-me + nl.altindag.ssl.trustme.App + + + + + repackage + + + + + + + + + + src/main/resources + + mainscreen.fxml + banner.txt + + + + + + \ No newline at end of file diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/App.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/App.java new file mode 100644 index 0000000..9dae1c5 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/App.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme; + +import javafx.application.Application; +import javafx.application.Platform; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.function.Function; + +@SpringBootApplication +public class App extends Application { + + private static final String TITLE = "Trust Me"; + private ConfigurableApplicationContext applicationContext; + private final Function fxmlLoaderFunction = fxml -> new FXMLLoader(this.getClass().getResource(fxml)); + + private Parent root; + + @Override + public void init() throws IOException { + applicationContext = new SpringApplicationBuilder(App.class) + .headless(false) + .run(getParameters().getRaw().toArray(String[]::new)); + + FXMLLoader fxmlLoader = fxmlLoaderFunction.apply("/mainscreen.fxml"); + fxmlLoader.setControllerFactory(applicationContext::getBean); + root = fxmlLoader.load(); + } + + @Override + public void start(Stage stage) { + Scene scene = new Scene(root); + stage.setTitle(TITLE); + stage.setScene(scene); + stage.setWidth(500); + stage.setHeight(400); + stage.setResizable(false); + + stage.show(); + } + + @Override + public void stop() { + Platform.exit(); + applicationContext.stop(); + } + + public static void main(String[] args) { + launch(args); + } + +} \ No newline at end of file diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/AppStarter.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/AppStarter.java new file mode 100644 index 0000000..f10a178 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/AppStarter.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme; + +public class AppStarter { + + public static void main(String[] args) { + App.main(args); + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/config/ClientConfig.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/config/ClientConfig.java new file mode 100644 index 0000000..2c9bada --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/config/ClientConfig.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.config; + +import nl.altindag.ssl.SSLFactory; +import nl.altindag.ssl.trustme.service.TrustMeService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import javax.net.ssl.X509ExtendedTrustManager; +import java.net.http.HttpClient; +import java.nio.file.Path; + +@Configuration +public class ClientConfig { + + private static final Path TRUSTSTORE_PATH = Path.of(System.getProperty("user.dir"), "truststore.jks"); + private static final char[] TRUSTSTORE_PASSWORD = "changeit".toCharArray(); + private static final String TRUSTSTORE_TYPE = "PKCS12"; + + @Bean + public HttpClient httpClient(SSLFactory sslFactory) { + return HttpClient.newBuilder() + .sslContext(sslFactory.getSslContext()) + .sslParameters(sslFactory.getSslParameters()) + .build(); + } + + @Bean + public SSLFactory sslFactory(@Lazy TrustMeService trustMeService) { + return SSLFactory.builder() + .withInflatableTrustMaterial(TRUSTSTORE_PATH, TRUSTSTORE_PASSWORD, TRUSTSTORE_TYPE, trustMeService::verify) + .build(); + } + + @Bean + public X509ExtendedTrustManager trustManager(SSLFactory sslFactory) { + return sslFactory.getTrustManager().orElseThrow(); + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/controller/SearchController.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/controller/SearchController.java new file mode 100644 index 0000000..9630621 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/controller/SearchController.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.controller; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import nl.altindag.ssl.trustme.exception.PingException; +import nl.altindag.ssl.trustme.service.PingService; +import nl.altindag.ssl.trustme.util.Logger; +import org.springframework.stereotype.Controller; + +import javax.net.ssl.SSLHandshakeException; +import java.net.URL; +import java.util.ResourceBundle; + +import static javafx.geometry.Pos.CENTER; + +@Controller +public class SearchController implements Initializable { + + @FXML + private TextField urlField; + @FXML + private TextArea loggerArea; + + private final PingService pingService; + + public SearchController(PingService pingService) { + this.pingService = pingService; + } + + @FXML + public void initialize() { + urlField.setAlignment(CENTER); + + urlField.textProperty().addListener((observableValue, oldValue, newValue) -> { + if (newValue.contains(" ")) { + urlField.setText(oldValue); + } + }); + } + + @Override + public void initialize(URL url, ResourceBundle resourceBundle) { + loggerArea.textProperty().bind(Logger.logContainerProperty()); + Logger.logContainerProperty().addListener((observable, newValue, oldValue) -> { + loggerArea.selectPositionCaret(loggerArea.getLength()); + loggerArea.deselect(); + }); + } + + @FXML + public void onEnter(ActionEvent event) { + String url = urlField.getText().toLowerCase(); + try { + pingService.ping(url); + Logger.log(String.format("Certificate of [%s] is already trusted", url)); + } catch (PingException pingException) { + if (pingException.getCause() instanceof SSLHandshakeException) { + Logger.log(String.format("Certificate of [%s] is not trusted", url)); + } + } + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/PingException.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/PingException.java new file mode 100644 index 0000000..ce952f8 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/PingException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.exception; + +public class PingException extends TrustMeException { + + public PingException(Throwable cause) { + super(cause); + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/TrustMeException.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/TrustMeException.java new file mode 100644 index 0000000..e11adb1 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/exception/TrustMeException.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.exception; + +public class TrustMeException extends RuntimeException { + + public TrustMeException(Throwable cause) { + super(cause); + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/service/PingService.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/service/PingService.java new file mode 100644 index 0000000..25747c8 --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/service/PingService.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.service; + +import nl.altindag.ssl.trustme.exception.PingException; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; + +@Service +public class PingService { + + private final HttpClient httpClient; + + public PingService(HttpClient httpClient) { + this.httpClient = httpClient; + } + + public void ping(String url) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .timeout(Duration.ofMinutes(1)) + .uri(URI.create(url)) + .build(); + + try { + httpClient.send(request, HttpResponse.BodyHandlers.discarding()); + } catch (IOException | InterruptedException e) { + throw new PingException(e); + } + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/service/TrustMeService.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/service/TrustMeService.java new file mode 100644 index 0000000..235fd6d --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/service/TrustMeService.java @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.service; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonType; +import nl.altindag.ssl.model.TrustManagerParameters; +import nl.altindag.ssl.trustme.util.Logger; +import nl.altindag.ssl.util.TrustManagerUtils; +import org.springframework.stereotype.Service; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.X509ExtendedTrustManager; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +@Service +public class TrustMeService { + + private static final Function hostnameExtractor = trustManagerParameters -> trustManagerParameters + .getSslEngine().map(SSLEngine::getPeerHost) + .or(() -> trustManagerParameters.getSocket().map(socket -> socket.getInetAddress().getHostName())) + .orElseThrow(); + + private final Map hostnameToShouldBeTrusted = new ConcurrentHashMap<>(); + + private final X509ExtendedTrustManager trustManager; + + public TrustMeService(X509ExtendedTrustManager trustManager) { + this.trustManager = trustManager; + } + + public boolean verify(TrustManagerParameters trustManagerParameters) { + String hostname = hostnameExtractor.apply(trustManagerParameters); + + Boolean isTrusted = hostnameToShouldBeTrusted.get(hostname); + if (isTrusted != null) { + return isTrusted; + } + + X509Certificate certificate = trustManagerParameters.getChain()[0]; + Platform.runLater(() -> askUserToTrustServerCertificate(hostname, certificate)); + return hostnameToShouldBeTrusted.getOrDefault(hostname, false); + } + + private void askUserToTrustServerCertificate(String hostname, X509Certificate certificate) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION, "Do you want to trust the certificate of " + hostname, ButtonType.YES, ButtonType.NO); + alert.setTitle("Trust confirmation"); + + Optional buttonType = alert.showAndWait(); + boolean shouldBeTrusted = buttonType.filter(type -> type == ButtonType.YES).isPresent(); + hostnameToShouldBeTrusted.put(hostname, shouldBeTrusted); + + if (shouldBeTrusted) { + Logger.log("User trusted server. Adding the certificate to the list of trusted certificates...."); + TrustManagerUtils.addCertificate(trustManager, certificate); + Logger.log("Server is now trusted"); + } else { + Logger.log("User decided not to trust the server. Not prompting anymore"); + } + } + +} diff --git a/trust-me/src/main/java/nl/altindag/ssl/trustme/util/Logger.java b/trust-me/src/main/java/nl/altindag/ssl/trustme/util/Logger.java new file mode 100644 index 0000000..a86999d --- /dev/null +++ b/trust-me/src/main/java/nl/altindag/ssl/trustme/util/Logger.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Thunderberry. + * + * 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 nl.altindag.ssl.trustme.util; + +import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; + +import java.util.ArrayList; +import java.util.List; + +public class Logger { + + private static final String LINE_BREAK = "\n"; + + private Logger() {} + + private static List logMessages = new ArrayList<>(); + private static SimpleStringProperty logContainer = new SimpleStringProperty(""); + + public static synchronized void log(String logMessage) { + Platform.runLater(() -> { + if (!logMessages.isEmpty() && logMessages.get(logMessages.size() - 1).contains(logMessage)) { + return; + } + + logMessages.add(logMessage); + logContainer.setValue(logContainer.getValue() + logMessage + LINE_BREAK); + }); + } + + public static SimpleStringProperty logContainerProperty() { + return logContainer; + } + +} diff --git a/trust-me/src/main/resources/banner.txt b/trust-me/src/main/resources/banner.txt new file mode 100644 index 0000000..03619d2 --- /dev/null +++ b/trust-me/src/main/resources/banner.txt @@ -0,0 +1,12 @@ +# ________ __ __ __ +# | | \ | \ | \ +# \$$$$$$$| $$____ __ __ _______ ____| $$ ______ ______ | $$____ ______ ______ ______ __ __ +# | $$ | $$ \| \ | | \ / $$/ \ / \| $$ \ / \ / \ / \| \ | \ +# | $$ | $$$$$$$| $$ | $| $$$$$$$| $$$$$$| $$$$$$| $$$$$$| $$$$$$$| $$$$$$| $$$$$$| $$$$$$| $$ | $$ +# | $$ | $$ | $| $$ | $| $$ | $| $$ | $| $$ $| $$ \$| $$ | $| $$ $| $$ \$| $$ \$| $$ | $$ +# | $$ | $$ | $| $$__/ $| $$ | $| $$__| $| $$$$$$$| $$ | $$__/ $| $$$$$$$| $$ | $$ | $$__/ $$ +# | $$ | $$ | $$\$$ $| $$ | $$\$$ $$\$$ | $$ | $$ $$\$$ | $$ | $$ \$$ $$ +# \$$ \$$ \$$ \$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$$\$$ \$$$$$$$ \$$$$$$$\$$ \$$ _\$$$$$$$ +# | \__| $$ +# \$$ $$ +# http://thunderberry.nl/ \$$$$$$ \ No newline at end of file diff --git a/trust-me/src/main/resources/mainscreen.fxml b/trust-me/src/main/resources/mainscreen.fxml new file mode 100644 index 0000000..e649e4d --- /dev/null +++ b/trust-me/src/main/resources/mainscreen.fxml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +