InitialKeyPair.java Source Code

/**
 * @author David K. Fischer
 *
 * This SwitchCrypt core class is licenced under the GNU General Public License, V3 (GPL V3). <br>
 * &nbsp;<br>
 * The original source code has been slightly modified so that no dependencies on other classes are required.
 */

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;


/**
 * The SwitchCrypt core library to encrypt and decrypt files. All methods are static.
 */
public class InitialKeyPair
{
    /**
     * The current product version
     */
    public static final String PRODUCT_VERSION = "1.1-H";


    /**
     * Get the current product version as a byte array.
     *
     * @return the current product version as a byte array, always 3 bytes
     *
     * @see #PRODUCT_VERSION
     */
    public static byte[] getProductVersionAsByteArray()
    {
        byte mayorVersion;
        byte minorVersion;
        byte patchLevel;

        int dotIndex = PRODUCT_VERSION.indexOf(".");
        mayorVersion = Byte.valueOf(PRODUCT_VERSION.substring(0, dotIndex));

        String remainingStr = PRODUCT_VERSION.substring(dotIndex + 1);
        int dashIndex = remainingStr.indexOf("-");
        minorVersion = Byte.valueOf(remainingStr.substring(0, dashIndex));

        remainingStr = remainingStr.substring(dashIndex + 1);
        patchLevel = (byte) remainingStr.charAt(0);

        byte[] result = new byte[3];
        result[0] = mayorVersion;
        result[1] = minorVersion;
        result[2] = patchLevel;
        return result;
    }


    /**
     * Private Constructor. Don't allow any constructor. All methods are static.
     */
    private InitialKeyPair()
    {
    }


    /**
     * The number of bytes of the salt which is used when hashing the password.
     */
    public static final int SALT_SIZE = 24;


    /**
     * The file name in which the salt of the password is stored.
     */
    public static final String SALT_FILE_NAME = "salt.dat";


    /**
     * The number of bits of the generated RSA keypair.
     */
    public static final int RSA_INTERNAL_KEYPAIR_LENGTH = 2048;


    /**
     * The file name in which the public key is stored.
     */
    public static final String PUBLIC_KEY_FILE_NAME = "public.key";


    /**
     * The file name in which the encrypted private key is stored.
     */
    public static final String ENCRYPTED_PRIVATE_KEY_FILE_NAME = "encryptedPrivate.key";


    /**
     * The number of bytes of the initial vector, used for symmetric encryption.
     */
    public static final int IV_SIZE = 16;


    /**
     * The number of bytes for symmetric file encryption keys (multiply this value by x*8 to get the encryption strength in bits).
     */
    private static final int FILE_KEY_SIZE = 32;


    /**
     * The magic pattern that is written at the start of each encrypted file
     */
    private static final byte[] ENCRYPTED_FILE_MAGIC_PATTERN = { 'q', 'a', 'c', 'r', 'y', 'p', 't', '|'};


    /**
     * The zip file name of the exported key pair.
     */
    public static final String EXPORT_KEYPAIR_ZIP_FILE_NAME = "keypair.zip";


    /**
     * The magic entry in a zip file that contains an exported key pair.
     */
    public static final String EXPORT_KEYPAIR_ZIP_FILE_MAGIC_ENTRY = "qamagic.dat";


    /**
     * The data of the magic entry in a zip file that contains an exported key pair.
     */
    public static final byte[] EXPORT_KEYPAIR_ZIP_FILE_MAGIC_ENTRY_DATA = { 'q', 'a', 'c', 'r', 'y', 'p', 't'};


    /**
     * The product version entry in a zip file that contains an exported key pair.
     */
    public static final String EXPORT_KEYPAIR_ZIP_FILE_PRODUCT_VERSION_ENTRY = "qaversion.dat";


    private static SecureRandom fileEncryptIVRandom = new SecureRandom();    // this random generator is exclusively used to generate new initialization vectors for encrypted files
    private static SecureRandom fileEncryptKeyRandom = new SecureRandom();   // this random generator is exclusively used to generate new symmetric keys for encrypted files


    /**
     * Generate a RSA key pair and a salt for the password. Then encrypt the private key
     * with the salted password. Finally, write the salt, the public key and the encrypted
     * private key to disk.
     *
     * @param password the password, used to encrypt the private key
     * @param configDir the directory to which the files of the salt, the public key and the encrypted private key are written
     *
     * @throws Exception if somewhat fails
     *
     * @see #SALT_FILE_NAME
     * @see #PUBLIC_KEY_FILE_NAME
     * @see #ENCRYPTED_PRIVATE_KEY_FILE_NAME
     */
    public static void generateAndWriteKeyPair(String password, File configDir) throws Exception
    {
        // check the configuration directory
        if (!configDir.isDirectory())
            throw new IOException("Invalid configuration directory: " + configDir.getCanonicalPath());

        if (!configDir.canWrite())
            throw new IOException("No write access to configuration directory: " + configDir.getCanonicalPath());

        FileOutputStream fos1 = null;
        FileOutputStream fos2 = null;
        FileOutputStream fos3 = null;

        try
        {
            // generate a salt and store it to disk
            SecureRandom secureRandom = new SecureRandom();
            byte[] saltBytes = new byte[SALT_SIZE];
            secureRandom.nextBytes(saltBytes);
            fos1 = new FileOutputStream(configDir.getPath() + File.separator + SALT_FILE_NAME);
            fos1.write(saltBytes);

            // hash the password together with the salt
            byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
            byte[] passwordAndSaltBytes = new byte[passwordBytes.length + SALT_SIZE];
            System.arraycopy(passwordBytes, 0, passwordAndSaltBytes, 0, passwordBytes.length);
            System.arraycopy(saltBytes, 0, passwordAndSaltBytes, passwordBytes.length, SALT_SIZE);
            MessageDigest sha256digest = MessageDigest.getInstance("SHA-256");
            byte[] hashedPassword = sha256digest.digest(passwordAndSaltBytes);

            // generate a new keypair
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(RSA_INTERNAL_KEYPAIR_LENGTH);
            KeyPair keyPair = kpg.generateKeyPair();

            // get the public and the private key
            RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();

            // verify the keypair length
            if (publicKey.getModulus().bitLength() != RSA_INTERNAL_KEYPAIR_LENGTH)
            {
                throw new SecurityException("Invalid length of internal generated RSA keypair. Expected = " + RSA_INTERNAL_KEYPAIR_LENGTH + " bits, generated = " + publicKey.getModulus().bitLength() + " bits");
            }

            // write the public key to disk
            X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded());
            fos2 = new FileOutputStream(configDir.getPath() + File.separator + PUBLIC_KEY_FILE_NAME);
            fos2.write(x509EncodedKeySpec.getEncoded());

            // get the private key in encoded format
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
            byte[] privateKeyEncoded = pkcs8EncodedKeySpec.getEncoded();

            // get the symmetric key based on the hashed password
            SecretKeySpec symmetricKey = new SecretKeySpec(hashedPassword, "AES");

            // generate an initialization vector
            byte[] iv = new byte[IV_SIZE];
            secureRandom = new SecureRandom();  // don't reuse old secureRandom
            secureRandom.nextBytes(iv);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // encrypt the private key with the symmetric key
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivParameterSpec);
            byte[] encryptedPrivateKeyEncoded = cipher.doFinal(privateKeyEncoded);

            // combine IV and encrypted private key
            byte[] ivAndEncryptedPrivateKey = new byte[IV_SIZE + encryptedPrivateKeyEncoded.length];
            System.arraycopy(ivParameterSpec.getIV(), 0, ivAndEncryptedPrivateKey, 0, IV_SIZE);
            System.arraycopy(encryptedPrivateKeyEncoded, 0, ivAndEncryptedPrivateKey, IV_SIZE, encryptedPrivateKeyEncoded.length);

            // write IV and encrypted private key to disk
            fos3 = new FileOutputStream(configDir.getPath() + File.separator + ENCRYPTED_PRIVATE_KEY_FILE_NAME);
            fos3.write(ivAndEncryptedPrivateKey);
        }
        finally
        {
            if (fos1 != null)
                fos1.close();
            if (fos2 != null)
                fos2.close();
            if (fos3 != null)
                fos3.close();
        }
    }


    /**
     * Read the public key from disk.
     *
     * @param configDir the directory from which the public key is read
     *
     * @return the public key
     *
     * @throws Exception if somewhat fails
     *
     * @see #PUBLIC_KEY_FILE_NAME
     */
    public static PublicKey readPublicKey(File configDir) throws Exception
    {
        // check the configuration directory
        if (!configDir.isDirectory())
            throw new IOException("Invalid configuration directory: " + configDir.getCanonicalPath());

        if (!configDir.canRead())
            throw new IOException("No read access to configuration directory: " + configDir.getCanonicalPath());

        // read the public Key
        byte[] encodedPublicKey = Files.readAllBytes(Paths.get(configDir.getCanonicalPath() + File.separator + PUBLIC_KEY_FILE_NAME));

        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(encodedPublicKey);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(publicKeySpec);
        return publicKey;
    }


    /**
     * Read the encrypted private key and the salt from disk. Then decrypt the private key
     * with the salted password.
     *
     * @param password the password, used to decrypt the private key
     * @param configDir the directory from which the encrypted private key and the salt is read

     * @return the decrypted private key
     *
     * @throws Exception if somewhat fails
     *
     * @see #SALT_FILE_NAME
     * @see #ENCRYPTED_PRIVATE_KEY_FILE_NAME
     */
    public static PrivateKey readEncryptedPrivateKey(String password, File configDir) throws Exception
    {
        // check the configuration directory
        if (!configDir.isDirectory())
            throw new IOException("Invalid configuration directory: " + configDir.getCanonicalPath());

        if (!configDir.canRead())
            throw new IOException("No read access to configuration directory: " + configDir.getCanonicalPath());

        // read the salt file
        byte[] saltBytes = Files.readAllBytes(Paths.get(configDir.getCanonicalPath() + File.separator + SALT_FILE_NAME));
        if (saltBytes.length != SALT_SIZE)
            throw new IOException("Invalid size of salt data, " + saltBytes.length + " bytes read, " + SALT_SIZE + " bytes expected");

        // hash the password together with the salt
        byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
        byte[] passwordAndSaltBytes = new byte[passwordBytes.length + SALT_SIZE];
        System.arraycopy(passwordBytes, 0, passwordAndSaltBytes, 0, passwordBytes.length);
        System.arraycopy(saltBytes, 0, passwordAndSaltBytes, passwordBytes.length, SALT_SIZE);
        MessageDigest sha256digest = MessageDigest.getInstance("SHA-256");
        byte[] hashedPassword = sha256digest.digest(passwordAndSaltBytes);

        // ... and use this hash as symmetric key
        SecretKeySpec symmetricKey = new SecretKeySpec(hashedPassword, "AES");

        // read the IV and the encrypted private key from disk
        byte[] ivAndEncryptedPrivateKey = Files.readAllBytes(Paths.get(configDir.getCanonicalPath() + File.separator + ENCRYPTED_PRIVATE_KEY_FILE_NAME));

        // extract the IV
        byte[] iv = new byte[IV_SIZE];
        System.arraycopy(ivAndEncryptedPrivateKey, 0, iv, 0, IV_SIZE);
        IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

        // extract the encrypted private key
        int encryptedSize = ivAndEncryptedPrivateKey.length - IV_SIZE;
        byte[] encryptedPrivateKeyEncoded = new byte[encryptedSize];
        System.arraycopy(ivAndEncryptedPrivateKey, IV_SIZE, encryptedPrivateKeyEncoded, 0, encryptedSize);

        // decrypt the private key
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
        cipher.init(Cipher.DECRYPT_MODE, symmetricKey, ivParameterSpec);
        byte[] privateKeyEncoded = cipher.doFinal(encryptedPrivateKeyEncoded);

        PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyEncoded);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);

        return privateKey;
    }


    /**
     * Encrypt a plain file by creating a new file which contains the encrypted data.
     *
     * @param plainInFile the existing input file which contains the plain data
     * @param encryptedOutFile the new output file to which the encrypted data are written
     * @param publicKey the public key used for encryption
     *
     * @throws Exception if somewhat fails
     */
    public static void encryptFile(File plainInFile, File encryptedOutFile, PublicKey publicKey, boolean isDryRun) throws Exception
    {
        // check input file
        if (plainInFile.isDirectory())
            throw new IOException("Invalid input file: is directory " + plainInFile.getCanonicalPath());

        if (!plainInFile.exists())
            throw new IOException("Invalid input file: file does not exists " + plainInFile.getCanonicalPath());

        if (!plainInFile.canRead())
            throw new IOException("Invalid input file: no read access for " + plainInFile.getCanonicalPath());

        // check output file
        if (encryptedOutFile.isDirectory())
            throw new IOException("Invalid output file: is directory " + encryptedOutFile.getCanonicalPath());

        // the path of the input file cannot be the same as the path of the output file
        if (plainInFile.getCanonicalPath().equalsIgnoreCase(encryptedOutFile.getCanonicalPath()))
            throw new IOException("Same path for input and output file is not supported: " + plainInFile.getCanonicalPath());

        OutputStream encryptedOutputStream = null;
        BufferedInputStream bin = null;

        try
        {
            encryptedOutputStream = new FileOutputStream(encryptedOutFile);

            // write first the magic pattern as plain data to the encrypted output file
            encryptedOutputStream.write(ENCRYPTED_FILE_MAGIC_PATTERN);

            // write the current product version
            encryptedOutputStream.write(getProductVersionAsByteArray());

            // generate a new iv for the file
            byte[] iv = new byte[IV_SIZE];
            fileEncryptIVRandom.nextBytes(iv);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // write the iv as plain data to the encrypted output file
            encryptedOutputStream.write(iv);

            // generate a new symmetric key for the file
            byte[] symmetricKeyBytes = new byte[FILE_KEY_SIZE];
            fileEncryptKeyRandom.nextBytes(symmetricKeyBytes);
            SecretKeySpec symmetricKey = new SecretKeySpec(symmetricKeyBytes, "AES");

            // encrypt the symmetric key with the public key
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] symmetricKeyBytesEncrypted = cipher.doFinal(symmetricKeyBytes);

            // write the size of the encrypted symmetric key
            encryptedOutputStream.write((symmetricKeyBytesEncrypted.length / 128));
            encryptedOutputStream.write((symmetricKeyBytesEncrypted.length % 128));

            // write the encrypted symmetric key to the encrypted output file
            encryptedOutputStream.write(symmetricKeyBytesEncrypted);

            // read the file data from disk and encrypt them by the new symmetric key
            Cipher fileCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            fileCipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivParameterSpec);

            bin = new BufferedInputStream(new FileInputStream(plainInFile));
            byte[] buffer = new byte[2048];
            int len = bin.read(buffer);
            while (len != -1)
            {
                // encrypt the file data on the fly
                byte[] encryptedBuffer = fileCipher.update(buffer, 0, len);

                // write the encrypted file data to the encrypted output file
                encryptedOutputStream.write(encryptedBuffer);

                // read next part of input file
                len = bin.read(buffer);
            }

            // final task for file encryption - flush buffers and close files
            byte[] encryptedBuffer = fileCipher.doFinal();
            encryptedOutputStream.write(encryptedBuffer);
        }
        catch (Throwable tr)
        {
            // try to delete the invalid output file
            if (encryptedOutputStream != null)
            {
                encryptedOutputStream.close();
                Files.delete(Paths.get(encryptedOutFile.getCanonicalPath()));
                encryptedOutputStream = null;
            }
            throw new Exception("Encryption failed for in file " + plainInFile.getCanonicalPath() + " to out file " + encryptedOutFile.getCanonicalPath(), tr);
        }
        finally
        {
            if (bin != null)
                bin.close();

            if (encryptedOutputStream != null)
                encryptedOutputStream.close();
        }
    }


    /**
     * Decrypt a file by creating a new file which contains the plain data.
     *
     * @param encryptedInFile the existing input file which contains the encrypted data
     * @param plainOutFile the new output file to which the plain data are written
     * @param privateKey the private key used for decryption
     *
     * @throws Exception if somewhat fails
     */
    public static void decryptFile(File encryptedInFile, File plainOutFile, PrivateKey privateKey) throws Exception
    {
        // check input file
        if (encryptedInFile.isDirectory())
            throw new IOException("Invalid input file: is directory " + encryptedInFile.getCanonicalPath());

        if (!encryptedInFile.canRead())
            throw new IOException("Invalid input file: no read access for " + encryptedInFile.getCanonicalPath());

        if (!encryptedInFile.exists())
        throw new IOException("Invalid input file: file does not exists " + encryptedInFile.getCanonicalPath());

        // check output file
        if (plainOutFile.isDirectory())
            throw new IOException("Invalid output file: is directory " + plainOutFile.getCanonicalPath());

        // the path of the input file cannot be the same as the path of the output file
        if (encryptedInFile.getCanonicalPath().equalsIgnoreCase(plainOutFile.getCanonicalPath()))
            throw new IOException("Same path for input and output file is not supported: " + encryptedInFile.getCanonicalPath());

        BufferedInputStream bin = null;
        OutputStream decryptedOutputStream = null;

        try
        {
            // open the encrypted in file
            bin = new BufferedInputStream(new FileInputStream(encryptedInFile));

            // read first the magic pattern as plain data from the encrypted input file - and verify the magic pattern
            byte[] magicPattern = new byte[ENCRYPTED_FILE_MAGIC_PATTERN.length];
            int len = bin.read(magicPattern);
            if (len != ENCRYPTED_FILE_MAGIC_PATTERN.length)
                throw new Exception("Invalid magic pattern of " + encryptedInFile.getPath());
            if (!Arrays.equals(magicPattern, ENCRYPTED_FILE_MAGIC_PATTERN))
                throw new Exception("Invalid magic pattern of " + encryptedInFile.getPath());

            // read the product version
            byte[] productVersion = new byte[3];
            len = bin.read(productVersion);
            if (len != 3)
                throw new IOException("Invalid product version");
            // System.out.println("productVersion = " + ProductSettings.getProductVersionFromByteArray(productVersion));

            // read the iv as plain data from the encrypted input file
            byte[] iv = new byte[IV_SIZE];
            len = bin.read(iv);
            if (len != IV_SIZE)
                throw new IOException("Invalid size of initial vector, " + len + " bytes read, " + IV_SIZE + " bytes expected");
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // read the size of the encrypted symmetric key
            int symmetricKeyBytesEncryptedLength = bin.read();
            symmetricKeyBytesEncryptedLength = (symmetricKeyBytesEncryptedLength * 128) + bin.read();

            // read the encrypted symmetric key
            byte[] symmetricKeyBytesEncrypted = new byte[symmetricKeyBytesEncryptedLength];
            len = bin.read(symmetricKeyBytesEncrypted);
            if (len != symmetricKeyBytesEncryptedLength)
                throw new IOException("Invalid size of encrypted symmetric key, " + len + " bytes read, " + symmetricKeyBytesEncryptedLength + " bytes expected");

            // decrypt the symmetric key by using the private key
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] symmetricKeyBytes = cipher.doFinal(symmetricKeyBytesEncrypted);
            SecretKeySpec symmetricKey = new SecretKeySpec(symmetricKeyBytes, "AES");

            // decrypt the file data
            Cipher fileCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            fileCipher.init(Cipher.DECRYPT_MODE, symmetricKey, ivParameterSpec);

            // open the decrypted out file
            decryptedOutputStream = new FileOutputStream(plainOutFile);

            byte[] buffer = new byte[2048];
            len = bin.read(buffer);
            while (len != -1)
            {
                byte[] decryptedData = fileCipher.update(buffer, 0, len);
                decryptedOutputStream.write(decryptedData);

                len = bin.read(buffer);
            }
            byte[] decryptedData = fileCipher.doFinal();
            decryptedOutputStream.write(decryptedData);
        }
        catch (Throwable tr)
        {
            // try to delete the invalid output file
            if (decryptedOutputStream != null)
            {
                decryptedOutputStream.close();
                Files.delete(Paths.get(plainOutFile.getCanonicalPath()));
                decryptedOutputStream = null;
            }
            throw new Exception("Decryption failed for in file " + encryptedInFile.getCanonicalPath() + " to out file " + plainOutFile.getCanonicalPath(), tr);
        }
        finally
        {
            if (bin != null)
                bin.close();

            if (decryptedOutputStream != null)
                decryptedOutputStream.close();
        }
    }


    /**
     * Get the decrypted data of an encrypted file, but leave the file encrypted on disk.
     *
     * @param encryptedFile the file which contains the encrypted data
     * @param privateKey the private key used for decryption
     *
     * @return the decrypted data
     *
     * @throws Exception if somewhat fails
     */
    public static byte[] decryptFileContent(File encryptedFile, PrivateKey privateKey) throws IOException, Exception
    {
        // check encrypted file
        if (encryptedFile.isDirectory())
            throw new IOException("Invalid input file: is directory " + encryptedFile.getCanonicalPath());

        if (!encryptedFile.canRead())
            throw new IOException("Invalid input file: no read access for " + encryptedFile.getCanonicalPath());

        if (!encryptedFile.exists())
            throw new IOException("Invalid input file: file does not exists " + encryptedFile.getCanonicalPath());

        BufferedInputStream bin = null;
        ByteArrayOutputStream decryptedOutputStream = null;
        try
        {
            // open the encrypted in file
            bin = new BufferedInputStream(new FileInputStream(encryptedFile));

            // read first the magic pattern as plain data from the encrypted input file - and verify the magic pattern
            byte[] magicPattern = new byte[ENCRYPTED_FILE_MAGIC_PATTERN.length];
            int len = bin.read(magicPattern);
            if (len != ENCRYPTED_FILE_MAGIC_PATTERN.length)
                throw new Exception("Invalid magic pattern of " + encryptedFile.getPath());
            if (!Arrays.equals(magicPattern, ENCRYPTED_FILE_MAGIC_PATTERN))
                throw new Exception("Invalid magic pattern of " + encryptedFile.getPath());

            // read the product version
            byte[] productVersion = new byte[3];
            len = bin.read(productVersion);
            if (len != 3)
                throw new IOException("Invalid product version");
            // System.out.println("productVersion = " + ProductSettings.getProductVersionFromByteArray(productVersion));

            // read the iv as plain data from the encrypted input file
            byte[] iv = new byte[IV_SIZE];
            len = bin.read(iv);
            if (len != IV_SIZE)
                throw new IOException("Invalid size of initial vector, " + len + " bytes read, " + IV_SIZE + " bytes expected");
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // read the size of the encrypted symmetric key
            int symmetricKeyBytesEncryptedLength = bin.read();
            symmetricKeyBytesEncryptedLength = (symmetricKeyBytesEncryptedLength * 128) + bin.read();

            // read the encrypted symmetric key
            byte[] symmetricKeyBytesEncrypted = new byte[symmetricKeyBytesEncryptedLength];
            len = bin.read(symmetricKeyBytesEncrypted);
            if (len != symmetricKeyBytesEncryptedLength)
                throw new IOException("Invalid size of encrypted symmetric key, " + len + " bytes read, " + symmetricKeyBytesEncryptedLength + " bytes expected");

            // decrypt the symmetric key by using the private key
            Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            byte[] symmetricKeyBytes = cipher.doFinal(symmetricKeyBytesEncrypted);
            SecretKeySpec symmetricKey = new SecretKeySpec(symmetricKeyBytes, "AES");

            // decrypt the file data
            Cipher fileCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            fileCipher.init(Cipher.DECRYPT_MODE, symmetricKey, ivParameterSpec);

            // open the decrypted out stream
            decryptedOutputStream = new ByteArrayOutputStream(2048);

            byte[] buffer = new byte[2048];
            len = bin.read(buffer);
            while (len != -1)
            {
                byte[] decryptedData = fileCipher.update(buffer, 0, len);
                decryptedOutputStream.write(decryptedData);

                len = bin.read(buffer);
            }
            byte[] decryptedData = fileCipher.doFinal();
            decryptedOutputStream.write(decryptedData);

            return decryptedOutputStream.toByteArray();
        }
        catch (Throwable tr)
        {
            throw new Exception("Decryption failed for file " + encryptedFile.getCanonicalPath(), tr);
        }
        finally
        {
            if (bin != null)
                bin.close();

            if (decryptedOutputStream != null)
                decryptedOutputStream.close();
        }
    }


    /**
     * Check if a file is already encrypted, which means that it contains the magic pattern.
     *
     * @param f the file to check
     *
     * @return true if the file is already encrypted
     *
     * @throws IOException if reading the file fails
     */
    public static boolean isEncryptedFile(File f) throws IOException
    {
        FileInputStream fin = null;
        try
        {
            fin = new FileInputStream(f);

            byte[] magicPattern = new byte[ENCRYPTED_FILE_MAGIC_PATTERN.length];
            int len = fin.read(magicPattern);

            if (len != ENCRYPTED_FILE_MAGIC_PATTERN.length)
                return false;

            if (!Arrays.equals(magicPattern, ENCRYPTED_FILE_MAGIC_PATTERN))
                return false;

            return true;
        }
        finally
        {
            if (fin != null)
                fin.close();
        }
    }


    /**
     * Change the password of the private key and write new files for the salt and the private key to disk.
     *
     * @param oldPassword the old password of the private key
     * @param newPassword the new password of the private key
     * @param configDir the directory in which the files of the salt and the encrypted private key is stored
     *
     * @throws Exception if somewhat fails
     */
    public static void changePrivateKeyPassword(String oldPassword, String newPassword, File configDir) throws Exception
    {
        // check the configuration directory
        if (!configDir.isDirectory())
            throw new IOException("Invalid configuration directory: " + configDir.getCanonicalPath());

        if (!configDir.canWrite())
            throw new IOException("No write access to configuration directory: " + configDir.getCanonicalPath());

        String saltFilePath = configDir.getPath() + File.separator + SALT_FILE_NAME;
        String tempSaltFilePath = configDir.getPath() + File.separator + SALT_FILE_NAME + "-tmp";
        String privateKeyFilePath = configDir.getPath() + File.separator + ENCRYPTED_PRIVATE_KEY_FILE_NAME;
        String tempPrivateKeyFilePath = configDir.getPath() + File.separator + ENCRYPTED_PRIVATE_KEY_FILE_NAME + "-tmp";

        FileOutputStream fos1 = null;
        FileOutputStream fos3 = null;

        try
        {
            PrivateKey privateKey = readEncryptedPrivateKey(oldPassword, configDir);

            // generate a new salt and store it to disk in a temporary file
            SecureRandom secureRandom = new SecureRandom();
            byte[] saltBytes = new byte[SALT_SIZE];
            secureRandom.nextBytes(saltBytes);
            fos1 = new FileOutputStream(tempSaltFilePath);
            fos1.write(saltBytes);

            // hash the password together with the salt
            byte[] passwordBytes = newPassword.getBytes(StandardCharsets.UTF_8);
            byte[] passwordAndSaltBytes = new byte[passwordBytes.length + SALT_SIZE];
            System.arraycopy(passwordBytes, 0, passwordAndSaltBytes, 0, passwordBytes.length);
            System.arraycopy(saltBytes, 0, passwordAndSaltBytes, passwordBytes.length, SALT_SIZE);
            MessageDigest sha256digest = MessageDigest.getInstance("SHA-256");
            byte[] hashedPassword = sha256digest.digest(passwordAndSaltBytes);

            // get the private key in encoded format
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
            byte[] privateKeyEncoded = pkcs8EncodedKeySpec.getEncoded();

            // get the symmetric key based on the hashed password
            SecretKeySpec symmetricKey = new SecretKeySpec(hashedPassword, "AES");

            // generate an initialization vector
            byte[] iv = new byte[IV_SIZE];
            secureRandom = new SecureRandom();  // don't reuse old secureRandom
            secureRandom.nextBytes(iv);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // encrypt the private key with the symmetric key
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivParameterSpec);
            byte[] encryptedPrivateKeyEncoded = cipher.doFinal(privateKeyEncoded);

            // combine IV and encrypted private key
            byte[] ivAndEncryptedPrivateKey = new byte[IV_SIZE + encryptedPrivateKeyEncoded.length];
            System.arraycopy(ivParameterSpec.getIV(), 0, ivAndEncryptedPrivateKey, 0, IV_SIZE);
            System.arraycopy(encryptedPrivateKeyEncoded, 0, ivAndEncryptedPrivateKey, IV_SIZE, encryptedPrivateKeyEncoded.length);

            // write IV and encrypted private key to disk in a temporary file
            fos3 = new FileOutputStream(tempPrivateKeyFilePath);
            fos3.write(ivAndEncryptedPrivateKey);

            // close the streams before copy the temporary files over the normal ones
            if (fos1 != null)
            {
                fos1.close();
                fos1 = null;
            }
            if (fos3 != null)
            {
                fos3.close();
                fos3 = null;
            }

            Files.copy(Paths.get(tempSaltFilePath), Paths.get(saltFilePath), REPLACE_EXISTING);
            Files.delete(Paths.get(tempSaltFilePath));

            Files.copy(Paths.get(tempPrivateKeyFilePath), Paths.get(privateKeyFilePath), REPLACE_EXISTING);
            Files.delete(Paths.get(tempPrivateKeyFilePath));
        }
        catch (Exception ex)
        {
            // close the streams before deleting the temporary files
            if (fos1 != null)
            {
                fos1.close();
                fos1 = null;
            }
            if (fos3 != null)
            {
                fos3.close();
                fos3 = null;
            }

            Files.deleteIfExists(Paths.get(tempSaltFilePath));
            Files.deleteIfExists(Paths.get(tempPrivateKeyFilePath));

            throw ex;
        }
        finally
        {
            if (fos1 != null)
                fos1.close();
            if (fos3 != null)
                fos3.close();
        }
    }


    /**
     * Export the key pair to a zip file.
     *
     * @param exportFile the zip file which is created
     * @param password the password of the encrypted private key
     * @param exportPassword the new/exported password of the encrypted private key
     * @param configDir the configuration directory
     *
     * @throws Exception if somewhat fails
     */
    public static void exportKeyPair(File exportFile, String password, String exportPassword, File configDir) throws Exception
    {
        ZipOutputStream zout = null;

        try
        {
            // open the zip file for write
            zout = new ZipOutputStream(new FileOutputStream(exportFile));

            // write first the magic entry to the zip file
            ZipEntry zipEntry = new ZipEntry(EXPORT_KEYPAIR_ZIP_FILE_MAGIC_ENTRY);
            zout.putNextEntry(zipEntry);
            zout.write(EXPORT_KEYPAIR_ZIP_FILE_MAGIC_ENTRY_DATA);
            zout.closeEntry();

            // write the product version to the zip file
            zipEntry = new ZipEntry(EXPORT_KEYPAIR_ZIP_FILE_PRODUCT_VERSION_ENTRY);
            zout.putNextEntry(zipEntry);
            zout.write(getProductVersionAsByteArray());
            zout.closeEntry();

            // generate a new salt and store it in the zip file
            SecureRandom secureRandom = new SecureRandom();
            byte[] saltBytes = new byte[SALT_SIZE];
            secureRandom.nextBytes(saltBytes);

            zipEntry = new ZipEntry(SALT_FILE_NAME);
            zout.putNextEntry(zipEntry);
            zout.write(saltBytes);
            zout.closeEntry();

            // hash the export password together with the new salt
            byte[] passwordBytes = exportPassword.getBytes(StandardCharsets.UTF_8);
            byte[] passwordAndSaltBytes = new byte[passwordBytes.length + SALT_SIZE];
            System.arraycopy(passwordBytes, 0, passwordAndSaltBytes, 0, passwordBytes.length);
            System.arraycopy(saltBytes, 0, passwordAndSaltBytes, passwordBytes.length, SALT_SIZE);
            MessageDigest sha256digest = MessageDigest.getInstance("SHA-256");
            byte[] hashedPassword = sha256digest.digest(passwordAndSaltBytes);

            // get the private key in encoded format
            PrivateKey privateKey = readEncryptedPrivateKey(password, configDir);
            PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(privateKey.getEncoded());
            byte[] privateKeyEncoded = pkcs8EncodedKeySpec.getEncoded();

            // get the symmetric key based on the hashed password
            SecretKeySpec symmetricKey = new SecretKeySpec(hashedPassword, "AES");

            // generate an initialization vector
            byte[] iv = new byte[IV_SIZE];
            secureRandom = new SecureRandom();  // don't reuse old secureRandom
            secureRandom.nextBytes(iv);
            IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);

            // encrypt the private key with the symmetric key
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(Cipher.ENCRYPT_MODE, symmetricKey, ivParameterSpec);
            byte[] encryptedPrivateKeyEncoded = cipher.doFinal(privateKeyEncoded);

            // combine IV and encrypted private key
            byte[] ivAndEncryptedPrivateKey = new byte[IV_SIZE + encryptedPrivateKeyEncoded.length];
            System.arraycopy(ivParameterSpec.getIV(), 0, ivAndEncryptedPrivateKey, 0, IV_SIZE);
            System.arraycopy(encryptedPrivateKeyEncoded, 0, ivAndEncryptedPrivateKey, IV_SIZE, encryptedPrivateKeyEncoded.length);

            // write IV and encrypted private key to the zip file
            zipEntry = new ZipEntry(ENCRYPTED_PRIVATE_KEY_FILE_NAME);
            zout.putNextEntry(zipEntry);
            zout.write(ivAndEncryptedPrivateKey);
            zout.closeEntry();

            // copy the public key to the zip file
            byte[] encodedPublicKey = Files.readAllBytes(Paths.get(configDir.getCanonicalPath() + File.separator + PUBLIC_KEY_FILE_NAME));
            zipEntry = new ZipEntry(PUBLIC_KEY_FILE_NAME);
            zout.putNextEntry(zipEntry);
            zout.write(encodedPublicKey);
            zout.closeEntry();
        }
        finally
        {
            if (zout != null)
                zout.close();
        }
    }


    /**
     * Internal test program.
     *
     * @param args [no args]
     */
    /*
    public static void main(String[] args)
    {
        try
        {
            InitialKeyPair.generateAndWriteKeyPair("MySecretPassword", new File("C:\\Scratch3"));

            PublicKey publicKey = InitialKeyPair.readPublicKey(new File("C:\\Scratch3"));

            PrivateKey privateKey = InitialKeyPair.readEncryptedPrivateKey("MySecretPassword", new File("C:\\Scratch3"));

            InitialKeyPair.encryptFile(new File("C:\\Scratch3\\PlainDocument1.txt"), new File("C:\\Scratch3\\PlainDocument1.txt-enc"), publicKey, false);

            InitialKeyPair.decryptFile(new File("C:\\Scratch3\\PlainDocument1.txt-enc"), new File("C:\\Scratch3\\PlainDocument1.txt-enc-dec"), privateKey, false);

        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
    */

}