package freenet.crypt;
import java.io.DataInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.engines.AESLightEngine;
import org.bouncycastle.crypto.modes.AEADBlockCipher;
import org.bouncycastle.crypto.params.AEADParameters;
import org.bouncycastle.crypto.params.KeyParameter;
public class AEADInputStream extends FilterInputStream {
private static final int MAC_SIZE_BITS = AEADOutputStream.MAC_SIZE_BITS;
private final AEADBlockCipher cipher;
private boolean finished;
/** Create a decrypting, authenticating InputStream. IMPORTANT: We only authenticate when
* closing the stream, so do NOT use Closer.close() etc and swallow IOException's on close(),
* as that's what we will throw if authentication fails. We will read the nonce from the
* stream; it functions similarly to an IV.
* @param is The underlying InputStream.
* @param key The encryption key.
* @param nonce The nonce. This serves the function of an IV. As a nonce, this MUST be unique.
* We will write it to the stream so the other side can pick it up, like an IV.
* @param mainCipher The BlockCipher for encrypting data. E.g. AES; not a block mode. This will
* be used for encrypting a fairly large amount of data so could be any of the 3 BC AES impl's.
* @param hashCipher The BlockCipher for the final hash. E.g. AES, not a block mode. This will
* not be used very much so should be e.g. an AESLightEngine. */
public AEADInputStream(InputStream is, byte[] key, BlockCipher hashCipher,
BlockCipher mainCipher) throws IOException {
super(is);
byte[] nonce = new byte[mainCipher.getBlockSize()];
new DataInputStream(is).readFully(nonce);
cipher = new OCBBlockCipher_v149(hashCipher, mainCipher);
KeyParameter keyParam = new KeyParameter(key);
AEADParameters params = new AEADParameters(keyParam, MAC_SIZE_BITS, nonce);
cipher.init(false, params);
excess = new byte[mainCipher.getBlockSize()];
excessEnd = 0;
excessPtr = 0;
}
public final int getIVSize() {
return cipher.getUnderlyingCipher().getBlockSize() / 8;
}
private final byte[] onebyte = new byte[1];
private final byte[] excess;
private int excessEnd;
private int excessPtr;
@Override
public int read() throws IOException {
int length = read(onebyte);
if(length <= 0) return -1;
else return onebyte[0];
}
@Override
public int read(byte[] buf) throws IOException {
return read(buf, 0, buf.length);
}
@Override
public int read(byte[] buf, int offset, int length) throws IOException {
if(length < 0) return -1;
if(length == 0) return 0;
if(excessEnd != 0) {
length = Math.min(length, excessEnd - excessPtr);
if(length > 0) {
System.arraycopy(excess, excessPtr, buf, offset, length);
excessPtr += length;
if(excessEnd == excessPtr) {
excessEnd = 0;
excessPtr = 0;
}
return length;
}
}
if(finished) return -1;
// FIXME OPTIMISE Can we avoid allocating new buffers here? We can't safely use in=out when
// calling cipher.processBytes().
while(true) {
byte[] temp = new byte[length];
int read = in.read(temp);
if(read == 0) return read; // Nasty ambiguous case.
if(read < 0) {
// End of stream.
// The last few bytes will still be in the cipher's buffer and have to be retrieved by doFinal().
try {
excessEnd = cipher.doFinal(excess, 0);
} catch (InvalidCipherTextException e) {
throw new AEADVerificationFailedException();
}
finished = true;
if(excessEnd > 0)
return read(buf, offset, length);
else
return -1;
}
if(read <= 0) return read;
assert(read <= length);
int outLength = cipher.getUpdateOutputSize(read);
if(outLength > length) {
byte[] outputTemp = new byte[outLength];
int decryptedBytes = cipher.processBytes(temp, 0, read, outputTemp, 0);
assert(decryptedBytes == outLength);
System.arraycopy(outputTemp, 0, buf, offset, length);
excessEnd = outLength - length;
assert(excessEnd < excess.length);
System.arraycopy(outputTemp, length, excess, 0, excessEnd);
return length;
} else {
int decryptedBytes = cipher.processBytes(temp, 0, read, buf, offset);
if(decryptedBytes > 0) return decryptedBytes;
}
}
}
@Override
public int available() throws IOException {
int excess = excessEnd - excessPtr;
if(excess > 0) return excess;
if(finished) return 0;
// FIXME Not very accurate as may include the MAC - or it may not, this is not the full
// length of the stream. Maybe we should return 0?
return in.available();
}
@Override
public long skip(long n) throws IOException {
// FIXME unit test skip()
long skipped = 0;
byte[] temp = new byte[excess.length];
while(n > 0) {
int excessLeft = excessEnd - excessPtr;
if(excessLeft > 0) {
if(n < excessLeft) {
excessPtr += (int)n;
return n;
}
n -= excessLeft;
skipped += excessLeft;
excessEnd = 0;
excessPtr = 0;
continue;
}
if(n < temp.length) {
int read = read(temp, 0, (int)n);
if(read <= 0) return skipped;
skipped += read;
n -= read;
} else {
int read = read(temp);
if(read <= 0) return skipped;
skipped += read;
n -= read;
}
}
return skipped;
}
@Override
public void close() throws IOException {
if(!finished)
// Must read the rest of the data to check hash integrity.
skip(Long.MAX_VALUE);
in.close();
}
@Override
public boolean markSupported() {
return false;
}
@Override
public void mark(int readlimit) {
throw new UnsupportedOperationException();
}
@Override
public void reset() throws IOException {
throw new IOException("Mark/reset not supported");
}
public static AEADInputStream createAES(InputStream is, byte[] key) throws IOException {
AESEngine mainCipher = new AESEngine();
AESLightEngine hashCipher = new AESLightEngine();
return new AEADInputStream(is, key, hashCipher, mainCipher);
}
}