Java tutorial
/******************************************************************************* * Copyright (c) 2013 "Ivo van Kamp" * * jEncrypt is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package net.sourceforge.jencrypt; import java.io.*; import java.nio.ByteBuffer; import java.nio.file.*; import java.security.GeneralSecurityException; import java.util.Collection; import java.util.Set; import javax.crypto.Cipher; import org.apache.commons.codec.DecoderException; import org.apache.commons.io.FilenameUtils; import net.sourceforge.jencrypt.lib.*; import java.nio.file.attribute.*; //@formatter:off /** * Encrypt and decrypt files with the jEncrypt file format. * * The jEncrypt file format: * * Header: * * salt - salt to use with PBKDF2 (size in jencrypt.ini) * IV - initialization vector (for AES equal to fixed block size of 128 bits) * * Followed by repeating blocks of: * * 04 bytes - full path size * full path - path string * 02 bytes - POSIX permissions of file or folder * 08 bytes - size of file * file - encrypted file * */ // @formatter:on class FileEncrypter { /** * Crypto wrapper instance for file en-/decryption. */ private CryptoWrapper cryptoWrapperFileContent; /** * Crypto wrapper instance for en-/decryption of file names, sizes, * permissions. */ private CryptoWrapper cryptoWrapperFileNamesAndSizes; /** * JEncryptLog instance (java.util.logging.Logger extension) */ private final JEncryptLog logger; /** * Configuration file reader object. */ private final ConfigHelper config; /** * Command line helper object. */ private final CommandLineHelper commandline; /** * Flag indicating POSIX compliance. */ private final boolean fileSystemIsPosixCompliant; /** * Instantiate the CryptoWrapper class with parameters from the commandline * and configuration file. */ FileEncrypter(ConfigHelper config, CommandLineHelper commandline, JEncryptLog logger) { this.config = config; this.commandline = commandline; this.logger = logger; this.fileSystemIsPosixCompliant = fileSystemIsPosixCompliant(); } /** * Modify file/folder permissions. * * @param file * @param permissions * @throws IOException */ private void setPosixFilePermissions(File file, Set<PosixFilePermission> permissions) throws IOException { // Convert File object to Path and call Files.setPosixFilePermissions(). Files.setPosixFilePermissions(Paths.get(file.getAbsolutePath()), permissions); } /** * Check if file system is Posix compliant */ private boolean fileSystemIsPosixCompliant() { try { Files.getPosixFilePermissions(Paths.get("")); } catch (Exception e) { return false; } return true; } /** * Shorten the "En-/decrypting [fileOrFolder]" message to 80 chars. * * @param fileOrFolder * @param cipherMode * @return shortened message */ private String getCipherMessage(String fileOrFolder, int cipherMode) { // Shorten encryption message to 80 characters if (fileOrFolder.length() > 55) fileOrFolder = "..." + fileOrFolder.substring(fileOrFolder.length() - 55, fileOrFolder.length()); return ((cipherMode == Cipher.ENCRYPT_MODE) ? "Encrypting" : "Decrypting") + ": '" + fileOrFolder + "' "; } /** * Encrypt file [inputFile] relative to [pathToEncrypt] into the archive * specified by [outputFile]. * * @param pathToEncrypt * @param inputFile * @param outputFile * @throws Exception */ private void encryptAndArchivePath(String folderToEncryptString, String inputFileRelativePath, OutputStream os) throws Exception { // Full path to file to encrypt String fullSourcePath = folderToEncryptString + File.separator + inputFileRelativePath; // Path to encrypt is a folder if it ends with a file separator boolean pathIsFolder = (inputFileRelativePath .charAt(inputFileRelativePath.length() - 1) == File.separatorChar); // Use a Unix separator for storing file paths inputFileRelativePath = FilenameUtils.normalize(inputFileRelativePath, true); // To indicate a folder add a Unix file separator at the end if (pathIsFolder) inputFileRelativePath += Utils.UNIX_SEPARATOR; File inputFile = new File(fullSourcePath); Set<PosixFilePermission> perms = null; // Get the "Encrypting [file] ... " message String message = getCipherMessage(inputFile.getName(), commandline.getCipherMode()); // Instantiate ProgressInfo ProgressInfo progress = new FileProgressInfo(message, inputFile.length(), config.getReadBufferSize()); assert (inputFileRelativePath.length() > 0); byte[] pathLength = ByteBuffer.allocate(4).putInt(inputFileRelativePath.length()).array(); byte[] pathLengthEnc = cryptoWrapperFileNamesAndSizes.cipherBytes(pathLength, Cipher.ENCRYPT_MODE); // write 4 bytes path size os.write(pathLengthEnc); byte[] fullPath = inputFileRelativePath.getBytes(); byte[] fullPathEnc = cryptoWrapperFileNamesAndSizes.cipherBytes(fullPath, Cipher.ENCRYPT_MODE); // write full path of encrypted file os.write(fullPathEnc); // Retain file permissions on Unix flavours if (fileSystemIsPosixCompliant()) { perms = Files.getPosixFilePermissions(Paths.get(fullSourcePath), LinkOption.NOFOLLOW_LINKS); } else { /* * On non-POSIX file systems use default Unix (umask 022) file and * directory permissions */ if (pathIsFolder) perms = PosixFilePermissions.fromString("rwxr-xr-x"); else perms = PosixFilePermissions.fromString("rw-r--r--"); } byte[] permBytes = Utils.permsToByte(perms); byte[] permBytesEnc = cryptoWrapperFileNamesAndSizes.cipherBytes(permBytes, Cipher.ENCRYPT_MODE); // write 2 bytes with the file's permissions os.write(permBytesEnc); // Start file encryption if the supplied relative path is not a folder if (!pathIsFolder) { long l = inputFile.length(); byte[] fileSize = ByteBuffer.allocate(8).putLong(l).array(); byte[] fileSizeEnc = cryptoWrapperFileNamesAndSizes.cipherBytes(fileSize, Cipher.ENCRYPT_MODE); // write 8 bytes filesize os.write(fileSizeEnc); cryptoWrapperFileContent.doCipherOperation(getInputFileStream(fullSourcePath), os, new File(fullSourcePath).length(), Cipher.ENCRYPT_MODE, progress); } }; /** * Dummy OutputStream to continue the en-/decryption process without storing * the output. */ private static class DummyOutputStream extends OutputStream { public void close() throws IOException { // do nothing } public void flush() throws IOException { // do nothing } public void write(byte[] arg0, int arg1, int arg2) throws IOException { // do nothing } public void write(byte[] arg0) throws IOException { // do nothing } public void write(int arg0) throws IOException { // do nothing } } private long decryptArchive(String inputFile, String outputFolder) throws Exception { String fileFormatErrorMessage = "Archive '" + inputFile + "' does not conform to the jEncrypt file format."; long fileCounter = 0; FileInputStream fis = getInputFileStream(inputFile); Set<PosixFilePermission> perms = null; byte[] initializationVectorBytes = new byte[CryptoWrapper.AES_BLOCK_SIZE_IN_BYTES]; byte[] saltBytes = new byte[config.getSaltSize()]; byte[] pathSizeBytes = new byte[4]; boolean pathIsFolder = false; outputFolder += File.separator; try { int nrOfBytesRead = fis.read(saltBytes); nrOfBytesRead += fis.read(initializationVectorBytes); if (nrOfBytesRead == (CryptoWrapper.AES_BLOCK_SIZE_IN_BYTES + config.getSaltSize())) { createCryptoWrappers(initializationVectorBytes, saltBytes); } else { throw new IllegalStateException(fileFormatErrorMessage); } while ((fis.read(pathSizeBytes)) > 0) { // get size of path byte[] pathSizeBytesDec = cryptoWrapperFileNamesAndSizes.cipherBytes(pathSizeBytes, Cipher.DECRYPT_MODE); // 4 byte pathsize int pathSize = Utils.byteArrayToInt(pathSizeBytesDec); // If pathSize > 50MB give warning if (pathSize > 50000000) { logger.warning("Warning: file to decrypt has a full path name of " + Utils.humanReadableByteCount(pathSize) + ".", true); } // get path string byte path[] = new byte[pathSize]; fis.read(path); byte[] pathDec = cryptoWrapperFileNamesAndSizes.cipherBytes(path, Cipher.DECRYPT_MODE); String pathStr = new String(pathDec); // If the path in the archive ends in a Unix file separator then // it's a folder pathIsFolder = (pathStr.charAt(pathStr.length() - 1) == Utils.UNIX_SEPARATOR); // Remove single, double dot path steps and end separator pathStr = FilenameUtils.normalizeNoEndSeparator(pathStr); // get permissions for file or directory byte[] filePermissions = new byte[2]; fis.read(filePermissions); byte[] filePermissionsDec = cryptoWrapperFileNamesAndSizes.cipherBytes(filePermissions, Cipher.DECRYPT_MODE); perms = Utils.byteToPerms(filePermissionsDec); // If the current item is a folder if (pathIsFolder) { File f = new File(outputFolder + pathStr); if (!f.mkdir()) { logger.warning("Error: Unable to create folder '" + pathStr + "'", true); } if (fileSystemIsPosixCompliant) setPosixFilePermissions(f, perms); } else { // get filesize byte b[] = new byte[8]; fis.read(b); byte[] fileSizeDec = cryptoWrapperFileNamesAndSizes.cipherBytes(b, Cipher.DECRYPT_MODE); // 8 byte filesize long fileSize = Utils.byteArrayToLong(fileSizeDec); File fout = new File(outputFolder + pathStr); OutputStream os = null; String message = ""; ProgressInfo progressInfo = null; if (fout.exists()) { logger.warning("Skipping '" + fout.getName() + "'. File already exists.", true); // Use dummy output stream to ensure the cipher counter // (AES CTR mode) will be incremented and have the // correct value for the decryption of the next file. os = new DummyOutputStream(); } else { os = new FileOutputStream(fout); message = getCipherMessage(pathStr, Cipher.DECRYPT_MODE); progressInfo = new FileProgressInfo(message, fileSize, cryptoWrapperFileContent.getReadBufferSize()); } cryptoWrapperFileContent.doCipherOperation(fis, os, fileSize, Cipher.DECRYPT_MODE, progressInfo); os.close(); if (fileSystemIsPosixCompliant) setPosixFilePermissions(fout, perms); // If a file was written to FileOutputStream if (progressInfo != null) { // Increment file counter ++fileCounter; // Ensure next progress info starts on a new line System.out.println(); } } } } catch (NegativeArraySizeException e) { throw new NegativeArraySizeException(fileFormatErrorMessage); } return fileCounter; }; private FileInputStream getInputFileStream(String inputFile) throws IOException { return new FileInputStream(checkFileExists(inputFile)); } private File checkFileExists(String fileString) throws IOException { File file = new File(fileString); if (!file.exists()) { throw new IOException("Cipher operation aborted. File " + file.getAbsolutePath() + " does not exist."); } return file; } /** * Create two wrappers: one for encryption of file meta data (full path, * size, permissions) and one for the actual encryption of the file data. * * @param initializationVector * @param salt * @throws IOException * @throws GeneralSecurityException * @throws DecoderException */ private void createCryptoWrappers(byte[] initializationVector, byte[] salt) throws IOException, GeneralSecurityException, DecoderException { final short keySize = config.getAlgorithmKeySize(); // @formatter:off // Configure CryptoWrapper for en/decrypting file content CryptoWrapper.CryptoWrapperBuilder cryptoWrapperBuilder = new CryptoWrapper.CryptoWrapperBuilder() .cipherName(config.getAlgorithmName()).transformationString(config.getAlgorithmParams()) .password(commandline.getPassword()).keySize(keySize).readBufferSize(config.getReadBufferSize()) .saltSize(config.getSaltSize()) .keyDerivationIterationCount(config.getKeyDerivationIterationCount()); // The decryption process retrieves the salt and IV from the archive to decrypt. if (initializationVector != null && salt != null) { cryptoWrapperBuilder.initializationVector(initializationVector); cryptoWrapperBuilder.salt(salt); } cryptoWrapperFileContent = cryptoWrapperBuilder.build(); byte[] hashedKey = Utils.hashAESKey(cryptoWrapperFileContent.getEncryptionKeyBytes(), keySize); byte[] hashedInitializationVector = Utils.hashAESKey(cryptoWrapperFileContent.getInitializationVector(), CryptoWrapper.AES_BLOCK_SIZE_IN_BITS); // Create a different CryptoWrapper for file metadata (filenames, sizes, permissions) cryptoWrapperFileNamesAndSizes = new CryptoWrapper.CryptoWrapperBuilder() .cipherName(config.getAlgorithmName()).key(hashedKey) .initializationVector(hashedInitializationVector).transformationString(config.getAlgorithmParams()) .keySize(config.getAlgorithmKeySize()).readBufferSize(config.getReadBufferSize()).build(); // @formatter:on } /** * Open the archive file which will hold the encrypted files. Write the salt * and initialization vector to the start of the file. * * @param targetFile * @return target file as OutputStream * @throws IOException */ private OutputStream openArchiveFileAndWriteHeader(String targetFile) throws IOException { File outputFile = new File(targetFile); OutputStream os = new FileOutputStream(outputFile); // Write the salt used for hashing the password, and the initialization // vector for AES en-/decryption, to the beginning of the output file. byte[] salt = cryptoWrapperFileContent.getSalt(); // write salt os.write(salt); byte[] IV = cryptoWrapperFileContent.getInitializationVector(); // write IV os.write(IV); return os; } /** * Cipher the file or folder [sourceFileOrFolder] with the key [keyFile] and * store the ciphered files in the optional [targetFolder]. */ public boolean cipherFileOrFolder() throws Exception { final String sourceFileOrFolder = commandline.getSourceFileOrFolder(); long fileCounter = 0; File sourcePath = new File(sourceFileOrFolder); String targetFileOrFolder = commandline.getTargetFolder(); OutputStream os = null; System.out.println(); try { if (commandline.getCipherMode() == Cipher.ENCRYPT_MODE) { /* * Create one wrapper for file names, sizes and permissions, and * one wrapper for the file contents. If no IV and salt are * given they will be generated using the SecureRandom class. */ createCryptoWrappers(null /* initializationVector */, null /* salt */); if (sourcePath.isDirectory()) { // Get complete tree of all files and folders in sourcePath FolderList folderList = new FolderList(sourcePath.getPath()); Collection<String> fileTree = folderList.getFilesAsString(); Collection<String> folderTree = folderList.getFoldersAsString(); if (fileTree.isEmpty()) { logger.severe("Directory '" + sourcePath.getName() + "' is empty", true); return false; } os = openArchiveFileAndWriteHeader(targetFileOrFolder); // Walk through all directories and store directory // structure in archive. for (String relativePath : folderTree) { // Encrypt file and add to archive encryptAndArchivePath(folderList.getArchiveTopFolder(), relativePath + File.separator, os); } // Walk through all files for (String relativePath : fileTree) { // Encrypt file and add to archive encryptAndArchivePath(folderList.getArchiveTopFolder(), relativePath, os); System.out.println(); ++fileCounter; } } else { os = openArchiveFileAndWriteHeader(targetFileOrFolder); String fullPath = FilenameUtils.getFullPath(sourcePath.getAbsolutePath()); encryptAndArchivePath(fullPath, sourcePath.getName(), os); System.out.println(); ++fileCounter; } os.close(); } else if (sourcePath.isFile()) { fileCounter = decryptArchive(sourcePath.getAbsolutePath(), targetFileOrFolder); } } catch (Exception e) { logger.warning("File ciphering operation failed: " + e.getMessage(), true); e.printStackTrace(); return false; } finally { if (os != null) os.close(); } System.out.println(); String endText = (commandline.getCipherMode() == Cipher.ENCRYPT_MODE) ? "encrypted" : "decrypted"; logger.info(fileCounter + " file(s) " + endText, true); return true; } }