net.sourceforge.jencrypt.lib.CryptoWrapper.java Source code

Java tutorial

Introduction

Here is the source code for net.sourceforge.jencrypt.lib.CryptoWrapper.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.lib;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

import java.io.*;
import java.security.*;
import java.security.spec.InvalidKeySpecException;

/**
 * CryptoWrapper is a wrapper for the Java Cryptographic Extension (JCE)
 * framework.
 */
public class CryptoWrapper {

    /**
     * Files are read part by part into a buffer the size of readBufferSize.
     * The read buffer is a byte array and can not be larger than Integer.MAX_SIZE.
     */
    private int readBufferSize;

    /**
     * The size of the salt in bytes.
     */
    private int saltSize;

    /**
     * Configurable jencrypt.ini parameter indicating the number of iterations
     * the PBKDF2 functions takes to derive a cryptographic key from a password.
     */
    private int keyDerivationIterationCount;

    /**
     * Core JCE class javax.crypto.Cipher
     */
    private final Cipher cipher;

    /**
     * The cryptographic key which determines the transformation of plaintext
     * data into ciphertext data (NIST IR 7298 Rev. 1)
     */

    private final Key cipherKey;

    /**
     * Section 3.1 of NIST SP 800-132 Recommendation for Password-Based Key
      * Derivation:
      * 
      *    Salt  A non-secret binary value that is used as an input to the key
      *          derivation function PBKDF specified in this Recommendation to
      *          allow the generation of a large set of keys for a given password.
     */

    private byte[] salt;

    /**
     * The IV is used in an initial step in the encryption of a message and in
     * the corresponding decryption of the message (NIST SP800-38a).
      * As long as the initial vector is random for each message encrypted under
      * the same key replay and swap attacks are infeasible (LibTomCrypt
      * Developer Manual by Tom St Denis).
     */
    private final Key initializationVector;

    /**
     * AES block size in bits (N.B. the Rijndael algorithm itself allows for
     * other sizes).
     */
    public static final short AES_BLOCK_SIZE_IN_BITS = 128;

    /**
     * AES block size in bytes (N.B. the Rijndael algorithm itself allows for
     * other sizes).
     */
    public static final int AES_BLOCK_SIZE_IN_BYTES = (AES_BLOCK_SIZE_IN_BITS >>> 3);

    private boolean isInitialized = false;

    /**
     * CryptoWrapper builder
     */
    public static class CryptoWrapperBuilder {

        private String password;
        private String cipherName;
        private String transformationString;
        private short keySize;
        private int readBufferSize;
        private Key initializationVector;
        private Key cipherKey;
        private byte[] salt;
        private int saltSize;
        private int keyDerivationIterationCount;

        /**
         * The encryption algorithm (e.g. DES/RSA/AES/etc).
         */
        public CryptoWrapperBuilder cipherName(String s) {
            cipherName = s;
            return this;
        }

        public CryptoWrapperBuilder salt(byte[] salt) {
            this.salt = salt;
            return this;
        }

        public CryptoWrapperBuilder saltSize(int saltSize) {
            this.saltSize = saltSize;
            return this;
        }

        public CryptoWrapperBuilder keyDerivationIterationCount(int keyDerivationIterationCount) {
            this.keyDerivationIterationCount = keyDerivationIterationCount;
            return this;
        }

        /**
         * A transformation always includes the name of a cryptographic
         * algorithm (e.g., DES), and may be followed by a feedback mode and
         * padding scheme.
         */
        public CryptoWrapperBuilder transformationString(String s) {
            transformationString = s;
            return this;
        }

        /**
         * The en/decryption key.
         * 
         * @throws DecoderException
         */
        public CryptoWrapperBuilder password(String password) throws DecoderException {
            this.password = password;
            return this;
        }

        /**
         * The en/decryption key.
         * 
         * @throws DecoderException
         */
        public CryptoWrapperBuilder key(byte[] key) throws DecoderException {
            cipherKey = bytesToKey(key);
            return this;
        }

        /**
         * Size of the key in bits.
         */
        public CryptoWrapperBuilder keySize(short s) {
            keySize = s;
            return this;
        }

        /**
         * The initialization vector.
         */
        public CryptoWrapperBuilder initializationVector(byte[] initializationVector) {
            this.initializationVector = bytesToKey(initializationVector);
            return this;
        }

        /**
         * Files are read into a buffer the size of readBufferSize.
         */
        public CryptoWrapperBuilder readBufferSize(int i) {
            readBufferSize = i;
            return this;
        }

        private Key bytesToKey(byte[] key) {
            if (this.cipherName != null) {
                return new SecretKeySpec(key, this.cipherName);
            } else {
                throw new IllegalStateException(
                        "Encryption algorithm not specified with a call to CryptoWrapperBuilder.cipherName()");
            }
        }

        /**
         * Calls the CryptoWrapper constructor with all parameters inside a
         * Builder instance.
         */
        public CryptoWrapper build() throws IOException, GeneralSecurityException {
            return new CryptoWrapper(this);
        }
    }

    /**
     * CryptoWrapper constructor creates a Cipher instance with the given
     * parameters.
     * 
     */
    private CryptoWrapper(CryptoWrapperBuilder builder) throws IOException, GeneralSecurityException {

        readBufferSize = builder.readBufferSize;
        cipher = Cipher.getInstance(builder.transformationString);
        saltSize = builder.saltSize;
        keyDerivationIterationCount = builder.keyDerivationIterationCount;

        // Salt to prevent rainbow-table assisted dictionary attacks.
        // Used to derive a key from the password with PBKDF2WithHmacSHA1.
        if (builder.salt == null) {
            salt = new byte[saltSize];
            SecureRandom secureRandom = new SecureRandom();
            secureRandom.nextBytes(salt);
        } else {
            salt = builder.salt;
        }

        if (builder.cipherKey == null) {
            cipherKey = new SecretKeySpec(getHashedPassword(builder), builder.cipherName);
        } else {
            cipherKey = builder.cipherKey;
        }

        /* If no IV given then generate a new one */
        if (builder.initializationVector == null) {
            initializationVector = getInitializationVector(builder);
        } else {
            initializationVector = builder.initializationVector;
        }
    }

    private Key getInitializationVector(CryptoWrapperBuilder builder) throws NoSuchAlgorithmException {

        KeyGenerator generator = KeyGenerator.getInstance(builder.cipherName);
        generator.init(CryptoWrapper.AES_BLOCK_SIZE_IN_BITS, new SecureRandom());
        return generator.generateKey();
    }

    private byte[] getHashedPassword(CryptoWrapperBuilder builder)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        /* Apply PBKDF2 (Password-Based Key Derivation Function 2) with
         * HMAC-SHA-1 to the password (for further details, see RFC-2898).
         */
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        PBEKeySpec spec = new PBEKeySpec(builder.password.toCharArray(), salt, keyDerivationIterationCount,
                builder.keySize);
        SecretKey secret = factory.generateSecret(spec);
        return secret.getEncoded();
    }

    /**
     * Return the encryption buffer size. E.g. To calculate the total number of
     * iterations of the encryption loop which is used to print the progress in
     * percentages of the encryption.
     */
    public int getReadBufferSize() {
        return readBufferSize;
    }

    public byte[] getSalt() {
        return salt;
    }

    public int getSaltSize() {
        return saltSize;
    }

    public byte[] getEncryptionKeyBytes() {
        return cipherKey.getEncoded();
    }

    public byte[] getInitializationVector() {
        return initializationVector.getEncoded();
    }

    /**
     * Read the encryption key into a byte array.
     * 
     * @param keyFile
     *            the file with the en/decryption key
     * @return byte[]
     */
    public byte[] readKeyFile(File keyFile) throws IOException {
        byte[] keyBytes = null;
        byte[] bytes = null;
        try {
            keyBytes = Utils.getBytesFromFile(keyFile);
            // convert hex to bytes
            bytes = new Hex().decode(keyBytes);
        } catch (Exception e) {
            throw new IOException("Error reading keyfile '" + keyFile.getName() + "': " + e.getMessage());
        }
        return bytes;
    }

    /**
     * Save the generated en/decryption key.
     */
    public void saveKeyFile(String file, Key k) throws IOException {
        BufferedWriter bw = null;
        try {
            FileWriter fw = new FileWriter(file);
            bw = new BufferedWriter(fw);
            bw.write(Hex.encodeHexString(k.getEncoded()));
            bw.flush();
        } catch (IOException e) {
            throw new IOException("Error saving key file :'" + file + "': " + e.getMessage());
        } finally {
            bw.close();
        }
    }

    public byte[] cipherBytes(byte[] bytesToCipher, int cipherMode) throws Exception {

        if (isInitialized == false) {
            try {
                cipher.init(cipherMode, cipherKey, new IvParameterSpec(initializationVector.getEncoded()));
            } catch (InvalidKeyException e) {
                throw new InvalidKeyException("Error : " + e.getMessage()
                        + "\nKey file corrupt or invalid key parameters."
                        + "\nTo use key sizes above 128 bits please install the JCE Unlimited Strength Jurisdiction Policy Files.");
            }
            isInitialized = true;
        }

        return cipher.update(bytesToCipher);
    }

    /**
     * De- or encrypt a file.
     * 
     * @param inputFileFullPath
     *            file to en- or decrypt
     * @param outputFileFullPath
     *            file with result of en-/decryption
     * @param cipherMode
     *            Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
     * @param progress
     *            progress info class to print progress percentage
     * 
     */
    public void doCipherOperation(InputStream in, OutputStream out, long nrOfBytesToCipher, int cipherMode,
            ProgressInfo progress) throws Exception {

        if (nrOfBytesToCipher > 0) {

            try {
                // If the nr of bytes to read is smaller than readBufferSize
                // use the nr of bytes as the max buffer size.
                int maxBufferSize = (int) Math.min(readBufferSize, nrOfBytesToCipher);
                byte[] readBuffer = new byte[maxBufferSize];

                if (isInitialized == false) {
                    cipher.init(cipherMode, cipherKey, new IvParameterSpec(initializationVector.getEncoded()));
                    isInitialized = true;
                }

                long nrOfBytesLeftToCipher = nrOfBytesToCipher;

                while (nrOfBytesLeftToCipher > 0) {

                    in.read(readBuffer, 0, maxBufferSize);

                    nrOfBytesLeftToCipher -= cipher.update(readBuffer, 0, maxBufferSize, readBuffer);

                    // If a ProgressInfo was given, calculate the current
                    // percentage and print it
                    if (progress != null)
                        progress.trackAndPrintProgress();

                    out.write(readBuffer, 0, maxBufferSize);

                    if (nrOfBytesLeftToCipher > 0 && nrOfBytesLeftToCipher < maxBufferSize) {
                        maxBufferSize = (int) nrOfBytesLeftToCipher;
                    }
                }
                //cipher.doFinal();

            } catch (IOException e) {
                throw new IOException("Error reading bytes to encrypt: " + e.getMessage());
            } catch (InvalidKeyException e) {
                throw new InvalidKeyException("Error : " + e.getMessage()
                        + "\nKey file corrupt or invalid key parameters."
                        + "\nTo use key sizes above 128 bits please install the JCE Unlimited Strength Jurisdiction Policy Files.");
            }
        } else {
            // Print progress even if nrOfBytesToCipher == 0
            if (progress != null)
                progress.trackAndPrintProgress();
        }
    }
}