//============================================================================== // 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.
*
*
* * java tribble.crypto.FileEncrypter * [-p passphrase] [-options...] file * * *
* Options: *
* 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