package freenet.crypt; 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 freenet.client.async.ClientContext; import freenet.node.NodeStarter; import freenet.support.api.Bucket; import freenet.support.io.BucketTools; import freenet.support.io.FilenameGenerator; import freenet.support.io.PersistentFileTracker; import freenet.support.io.ResumeFailedException; import freenet.support.io.StorageFormatException; /** Encrypted and authenticated Bucket implementation using AES cipher and OCB mode. Warning: * Avoid using Closer.close() on InputStream's opened on this Bucket. The MAC is only checked when * the end of the bucket is reached, which may be in read() or may be in close(). * @author toad */ public class AEADCryptBucket implements Bucket, Serializable { private static final long serialVersionUID = 1L; private final Bucket underlying; private final byte[] key; private boolean readOnly; static final int OVERHEAD = AEADOutputStream.AES_OVERHEAD; public AEADCryptBucket(Bucket underlying, byte[] key) { this.underlying = underlying; this.key = Arrays.copyOf(key, key.length); } protected AEADCryptBucket() { // For serialization. underlying = null; key = null; } @Override public OutputStream getOutputStream() throws IOException { return new BufferedOutputStream(getOutputStreamUnbuffered()); } @Override public OutputStream getOutputStreamUnbuffered() throws IOException { synchronized(this) { if(readOnly) throw new IOException("Read only"); } OutputStream os = underlying.getOutputStreamUnbuffered(); return AEADOutputStream.createAES(os, key, NodeStarter.getGlobalSecureRandom()); } public InputStream getInputStream() throws IOException { return new BufferedInputStream(getInputStreamUnbuffered()); } @Override public InputStream getInputStreamUnbuffered() throws IOException { InputStream is = underlying.getInputStreamUnbuffered(); return AEADInputStream.createAES(is, key); } @Override public String getName() { return "AEADEncrypted:"+underlying.getName(); } @Override public long size() { return underlying.size() - OVERHEAD; } @Override public synchronized boolean isReadOnly() { return readOnly; } @Override public synchronized void setReadOnly() { readOnly = true; } @Override public void free() { underlying.free(); } @Override public Bucket createShadow() { Bucket undershadow = underlying.createShadow(); AEADCryptBucket ret = new AEADCryptBucket(undershadow, key); ret.setReadOnly(); return ret; } @Override public void onResume(ClientContext context) throws ResumeFailedException { underlying.onResume(context); } public static final int MAGIC = 0xb25b32d6; static final int VERSION = 1; @Override public void storeTo(DataOutputStream dos) throws IOException { dos.writeInt(MAGIC); dos.writeInt(VERSION); dos.writeByte(key.length); dos.write(key); dos.writeBoolean(readOnly); underlying.storeTo(dos); } public AEADCryptBucket(DataInputStream dis, FilenameGenerator fg, PersistentFileTracker persistentFileTracker, MasterSecret masterKey) throws IOException, StorageFormatException, ResumeFailedException { // Magic already read by caller. int version = dis.readInt(); if(version != VERSION) throw new StorageFormatException("Unknown version "+version); int keyLength = dis.readByte(); if(keyLength < 0 || !(keyLength == 16 || keyLength == 24 || keyLength == 32)) throw new StorageFormatException("Unknown key length "+keyLength); // FIXME validate this in a more permanent way key = new byte[keyLength]; dis.readFully(key); readOnly = dis.readBoolean(); underlying = BucketTools.restoreFrom(dis, fg, persistentFileTracker, masterKey); } }