package net.i2p.crypto; /* * free (adj.): unencumbered; not under the control of others * Written by jrandom in 2003 and released into the public domain * with no warranty of any kind, either expressed or implied. * It probably won't make your computer catch on fire, or eat * your children, but it might. Use at your own risk. * */ import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.SessionKey; import net.i2p.util.Log; /** * This writes everything as CBC with PKCS#5 padding, but each block is padded * so as soon as a block is received it can be decrypted (rather than wait for * an arbitrary number of blocks to arrive). That means that each block sent * will contain exactly one padding byte (unless it was flushed with * numBytes % (BLOCK_SIZE-1) != 0, in which case that last block will be padded * with up to 15 bytes). So we have an expansion factor of 6.25%. c'est la vie * */ public class AESOutputStream extends FilterOutputStream { private Log _log; private I2PAppContext _context; private SessionKey _key; private byte[] _lastBlock; /** * buffer containing the unwritten bytes. The first unwritten * byte is _lastCommitted+1, and the last unwritten byte is _nextWrite-1 * (aka the next byte to be written on the array is _nextWrite) */ private byte[] _unencryptedBuf; private byte _writeBlock[]; /** how many bytes have we been given since we flushed it to the stream? */ private int _writesSinceCommit; private long _cumulativeProvided; // how many bytes provided to this stream private long _cumulativeWritten; // how many bytes written to the underlying stream private long _cumulativePadding; // how many bytes of padding written public final static float EXPANSION_FACTOR = 1.0625f; // 6% overhead w/ the padding private final static int BLOCK_SIZE = 16; private final static int MAX_BUF = 256; public AESOutputStream(I2PAppContext context, OutputStream source, SessionKey key, byte[] iv) { super(source); _context = context; _log = context.logManager().getLog(AESOutputStream.class); _key = key; _lastBlock = new byte[BLOCK_SIZE]; System.arraycopy(iv, 0, _lastBlock, 0, BLOCK_SIZE); _unencryptedBuf = new byte[MAX_BUF]; _writeBlock = new byte[BLOCK_SIZE]; _writesSinceCommit = 0; } @Override public void write(int val) throws IOException { _cumulativeProvided++; _unencryptedBuf[_writesSinceCommit++] = (byte)(val & 0xFF); if (_writesSinceCommit == _unencryptedBuf.length) doFlush(); } @Override public void write(byte src[]) throws IOException { write(src, 0, src.length); } @Override public void write(byte src[], int off, int len) throws IOException { // i'm too lazy to unroll this into the partial writes (dealing with // wrapping around the buffer size) for (int i = 0; i < len; i++) write(src[i+off]); } @Override public void close() throws IOException { flush(); out.close(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Cumulative bytes provided to this stream / written out / padded: " + _cumulativeProvided + "/" + _cumulativeWritten + "/" + _cumulativePadding); } @Override public void flush() throws IOException { doFlush(); out.flush(); } private void doFlush() throws IOException { if (_log.shouldLog(Log.INFO)) _log.info("doFlush(): writesSinceCommit=" + _writesSinceCommit); writeEncrypted(); _writesSinceCommit = 0; } /** * Encrypt an arbitrary size array with AES using CBC and PKCS#5 padding, * write it to the stream, and set _lastBlock to the last encrypted * block. This operation works by taking every (BLOCK_SIZE-1) bytes * from the src, padding it with PKCS#5 (aka adding 0x01), and encrypting * it. If the last block doesn't contain exactly (BLOCK_SIZE-1) bytes, it * is padded with PKCS#5 as well (adding # padding bytes repeated that many * times). * */ private void writeEncrypted() throws IOException { int numBlocks = _writesSinceCommit / (BLOCK_SIZE - 1); if (_log.shouldLog(Log.INFO)) _log.info("writeE(): #=" + _writesSinceCommit + " blocks=" + numBlocks); for (int i = 0; i < numBlocks; i++) { DataHelper.xor(_unencryptedBuf, i * 15, _lastBlock, 0, _writeBlock, 0, 15); // the padding byte for "full" blocks _writeBlock[BLOCK_SIZE - 1] = (byte)(_lastBlock[BLOCK_SIZE - 1] ^ 0x01); _context.aes().encrypt(_writeBlock, 0, _writeBlock, 0, _key, _lastBlock, BLOCK_SIZE); out.write(_writeBlock); System.arraycopy(_writeBlock, 0, _lastBlock, 0, BLOCK_SIZE); _cumulativeWritten += BLOCK_SIZE; _cumulativePadding++; } if (_writesSinceCommit % 15 != 0) { // we need to do non trivial padding int remainingBytes = _writesSinceCommit - numBlocks * 15; int paddingBytes = BLOCK_SIZE - remainingBytes; if (_log.shouldLog(Log.DEBUG)) _log.debug("Padding " + _writesSinceCommit + " with " + paddingBytes + " bytes in " + (numBlocks+1) + " blocks"); System.arraycopy(_unencryptedBuf, numBlocks * 15, _writeBlock, 0, remainingBytes); Arrays.fill(_writeBlock, remainingBytes, BLOCK_SIZE, (byte) paddingBytes); DataHelper.xor(_writeBlock, 0, _lastBlock, 0, _writeBlock, 0, BLOCK_SIZE); _context.aes().encrypt(_writeBlock, 0, _writeBlock, 0, _key, _lastBlock, BLOCK_SIZE); out.write(_writeBlock); System.arraycopy(_writeBlock, 0, _lastBlock, 0, BLOCK_SIZE); _cumulativePadding += paddingBytes; _cumulativeWritten += BLOCK_SIZE; } } }