net.sourceforge.jencrypt.FileEncrypter.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.jencrypt.FileEncrypter.java

Source

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