/** * @author David K. Fischer * * This SwitchCrypt core class is licenced under the GNU General Public License, V3 (GPL V3).
*  
* 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(); } } */ }