package ch.ge.ve.offlineadmin.controller; /*- * #%L * Admin offline * %% * Copyright (C) 2015 - 2016 République et Canton de Genève * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import ch.ge.ve.commons.crypto.ballot.AuthenticatedBallot; import ch.ge.ve.commons.crypto.ballot.BallotCipherService; import ch.ge.ve.commons.crypto.ballot.EncryptedBallotAndWrappedKey; import ch.ge.ve.commons.crypto.exceptions.AuthenticationTagMismatchException; import ch.ge.ve.commons.crypto.exceptions.CryptoConfigurationRuntimeException; import ch.ge.ve.commons.crypto.exceptions.PrivateKeyPasswordMismatchException; import ch.ge.ve.commons.crypto.utils.SecureRandomFactory; import ch.ge.ve.commons.fileutils.OutputFilesPattern; import ch.ge.ve.commons.fileutils.StreamHasher; import ch.ge.ve.commons.properties.PropertyConfigurationException; import ch.ge.ve.commons.properties.PropertyConfigurationService; import ch.ge.ve.offlineadmin.exception.KeyProvisioningRuntimeException; import ch.ge.ve.offlineadmin.exception.MissingKeyFilesException; import ch.ge.ve.offlineadmin.exception.ProcessInterruptedException; import ch.ge.ve.offlineadmin.services.BallotCipherServiceFactory; import ch.ge.ve.offlineadmin.util.FileUtils; import ch.ge.ve.offlineadmin.util.LogLevel; import ch.ge.ve.offlineadmin.util.PropertyConfigurationServiceFactory; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.geometry.Orientation; import javafx.scene.control.Button; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ScrollBar; import javafx.scene.layout.GridPane; import javafx.util.Callback; import org.apache.log4j.Logger; import javax.xml.bind.DatatypeConverter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.ResourceBundle; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static ch.ge.ve.offlineadmin.util.SecurityConstants.CERT_PUBLIC_KEY_FILENAME_PATTERN; /** * This controller manages the key testing tab */ public class KeyTestingController extends InterruptibleProcessController { private static final Logger LOGGER = Logger.getLogger(KeyTestingController.class); private final ObservableList<String> plainTexts = FXCollections.observableArrayList(); private final ObservableList<AuthenticatedBallot> cipherTexts = FXCollections.observableArrayList(); private final ObservableList<String> decryptedTexts = FXCollections.observableArrayList(); private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz;: "; private static final int LENGTH = 256; private final SecureRandom random = SecureRandomFactory.createPRNG(); @FXML private ResourceBundle resources; @FXML private GridPane mainGrid; /* * By appending <tt>Controller</tt> to the <tt>fx:id</tt> of the node, its controller is resolved automatically */ @FXML private ConsoleOutputControl consoleOutputController; @FXML private ListView<String> plainTextList; @FXML private ListView<AuthenticatedBallot> cipherTextList; @FXML private ListView<String> decryptedTextList; @FXML private Button encryptButton; @FXML private Button decryptButton; private FileUtils fileUtils; private StreamHasher streamHasher; private PropertyConfigurationService propertyConfigurationService; private BallotCipherServiceFactory ballotCipherServiceFactory; private PasswordDialogController passwordDialogController; @FXML private void initialize() throws IOException { propertyConfigurationService = new PropertyConfigurationServiceFactory().propertyConfigurationService(); streamHasher = new StreamHasher(propertyConfigurationService); ballotCipherServiceFactory = new BallotCipherServiceFactory(propertyConfigurationService); passwordDialogController = new PasswordDialogController(resources, consoleOutputController); plainTextList.setItems(plainTexts); cipherTextList.setItems(cipherTexts); customizeCipherTextCellFactory(); decryptedTextList.setItems(decryptedTexts); initializeBindings(); plainTexts.add(""); plainTexts.addAll(Stream.generate(this::createRandomString).limit(4).collect(Collectors.toList())); fileUtils = new FileUtils(resources); } private String createRandomString() { IntStream intStream = random.ints(LENGTH, 0, ALPHABET.length()); Stream<Character> characterStream = intStream.boxed().map(ALPHABET::charAt); return characterStream.map(Object::toString).reduce((acc, e) -> acc + e).get(); } private void customizeCipherTextCellFactory() { cipherTextList.setCellFactory(new Callback<ListView<AuthenticatedBallot>, ListCell<AuthenticatedBallot>>() { @Override public ListCell<AuthenticatedBallot> call(ListView<AuthenticatedBallot> param) { return new ListCell<AuthenticatedBallot>() { @Override protected void updateItem(AuthenticatedBallot item, boolean empty) { super.updateItem(item, empty); if (item != null) { setText(DatatypeConverter.printHexBinary(item.getAuthenticatedEncryptedBallot())); } } }; } }); } private void initializeBindings() { encryptButton.setDisable(true); BooleanBinding plainTextsEmpty = Bindings.createBooleanBinding(plainTexts::isEmpty, plainTexts); plainTextsEmpty.addListener( (observable, oldValue, isPlainTextListEmpty) -> encryptButton.setDisable(isPlainTextListEmpty) ); decryptButton.setDisable(true); cipherTexts.addListener((ListChangeListener<AuthenticatedBallot>) c -> decryptButton.setDisable(c.getList().isEmpty())); } private void bindScrollBars() { ScrollBar plainTextScrollBar = plainTextList.lookupAll(".scroll-bar").stream().map(e -> (ScrollBar) e).filter(e -> e.getOrientation().equals(Orientation.HORIZONTAL)).findFirst().orElse(null); ScrollBar decryptedTextScrollBar = decryptedTextList.lookupAll(".scroll-bar").stream().map(e -> (ScrollBar) e).filter(e -> e.getOrientation().equals(Orientation.HORIZONTAL)).findFirst().orElse(null); if (plainTextScrollBar != null && decryptedTextScrollBar != null) { plainTextScrollBar.valueProperty().bindBidirectional(decryptedTextScrollBar.valueProperty()); } else { LOGGER.error("couldn't find scrollbars"); } } @Override protected ResourceBundle getResourceBundle() { return resources; } /** * Perform a test encryption using the keys chosen by the user, and store the encrypted values in a local buffer. */ @FXML public void testEncryption() { cipherTexts.clear(); File selectedFile = fileUtils.promptKeyDirectory(); if (selectedFile != null) { try { consoleOutputController.logOnScreen(resources.getString("key_testing.encryption.start")); BallotCipherService ballotCipherService = ballotCipherServiceFactory.encryptionBallotCipherService(selectedFile); logPublicKeyHash(selectedFile); int index = 1; for (String plainText : plainTexts) { cipherTexts.add(ballotCipherService.encryptBallotThenWrapForAuthentication(plainText, index++)); } consoleOutputController.logOnScreen(resources.getString("key_testing.encryption.end")); } catch (MissingKeyFilesException e) { consoleOutputController.logOnScreen(resources.getString("keys_not_found_in_directory"), LogLevel.WARN); LOGGER.warn(PROCESS_INTERRUPTED_MESSAGE, e); } catch (IOException | PropertyConfigurationException e) { handleInterruption(new ProcessInterruptedException(resources.getString("key_testing.exception_occurred"), e)); LOGGER.error(PROCESS_INTERRUPTED_MESSAGE, e); } } } /** * Perform a decryption test, using the keys chosen by the user, and the cipher texts stored in the temporary buffer * during a previous execution of the encryption test */ @FXML public void testDecryption() { consoleOutputController.logOnScreen(resources.getString("key_testing.decryption.start")); decryptedTexts.clear(); File selectedFile = fileUtils.promptKeyDirectory(); if (selectedFile != null) { try { BallotCipherService ballotCipherService = ballotCipherServiceFactory.decryptionBallotCipherService(selectedFile); logPublicKeyHash(selectedFile); List<EncryptedBallotAndWrappedKey> encryptedBallotAndWrappedKeys = new ArrayList<>(); for (AuthenticatedBallot cipherText : cipherTexts) { encryptedBallotAndWrappedKeys.add(ballotCipherService.verifyAuthenticationThenUnwrap(cipherText)); } StringProperty password1 = new SimpleStringProperty(); StringProperty password2 = new SimpleStringProperty(); passwordDialogController.promptForPasswords(password1, password2, false); ballotCipherService.loadBallotKeyCipherPrivateKey(password1.getValue() + password2.getValue()); for (EncryptedBallotAndWrappedKey encryptedBallotAndWrappedKey : encryptedBallotAndWrappedKeys) { decryptedTexts.add(ballotCipherService.decryptBallot(encryptedBallotAndWrappedKey)); } bindScrollBars(); consoleOutputController.logOnScreen(resources.getString("key_testing.decryption.end")); } catch (MissingKeyFilesException e) { consoleOutputController.logOnScreen(resources.getString("keys_not_found_in_directory"), LogLevel.WARN); LOGGER.warn(PROCESS_INTERRUPTED_MESSAGE, e); } catch (PrivateKeyPasswordMismatchException e) { consoleOutputController.logOnScreen(resources.getString("key_password_mismatch"), LogLevel.WARN); LOGGER.warn("key password mismatch", e); } catch (ProcessInterruptedException e) { consoleOutputController.logOnScreen(resources.getString("key_testing.process_interrupted"), LogLevel.WARN); LOGGER.warn(PROCESS_INTERRUPTED_MESSAGE, e); } catch (PropertyConfigurationException | IOException | AuthenticationTagMismatchException e) { handleInterruption(new ProcessInterruptedException(resources.getString("key_testing.exception_occurred"), e)); LOGGER.error(PROCESS_INTERRUPTED_MESSAGE, e); } } } private void logPublicKeyHash(File selectedDirectory) throws PropertyConfigurationException, IOException { Pattern pubKeyPattern = Pattern.compile(propertyConfigurationService.getConfigValue(CERT_PUBLIC_KEY_FILENAME_PATTERN)); final Optional<Path> pubKey = new OutputFilesPattern().findFirstFileByPattern(pubKeyPattern, selectedDirectory.toPath()); if (!pubKey.isPresent()) { throw new KeyProvisioningRuntimeException("Public key was not found in directory:" + selectedDirectory.toPath()); } else { FileInputStream fileInputStream = new FileInputStream(pubKey.get().toFile()); // needs to be SHA-1, since windows only displays the sha1 hash when viewing a certificate's details MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { throw new CryptoConfigurationRuntimeException("Cannot instantiate message digest for file hashing", e); } byte[] hash = streamHasher.computeHash(fileInputStream, digest); String hashString = DatatypeConverter.printHexBinary(hash); consoleOutputController.logOnScreen(String.format(resources.getString("key_testing.public_key_hash"), hashString)); } } void setFileUtils(FileUtils fileUtils) { this.fileUtils = fileUtils; } void setStreamHasher(StreamHasher streamHasher) { this.streamHasher = streamHasher; } void setBallotCipherServiceFactory(BallotCipherServiceFactory ballotCipherServiceFactory) { this.ballotCipherServiceFactory = ballotCipherServiceFactory; } void setPasswordDialogController(PasswordDialogController passwordDialogController) { this.passwordDialogController = passwordDialogController; } void setCipherTexts(List<AuthenticatedBallot> authenticatedBallots) { cipherTexts.clear(); cipherTexts.addAll(authenticatedBallots); } }