public class AuthenticatedBallot {
    /**
     * the random key k_i, wrapped with the Electoral Officers public key
     */
    private final byte[] wrappedKey;

    /**
     * the doubly encrypted ballot (once with key k_i, once with the integrity key, with the ballot index as associated data)
     */
    private final byte[] authenticatedEncryptedBallot;

    /**
     * the ballot index (by order of insertion)
     */
    private final int ballotIndex;

    /**
     * the tag produced by the second layer of authenticated encryption
     */
    private final byte[] tag;


    /**
     * @param wrappedKey the random key k_i, wrapped with the Electoral Officers public key
     * @param authenticatedEncryptedBallot the doubly encrypted ballot (once with key k_i, once with the integrity key, with the ballot index as associated data)
     * @param ballotIndex the ballot index (by order of insertion)
     * @param tag the tag produced by the second layer of authenticated encryption
     */
    public AuthenticatedBallot(byte[] wrappedKey, byte[] authenticatedEncryptedBallot, int ballotIndex, byte[] tag) {
        this.wrappedKey = wrappedKey;
        this.authenticatedEncryptedBallot = authenticatedEncryptedBallot;
        this.ballotIndex = ballotIndex;
        this.tag = tag;
    }

    /**                                                      *

     * @return the wrappedKey
     */
    public byte[] getWrappedKey() {
        return wrappedKey;
    }

    /**
     * @return the authenticatedEncryptedBallot
     */
    public byte[] getAuthenticatedEncryptedBallot() {
        return authenticatedEncryptedBallot;
    }

    /**
     * @return the ballotIndex
     */
    public int getBallotIndex() {
        return ballotIndex;
    }

    /**
     * @return the authentication tag
     */
    public byte[] getTag() {
        return tag;
    }
} public class BallotCipherService {
    public static final int AEAD_TAG_SIZE = 128;
    private final BallotCiphersProvider ciphersProvider;

    private final PropertyConfigurationService propertyConfigurationService;

    /**
     * The default constructor.
     *
     * @param ciphersProvider              the {@link BallotCiphersProvider} to use
     * @param propertyConfigurationService the {@link PropertyConfigurationService} defining the algorithms to use and other parameters.
     */
    public BallotCipherService(BallotCiphersProvider ciphersProvider, PropertyConfigurationService propertyConfigurationService) {
        this.ciphersProvider = ciphersProvider;
        this.propertyConfigurationService = propertyConfigurationService;
        ciphersProvider.setPropertyConfigurationService(propertyConfigurationService);
    }

    private static byte[] toByteArray(SealedObject sealedBallot) throws IOException {
        byte[] cipheredBallot;
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutput out = null;
        try {
            out = new ObjectOutputStream(bos);
            out.writeObject(sealedBallot);
            cipheredBallot = bos.toByteArray();
        } finally {
            if (out != null) {
                out.close();
            }
            bos.close();
        }
        return cipheredBallot;
    }

    private static SealedObject toSealedObject(byte[] ballotContentBytes) throws IOException, ClassNotFoundException {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(ballotContentBytes));
        return (SealedObject) ois.readObject();
    }

    /**
     * Retrieves the plain name of the algorithm from the transformation description.
     *
     * @param transformation expected input examples: "AES/CBC/PKCS5PADDING", "AES", ...
     * @return the name of the underlying algorithm (<i>e.g.</i> "AES", "RSA", ...)
     */
    private static String getAlgoPlainName(String transformation) {
        return transformation.split("/")[0];
    }

    /**
     * Encrypts the ballot contents (supports any String), to an AuthenticatedBallot
     * <p>The process is two-fold</p>
     * <ul>
     * <li>First, the ballot is encrypted using the Election officials public key and standard mixed encryption</li>
     * <li>Second, the resulting encrypted ballot and wrapped key are encrypted a second time, using an AEAD cipher,
     * using the ballot index as associated data.</li>
     * </ul>
     * <p>The result is an AuthenticatedBallot</p>
     *
     * @param plainText   the plainText to be encrypted
     * @param ballotIndex the index of the ballot (since it is authenticated, it prevents the copy of one vote to another)
     * @return an AuthenticatedBallot ready for storage
     * @throws CryptoConfigurationRuntimeException
     * @throws CryptoOperationRuntimeException
     */
    public AuthenticatedBallot encryptBallotThenWrapForAuthentication(String plainText, int ballotIndex)  {
        Cipher ballotKeyCipher = ciphersProvider.getBallotKeyCipher();
        Cipher ballotCipher = ciphersProvider.getBallotCipher();

        // Generate a random symmetric key, renewed for each ballot
        Key plainSymmetricKey = createNewRandomSymmetricKey(ballotCipher);

        // Initialise the first layer symmetric cipher and  perform the first layer of symmetric encryption
        byte[] encryptedBallot = doFirstLayerEncryption(plainText, ballotCipher, plainSymmetricKey);

        Key integrityKey = ciphersProvider.getIntegrityCheckSecretKey();
        Cipher integrityCipher = ciphersProvider.getIntegrityCipher(propertyConfigurationService);

        byte[] authenticatedBallot = aeadEncrypt(integrityCipher, integrityKey, ballotIndex, encryptedBallot);

        // Due to the lack of an API to retrieve the tag from the cipher, it must be extracted from the resulting ciphertext
        // BouncyCastle simply appends the tag to the ciphertext and verifies it upon decryption
        byte[] tag = Arrays.copyOfRange(authenticatedBallot, authenticatedBallot.length - (AEAD_TAG_SIZE / Byte.SIZE), authenticatedBallot.length);

        // Wrapping of the random symmetric key using the Electoral Officers' public key
        byte[] wrappedKey = wrapKey(ballotKeyCipher, plainSymmetricKey);

        return new AuthenticatedBallot(wrappedKey, authenticatedBallot, ballotIndex, tag);
    }

    private Key createNewRandomSymmetricKey(Cipher ballotCipher) {
        KeyGenerator generator;
        try {
            generator = KeyGenerator.getInstance(getAlgoPlainName(ballotCipher.getAlgorithm()));
        } catch (NoSuchAlgorithmException e) {
            throw new CryptoConfigurationRuntimeException("first layer symmetric cipher key algorithm is invalid", e);
        }
        generator.init(ciphersProvider.getBallotCipherSize(), SecureRandomFactory.createPRNG());
        return generator.generateKey();
    }

    private byte[] doFirstLayerEncryption(String plainText, Cipher ballotCipher, Key plainSymmetricKey) {
        try {
            ballotCipher.init(Cipher.ENCRYPT_MODE, plainSymmetricKey, SecureRandomFactory.createPRNG());
        } catch (InvalidKeyException e) {
            throw new CryptoConfigurationRuntimeException("first layer symmetric cipher key is invalid", e);
        }
        byte[] encryptedBallot;
        try {
            SealedObject sealedObject = new SealedObject(plainText, ballotCipher);
            encryptedBallot = toByteArray(sealedObject);
        } catch (IOException | IllegalBlockSizeException e) {
            throw new CryptoOperationRuntimeException("cannot seal message", e);
        }
        return encryptedBallot;
    }

    private byte[] aeadEncrypt(Cipher cipher, Key integrityKey, int ballotIndex, byte[] encryptedBallot) {
        byte[] ballotIndexBytes = BigInteger.valueOf(ballotIndex).toByteArray();
        GCMParameterSpec spec = new GCMParameterSpec(AEAD_TAG_SIZE, ballotIndexBytes);
        byte[] result;
        try {
            cipher.init(Cipher.ENCRYPT_MODE, integrityKey, spec);
            cipher.updateAAD(ballotIndexBytes);

            result = cipher.doFinal(encryptedBallot);
        } catch (BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
            throw new CryptoOperationRuntimeException("error while encrypting the cipher text", e);
        }

        return result;
    }

    private byte[] wrapKey(Cipher ballotKeyCipher, Key plainSymmetricKey) {
        byte[] wrappedKey;
        try {
            ballotKeyCipher.init(Cipher.WRAP_MODE, ciphersProvider.getBallotKeyCipherPublicKey(), SecureRandomFactory.createPRNG());
            wrappedKey = ballotKeyCipher.wrap(plainSymmetricKey);
        } catch (InvalidKeyException e) {
            throw new CryptoConfigurationRuntimeException("wrapping public key is invalid", e);
        } catch (IllegalBlockSizeException e) {
            throw new CryptoOperationRuntimeException("cannot wrap key", e);
        }
        return wrappedKey;
    }

    /**
     * Performs the first layer of decryption of a ballot, verifying its authenticity
     *
     * @param authenticatedBallot an AuthenticatedBallot, as previously built by {@link #encryptBallotThenWrapForAuthentication(String, int)} and stored in the database
     * @return an EncryptedBallotAndWrappedKey
     * @throws AuthenticationTagMismatchException
     * @throws CryptoOperationRuntimeException
     */
    public EncryptedBallotAndWrappedKey verifyAuthenticationThenUnwrap(AuthenticatedBallot authenticatedBallot) throws AuthenticationTagMismatchException {
        Key integrityKey = ciphersProvider.getIntegrityCheckSecretKey();
        Cipher integrityCipher = ciphersProvider.getIntegrityCipher(propertyConfigurationService);

        byte[] bytes;
        try {
            bytes = aeadDecrypt(integrityCipher, integrityKey, authenticatedBallot.getBallotIndex(), authenticatedBallot.getAuthenticatedEncryptedBallot());
        } catch (AEADBadTagException e) {
            throw new AuthenticationTagMismatchException(e.getMessage());
        }

        try {
            return new EncryptedBallotAndWrappedKey(toSealedObject(bytes), authenticatedBallot.getWrappedKey());
        } catch (IOException | ClassNotFoundException e) {
            throw new CryptoOperationRuntimeException("second layer decryption error", e);
        }
    }

    private byte[] aeadDecrypt(Cipher cipher, Key integrityKey, int ballotIndex, byte[] authenticatedEncryptedBallot) throws AEADBadTagException {
        byte[] ballotIndexBytes = BigInteger.valueOf(ballotIndex).toByteArray();
        GCMParameterSpec spec = new GCMParameterSpec(AEAD_TAG_SIZE, ballotIndexBytes);
        byte[] result;
        try {
            cipher.init(Cipher.DECRYPT_MODE, integrityKey, spec);
            cipher.updateAAD(ballotIndexBytes);

            result = cipher.doFinal(authenticatedEncryptedBallot);
        } catch (AEADBadTagException e) {
            // In case of a tag mismatch, we want the exception to be handled by the caller
            throw e;
        } catch (BadPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException e) {
            throw new CryptoOperationRuntimeException("error while decrypting the cipher text", e);
        }
        return result;
    }

    /**
     * Performs the second layer of decryption of the ballot, using the Election Officers' private key and standard
     * mixed encryption
     *
     * @param encryptedBallotAndWrappedKey an EncryptedBallotAndWrappedKey, as provided per a previous call to {@link #verifyAuthenticationThenUnwrap(AuthenticatedBallot)}
     * @return the ballot's original contents
     * @throws CryptoConfigurationRuntimeException
     * @throws CryptoOperationRuntimeException
     */
    public String decryptBallot(EncryptedBallotAndWrappedKey encryptedBallotAndWrappedKey) {
        Cipher ballotKeyCipher = ciphersProvider.getBallotKeyCipher();
        Cipher ballotCipher = ciphersProvider.getBallotCipher();

        // Unwrap the random key k_i, using the Election Officers' private key
        try {
            ballotKeyCipher.init(Cipher.UNWRAP_MODE, ciphersProvider.getBallotKeyCipherPrivateKey());
        } catch (InvalidKeyException e) {
            throw new CryptoConfigurationRuntimeException("decryption key is invalid", e);
        }
        Key plainSymmetricKey;
        try {
            plainSymmetricKey = ballotKeyCipher.unwrap(encryptedBallotAndWrappedKey.getWrappedKey(), getAlgoPlainName(ballotCipher.getAlgorithm()), Cipher.SECRET_KEY);
        } catch (InvalidKeyException | NoSuchAlgorithmException e) {
            throw new CryptoOperationRuntimeException("key unwrapping error", e);
        }

        // Decrypt the ballot using the unwrapped key k_i
        SealedObject sealedBallot = encryptedBallotAndWrappedKey.getEncryptedBallot();
        try {
            return (String) sealedBallot.getObject(plainSymmetricKey);
        } catch (IOException | ClassNotFoundException | NoSuchAlgorithmException | InvalidKeyException e) {
            throw new CryptoOperationRuntimeException("ballot decryption error", e);
        }
    }

    /**
     * Unlocks the private key for decryption.
     *
     * @param password the password of the key file
     * @throws PrivateKeyPasswordMismatchException
     */
    public void loadBallotKeyCipherPrivateKey(String password) throws PrivateKeyPasswordMismatchException {
        ciphersProvider.loadBallotKeyCipherPrivateKey(password);
    }

    /**
     * Sets the path to the private key file.
     *
     * @param privateKeyFileName the path to the private key
     */
    public void setPrivateKeyFileName(String privateKeyFileName) {
        ciphersProvider.invalidatePrivateKeyCache();
        propertyConfigurationService.addConfigValue(BallotCiphersProvider.PRIVATE_KEY_FILE_NAME, privateKeyFileName);
    }

    /**
     * Sets the path to the public key file
     *
     * @param publicKeyFileName the path to the public key
     */
    public void setPublicKeyFileName(String publicKeyFileName) {
        ciphersProvider.invalidatePublicKeyCache();
        propertyConfigurationService.addConfigValue(BallotCiphersProvider.PUBLIC_KEY_FILE_NAME, publicKeyFileName);
    }

    /**
     * Sets the path to the integrity key file
     *
     * @param integrityKeyFileName the path to the integrity key
     */
    public void setIntegrityKeyFileName(String integrityKeyFileName) {
        ciphersProvider.invalidateIntegrityKeyCache();
        propertyConfigurationService.addConfigValue(BallotCiphersProvider.INTEGRITY_KEY_FILE_NAME, integrityKeyFileName);
    }

} public class EncryptedBallotAndWrappedKey implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * a ballot encrypted with a random key k_i
     */
    private final SealedObject encryptedBallot;

    /**
     * the random key k_i, wrapped with the Electoral Officers public key
     */
    private final byte[] wrappedKey;

    /**
     * @param encryptedBallot a sealed object containing the text representation of the ballot, encrypted with key <tt>k_i</tt>
     * @param wrappedKey      an array of bytes containing the key k_i wrapped with an asymmetric encryption algorithm
     */
    public EncryptedBallotAndWrappedKey(SealedObject encryptedBallot, byte[] wrappedKey) {
        this.encryptedBallot = encryptedBallot;
        this.wrappedKey = wrappedKey;
    }

    /**
     * @return the encryptedBallot
     */
    public SealedObject getEncryptedBallot() {
        return encryptedBallot;
    }

    /**
     * @return the wrappedKey
     */
    public byte[] getWrappedKey() {
        return wrappedKey;
    }
} public class AuthenticationTagMismatchException extends Exception {
    public AuthenticationTagMismatchException(String message) {
        super(message);
    }
} public class CryptoConfigurationRuntimeException extends RuntimeException {

    public CryptoConfigurationRuntimeException(String message) {
        super(message);
    }

    public CryptoConfigurationRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }
} public class PrivateKeyPasswordMismatchException extends Exception {
    public PrivateKeyPasswordMismatchException(String message) {
        super(message);
    }

    public PrivateKeyPasswordMismatchException(String message, Throwable cause) {
        super(message, cause);
    }

    public PrivateKeyPasswordMismatchException(Throwable cause) {
        super(cause);
    }

    public PrivateKeyPasswordMismatchException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
} public class SecureRandomFactory {
    // Mask default constructor, this class shouldn't be instantiated
    private SecureRandomFactory() {}

    /**
     * Important notice from the SecureRandom javadoc:
     * <p>The returned SecureRandom object has not been seeded. To seed the
     * returned object, call the <code>setSeed</code> method.
     * If <code>setSeed</code> is not called, the first call to
     * <code>nextBytes</code> will force the SecureRandom object to seed itself.
     * This self-seeding will not occur if <code>setSeed</code> was
     * previously called.</p>
     *
     * @return a new, not already seeded, Pseudo Random Number Generator instance.
     */
    public static SecureRandom createPRNG() {
        try {
            return SecureRandom.getInstance("SHA1PRNG", "SUN");
        } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
            throw new CryptoConfigurationRuntimeException("Error creating PRNG", e);
        }
    }

} public class OutputFilesPattern {
    public static final String DATE_TIME_FORMAT = "yyyy-MM-dd-HH'h'mm'm'ss's'";
    public static final String DATE_FORMAT = "yyyyMMdd";
    private static final int FIND_FILES_MAX_DEPTH = 10;

    private final DateTimeFormatter datetimeFormat = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
    private final DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern(DATE_FORMAT);


    /**
     * Resolves the parameters {date}, {datetime} and {user}
     *
     * @param pattern
     * @param user
     * @param date
     * @return
     */
    public String injectParams(String pattern, String user, ZonedDateTime date) {
        String result = pattern.replace("{date}", getDateAsString(date));
        result = result.replace("{datetime}", getDateTimeAsString(date));
        result = result.replace("{user}", user);
        return result;
    }

    /**
     * Resolves the parameters {date}, {datetime} and {user}.</br>
     * user is resolved from environment variable 'user.name'
     *
     * @param pattern parametrized file name
     * @param date    the date and time of the file generation
     * @return the final file name
     */
    public String injectParams(String pattern, ZonedDateTime date) {
        return injectParams(pattern, getSystemUserName(), date);
    }


    /**
     * Resolves the parameters {date}, {datetime}, {user} and {operationCode}
     *
     * @param pattern       parametrized file name
     * @param user          user
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @return the final file name
     */
    public String injectParams(String pattern, String user, ZonedDateTime date, String operationCode) {
        String result = injectParams(pattern, user, date);
        result = result.replace("{operationCode}", operationCode);
        return result;
    }

    /**
     * Resolves the parameters {date}, {datetime}, {user} and {operationCode}
     *
     * @param pattern       parametrized file name
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @return the final file name
     */
    public String injectParams(String pattern, ZonedDateTime date, String operationCode) {
        return injectParams(pattern, getSystemUserName(), date, operationCode);
    }


    /**
     * Resolves all the parameters.
     *
     * @param pattern       parametrized file name
     * @param user          user
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @param hostingCode   the canton of the operation
     * @return the final file name
     */
    public String injectParams(String pattern, String user, ZonedDateTime date, String operationCode, String hostingCode) {
        String result = injectParams(pattern, user, date, operationCode);
        result = result.replace("{canton}", hostingCode);
        return result;
    }

    /**
     * Resolves all the parameters.
     *
     * @param pattern       parametrized file name
     * @param user          user
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @param hostingCode   the canton of the operation
     * @param votersName    the name of the voters group
     * @return the final file name
     */
    public String injectParams(String pattern, String user, ZonedDateTime date, String operationCode, String hostingCode, String votersName) {
        String result = injectParams(pattern, user, date, operationCode, hostingCode);
        result = result.replace("{votersName}", votersName);
        return result;
    }

    /**
     * Resolves all the parameters.
     *
     * @param pattern       parametrized file name
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @param hostingCode   the canton of the operation
     * @return the final file name
     */
    public String injectParams(String pattern, ZonedDateTime date, String operationCode, String hostingCode) {
        return injectParams(pattern, getSystemUserName(), date, operationCode, hostingCode);
    }

    /**
     * Resolves all the parameters.
     *
     * @param pattern       parametrized file name
     * @param date          the date and time of the file generation
     * @param operationCode the code of the operation
     * @param hostingCode   the canton of the operation
     * @param votersName    the name of the voters group
     * @return the final file name
     */
    public String injectParams(String pattern, ZonedDateTime date, String operationCode, String hostingCode, String votersName) {
        return injectParams(pattern, getSystemUserName(), date, operationCode, hostingCode, votersName);
    }

    /**
     * Tries to find a file matching a given <i>filenamePattern</i> regular expression.
     *
     * @param filenamePattern regular expression pattern that the file should match
     * @param rootPath        path from which to recursively search for the file
     * @return the first file matching the pattern from the root directory, or an empty optional if none matched
     */
    public Optional<Path> findFirstFileByPattern(final Pattern filenamePattern, Path rootPath) {
        try (Stream<Path> pathStream = Files.find(
                rootPath,
                FIND_FILES_MAX_DEPTH,
                (path, attr) -> filenamePattern.matcher(path.getFileName().toString()).matches())) {
            return pathStream.sorted().findFirst();
        } catch (IOException e) {
            throw new FileOperationRuntimeException("Cannot walk file tree", e);
        }
    }

    private String getDateAsString(ZonedDateTime date) {
        return dateFormat.format(date);
    }

    private String getDateTimeAsString(ZonedDateTime date) {
        return datetimeFormat.format(date);
    }

    private static String getSystemUserName() {
        return System.getProperty("user.name");
    }
} public class StreamHasher {
    private final PropertyConfigurationService propertyConfigurationService;
    private ThreadLocal<MessageDigest> messageDigestThreadLocal = new ThreadLocal<>();

    /**
     * Constructor
     *
     * @param propertyConfigurationService the service defining the required properties
     */
    public StreamHasher(PropertyConfigurationService propertyConfigurationService) {
        this.propertyConfigurationService = propertyConfigurationService;
    }

    /**
     * Compute in a thread safe manner the hash of the file for which an inputStream is
     * provided with the default configured message digest, as in common-crypto.properties.
     *
     * @param inputStream an input stream for the file for which the hash should be computed
     * @return the hash of the file
     * @throws IOException
     */
    public byte[] threadSafeComputeHash(InputStream inputStream) throws IOException {
        return computeHash(inputStream, getThreadSafeMessageDigest());
    }

    /**
     * Compute the hash of the file for which an inputStream is provided, using the provided digest
     *
     * @param inputStream an input stream for which the hash should be computed
     * @param digest      the digest to be used for computing the hash
     * @return the hash of the stream contents
     * @throws IOException
     */
    public byte[] computeHash(InputStream inputStream, MessageDigest digest) throws IOException {
        byte[] bytes = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(bytes)) != -1) {
            digest.update(bytes, 0, bytesRead);
        }

        return digest.digest();
    }

    private MessageDigest getThreadSafeMessageDigest() {
        if (messageDigestThreadLocal.get() == null) {
            messageDigestThreadLocal.set(new MessageDigestFactory(propertyConfigurationService).getInstance());
        }
        return messageDigestThreadLocal.get();
    }
} public class PropertyConfigurationException extends Exception {
    public PropertyConfigurationException(String message, Exception cause) {
        super(message, cause);
    }

    public PropertyConfigurationException(String message) {
        super(message);
    }
} public class PropertyConfigurationService {
    private static final Logger LOG = Logger.getLogger(PropertyConfigurationService.class);

    private Properties properties = new Properties();

    /**
     * Creates the configuration service, sourcing itself from the declared configuration providers
     * (using the Java ServiceLoader).
     */
    public PropertyConfigurationService() {
        loadConfigurationProviders();
    }

    /**
     * Creates a new configuration service that sources itself from a properties file, that
     * is retrieved from the classpath, and from the declared configuration providers
     * (using the Java ServiceLoader).
     *
     * @param propertiesFilePath path to the properties file to use as a source, in the format needed by the classloader.
     * @throws PropertyConfigurationRuntimeException if fails to load config
     */
    public PropertyConfigurationService(String propertiesFilePath) {
        final InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(propertiesFilePath);
        if (resourceAsStream == null) {
            throw new PropertyConfigurationRuntimeException("Properties file cannot be found: " + propertiesFilePath);
        }
        loadConfig(resourceAsStream);
    }


    /**
     * Creates a new configuration service that sources itself from from the declared configuration
     * providers (using the Java ServiceLoader), and using its default values from a properties object
     *
     * @param properties properties object to be used as defaults
     */
    public PropertyConfigurationService(final Properties properties) {
        this.properties = new Properties(properties);
        loadConfigurationProviders();
    }

    private void loadConfig(InputStream inputStream) {
        try {
            properties.load(inputStream);
        } catch (IOException e) {
            throw new PropertyConfigurationRuntimeException("Unable to load the base configuration", e);
        }
        loadConfigurationProviders();
    }

    private void loadConfigurationProviders() {
        // load complementary properties from the service loader
        ServiceLoader<PropertyConfigurationProvider> providers = ServiceLoader.load(PropertyConfigurationProvider.class);
        for (PropertyConfigurationProvider provider : providers) {
            final Properties providerProperties = provider.getProperties();
            for (Map.Entry<Object, Object> entry : providerProperties.entrySet()) {
                // check if property is not already defined
                if (properties.get(entry.getKey()) != null) {
                    throw new PropertyConfigurationRuntimeException(
                            String.format("Property [%s] is already defined with value [%s]",
                                    entry.getKey(),
                                    entry.getValue()
                            ));
                } else {
                    properties.put(entry.getKey(), entry.getValue());
                }
            }
        }
    }

    /**
     * Gets a property value as an Integer.
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public int getConfigValueAsInt(String key) throws PropertyConfigurationException {
        int valeur;
        try {
            valeur = Integer.parseInt(getConfigValue(key));
        } catch (NumberFormatException nfe) {
            throw new PropertyConfigurationException(String.format("The value [%s] for key [%s] is not an integer", getConfigValue(key), key));
        }
        return valeur;
    }

    /**
     * Gets a property value as a Boolean.
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public boolean getConfigValueAsBoolean(String key) throws PropertyConfigurationException {
        return Boolean.parseBoolean(getConfigValue(key));

    }


    /**
     * Gets a property value as a Long.
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public long getConfigValueAsLong(String key) throws PropertyConfigurationException {
        long valeur;
        try {
            valeur = Long.parseLong(getConfigValue(key));
        } catch (NumberFormatException nfe) {
            throw new PropertyConfigurationException(String.format("The value [%s] for key [%s] is not a long", getConfigValue(key), key));
        }
        return valeur;
    }

    /**
     * Gets a property value as a String.
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public String getConfigValue(String key) throws PropertyConfigurationException {
        String retour = properties.getProperty(key);
        if (retour == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug(String.format("Value %s does not exist", key));
            }
            throw new PropertyConfigurationException(String.format("The property [%s] does not exist", key));
        }
        return retour;
    }

    /**
     * Gets a property value as an array of String.
     * The property values have to be formatted with each value separated by a comma,
     * e.g "value1,value2"
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public String[] getConfigValueAsArray(String key) throws PropertyConfigurationException {
        String configValue = properties.getProperty(key);
        if (configValue == null) {
            throw new PropertyConfigurationException("The property [" + key + "] does not exist");
        }
        ArrayList<String> elementList = Lists.newArrayList();
        Iterable<String> elements = Splitter.on(",").split(configValue);
        for (String element : elements) {
            elementList.add(element.trim());
        }
        return Iterables.toArray(elementList, String.class);
    }

    /**
     * Gets a property value as an array of Long.
     * The property values have to be formatted with each value separated by a comma,
     * e.g "value1,value2"
     *
     * @param key the property key
     * @return the property value
     * @throws PropertyConfigurationException if fails to find or convert the property
     */
    public long[] getConfigValueAsArrayLong(String key) throws PropertyConfigurationException {
        final String[] strings = getConfigValueAsArray(key);
        long[] values = new long[strings.length];
        for (int i = 0; i < strings.length; i++) {
            final String string = strings[i];
            try {
                values[i] = Long.parseLong(string);
            } catch (NumberFormatException nfe) {
                throw new PropertyConfigurationException(String.format("The value [%s] in [%s] for key [%s] is not a long", string, getConfigValue(key), key));
            }
        }
        return values;
    }

    /**
     * Gets a property value as a String, using a prefix and a key to define the actual key in the properties.
     *
     * @param subConfigPrefix prefix of the configuration key
     * @param key             other part of the configuration key
     * @return the property value
     * @throws PropertyConfigurationException PropertyConfigurationException if fails to find or convert the property
     */
    public String getSubConfigValue(String subConfigPrefix, String key) throws PropertyConfigurationException {
        return getConfigValue(buildKey(subConfigPrefix, key));
    }

    private static String buildKey(String subConfigPrefix, String key) {
        return subConfigPrefix != null ? subConfigPrefix + "." + key : key;
    }

    /**
     * @return all the properties
     */
    public Properties getProperties() {
        return properties;
    }

    /**
     * Tells whether the key is defined in the properties.
     *
     * @param key the property key
     * @return <code>true</code> if the property id defined, <code>false</code> otherwise
     */
    public boolean isDefined(String key) {
        return properties.getProperty(key) != null;
    }

    /**
     * Tells whether the key is defined in the properties.
     *
     * @param subConfigPrefix prefix of the configuration key
     * @param key             other part of the configuration key
     * @return <code>true</code> if the property id defined, <code>false</code> otherwise
     */
    public boolean isDefined(String subConfigPrefix, String key) {
        return isDefined(buildKey(subConfigPrefix, key));
    }

    /**
     * Add a key/value pair to the configuration properties
     *
     * @param key   property key
     * @param value property value
     */
    public void addConfigValue(String key, String value) {
        assert properties != null;
        properties.put(key, value);
    }
} public class KeyProvisioningRuntimeException extends RuntimeException {
    public KeyProvisioningRuntimeException(String message) {
        super(message);
    }

    public KeyProvisioningRuntimeException(String message, Throwable cause) {
        super(message, cause);
    }

    public KeyProvisioningRuntimeException(Throwable cause) {
        super(cause);
    }

    public KeyProvisioningRuntimeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
} public class MissingKeyFilesException extends Exception {
    public MissingKeyFilesException(String message) {
        super(message);
    }

    public MissingKeyFilesException(String message, Throwable cause) {
        super(message, cause);
    }

    public MissingKeyFilesException(Throwable cause) {
        super(cause);
    }

    public MissingKeyFilesException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
} public class ProcessInterruptedException extends Exception {
    /**
     * Constructor usually used when the interruption was caused by a user action
     *
     * @param message the message explaining the interruption
     */
    public ProcessInterruptedException(String message) {
        super(message);
    }

    /**
     * Contructor used when the interruption is caused by a previous exception
     *
     * @param message the message explaining the interruption
     * @param cause   the cause of the interruption
     */
    public ProcessInterruptedException(String message, Throwable cause) {
        super(message, cause);
    }
} public class BallotCipherServiceFactory {

    private final OutputFilesPattern outputFilesPattern = new OutputFilesPattern();
    private final PropertyConfigurationService propertyConfigurationService;
    private String integrityKeyFilenamePatternProperty;
    private String privateKeyFilenamePatternProperty;
    private String publicKeyFilenamePatternProperty;

    public BallotCipherServiceFactory(PropertyConfigurationService propertyConfigurationService) {
        this.propertyConfigurationService = propertyConfigurationService;
        init(propertyConfigurationService);
    }

    private void init(PropertyConfigurationService propertyConfigurationService) {
        try {
            integrityKeyFilenamePatternProperty = propertyConfigurationService.getConfigValue(INTEGRITY_KEY_FILENAME_PATTERN);
            publicKeyFilenamePatternProperty = propertyConfigurationService.getConfigValue(CERT_PUBLIC_KEY_FILENAME_PATTERN);
            privateKeyFilenamePatternProperty = propertyConfigurationService.getConfigValue(CERT_PRIVATE_KEY_FILENAME_PATTERN);
        } catch (PropertyConfigurationException e) {
            throw new CryptoConfigurationRuntimeException("key files names configuration missing", e);
        }
    }

    /**
     * Creates an instance of a {@link ch.ge.ve.commons.crypto.ballot.BallotCipherService} for encryption
     *
     * @param keyDirectory the directory containing the keys
     * @return an initialized {@link ch.ge.ve.commons.crypto.ballot.BallotCipherService} ready for encryption
     * @throws MissingKeyFilesException if the directory does not contain the expected files
     */
    public BallotCipherService encryptionBallotCipherService(File keyDirectory) throws MissingKeyFilesException {
        Preconditions.checkNotNull(keyDirectory);
        Preconditions.checkArgument(keyDirectory.isDirectory());

        EncryptionBallotCiphersProvider ciphersProvider = new EncryptionBallotCiphersProvider();

        List<Pattern> expectedFilesNamePatterns = new ArrayList<>();
        Pattern integrityKeyFileNamePattern = Pattern.compile(integrityKeyFilenamePatternProperty);
        expectedFilesNamePatterns.add(integrityKeyFileNamePattern);
        Pattern publicKeyFilenamePattern = Pattern.compile(publicKeyFilenamePatternProperty);
        expectedFilesNamePatterns.add(publicKeyFilenamePattern);

        directoryContainsRequiredKeys(keyDirectory, expectedFilesNamePatterns);

        BallotCipherService ballotCipherService = new BallotCipherService(ciphersProvider, propertyConfigurationService);

        final Optional<Path> integrityKeyPath = outputFilesPattern.findFirstFileByPattern(integrityKeyFileNamePattern, keyDirectory.toPath());
        ballotCipherService.setIntegrityKeyFileName(
                integrityKeyPath
                        .orElseThrow(() -> new MissingKeyFilesException("integrity key file not found in directory: " + keyDirectory.getAbsolutePath()))
                        .toString()
        );

        final Optional<Path> pubKeyPath = outputFilesPattern.findFirstFileByPattern(publicKeyFilenamePattern, keyDirectory.toPath());
        ballotCipherService.setPublicKeyFileName(
                pubKeyPath
                        .orElseThrow(() -> new MissingKeyFilesException("public key file not found in directory: " + keyDirectory.getAbsolutePath()))
                        .toString()
        );

        return ballotCipherService;
    }

    /**
     * Creates an instance of a {@link ch.ge.ve.commons.crypto.ballot.BallotCipherService} for decryption
     *
     * @param keyDirectory the directory containing the keys
     * @return an initialized {@link ch.ge.ve.commons.crypto.ballot.BallotCipherService} ready for decryption
     * @throws MissingKeyFilesException if the directory does not contain the expected files
     */
    public BallotCipherService decryptionBallotCipherService(File keyDirectory) throws MissingKeyFilesException {
        Preconditions.checkNotNull(keyDirectory);
        Preconditions.checkArgument(keyDirectory.isDirectory());

        DecryptionBallotCiphersProvider ciphersProvider = new DecryptionBallotCiphersProvider(keyDirectory);

        List<Pattern> expectedFilesNamesPatterns = new ArrayList<>();
        Pattern integrityKeyFileNamePattern = Pattern.compile(integrityKeyFilenamePatternProperty);
        expectedFilesNamesPatterns.add(integrityKeyFileNamePattern);
        Pattern privateKeyFilenamePattern = Pattern.compile(privateKeyFilenamePatternProperty);
        expectedFilesNamesPatterns.add(privateKeyFilenamePattern);

        directoryContainsRequiredKeys(keyDirectory, expectedFilesNamesPatterns);

        BallotCipherService ballotCipherService = new BallotCipherService(ciphersProvider, propertyConfigurationService);

        final Optional<Path> integrityKeyPath = outputFilesPattern.findFirstFileByPattern(integrityKeyFileNamePattern, keyDirectory.toPath());
        ballotCipherService.setIntegrityKeyFileName(
                integrityKeyPath
                        .orElseThrow(() -> new MissingKeyFilesException("integrity key file not found in directory: " + keyDirectory.getAbsolutePath()))
                        .toString()
        );

        final Optional<Path> privKeyPath = outputFilesPattern.findFirstFileByPattern(privateKeyFilenamePattern, keyDirectory.toPath());
        ballotCipherService.setPrivateKeyFileName(
                privKeyPath
                        .orElseThrow(() -> new MissingKeyFilesException("private key file not found in directory: " + keyDirectory.getAbsolutePath()))
                        .toString()
        );

        return ballotCipherService;
    }

    private void directoryContainsRequiredKeys(File keyDirectory, List<Pattern> expectedFilesNames) throws MissingKeyFilesException {
        List<String> filenames = Arrays.asList(keyDirectory.list());
        if (!expectedFilesNames.stream().allMatch(pattern -> filenames.stream().anyMatch(pattern.asPredicate()))) {
            throw new MissingKeyFilesException("key files not found in directory: " + keyDirectory.getAbsolutePath());
        }
    }

} public class FileUtils {
    private final ResourceBundle resources;

    private File initialDirectory = getUserHome();

    public FileUtils(ResourceBundle resources) {
        this.resources = resources;
    }

    /**
     * @return the user's home directory
     */
    public File getUserHome() {
        return new File(System.getProperty("user.home"));
    }

    /**
     * Prompts for the keys directory
     * @return the directory
     */
    public File promptKeyDirectory() {
        return getDirectory(resources.getString("file_utils.directory_chooser.key_directory.title"));
    }

    /**
     * Prompts for the encrypted ballots file
     * @return the file
     */
    public File promptEncryptedBallotsFile() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle(resources.getString("file_utils.file_chooser.encrypted_ballots.title"));
        fileChooser.getExtensionFilters().clear();
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
                resources.getString("file_utils.file_chooser.encrypted_ballots.filter"), "*.ser"));
        fileChooser.setInitialDirectory(getUserHome());

        return fileChooser.showOpenDialog(null);
    }

    public File getDirectory(String title, File initialDirectory) {
        DirectoryChooser directoryChooser = new DirectoryChooser();
        directoryChooser.setTitle(title);
        directoryChooser.setInitialDirectory(initialDirectory);
        return directoryChooser.showDialog(null);
    }

    private File getDirectory(String title) {
        return getDirectory(title, initialDirectory);
    }
} public enum LogLevel {
    OK("ok-text", FontAwesomeIcon.CHECK.name()),
    WARN("warn-text", FontAwesomeIcon.EXCLAMATION_TRIANGLE.name()),
    ERROR("error-text", FontAwesomeIcon.EXCLAMATION_CIRCLE.name());


    private final String styleClass;
    private final String glyphName;

    /**
     * @param styleClass css style
     * @param glyphName icon name
     */
    LogLevel(String styleClass, String glyphName) {
        this.styleClass = styleClass;
        this.glyphName = glyphName;
    }

    /**
     * Getter on the css style
     * @return the css style
     */
    public String getStyleClass() {
        return styleClass;
    }

    /**
     * Getter on the icon name
     * @return the icon name
     */
    public String getGlyphName() {
        return glyphName;
    }
} public class PropertyConfigurationServiceFactory {
    /**
     * Creates a {@link PropertyConfigurationService} based on the default application's configuration file
     * @return the configuration service
     */
    public PropertyConfigurationService propertyConfigurationService() {
        return new PropertyConfigurationService(ADMIN_OFFLINE_CONFIGURATION_FILE);
    }

} public static final String CERT_PUBLIC_KEY_FILENAME_PATTERN = "cert.public.filename.pattern";
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;
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%
 */






/**
 * 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;