//============================================================================== // FileEncrypter.java //============================================================================== package tribble.crypto; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.lang.Exception; import java.lang.RuntimeException; import java.lang.String; import java.lang.StringBuilder; import java.lang.System; import java.lang.Throwable; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.Random; import java.util.zip.Deflater; import java.util.zip.Inflater; import tribble.io.Base64DecoderInputStream; import tribble.io.Base64EncoderOutputStream; import tribble.io.DeflaterInputStream; import tribble.io.InflaterOutputStream; import tribble.crypto.AESCipher; import tribble.crypto.SymmetricCipher; /******************************************************************************* * Encrypts or decrypts a data file using a stream cipher. * *

* A file can be encrypted or decrypted by supplying a passphrase. * The passphrase is hashed (using the SHA-1 algorithm) to generate a 128-bit * encryption key, which is then used to encrypt or decrypt the contents of a * specified file (using an AES-128 CFB-8 stream cipher algorithm). * *

* Note: * The key hashing method has been changed in revision 2.1 (2009-01-27). * The '-p1' command line option is provided for backward compatibility, * so that data files that were encrypted with earlier versions of this program * can still be decrypted. * * *

*
Source code:
*
* http://david.tribble.com/src/java/tribble/crypto/FileEncrypter.java *
*
Documentation:
*
* http://david.tribble.com/docs/tribble/crypto/FileEncrypter.html *
*
* * * @version $Revision: 2.4 $ $Date: 2009/04/05 17:00:23 $ * @since 2005-03-26 * @author David R. Tribble (david@tribble.com)
* Copyright ©2005-2009 by David R. Tribble, all rights reserved.
* Permission is granted to any person or entity except those designated * by the United States Department of State as a terrorist or terrorist * government or agency, to use and distribute this source code provided * that the original copyright notice remains present and unaltered. * * @see AESCipher * @see StreamCipherSpi */ public class FileEncrypter { static final String REV = "@(#)tribble/crypto/FileEncrypter.java $Revision: 2.4 $ $Date: 2009/04/05 17:00:23 $\n"; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Constants // Ciphering modes public static final int MODE_NONE = 0; public static final int MODE_CFB8 = 1; public static final int MODE_OFB128 = 2; /** Exit code: Success. */ public static final int RC_OKAY = 0; /** Exit code: Can't read input. */ public static final int RC_READ = 1; /** Exit code: Can't write output. */ public static final int RC_WRITE = 2; /** Exit code: Bad passphrase. */ public static final int RC_PASSWORD = 3; /** Exit code: Bad command usage. */ public static final int RC_USAGE = 255; /** Hashing algorithm to use to convert a passphrase into a key. */ protected static final String HASH_ALG = "SHA"; /** Stream cipher key size (in bytes). */ protected static final int KEY_LEN = 128/8; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Static variables /** Enable verbose debugging output. */ public static boolean s_debugs; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Static methods /*************************************************************************** * Encrypt or decrypt a data file. * *

* * Usage *

* * java tribble.crypto.FileEncrypter * [-p passphrase] [-options...] file * * *

* Options: *

*
-a *
* Encode/decode the data as base-64 ASCII text. * By default, the encrypted data is binary. This option causes encrypted * data to be written as ASCII text, using in a base-64 (radix-64) * character encoding. For decryption, this option specifies that the * input is in base-64 ASCII form. * *
-c *
* Compress the data (default). * By default, the data is compressed (using the GZIP/PKZIP "deflate" * compression algorithm) prior to being encrypted, and uncompressed after * being decrypted. In addition to making the encrypted output smaller, * this also makes it harder to crack the encryption. * *
-cfb8 *
* Use a CFB-8 (Cipher FeedBack, 8-bit) ciphering mode. * (This is the default.) * *
-ofb128 *
* Use an OFB-128 (Output FeedBack, 128-bit) ciphering mode. * *
-D *
* Display verbose messages. * These messages are written to the standard error output. * *
-d *
* Decrypt the file. * *
-e *
* Encrypt the file (the default). * *
-nc *
* Do not compress the data. * This disables compression/decompression of the data (-c). * * * *
-o output-file *
* Specifies the file to write the encrypted output data to. * If this option is not specified, the output is written to the standard * output stream by default. * *
-p passphrase *
* Specifies the encryption passphrase. This is hashed into the symmetric * cipher key used for encryption/decryption. * *
-pe var *
* Specifies an environment variable containing the encryption passphrase. * * * *
-pp name *
* Specifies a Java property name containing the encryption passphrase. * *
-p1 *
* Specifies that the encryption passphrase is to be hashed into the * symmetric cipher key using an algorithm that is backward compatible * with revisions prior to 2.1. *
* *

* If a passphrase is not specified, it will be read from the standard input. * * @param args * Command line arguments. * * @throws Exception * Thrown if an I/O or encryption error occurs. * * @since 1.1, 2005-06-26 */ public static void main(String[] args) throws Exception { int i; String inFname = "-"; String outFname = "-"; String pwd = null; boolean encrypt = true; boolean hasIV = true; boolean squeezed = true; boolean base64 = false; boolean keyV1 = false; int mode = MODE_CFB8; byte[] key; InputStream in = null; OutputStream out = null; FileEncrypter enc; long nBytes; // Parse the command line args for (i = 0; i < args.length && args[i].charAt(0) == '-'; i++) { if (args[i].equals("-")) break; else if (args[i].equals("-a")) base64 = true; else if (args[i].equals("-c")) squeezed = true; else if (args[i].equals("-cfb8")) mode = MODE_CFB8; else if (args[i].equals("-ofb128")) mode = MODE_OFB128; else if (args[i].equals("-D")) s_debugs = true; else if (args[i].equals("-d")) encrypt = false; else if (args[i].equals("-e")) encrypt = true; else if (args[i].equals("-nc")) squeezed = false; /*+++INCOMPLETE else if (args[i].equals("-ni")) inclInfo = false; +++*/ else if (args[i].equals("-niv")) hasIV = false; else if (args[i].equals("-o")) outFname = args[++i]; else if (args[i].equals("-p")) { pwd = args[++i]; args[i] = ""; } else if (args[i].equals("-pe")) pwd = System.getenv(args[++i]); else if (args[i].equals("-pp")) pwd = System.getProperty(args[++i]); /*+++INCOMPLETE if (args[i].equals("-pf")) { pwdFile = args[++i]; args[i] = ""; } +++*/ else if (args[i].equals("-p1")) keyV1 = true; else { System.err.println("Bad option: '" + args[i] + "'"); System.exit(RC_USAGE); } } // Check command line args if (i >= args.length) { // Display a usage message System.out.println("Encrypt or decrypt a file."); System.out.println(); System.out.println("usage: java " + FileEncrypter.class.getName() + " [-option...] file"); System.out.println(); System.out.println("Options:"); System.out.println(" -a " + "Encode/decode the data as base-64 ASCII text."); System.out.println(" -c " + "Compress/decompress the data (default)."); System.out.println(" -cfb8 " + "Use a CFB-8 (Cipher FeedBack, 8-bit) ciphering mode " + "(default)."); System.out.println(" -ofb128 " + "Use an OFB-128 (Output FeedBack, 128-bit) ciphering mode."); System.out.println(" -D " + "Display verbose messages."); System.out.println(" -d " + "Decrypt the file."); System.out.println(" -e " + "Encrypt the file (default)."); System.out.println(" -nc " + "Do not compress/decompress the data."); /*+++INCOMPLETE System.out.println(" -ni " + "Do not embed file info."); +++*/ System.out.println(" -o file " + "Output file (default is standard output)."); System.out.println(" -p phrase " + "Encryption passphrase."); System.out.println(" -pe var " + "Environment variable containing the encryption passphrase."); /*+++INCOMPLETE System.out.println(" -pf file " + "File containing the passphrase."); +++*/ System.out.println(" -pp name " + "Java property name containing the encryption passphrase."); System.out.println(" -p1 " + "Hash the encryption passphrase using revision 1.x " + "algorithm."); System.out.println(); System.out.println("An input filename of \"-\" reads from the " + "standard input."); System.out.println("An output filename of \"-\" writes to the " + "standard output."); System.out.println("If a passphrase is not specified, it will be " + "read from the standard input."); // Punt System.exit(RC_USAGE); } // Open the input file inFname = args[i++]; if (inFname.equals("-")) { // Read from standard input in = System.in; } else { try { File inFile; // Read from a named file inFile = new File(inFname); if (!inFile.exists()) throw new IOException("File does not exist"); if (!inFile.canRead()) throw new IOException("Can't read file"); in = new FileInputStream(inFname); } catch (IOException ex) { System.err.println("Can't read: " + inFname); System.err.println(ex.getMessage()); System.exit(RC_READ); } } // Open the output file if (outFname.equals("-")) { // Write to standard output out = System.out; } else { try { // Write to a named file out = new FileOutputStream(outFname); } catch (IOException ex) { System.err.println("Can't write: " + inFname); System.err.println(ex.getMessage()); System.exit(RC_WRITE); } } // Get the passphrase if it was not already specified if (pwd == null) { BufferedReader stdin; //+FIXME: This should read user-entered text while echoing '*' chars // Prompt for a passphrase stdin = new BufferedReader(new InputStreamReader(System.in)); System.err.print("Passphrase? "); System.err.flush(); pwd = stdin.readLine(); /*+FIXME: This should read user-entered text while echoing '*' chars String pwd2; System.err.print("Re-enter? "); System.err.flush(); pwd2 = stdin.readLine(); if (!pwd.equals(pwd2)) { System.err.println("Passphrases do not match. Quit."); System.exit(RC_PASSWORD); } +*/ } // Initialize the file encrypter/decrypter enc = new FileEncrypter(); enc.m_inFname = inFname; enc.m_outFname = outFname; // Derive the encryption key from the passphrase if (s_debugs) System.err.println("Hash the passphrase"); if (keyV1) key = deriveKey_v1(pwd, KEY_LEN); else key = deriveKey(pwd, KEY_LEN); pwd = ""; // Encrypt/decrypt the data file if (encrypt) nBytes = enc.encrypt(in, out, key, hasIV, squeezed, base64, mode); else nBytes = enc.decrypt(in, out, key, hasIV, squeezed, base64, mode); // Done enc.reset(); if (s_debugs) System.err.println("File: " + inFname + ", bytes: " + nBytes); } /*************************************************************************** * Derive a cipher encryption key from a passphrase. * (Version 2.x.) * *

* An encryption key is derived by hashing (using SHA-1) the text passphrase * and extracting the upper bits of the hash. * * @param pwd * A user-supplied passphrase. * * @param keyLen * Length of the key to generate (in bytes). * * @return * An encryption key derived from the passphrase. * * @throws RuntimeException (unchecked) * Thrown if the hashing (message digest) class could not be loaded. * * @since 1.2, 2005-06-28 */ public static byte[] deriveKey(String pwd, int keyLen) { byte[] p; byte[] k; int len; // Convert the passphrase into bytes (2 bytes per char) len = pwd.length(); p = new byte[len*2]; for (int i = 0; i < len; i++) { p[i+i+0] = (byte) (pwd.charAt(i) >> 8); p[i+i+1] = (byte) (pwd.charAt(i)); } // Hash the passphrase bytes into a binary encryption key k = deriveKey(p, keyLen); // Wipe sensitive data for (int i = 0; i < p.length; i++) p[i] = (byte) 0x00; return k; } /*************************************************************************** * Derive a cipher encryption key from a passphrase. * (Version 1.x.) * *

* Note: This method has been renamed as of revision 2.1, being * replaced with a new algorithm used in method * {@link #deriveKey(String, int) deriveKey()}. * *

* An encryption key is derived by hashing (using SHA-1) the text passphrase * and extracting the upper bits of the hash. * * @param pwd * A user-supplied passphrase. * * @param keyLen * Length of the key to generate (in bytes). * * @return * An encryption key derived from the passphrase. * * @throws RuntimeException (unchecked) * Thrown if the hashing (message digest) class could not be loaded. * * @since 2.4, 2009-04-05 */ public static byte[] deriveKey_v1(String pwd, int keyLen) { byte[] p; byte[] k; // Hash the encryption passphrase into a binary encryption key p = pwd.getBytes(); // Hash the passphrase bytes into a binary encryption key k = deriveKey(p, keyLen); // Wipe sensitive data for (int i = 0; i < p.length; i++) p[i] = (byte) 0x00; return k; } /*************************************************************************** * Derive an encryption key from a passphrase. * *

* An encryption key is derived by hashing (using SHA-1) the text passphrase * and extracting the upper bits of the hash. * * @param pwd * A user-supplied text passphrase. * * @param keyLen * Length of the key to generate (in bytes). * * @return * An encryption key derived from the passphrase. * * @throws RuntimeException (unchecked) * Thrown if the hashing (message digest) class could not be loaded. * * @since 1.2, 2005-06-28 */ public static byte[] deriveKey(/*const*/ byte[] pwd, int keyLen) { byte[] k; // Get the hash (message digest) algorithm and hash the passphrase try { MessageDigest md; if (s_debugs) System.err.println("pwd: [" + toHexString(pwd) + "]"); // Hash the passphrase if (s_debugs) System.err.println("Initialize " + HASH_ALG + " hash algorithm"); md = MessageDigest.getInstance(HASH_ALG); k = md.digest(pwd); if (s_debugs) System.err.println("hash: [" + toHexString(k) + "]"); // Wipe sensitive data md.reset(); md = null; } catch (NoSuchAlgorithmException ex) { throw new RuntimeException("Can't get " + HASH_ALG + " message digest object"); } // Resize the hashed passphrase into a binary key if (k.length != keyLen) { byte[] h; h = k; k = new byte[keyLen]; for (int i = 0; i < keyLen; i++) k[i] = h[i % h.length]; } if (s_debugs) System.err.println("key: [" + toHexString(k) + "]"); return k; } /*************************************************************************** * Convert an array of bytes to a hexadecimal string. * * @since 2.1, 2009-01-22 */ private static String toHexString(/*const*/ byte[] b, int off, int len) { StringBuilder s; // Convert bytes to "dd" hexadecimal strings s = new StringBuilder(len*3); for (int i = 0; i < len; i++) { int d; if (i > 0) s.append(' '); d = (b[off] >> 4) & 0xF; s.append((char) (d < 0xA ? d + '0' : d - 0xA + 'A')); d = b[off] & 0xF; s.append((char) (d < 0xA ? d + '0' : d - 0xA + 'A')); off++; } return s.toString(); } /*************************************************************************** * Convert an array of bytes to a hexadecimal string. * * @since 2.1, 2009-01-22 */ private static String toHexString(/*const*/ byte[] b) { return toHexString(b, 0, b.length); } /*************************************************************************** * Convert a single byte to a hexadecimal string. * * @since 2.1, 2009-01-27 */ private static String toHexString(int b) { char[] ch; int d; ch = new char[2]; d = (b >> 4) & 0xF; ch[0] = (char) (d < 0xA ? d + '0' : d - 0xA + 'A'); d = b & 0xF; ch[1] = (char) (d < 0xA ? d + '0' : d - 0xA + 'A'); return new String(ch); } // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Variables /** Plaintext input filename. */ private String m_inFname = "-"; /** Encrypted output filename. */ private String m_outFname = "-"; /** Plaintext (or ciphertext) input data stream. */ private InputStream m_in; /** Encrypted (or decrypted) output data stream. */ private OutputStream m_out; /** Encryption cipher. */ private SymmetricCipher m_cipher; /** Pseudo-random IV block. */ private byte[] m_iv; /** Encryption cipher key (hashed from a passphrase). */ private byte[] m_key; /** Stream cipher state block. */ private byte[] m_state; /** Stream cipher output block. */ private byte[] m_eBuf; /** Cipher block size (in bytes). */ private int m_blockLen; /** Encrypted data has prepended random IV block. */ private boolean m_hasIV; // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Constructors /*************************************************************************** * Default constructor. * * @since 1.1, 2005-06-26 */ public FileEncrypter() { } // ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ // Methods /*************************************************************************** * Wipe all sensitive information from this file encrypter/decrypter. * * @since 1.1, 2005-06-26 */ public void reset() { // Wipe sensitive data if (m_cipher != null) { m_cipher.clear(); m_cipher = null; } if (m_key != null) { for (int i = 0; i < m_key.length; i++) m_key[i] = 0x00; m_key = null; } if (m_state != null) { for (int i = 0; i < m_state.length; i++) m_state[i] = 0x00; m_state = null; } // Disassociate the I/O streams m_in = null; m_out = null; } /*************************************************************************** * Encrypt an input stream. * *

* Random IV bytes are prepended to the output stream to make it harder * to crack the encryption. * Note: Not using a random prepended IV severly compromises the * security of the stream cipher. * Likewise, no two messages (files) should ever be encrypted using the same * passphrase+IV combination. * *

* Stream Cipher - CFB-8 Encryption Mode *

*

    *         Random +---------------+
    *         IV     | | | | | | | | |  (128 bits, 16 bytes)
    *                +---------------+
    *                        :
    *                        v
    *         State  +-------------+-+
    *         Block  | | | | | | | |C| <------+
    *                +-------------+-+        :
    *                 Si     :                :
    *                        :                :
    *     +---------------+  :                :
    * Key | | | | | | | | |  :                :
    *     +---------------+  :                :
    *           :            v                :
    *           :    +===============+        :
    *           +--> [               ]        :
    *                [    Cipher     ]        :
    *                [               ]        :
    *                +===============+        :
    *                        :                :
    *                        v                :
    *      Encrypted +-------------+-+        :
    *        Block   |E| | | | | | | |        :
    *                +-------------+-+        :
    *                 :             Ei        :
    *                 v                      +-+     Encrypted
    *              [ XOR ] ----------------> |C| --> Output
    *                 ^                      +-+     Stream
    *                 :                       Ci
    * Plaintext      +-+
    * Input -------> |P|
    * Stream         +-+
    *                 Pi 
* *

* The state block is initially filled with the IV. As each plaintext byte * Pi is read from the input stream, the state block is encrypted using the * encryption key to produce the next encryption keystream block Ei. * The last byte of the keystream block is XORed with the plaintext input * byte Pi to produce the output ciphertext byte Ci, which is written to the * output stream. The encrypted byte Ci is then shifted into the state * block, which prepares the state block for the next input byte Pi+1. * *

* Stream Cipher - OFB-128 Encryption Mode *

*

    *         Random +---------------+
    *         IV     | | | | | | | | |  (128 bits, 16 bytes)
    *                +---------------+
    *                        :
    *                        v
    *         State  +---------------+
    *         Block  | | | | | | | | | <---+
    *                +---------------+     :
    *                  Si    :             :
    *                        :             :
    *     +---------------+  :             :
    * Key | | | | | | | | |  :             :
    *     +---------------+  :             :
    *           :            v             :
    *           :    +===============+     :
    *           +--> [               ]     :
    *                [    Cipher     ]     :
    *                [               ]     :
    *                +===============+     :
    *                        :             :
    *                        v             :
    *      Encrypted +---------------+     :
    *        Block   | | | | | | | | | ----+
    *                +---------------+
    *                  Ei    :
    *                        v        +-+-+-+-+-+-+-+-+
    *                     [ XOR ] --> | | | | | | |X|X| --> Output
    *                        ^        +-+-+-+-+-+-+-+-+     Stream
    *                        :          Ci
    * Input          +-+-+-+-+-+-+-+-+
    * Stream ------> | | | | | | |X|X|
    *                +-+-+-+-+-+-+-+-+
    *                  Pi 
* *

* The state block is initially filled with the IV. As each block of * plaintext bytes Pi is read from the input stream, the state block is * encrypted using the encryption key to produce the next encryption * keystream block Ei. The bytes of the keystream block are XORed with the * plaintext input bytes Pi to produce the output ciphertext bytes Ci, which * are written to the output stream. The encrypted bytes Ei are then shifted * into the state block, which prepares the state block for the next input * block Pi+1. * * @param in * A binary input stream to encrypt. * * @param out * A binary output stream to write the encrypted data to. * * @param key * Encryption key, which must be either 128, 192, or 256 bits (16, 24, or 32 * bytes) long. * * @param hasIV * If true, the encrypted output stream will have random IV bytes prepended * to it, otherwise not. * * @param squeezed * * If true, the plaintext data from the input stream is compressed (using * the ZIP "deflate" compression method) prior to being encrypted. This not * only makes the resulting encrypted output stream smaller than the original * input stream, it also makes it harder to crack the encrypted data using * chosen plaintext attacks. * * @param base64 * * If true, the encrypted output data is written as base-64 (a.k.a. radix-64) * ASCII text instead of as binary data. This character text data is * suitable for inclusion in normal text and email documents. * * @param mode * * Ciphering mode to use, which is one of the * {@link MODE_NONE MODE_XXX} constants. * * @return * Number of bytes encrypted. * * @throws IOException * Thrown if either in or out is null. * * @throws InvalidKeyException * If key is not the correct length or otherwise invalid. * * @since 1.1, 2005-05-26 */ public long encrypt(InputStream in, OutputStream out, byte[] key, boolean hasIV, boolean squeezed, boolean base64, int mode) throws IOException, InvalidKeyException { long nBytes; // Set up setup(in, out, key, hasIV); m_hasIV = hasIV; // Handle compressed input stream if (squeezed) m_in = new DeflaterInputStream(m_in, new Deflater(Deflater.BEST_COMPRESSION, true)); // Handle base-64 ASCII encoded output stream if (base64) m_out = new Base64EncoderOutputStream(m_out); // Encrypt data from the input stream, using a stream cipher switch (mode) { case MODE_CFB8: default: nBytes = encryptCFB(); break; case MODE_OFB128: nBytes = encryptOFB(); break; } // Clean up m_in.close(); m_out.close(); // Done if (s_debugs) System.err.println("Encrypted bytes: " + nBytes); return nBytes; } /*************************************************************************** * Decrypt an input stream. * *

* Random IV bytes may have been prepended to the output stream to make it * harder to crack the encryption. * Note: Not using a random prepended IV severly compromises the * security of the stream cipher. * Likewise, no two messages (files) should ever be encrypted using the same * passphrase+IV combination. * *

* Stream Cipher - CFB-8 Decryption Mode *

* The decryption algorithm is identical to the encryption algorithm. *

    *         Random +---------------+
    *         IV     | | | | | | | | |  (128 bits, 16 bytes)
    *                +---------------+
    *                        :
    *                        v
    *         State  +-------------+-+
    *         Block  | | | | | | | |P| <------+
    *                +-------------+-+        :
    *                  Si    :                :
    *                        :                :
    *     +---------------+  :                :
    * Key | | | | | | | | |  :                :
    *     +---------------+  :                :
    *           :            v                :
    *           :    +===============+        :
    *           +--> [               ]        :
    *                [    Cipher     ]        :
    *                [               ]        :
    *                +===============+        :
    *                        :                :
    *                        v                :
    *      Encrypted +-------------+-+        :
    *        Block   | | | | | | | |E|        :
    *                +-------------+-+        :
    *                 :             Ei        :
    *                 v                  +-+  :      Decrypted
    *              [ XOR ] ------------> |P| ------> Output
    *                 ^                  +-+  :      Stream
    *                 :                   Pi  :
    * Encrypted      +-+                      :
    * Input -------> |C| ---------------------+
    * Stream         +-+
    *                 Ci 
* *

* The state block is initially filled with the IV. As each ciphertext byte * Ci is read from the input stream, the state block is encrypted using the * encryption key to produce the next encryption keystream block Ei. * The last byte of the keystream block is XORed with the ciphertext input * byte Ci to produce the output plaintext byte Pi, which is written to the * output stream. The encrypted byte Ci is then shifted into the state * block, which prepares the state block for the next input byte Ci+1. * *

* Stream Cipher - OFB-128 Decryption Mode *

* The decryption algorithm is identical to the encryption algorithm. *

    *         Random +---------------+
    *         IV     | | | | | | | | |  (128 bits, 16 bytes)
    *                +---------------+
    *                        :
    *                        v
    *         State  +---------------+
    *         Block  | | | | | | | | | <---+
    *                +---------------+     :
    *                  Si    :             :
    *                        :             :
    *     +---------------+  :             :
    * Key | | | | | | | | |  :             :
    *     +---------------+  :             :
    *           :            v             :
    *           :    +===============+     :
    *           +--> [               ]     :
    *                [    Cipher     ]     :
    *                [               ]     :
    *                +===============+     :
    *                        :             :
    *                        v             :
    *      Encrypted +---------------+     :
    *        Block   | | | | | | | | | ----+
    *                +---------------+
    *                  Ei    :
    *                        v        +-+-+-+-+-+-+-+-+
    *                     [ XOR ] --> | | | | | | |X|X| --> Output
    *                        ^        +-+-+-+-+-+-+-+-+     Stream
    *                        :          Pi
    * Input          +-+-+-+-+-+-+-+-+
    * Stream ------> | | | | | | |X|X|
    *                +-+-+-+-+-+-+-+-+
    *                  Ci 
*

* The state block is initially filled with the IV. As each block of * ciphertext bytes Ci is read from the input stream, the state block is * encrypted using the encryption key to produce the next encryption * keystream block Ei. The bytes of the keystream block are XORed with the * ciphertext input bytes Ci to produce the output plaintext bytes Pi, which * are written to the output stream. The encrypted bytes Ei are then shifted * into the state block, which prepares the state block for the next input * block Ci+1. * * @param in * A binary input stream to decrypt. * * @param out * A binary output stream to write the decrypted data to. * * @param key * Encryption key, which must be either 128, 192, or 256 bits (16, 24, or 32 * bytes) long. * * @param hasIV * If true, the encrypted input stream has random IV bytes prepended to it, * otherwise not. * * @param squeezed * * If true, the decrypted data from the input stream is decompressed (using * the ZIP "inflate" compression method) after being decrypted. * * @param base64 * * If true, the encrypted input data is read as base-64 (a.k.a. radix-64) * ASCII text instead of as binary data. * * @param mode * * Ciphering mode to use, which is one of the * {@link MODE_NONE MODE_XXX} constants. * * @return * Number of bytes encrypted. * * @throws IOException * Thrown if either in or out is null. * * @throws InvalidKeyException * If key is not the correct length or otherwise invalid. * * @since 1.1, 2005-05-26 */ public long decrypt(InputStream in, OutputStream out, byte[] key, boolean hasIV, boolean squeezed, boolean base64, int mode) throws IOException, InvalidKeyException { long nBytes; // Set up setup(in, out, key, false); m_hasIV = hasIV; // Handle base-64 ASCII encoded input stream if (base64) m_in = new Base64DecoderInputStream(m_in); // Handle decompressed output stream if (squeezed) m_out = new InflaterOutputStream(m_out, new Inflater(true)); // Encrypt data from the input stream, using a stream cipher switch (mode) { case MODE_CFB8: default: nBytes = decryptCFB(); break; case MODE_OFB128: nBytes = decryptOFB(); break; } // Clean up m_in.close(); m_out.close(); // Done if (s_debugs) System.err.println("Decrypted bytes: " + nBytes); return nBytes; } /*************************************************************************** * Finalization. * Wipes all sensitive information from this file encrypter/decrypter. * * @see #reset * * @since 1.1, 2005-03-30 */ protected synchronized void finalize() throws Throwable { // Wipe sensitive data reset(); // Cascade super.finalize(); } /*************************************************************************** * Initialize this encrypter/decrypter. * * @param in * A binary input stream to encrypt/decrypt. * * @param out * A binary output stream to write the encrypted/decrypted data to. * * @param key * Encryption key, which must be either 128 bits (16 bytes), 192 (24 bytes), * or 256 (32 bytes) long. * * @param hasIV * If true, the encrypted stream has random IV bytes prepended to it, * otherwise not. * * @throws IOException * Thrown if either in or out is null. * * @throws InvalidKeyException * If key is not the correct length or otherwise invalid. * * @since 1.1, 2005-05-26 */ private void setup(InputStream in, OutputStream out, /*const*/ byte[] key, boolean hasIV) throws IOException, InvalidKeyException { // Sanity checks if (in == null) throw new IOException("Null input stream"); if (out == null) throw new IOException("Null output stream"); // Set up the input and output streams if (in instanceof BufferedInputStream) m_in = (BufferedInputStream) in; else m_in = new BufferedInputStream(in); if (out instanceof BufferedOutputStream) m_out = (BufferedOutputStream) out; else m_out = new BufferedOutputStream(out); // Set up the encryption key if (key.length >= 256/8) m_key = new byte[256/8]; else if (key.length >= 192/8) m_key = new byte[192/8]; else if (key.length >= 128/8) m_key = new byte[128/8]; else throw new InvalidKeyException("Invalid key length (" + key.length + ")"); for (int i = 0; i < m_key.length; i++) m_key[i] = (i < key.length ? key[i] : 0x00); // Set up the stream cipher if (s_debugs) System.err.println("Initialize AES cipher algorithm"); m_cipher = new AESCipher(); m_cipher.initialize(m_key, false); m_blockLen = m_cipher.getBlockSize(); m_eBuf = new byte[m_blockLen]; if (m_state != null) { for (int i = 0; i < m_state.length; i++) m_state[i] = 0x00; } if (m_state == null || m_state.length < m_blockLen) m_state = new byte[m_blockLen]; // Set up the initial random IV bytes m_iv = new byte[m_blockLen]; if (hasIV) { long seed; Random rand; // Generate pseudo-random IV bytes from the current date seed = (new Date()).getTime(); seed ^= (seed << 40); rand = new Random(seed); rand.nextBytes(m_iv); } } /*************************************************************************** * Encrypt the input stream using a CFB-8 cipher, writing to the output * stream. * * @throws IOException * Thrown if an I/O error occurs. * * @return * Number of bytes encrypted. * * @since 2.4, 2009-04-05 (1.1, 2005-05-26) */ private long encryptCFB() throws IOException { byte[] eBuf; byte[] state; long nBytes; int blockLen; int eCnt; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; eCnt = -1; // Prepend the random IV block to the output stream if (s_debugs) System.err.println("IV: [" + toHexString(m_iv) + "]"); if (m_hasIV) { System.arraycopy(m_iv, 0, state, 0, blockLen); m_out.write(m_iv, 0, blockLen); } // Encrypt data from the input stream, using a CFB-8 stream cipher nBytes = 0; for (;;) { int b; // Read the next data byte from the input stream b = m_in.read(); if (b < 0) break; // Generate the next ciphering block m_cipher.blockEncrypt(state, 0, eBuf, 0); // Encrypt the data byte b ^= eBuf[0]; for (int i = 1; i < blockLen; i++) state[i-1] = state[i]; state[blockLen-1] = (byte) b; // Write the encrypted data byte m_out.write(b); nBytes++; } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = 0x00; m_out.flush(); return nBytes; } /*************************************************************************** * Decrypt the input stream using a CFB-8 cipher, writing to the output * stream. * * @throws IOException * Thrown if an I/O error occurs. * * @return * Number of bytes decrypted. * * @since 2.4, 2009-04-05 (1.1, 2005-05-26) */ private long decryptCFB() throws IOException { byte[] eBuf; byte[] state; long nBytes; int blockLen; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; // Read the prepended random IV block from the input stream if (m_hasIV) { int len; len = m_in.read(m_iv, 0, blockLen); if (len != blockLen) throw new IOException("Encrypted input is truncated (" + len + "/" + blockLen + ")"); System.arraycopy(m_iv, 0, state, 0, blockLen); } if (s_debugs) System.err.println("IV: [" + toHexString(m_iv) + "]"); // Decrypt data from the input stream, using a CFB-8 stream cipher nBytes = 0; for (;;) { int b; // Read the next data byte from the input stream b = m_in.read(); if (b < 0) break; // Generate the next ciphering block m_cipher.blockEncrypt(state, 0, eBuf, 0); // Decrypt the data byte for (int i = 1; i < blockLen; i++) state[i-1] = state[i]; state[blockLen-1] = (byte) b; b ^= eBuf[0]; // Write the decrypted data byte m_out.write(b); nBytes++; } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = 0x00; m_out.flush(); return nBytes; } /*************************************************************************** * Encrypt the input stream using an OFB-128 cipher, writing to the output * stream. * * @throws IOException * Thrown if an I/O error occurs. * * @return * Number of bytes encrypted. * * @since 2.4, 2009-04-05 */ private long encryptOFB() throws IOException { byte[] eBuf; byte[] oBuf; byte[] state; long nBytes; int blockLen; int eCnt; boolean hasData; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; oBuf = new byte[blockLen]; eCnt = 0; hasData = true; // Prepend the random IV block to the output stream if (s_debugs) System.err.println("IV: [" + toHexString(m_iv) + "]"); if (m_hasIV) { System.arraycopy(m_iv, 0, state, 0, blockLen); m_out.write(m_iv, 0, blockLen); } // Encrypt data from the input stream, using an OFB stream cipher nBytes = 0; do { if (eCnt == 0) { // Cycle the block cipher through the next round m_cipher.blockEncrypt(state, 0, eBuf, 0); System.arraycopy(eBuf, 0, state, 0, blockLen); } // Encrypt the input plaintext data with the ciphering block while (eCnt < blockLen) { int b; // Read the next plaintext byte from the input stream b = m_in.read(); if (b < 0) { hasData = false; break; } // Encrypt the byte oBuf[eCnt] = (byte) (eBuf[eCnt] ^ b); eCnt++; } if (eCnt >= blockLen) { // Write a full ciphertext block to the output m_out.write(oBuf, 0, blockLen); nBytes += blockLen; eCnt = 0; } } while (hasData); // Write the final partial ciphertext block to the output if (eCnt > 0) { m_out.write(oBuf, 0, eCnt); nBytes += eCnt; } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = oBuf[i] = 0x00; m_out.flush(); return nBytes; } /*************************************************************************** * Decrypt the input stream using an OFB-128 cipher, writing to the output * stream. * * @throws IOException * Thrown if an I/O error occurs. * * @return * Number of bytes encrypted. * * @since 2.4, 2009-04-05 */ private long decryptOFB() throws IOException { byte[] eBuf; byte[] oBuf; byte[] state; long nBytes; int blockLen; int eCnt; boolean hasData; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; oBuf = new byte[blockLen]; eCnt = 0; hasData = true; // Read the prepended random IV block from the input stream if (m_hasIV) { int len; len = m_in.read(m_iv, 0, blockLen); if (len != blockLen) throw new IOException("Encrypted input is truncated (" + len + "/" + blockLen + ")"); System.arraycopy(m_iv, 0, state, 0, blockLen); } if (s_debugs) System.err.println("IV: [" + toHexString(m_iv) + "]"); // Decrypt data from the input stream, using an OFB stream cipher nBytes = 0; do { if (eCnt == 0) { // Cycle the block cipher through the next round m_cipher.blockEncrypt(state, 0, eBuf, 0); System.arraycopy(eBuf, 0, state, 0, blockLen); } // Encrypt the input plaintext data with the ciphering block while (eCnt < blockLen) { int b; // Read the next plaintext byte from the input stream b = m_in.read(); if (b < 0) { hasData = false; break; } // Decrypt the byte oBuf[eCnt] = (byte) (eBuf[eCnt] ^ b); eCnt++; } if (eCnt >= blockLen) { // Write a full ciphertext block to the output m_out.write(oBuf, 0, blockLen); nBytes += blockLen; eCnt = 0; } } while (hasData); // Write the final partial ciphertext block to the output if (eCnt > 0) { m_out.write(oBuf, 0, eCnt); nBytes += eCnt; } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = oBuf[i] = 0x00; m_out.flush(); return nBytes; } } // End FileEncrypter.java