package freenet.crypt; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.EOFException; import java.io.FilterInputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.nio.ByteBuffer; import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.util.Arrays; import javax.crypto.SecretKey; import org.bouncycastle.crypto.SkippingStreamCipher; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.ParametersWithIV; import freenet.client.async.ClientContext; import freenet.crypt.EncryptedRandomAccessBuffer.kdfInput; import freenet.support.Fields; import freenet.support.Logger; import freenet.support.api.Bucket; import freenet.support.api.LockableRandomAccessBuffer; import freenet.support.api.RandomAccessBucket; import freenet.support.io.BucketTools; import freenet.support.io.FilenameGenerator; import freenet.support.io.NullInputStream; import freenet.support.io.PersistentFileTracker; import freenet.support.io.ResumeFailedException; import freenet.support.io.StorageFormatException; import freenet.support.io.TempFileBucket; /** A Bucket encrypted using the same format as an EncryptedRandomAccessBuffer, which can therefore * be converted easily when needed. * @author toad */ public class EncryptedRandomAccessBucket implements RandomAccessBucket, Serializable { private static final long serialVersionUID = 1L; private final EncryptedRandomAccessBufferType type; private final RandomAccessBucket underlying; private transient ParametersWithIV cipherParams;//includes key private transient SecretKey headerMacKey; private transient volatile boolean isFreed = false; private transient SecretKey unencryptedBaseKey; private transient SecretKey headerEncKey; private transient byte[] headerEncIV; private int version; private transient MasterSecret masterKey; private static final long END_MAGIC = 0x2c158a6c7772acd3L; private static final int VERSION_AND_MAGIC_LENGTH = 12; public EncryptedRandomAccessBucket(EncryptedRandomAccessBufferType type, RandomAccessBucket underlying, MasterSecret masterKey) { this.type = type; this.underlying = underlying; this.masterKey = masterKey; baseSetup(masterKey); } /** Setup methods that don't depend on the actual key */ private void baseSetup(MasterSecret masterKey) { MasterSecret masterSecret = masterKey; this.headerEncKey = masterSecret.deriveKey(type.encryptKey); this.headerMacKey = masterSecret.deriveKey(type.macKey); version = type.bitmask; } private SkippingStreamCipher setup(OutputStream os) throws GeneralSecurityException, IOException { this.headerEncIV = KeyGenUtils.genIV(type.encryptType.ivSize).getIV(); this.unencryptedBaseKey = KeyGenUtils.genSecretKey(type.encryptKey); writeHeader(os); setupKeys(); SkippingStreamCipher cipherWrite = this.type.get(); cipherWrite.init(true, cipherParams); return cipherWrite; } private void writeHeader(OutputStream os) throws GeneralSecurityException, IOException { byte[] header = new byte[type.headerLen]; int offset = 0; int ivLen = headerEncIV.length; System.arraycopy(headerEncIV, 0, header, offset, ivLen); offset += ivLen; byte[] encryptedKey = null; try { CryptByteBuffer crypt = new CryptByteBuffer(type.encryptType, headerEncKey, headerEncIV); encryptedKey = crypt.encryptCopy(unencryptedBaseKey.getEncoded()); } catch (InvalidKeyException e) { throw new GeneralSecurityException("Something went wrong with key generation. please " + "report", e.fillInStackTrace()); } catch (InvalidAlgorithmParameterException e) { throw new GeneralSecurityException("Something went wrong with key generation. please " + "report", e.fillInStackTrace()); } System.arraycopy(encryptedKey, 0, header, offset, encryptedKey.length); offset += encryptedKey.length; byte[] ver = ByteBuffer.allocate(4).putInt(version).array(); try { MessageAuthCode mac = new MessageAuthCode(type.macType, headerMacKey); byte[] macResult = Fields.copyToArray(mac.genMac(headerEncIV, unencryptedBaseKey.getEncoded(), ver)); System.arraycopy(macResult, 0, header, offset, macResult.length); offset += macResult.length; } catch (InvalidKeyException e) { throw new GeneralSecurityException("Something went wrong with key generation. please " + "report", e.fillInStackTrace()); } System.arraycopy(ver, 0, header, offset, ver.length); offset +=ver.length; byte[] magic = ByteBuffer.allocate(8).putLong(END_MAGIC).array(); System.arraycopy(magic, 0, header, offset, magic.length); os.write(header); } private SkippingStreamCipher setup(InputStream is) throws IOException, GeneralSecurityException { byte[] fullHeader = new byte[type.headerLen]; try { new DataInputStream(is).readFully(fullHeader); } catch (EOFException e) { throw new IOException("Underlying RandomAccessBuffer is not long enough to include the " + "footer."); } byte[] header = Arrays.copyOfRange(fullHeader, fullHeader.length-VERSION_AND_MAGIC_LENGTH, fullHeader.length); int offset = 0; int readVersion = ByteBuffer.wrap(header, offset, 4).getInt(); offset += 4; long magic = ByteBuffer.wrap(header, offset, 8).getLong(); if(END_MAGIC != magic) { throw new IOException("This is not an EncryptedRandomAccessBuffer!"); } if(readVersion != version){ throw new IOException("Version of the underlying RandomAccessBuffer is " + "incompatible with this ERATType"); } if(!verifyHeader(fullHeader)) throw new GeneralSecurityException("MAC is incorrect"); setupKeys(); SkippingStreamCipher cipherRead = this.type.get(); cipherRead.init(false, cipherParams); return cipherRead; } private boolean verifyHeader(byte[] fullHeader) throws IOException, InvalidKeyException { byte[] footer = Arrays.copyOfRange(fullHeader, 0, fullHeader.length-VERSION_AND_MAGIC_LENGTH); int offset = 0; headerEncIV = new byte[type.encryptType.ivSize]; System.arraycopy(footer, offset, headerEncIV, 0, headerEncIV.length); offset += headerEncIV.length; int keySize = type.encryptKey.keySize >> 3; byte[] encryptedKey = new byte[keySize]; System.arraycopy(footer, offset, encryptedKey, 0, keySize); offset += keySize; try { CryptByteBuffer crypt = new CryptByteBuffer(type.encryptType, headerEncKey, headerEncIV); unencryptedBaseKey = KeyGenUtils.getSecretKey(type.encryptKey, crypt.decryptCopy(encryptedKey)); } catch (InvalidKeyException e) { throw new IOException("Error reading encryption keys from header."); } catch (InvalidAlgorithmParameterException e) { throw new IOException("Error reading encryption keys from header."); } byte[] mac = new byte[type.macLen]; System.arraycopy(footer, offset, mac, 0, type.macLen); byte[] ver = ByteBuffer.allocate(4).putInt(version).array(); MessageAuthCode authcode = new MessageAuthCode(type.macType, headerMacKey); return authcode.verifyData(mac, headerEncIV, unencryptedBaseKey.getEncoded(), ver); } private void setupKeys() { ParametersWithIV tempPram = null; try{ KeyParameter cipherKey = new KeyParameter(KeyGenUtils.deriveSecretKey(unencryptedBaseKey, EncryptedRandomAccessBuffer.class, kdfInput.underlyingKey.input, type.encryptKey).getEncoded()); tempPram = new ParametersWithIV(cipherKey, KeyGenUtils.deriveIvParameterSpec(unencryptedBaseKey, EncryptedRandomAccessBuffer.class, kdfInput.underlyingIV.input, type.encryptKey).getIV()); } catch(InvalidKeyException e) { throw new IllegalStateException(e); // Must be a bug. } this.cipherParams = tempPram; } class MyOutputStream extends FilterOutputStream { private final SkippingStreamCipher cipherWrite; public MyOutputStream(OutputStream out, SkippingStreamCipher cipher) { super(out); this.cipherWrite = cipher; } private final byte[] one = new byte[1]; @Override public void write(int x) throws IOException { one[0] = (byte)x; write(one); } @Override public void write(byte[] buf) throws IOException { write(buf, 0, buf.length); } @Override public void write(byte[] buf, int offset, int length) throws IOException { byte[] ciphertext = new byte[length]; cipherWrite.processBytes(buf, offset, length, ciphertext, 0); out.write(ciphertext); } } @Override public OutputStream getOutputStreamUnbuffered() throws IOException { if(isFreed){ throw new IOException("This RandomAccessBuffer has already been closed. This should not" + " happen."); } OutputStream uos = underlying.getOutputStreamUnbuffered(); try { return new MyOutputStream(uos, setup(uos)); } catch (GeneralSecurityException e) { Logger.error(this, "Unable to create encrypted bucket: "+e, e); throw new IOException(e); } } class MyInputStream extends FilterInputStream { private final SkippingStreamCipher cipherRead; public MyInputStream(InputStream in, SkippingStreamCipher cipher) { super(in); this.cipherRead = cipher; } private byte[] one = new byte[1]; @Override public int read() throws IOException { int readBytes = read(one); if(readBytes <= 0) return readBytes; return one[0] & 0xFF; } @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 { int readBytes = in.read(buf, offset, length); if(readBytes <= 0) return readBytes; cipherRead.processBytes(buf, offset, readBytes, buf, offset); return readBytes; } } @Override public InputStream getInputStreamUnbuffered() throws IOException { if(size() == 0) return new NullInputStream(); if(isFreed){ throw new IOException("This RandomAccessBuffer has already been closed. This should not" + " happen."); } InputStream is = underlying.getInputStreamUnbuffered(); try { return new MyInputStream(is, setup(is)); } catch (GeneralSecurityException e) { Logger.error(this, "Unable to read encrypted bucket: "+e, e); throw new IOException(e); } } @Override public String getName() { return getClass().getName()+":"+underlying.getName(); } @Override public long size() { long size = underlying.size(); if(size == 0) return 0; return size - type.headerLen; } @Override public boolean isReadOnly() { return underlying.isReadOnly(); } @Override public void setReadOnly() { underlying.setReadOnly(); } @Override public void free() { if(isFreed) return; isFreed = true; underlying.free(); } @Override public RandomAccessBucket createShadow() { RandomAccessBucket copy = underlying.createShadow(); return new EncryptedRandomAccessBucket(type, copy, masterKey); } @Override public LockableRandomAccessBuffer toRandomAccessBuffer() throws IOException { if(underlying.size() < type.headerLen) throw new IOException("Converting empty bucket"); underlying.setReadOnly(); LockableRandomAccessBuffer r = underlying.toRandomAccessBuffer(); try { return new EncryptedRandomAccessBuffer(type, r, masterKey, false); } catch (GeneralSecurityException e) { Logger.error(this, "Unable to convert encrypted bucket: "+e, e); throw new IOException(e); } } @Override public OutputStream getOutputStream() throws IOException { return new BufferedOutputStream(getOutputStreamUnbuffered()); } @Override public InputStream getInputStream() throws IOException { return new BufferedInputStream(getInputStreamUnbuffered()); } @Override public void onResume(ClientContext context) throws ResumeFailedException { underlying.onResume(context); this.masterKey = context.getPersistentMasterSecret(); baseSetup(masterKey); } public static final int MAGIC = 0xd8ba4c7e; @Override public void storeTo(DataOutputStream dos) throws IOException { dos.writeInt(MAGIC); dos.writeInt(type.bitmask); underlying.storeTo(dos); } public EncryptedRandomAccessBucket(DataInputStream dis, FilenameGenerator fg, PersistentFileTracker persistentFileTracker, MasterSecret masterKey2) throws IOException, ResumeFailedException, StorageFormatException { type = EncryptedRandomAccessBufferType.getByBitmask(dis.readInt()); if(type == null) throw new ResumeFailedException("Unknown EncryptedRandomAccessBucket type"); underlying = (RandomAccessBucket) BucketTools.restoreFrom(dis, fg, persistentFileTracker, masterKey2); this.baseSetup(masterKey2); } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + type.hashCode(); result = prime * result + underlying.hashCode(); return result; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } EncryptedRandomAccessBucket other = (EncryptedRandomAccessBucket) obj; if (type != other.type) { return false; } return underlying.equals(other.underlying); } public RandomAccessBucket getUnderlying() { return underlying; } }