/**
* @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();
}
}
*/
}