//============================================================================== // FileEncrypter.java //============================================================================== package tribble.crypto; // System imports 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.System; import java.lang.Throwable; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.zip.Deflater; import java.util.zip.Inflater; // Local imports 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). * * * @version $Revision: 1.6 $ $Date: 2006/04/15 19:44:46 $ * @since 2005-03-26 * @author * David R. Tribble * (david@tribble.com). *
* * Copyright ©2005-2006 by David R. Tribble, all rights reserved. *
* Permission is granted to freely use and distribute this source code * provided that the original copyright and authorship notices remain * intact. * * @see AESCipher * @see StreamCipherSpi */ public class FileEncrypter { // Identification /** Revision information. */ static final String REV = "@(#)tribble/crypto/FileEncrypter.java $Revision: 1.6 $ $Date: 2006/04/15 19:44:46 $\n"; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Protected constants /** 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; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Public static methods /*************************************************************************** * Encrypt or decrypt a data file. * *

* * Usage *

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

* Options: *

*
-d *
* Decrypt the file. * *
-e *
* Encrypt the file (the default). * *
-nc *
* Do not compress the data. * 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. * * * * * *
-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. * * *
* *

* 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 salted = true; boolean squeezed = true; byte[] key; InputStream in; OutputStream out; FileEncrypter enc; // 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("-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("-ns")) salted = false; else if (args[i].equals("-p")) { pwd = args[++i]; args[i] = ""; } /*+++INCOMPLETE if (args[i].equals("-pf")) { pwdFile = args[++i]; args[i] = ""; } +++*/ else if (args[i].equals("-o")) outFname = args[++i]; else { System.err.println("Bad option: '" + args[i] + "'"); System.exit(127); } } // Check command line args if (i >= args.length) { // Display a usage message System.out.println("Encrypt 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(" -d " + "Decrypt the file."); System.out.println(" -e " + "Encrypt the file (default)."); System.out.println(" -nc " + "Do not compress/uncompress the data."); /*+++INCOMPLETE System.out.println(" -ni " + "Do not embed file info."); +++*/ /*+++REMOVED (1.6, 2006-04-15) System.out.println(" -ns " + "Do not prepend random salt."); +++*/ System.out.println(" -o file " + "Output file (default is standard output)."); System.out.println(" -p phrase " + "Encryption passphrase."); /*+++INCOMPLETE System.out.println(" -pf file " + "File containing the passphrase."); +++*/ System.out.println(); System.out.println("If a passphrase is not specified, it will be " + "read from the standard input."); // Punt System.exit(255); } // Open the input file inFname = args[i++]; if (inFname.equals("-")) { // Read from standard input in = System.in; } else { File inFile; // Read from a named file inFile = new File(inFname); if (!inFile.exists() || !inFile.canRead()) throw new IOException("Can't read: " + inFname); in = new FileInputStream(inFname); } // Open the output file if (outFname.equals("-")) { // Write to standard output out = System.out; } else { // Write to a named file out = new FileOutputStream(outFname); } // Handle compressed/decompressed input/output stream if (squeezed) { if (encrypt) { Deflater defl; // Compress the data prior to encryption defl = new Deflater(Deflater.BEST_COMPRESSION, true); in = new DeflaterInputStream(in, defl); } else { Inflater infl; // Uncompress the data after decryption infl = new Inflater(true); out = new InflaterOutputStream(out, infl); } } // Get the passphrase if (pwd == null) { BufferedReader stdin; // Prompt for a passphrase stdin = new BufferedReader(new InputStreamReader(System.in)); System.err.print("Password? "); System.err.flush(); pwd = stdin.readLine(); } // Initialize the file encrypter/decrypter enc = new FileEncrypter(); enc.m_inFname = inFname; enc.m_outFname = outFname; // Derive the encryption key from the passphrase key = deriveKey(pwd, KEY_LEN); pwd = ""; // Encrypt/decrypt the data file if (encrypt) enc.encrypt(in, out, key, salted); else enc.decrypt(in, out, key, salted); enc.reset(); // Clean up if (!inFname.equals("-")) in.close(); if (!outFname.equals("-")) out.close(); } /*************************************************************************** * 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 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; // Hash the encryption passphrase into a binary encryption key p = pwd.getBytes(); 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; // Hash the passphrase md = MessageDigest.getInstance(HASH_ALG); k = md.digest(pwd); // 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[] o; int i; int j; o = k; k = new byte[keyLen]; for (i = j = 0; i < keyLen; i++, j++) { if (j >= o.length) j = 0; k[i] = o[j]; } } return (k); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Private variables /** Plaintext input filename. */ private String m_inFname = "-"; /** Encrypted output filename. */ private String m_outFname = "-"; /** Plaintext input data stream. */ private InputStream m_in; /** Encrypted output data stream. */ private OutputStream m_out; /** Encryption cipher. */ private SymmetricCipher m_cipher; /** Pseudo-random salt (IV block). */ private byte[] m_salt; /** 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 salt. */ private boolean m_hasSalt; // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Public constructors /*************************************************************************** * Default constructor. * * @since 1.1, 2005-06-26 */ public FileEncrypter() { // Initialize } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Public 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. * *

* Stream Cipher - CFB-8 Encryption Mode *

*

    *                Random +---------+
    *                Salt   |         |
    *                       +---------+
    *                            :
    *                            v
    *                State  +-------+-+
    *                Block  |       | | <------+
    *                     i +-------+-+        :
    *                            :             :
    *                            v             :
    *                      +===========+       :
    *        +-------+     [           ]       :
    *    Key |       | --> [  Cipher   ]       :
    *        +-------+     [           ]       :
    *                      +===========+       :
    *                            :             :
    *                            v             :
    *             Encrypted +-------+-+        :
    *               Block   |       | | Ei     :
    *                       +-------+-+        :
    *                                :         :
    *                                v        +-+
    *                             [ XOR ] --> | | --> Output
    *                                ^        +-+     Stream
    *                                :         Ci
    *    Input                      +-+
    *    Stream  -----------------> | |
    *                               +-+
    *                                Pi 
* *

* The state block is initially filled with the IV (salt). 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 leftmost 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 ciphertext byte Ci is then * shifted into the state block, which prepares the state block for the next * input byte Pi+1. * *

* Random salt bytes are prepended to the output stream to make it harder * to crack the encryption. * * * @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 salted * If true, the encrypted output stream will have random salt bytes prepended * to it, otherwise not. *
* Note: Not using a random prepended IV (salt) severly compromises the * security of the stream cipher; no two messages (files) should ever be * encrypted using the same passphrase/salt combination. * * @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 void encrypt(InputStream in, OutputStream out, /*const*/ byte[] key, boolean salted) throws IOException, InvalidKeyException { // Set up setup(in, out, key, salted); // Encrypt data from the input stream, using a CFB stream cipher encrypt(); } /*************************************************************************** * Decrypt an input stream. * *

* Stream Cipher - CFB-8 Decryption Mode *

*

    *                Random +---------+
    *                Salt   |         |
    *                       +---------+
    *                            :
    *                            v
    *                State  +-------+-+
    *                Block  |       | | <---+
    *                     i +-------+-+     :
    *                            :          :
    *                            v          :
    *                      +===========+    :
    *        +-------+     [           ]    :
    *    Key |       | --> [  Cipher   ]    :
    *        +-------+     [           ]    :
    *                      +===========+    :
    *                            :          :
    *                            v          :
    *             Encrypted +-------+-+     :
    *               Block   |       | | Ei  :
    *                       +-------+-+     :
    *                                :      :
    *                                v      :    +-+
    *                             [ XOR ] -----> | | --> Output
    *                                ^      :    +-+     Stream
    *                                :      :     Pi
    *    Input                      +-+     :
    *    Stream  -----------------> | | ----+
    *                               +-+
    *                                Ci 
* *

* The state block is initially filled with the IV (salt). 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 leftmost 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 ciphertext byte Ci is then * shifted into the state block, which prepares the state block for the next * input byte Ci+1. * *

* Random salt bytes are prepended to the output stream to make it harder * to crack the encryption. * * * @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 salted * If true, the encrypted input stream has random salt bytes prepended to it, * otherwise not. *
* Note: Not using a random prepended IV (salt) severly compromises the * security of the stream cipher; no two messages (files) should ever be * encrypted using the same passphrase/salt combination. * * @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 void decrypt(InputStream in, OutputStream out, /*const*/ byte[] key, boolean salted) throws IOException, InvalidKeyException { // Set up setup(in, out, key, salted); // Encrypt data from the input stream, using a CFB stream cipher decrypt(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Protected methods /*************************************************************************** * 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(); } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // Private methods /*************************************************************************** * 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 salted * If true, the encrypted stream has random salt 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 salted) 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 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+1) m_state = new byte[m_blockLen+1]; // Generate initial random salt bytes m_hasSalt = salted; if (salted) { byte[] k; // Generate pseudo-random salt bytes from the current date m_salt = new byte[m_blockLen]; k = deriveKey(Long.toString((new Date()).getTime()), m_blockLen); for (int i = 0; i < m_blockLen; i++) m_salt[i] = k[i%m_blockLen]; for (int i = 0; i < k.length; i++) k[i] = 0x00; } } /*************************************************************************** * Encrypt the input stream, writing to the output stream. * * * @throws IOException * Thrown if an I/O error occurs. * * @since 1.1, 2005-05-26 */ private void encrypt() throws IOException { byte[] eBuf; byte[] state; int blockLen; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; // Prepend random salt bytes (IV block) to the output stream if (m_hasSalt) { System.arraycopy(m_salt, 0, state, 0, blockLen); m_out.write(m_salt, 0, blockLen); } // Encrypt data from the input stream, using a CFB stream cipher for (;;) { int b; // Read the next data byte from the input stream b = m_in.read(); if (b < 0) break; // Cycle the stream cipher through one round m_cipher.blockEncrypt(state, 0, eBuf, 0); // Encrypt the data byte b ^= eBuf[0]; state[blockLen] = (byte) b; for (int i = 0; i < blockLen; i++) state[i] = state[i+1]; // Write the encrypted data byte m_out.write(b); } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = 0x00; m_out.flush(); } /*************************************************************************** * Decrypt the input stream, writing to the output stream. * * * @throws IOException * Thrown if an I/O error occurs. * * @since 1.1, 2005-05-26 */ private void decrypt() throws IOException { byte[] eBuf; byte[] state; int blockLen; // Set up blockLen = m_blockLen; state = m_state; eBuf = m_eBuf; // Prepend random salt bytes (IV block) to the output stream if (m_hasSalt) { int len; len = m_in.read(m_salt, 0, blockLen); if (len != blockLen) throw new IOException("Encrypted input is truncated (" + len + "/" + blockLen + ")"); System.arraycopy(m_salt, 0, state, 0, blockLen); } // Decrypt data from the input stream, using a CFB stream cipher for (;;) { int b; // Read the next data byte from the input stream b = m_in.read(); if (b < 0) break; // Cycle the stream cipher through one round m_cipher.blockEncrypt(state, 0, eBuf, 0); // Decrypt the data byte state[blockLen] = (byte) b; for (int i = 0; i < blockLen; i++) state[i] = state[i+1]; b ^= eBuf[0]; // Write the decrypted data byte m_out.write(b); } // Clean up for (int i = 0; i < blockLen; i++) eBuf[i] = 0x00; m_out.flush(); } } // End FileEncrypter.java