/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.support.io;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Random;
import freenet.client.async.ClientContext;
import freenet.crypt.MasterSecret;
import freenet.crypt.PCFBMode;
import freenet.crypt.RandomSource;
import freenet.crypt.UnsupportedCipherException;
import freenet.crypt.ciphers.Rijndael;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.api.Bucket;
import freenet.support.math.MersenneTwister;
/**
* A proxy Bucket which adds:
* - Encryption with the supplied cipher, and a random, ephemeral key.
* - Padding to the next PO2 size.
*
* CRYPTO WARNING: This uses PCFB with no IV. That means it is only safe if the key is unique!
*/
public class PaddedEphemerallyEncryptedBucket implements Bucket, Serializable {
private static final long serialVersionUID = 1L;
private final Bucket bucket;
private final int minPaddedSize;
/** The decryption key. */
private final byte[] key;
private final byte[] iv;
private transient byte[] randomSeed;
private long dataLength;
private boolean readOnly;
private transient int lastOutputStream;
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
/**
* Create a padded encrypted proxy bucket.
* @param bucket The bucket which we are proxying to. Must be empty.
* @param pcfb The encryption mode with which to encipher/decipher the data.
* @param minSize The minimum padded size of the file (after it has been closed).
* @param strongPRNG a strong prng we will key from.
* @param weakPRNG a week prng we will padd from.
* Serialization: Note that it is not our responsibility to free the random number generators,
* but we WILL free the underlying bucket.
* @throws UnsupportedCipherException
*/
public PaddedEphemerallyEncryptedBucket(Bucket bucket, int minSize, RandomSource strongPRNG, Random weakPRNG) {
this.bucket = bucket;
if(bucket.size() != 0) throw new IllegalArgumentException("Bucket must be empty");
byte[] tempKey = new byte[32];
randomSeed = new byte[32];
weakPRNG.nextBytes(randomSeed);
strongPRNG.nextBytes(tempKey);
this.key = tempKey;
this.iv = new byte[32];
strongPRNG.nextBytes(iv);
this.minPaddedSize = minSize;
readOnly = false;
lastOutputStream = 0;
dataLength = 0;
}
public PaddedEphemerallyEncryptedBucket(PaddedEphemerallyEncryptedBucket orig, Bucket newBucket) {
this.dataLength = orig.dataLength;
this.key = orig.key.clone();
this.randomSeed = null; // Will be read-only
setReadOnly();
this.bucket = newBucket;
this.minPaddedSize = orig.minPaddedSize;
if(orig.iv != null) {
iv = Arrays.copyOf(orig.iv, 32);
} else {
iv = null;
}
}
protected PaddedEphemerallyEncryptedBucket() {
// For serialization.
bucket = null;
minPaddedSize = 0;
key = null;
iv = null;
randomSeed = null;
}
@Override
public OutputStream getOutputStream() throws IOException {
return new BufferedOutputStream(getOutputStreamUnbuffered());
}
public OutputStream getOutputStreamUnbuffered() throws IOException {
if(readOnly) throw new IOException("Read only");
OutputStream os = bucket.getOutputStreamUnbuffered();
synchronized(this) {
dataLength = 0;
}
return new PaddedEphemerallyEncryptedOutputStream(os, ++lastOutputStream);
}
private class PaddedEphemerallyEncryptedOutputStream extends OutputStream {
final PCFBMode pcfb;
final OutputStream out;
final int streamNumber;
private boolean closed;
public PaddedEphemerallyEncryptedOutputStream(OutputStream out, int streamNumber) {
this.out = out;
dataLength = 0;
this.streamNumber = streamNumber;
pcfb = getPCFB();
}
@Override
public void write(int b) throws IOException {
synchronized(PaddedEphemerallyEncryptedBucket.this) {
if(closed) throw new IOException("Already closed!");
if(streamNumber != lastOutputStream)
throw new IllegalStateException("Writing to old stream in "+getName());
}
//if((b < 0) || (b > 255))
// throw new IllegalArgumentException();
int toWrite = pcfb.encipher(b);
synchronized(PaddedEphemerallyEncryptedBucket.this) {
out.write(toWrite);
dataLength++;
}
}
// Override this or FOS will use write(int)
@Override
public void write(byte[] buf) throws IOException {
synchronized(PaddedEphemerallyEncryptedBucket.this) {
if(closed)
throw new IOException("Already closed!");
if(streamNumber != lastOutputStream)
throw new IllegalStateException("Writing to old stream in "+getName());
}
write(buf, 0, buf.length);
}
@Override
public void write(byte[] buf, int offset, int length) throws IOException {
synchronized(PaddedEphemerallyEncryptedBucket.this) {
if(closed) throw new IOException("Already closed!");
if(streamNumber != lastOutputStream)
throw new IllegalStateException("Writing to old stream in "+getName());
}
if(length == 0) return;
byte[] enc = Arrays.copyOfRange(buf, offset, offset + length);
pcfb.blockEncipher(enc, 0, enc.length);
synchronized(PaddedEphemerallyEncryptedBucket.this) {
out.write(enc, 0, enc.length);
dataLength += enc.length;
}
}
@Override
@SuppressWarnings("cast")
public void close() throws IOException {
try {
Random random = new MersenneTwister(randomSeed);
synchronized(PaddedEphemerallyEncryptedBucket.this) {
if(closed) return;
if(streamNumber != lastOutputStream) {
Logger.normal(this, "Not padding out to length because have been superceded: "+getName());
return;
}
long finalLength = paddedLength();
long padding = finalLength - dataLength;
int sz = 65536;
if(padding < (long)sz)
sz = (int)padding;
byte[] buf = new byte[sz];
long writtenPadding = 0;
while(writtenPadding < padding) {
int left = (int) Math.min((long) (padding - writtenPadding), (long) buf.length);
random.nextBytes(buf);
out.write(buf, 0, left);
writtenPadding += left;
}
}
} finally {
closed = true;
out.flush();
out.close();
}
}
}
@Override
public InputStream getInputStream() throws IOException {
return new BufferedInputStream(getInputStreamUnbuffered());
}
@Override
public InputStream getInputStreamUnbuffered() throws IOException {
return new PaddedEphemerallyEncryptedInputStream(bucket.getInputStreamUnbuffered());
}
private class PaddedEphemerallyEncryptedInputStream extends InputStream {
final InputStream in;
final PCFBMode pcfb;
long ptr;
public PaddedEphemerallyEncryptedInputStream(InputStream in) {
this.in = in;
pcfb = getPCFB();
ptr = 0;
}
@Override
public int read() throws IOException {
if(ptr >= dataLength) return -1;
int x = in.read();
if(x == -1) return x;
ptr++;
return pcfb.decipher(x);
}
@Override
public final int available() {
int x = (int)Math.min(dataLength - ptr, Integer.MAX_VALUE);
return (x < 0) ? 0 : x;
}
@Override
public int read(byte[] buf, int offset, int length) throws IOException {
// FIXME remove debugging
if((length+offset > buf.length) || (offset < 0) || (length < 0))
throw new ArrayIndexOutOfBoundsException("a="+offset+", b="+length+", length "+buf.length);
int x = available();
if(x <= 0) return -1;
length = Math.min(length, x);
int readBytes = in.read(buf, offset, length);
if(readBytes <= 0) return readBytes;
ptr += readBytes;
pcfb.blockDecipher(buf, offset, readBytes);
return readBytes;
}
@Override
public int read(byte[] buf) throws IOException {
return read(buf, 0, buf.length);
}
@Override
public long skip(long bytes) throws IOException {
byte[] buf = new byte[(int)Math.min(4096, bytes)];
long skipped = 0;
while(skipped < bytes) {
int x = read(buf, 0, (int)Math.min(bytes-skipped, buf.length));
if(x <= 0) return skipped;
skipped += x;
}
return skipped;
}
@Override
public void close() throws IOException {
in.close();
}
}
public synchronized long paddedLength() {
return paddedLength(dataLength, minPaddedSize);
}
public static final int MIN_PADDED_SIZE = 1024;
/**
* Return the length of the data in the proxied bucket, after padding.
*/
public static long paddedLength(long dataLength, long minPaddedSize) {
long size = dataLength;
if(size < minPaddedSize) size = minPaddedSize;
if(size == minPaddedSize) return size;
long min = minPaddedSize;
long max = (long)minPaddedSize << 1;
while(true) {
if(max < 0)
throw new Error("Impossible size: "+size+" - min="+min+", max="+max);
if(size < min)
throw new IllegalStateException("???");
if((size >= min) && (size <= max)) {
if(logMINOR)
Logger.minor(PaddedEphemerallyEncryptedBucket.class, "Padded: "+max+" was: "+dataLength);
return max;
}
min = max;
max = max << 1;
}
}
private synchronized Rijndael getRijndael() {
Rijndael aes;
try {
aes = new Rijndael(256, 256);
} catch (UnsupportedCipherException e) {
throw new Error(e);
}
aes.initialize(key);
return aes;
}
@SuppressWarnings("deprecation")
public PCFBMode getPCFB() {
Rijndael aes = getRijndael();
if(iv != null)
return PCFBMode.create(aes, iv);
else
// FIXME CRYPTO We should probably migrate all old buckets automatically so we can get rid of this?
// Since the key is unique it is actually almost safe to use all zeros IV, but it's better to use a real IV.
return PCFBMode.create(aes);
}
@Override
public String getName() {
return "Encrypted:"+bucket.getName();
}
@Override
public String toString() {
return super.toString()+ ':' +bucket;
}
@Override
public synchronized long size() {
return dataLength;
}
@Override
public boolean isReadOnly() {
return readOnly;
}
@Override
public void setReadOnly() {
readOnly = true;
}
/**
* @return The underlying Bucket.
*/
public Bucket getUnderlying() {
return bucket;
}
@Override
public void free() {
bucket.free();
}
/**
* Get the decryption key.
*/
public byte[] getKey() {
return key;
}
@Override
public Bucket createShadow() {
Bucket newUnderlying = bucket.createShadow();
if(newUnderlying == null) return null;
return new PaddedEphemerallyEncryptedBucket(this, newUnderlying);
}
@Override
public void onResume(ClientContext context) throws ResumeFailedException {
randomSeed = new byte[32];
context.fastWeakRandom.nextBytes(randomSeed);
bucket.onResume(context);
}
public static final int MAGIC = 0x66c71fc9;
static final int VERSION = 1;
@Override
public void storeTo(DataOutputStream dos) throws IOException {
dos.writeInt(MAGIC);
dos.writeInt(VERSION);
dos.writeInt(minPaddedSize);
dos.write(key);
if(iv != null) {
dos.writeBoolean(true);
dos.write(iv);
} else {
dos.writeBoolean(false);
}
// randomSeed should be recovered in onResume().
dos.writeLong(dataLength);
dos.writeBoolean(readOnly);
bucket.storeTo(dos);
}
protected PaddedEphemerallyEncryptedBucket(DataInputStream dis, FilenameGenerator fg,
PersistentFileTracker persistentFileTracker, MasterSecret masterKey)
throws StorageFormatException, IOException, ResumeFailedException {
int version = dis.readInt();
if(version != VERSION) throw new StorageFormatException("Bad version");
minPaddedSize = dis.readInt();
key = new byte[32];
dis.readFully(key);
if(dis.readBoolean()) {
iv = new byte[32];
dis.readFully(iv);
} else {
iv = null;
}
dataLength = dis.readLong();
readOnly = dis.readBoolean();
bucket = BucketTools.restoreFrom(dis, fg, persistentFileTracker, masterKey);
}
}