I have the exact same need as you. Some guy called 'maybeWeCouldStealAVa' wrote a good implementation in: How to append to AES encrypted file , however this suffered from not being flushable - you would have to close and reopen the file each time you flush a message, to be sure not to lose anything.
So I've written my own class to do this:
import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.security.*;
public class FlushableCipherOutputStream extends OutputStream
{
private static int HEADER_LENGTH = 16;
private SecretKeySpec key;
private RandomAccessFile seekableFile;
private boolean flushGoesStraightToDisk;
private Cipher cipher;
private boolean needToRestoreCipherState;
/** the buffer holding one byte of incoming data */
private byte[] ibuffer = new byte[1];
/** the buffer holding data ready to be written out */
private byte[] obuffer;
/** Each time you call 'flush()', the data will be written to the operating system level, immediately available
* for other processes to read. However this is not the same as writing to disk, which might save you some
* data if there's a sudden loss of power to the computer. To protect against that, set 'flushGoesStraightToDisk=true'.
* Most people set that to 'false'. */
public FlushableCipherOutputStream(String fnm, SecretKeySpec _key, boolean append, boolean _flushGoesStraightToDisk)
throws IOException
{
this(new File(fnm), _key, append,_flushGoesStraightToDisk);
}
public FlushableCipherOutputStream(File file, SecretKeySpec _key, boolean append, boolean _flushGoesStraightToDisk)
throws IOException
{
super();
if (! append)
file.delete();
seekableFile = new RandomAccessFile(file,"rw");
flushGoesStraightToDisk = _flushGoesStraightToDisk;
key = _key;
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
byte[] iv = new byte[16];
byte[] headerBytes = new byte[HEADER_LENGTH];
long fileLen = seekableFile.length();
if (fileLen % 16L != 0L) {
throw new IllegalArgumentException("Invalid file length (not a multiple of block size)");
} else if (fileLen == 0L) {
// new file
// You can write a 16 byte file header here, including some file format number to represent the
// encryption format, in case you need to change the key or algorithm. E.g. "100" = v1.0.0
headerBytes[0] = 100;
seekableFile.write(headerBytes);
// Now appending the first IV
SecureRandom sr = new SecureRandom();
sr.nextBytes(iv);
seekableFile.write(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
} else if (fileLen <= 16 + HEADER_LENGTH) {
throw new IllegalArgumentException("Invalid file length (need 2 blocks for iv and data)");
} else {
// file length is at least 2 blocks
needToRestoreCipherState = true;
}
} catch (InvalidKeyException e) {
throw new IOException(e.getMessage());
} catch (NoSuchAlgorithmException e) {
throw new IOException(e.getMessage());
} catch (NoSuchPaddingException e) {
throw new IOException(e.getMessage());
} catch (InvalidAlgorithmParameterException e) {
throw new IOException(e.getMessage());
}
}
/**
* Writes one _byte_ to this output stream.
*/
public void write(int b) throws IOException {
if (needToRestoreCipherState)
restoreStateOfCipher();
ibuffer[0] = (byte) b;
obuffer = cipher.update(ibuffer, 0, 1);
if (obuffer != null) {
seekableFile.write(obuffer);
obuffer = null;
}
}
/** Writes a byte array to this output stream. */
public void write(byte data[]) throws IOException {
write(data, 0, data.length);
}
/**
* Writes <code>len</code> bytes from the specified byte array
* starting at offset <code>off</code> to this output stream.
*
* @param data the data.
* @param off the start offset in the data.
* @param len the number of bytes to write.
*/
public void write(byte data[], int off, int len) throws IOException
{
if (needToRestoreCipherState)
restoreStateOfCipher();
obuffer = cipher.update(data, off, len);
if (obuffer != null) {
seekableFile.write(obuffer);
obuffer = null;
}
}
/** The tricky stuff happens here. We finalise the cipher, write it out, but then rewind the
* stream so that we can add more bytes without padding. */
public void flush() throws IOException
{
try {
if (needToRestoreCipherState)
return; // It must have already been flushed.
byte[] obuffer = cipher.doFinal();
if (obuffer != null) {
seekableFile.write(obuffer);
if (flushGoesStraightToDisk)
seekableFile.getFD().sync();
needToRestoreCipherState = true;
}
} catch (IllegalBlockSizeException e) {
throw new IOException("Illegal block");
} catch (BadPaddingException e) {
throw new IOException("Bad padding");
}
}
private void restoreStateOfCipher() throws IOException
{
try {
// I wish there was a more direct way to snapshot a Cipher object, but it seems there's not.
needToRestoreCipherState = false;
byte[] iv = cipher.getIV(); // To help avoid garbage, re-use the old one if present.
if (iv == null)
iv = new byte[16];
seekableFile.seek(seekableFile.length() - 32);
seekableFile.read(iv);
byte[] lastBlockEnc = new byte[16];
seekableFile.read(lastBlockEnc);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] lastBlock = cipher.doFinal(lastBlockEnc);
seekableFile.seek(seekableFile.length() - 16);
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] out = cipher.update(lastBlock);
assert out == null || out.length == 0;
} catch (Exception e) {
throw new IOException("Unable to restore cipher state");
}
}
public void close() throws IOException
{
flush();
seekableFile.close();
}
}
Here's an example of using it:
import org.junit.Test;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.io.BufferedWriter;
public class TestFlushableCipher {
private static byte[] keyBytes = new byte[] {
// Change these numbers, lest other StackOverflow readers can decrypt your files.
-53, 93, 59, 108, -34, 17, -72, -33, 126, 93, -62, -50, 106, -44, 17, 55
};
private static SecretKeySpec key = new SecretKeySpec(keyBytes,"AES");
private static int HEADER_LENGTH = 16;
private static BufferedWriter flushableEncryptedBufferedWriter(File file, boolean append) throws Exception
{
FlushableCipherOutputStream fcos = new FlushableCipherOutputStream(file, key, append, false);
return new BufferedWriter(new OutputStreamWriter(fcos, "UTF-8"));
}
private static InputStream readerEncryptedByteStream(File file) throws Exception
{
FileInputStream fin = new FileInputStream(file);
byte[] iv = new byte[16];
byte[] headerBytes = new byte[HEADER_LENGTH];
if (fin.read(headerBytes) < HEADER_LENGTH)
throw new IllegalArgumentException("Invalid file length (failed to read file header)");
if (headerBytes[0] != 100)
throw new IllegalArgumentException("The file header does not conform to our encrypted format.");
if (fin.read(iv) < 16) {
throw new IllegalArgumentException("Invalid file length (needs a full block for iv)");
}
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return new CipherInputStream(fin,cipher);
}
private static BufferedReader readerEncrypted(File file) throws Exception
{
InputStream cis = readerEncryptedByteStream(file);
return new BufferedReader(new InputStreamReader(cis));
}
@Test
public void test() throws Exception {
File zfilename = new File("c:\\WebEdvalData\\log.x");
BufferedWriter cos = flushableEncryptedBufferedWriter(zfilename, false);
cos.append("Sunny ");
cos.append("and green. \n");
cos.close();
int spaces=0;
for (int i = 0; i<10; i++) {
cos = flushableEncryptedBufferedWriter(zfilename, true);
for (int j=0; j < 2; j++) {
cos.append("Karelia and Tapiola" + i);
for (int k=0; k < spaces; k++)
cos.append(" ");
spaces++;
cos.append("and other nice things. \n");
cos.flush();
tail(zfilename);
}
cos.close();
}
BufferedReader cis = readerEncrypted(zfilename);
String msg;
while ((msg=cis.readLine()) != null) {
System.out.println(msg);
}
cis.close();
}
private void tail(File filename) throws Exception
{
BufferedReader infile = readerEncrypted(filename);
String last = null, secondLast = null;
do {
String msg = infile.readLine();
if (msg == null)
break;
if (! msg.startsWith("}")) {
secondLast = last;
last = msg;
}
} while (true);
if (secondLast != null)
System.out.println(secondLast);
System.out.println(last);
System.out.println();
}
}