/* 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.crypt;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.util.concurrent.locks.ReentrantLock;
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.support.Fields;
import freenet.support.Logger;
import freenet.support.api.LockableRandomAccessBuffer;
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;
/**
* EncryptedRandomAccessBuffer is a encrypted RandomAccessBuffer implementation using a
* SkippingStreamCipher.
* @author unixninja92
* Suggested EncryptedRandomAccessBufferType to use: ChaCha128
*/
public final class EncryptedRandomAccessBuffer implements LockableRandomAccessBuffer, Serializable {
private static final long serialVersionUID = 1L;
private final ReentrantLock readLock = new ReentrantLock();
private final ReentrantLock writeLock = new ReentrantLock();
private final EncryptedRandomAccessBufferType type;
private final LockableRandomAccessBuffer underlyingBuffer;
private transient SkippingStreamCipher cipherRead;
private transient SkippingStreamCipher cipherWrite;
private transient ParametersWithIV cipherParams;//includes key
private transient SecretKey headerMacKey;
private transient volatile boolean isClosed = false;
private transient SecretKey unencryptedBaseKey;
private transient SecretKey headerEncKey;
private transient byte[] headerEncIV;
private int version;
private static final long END_MAGIC = 0x2c158a6c7772acd3L;
private static final int VERSION_AND_MAGIC_LENGTH = 12;
/**
* Creates an instance of EncryptedRandomAccessBuffer wrapping underlyingBuffer. Keys for key
* encryption and MAC generation are derived from the MasterSecret. If this is a new ERAT then
* keys are generated and the footer is written to the end of the underlying RAT. Otherwise the
* footer is read from the underlying RAT.
* @param type The algorithms to be used for the ERAT
* @param underlyingBuffer The underlying RAT that will be storing the data. Must be larger than
* the footer size specified in type.
* @param masterKey The MasterSecret that will be used to derive various keys.
* @param newFile If true, treat it as a new file, and writer a header. If false, the ERAT must
* already have been initialised.
* @throws IOException
* @throws GeneralSecurityException
*/
public EncryptedRandomAccessBuffer(EncryptedRandomAccessBufferType type,
LockableRandomAccessBuffer underlying, MasterSecret masterKey, boolean newFile) throws IOException,
GeneralSecurityException{
this.type = type;
this.underlyingBuffer = underlying;
setup(masterKey, newFile);
}
private void setup(MasterSecret masterKey, boolean newFile) throws IOException, GeneralSecurityException {
this.cipherRead = this.type.get();
this.cipherWrite = this.type.get();
MasterSecret masterSecret = masterKey;
this.headerEncKey = masterSecret.deriveKey(type.encryptKey);
this.headerMacKey = masterSecret.deriveKey(type.macKey);
if(underlyingBuffer.size() < type.headerLen){
throw new IOException("Underlying RandomAccessBuffer is not long enough to include the "
+ "footer.");
}
byte[] header = new byte[VERSION_AND_MAGIC_LENGTH];
int offset = 0;
underlyingBuffer.pread(type.headerLen-VERSION_AND_MAGIC_LENGTH, header, offset,
VERSION_AND_MAGIC_LENGTH);
int readVersion = ByteBuffer.wrap(header, offset, 4).getInt();
offset += 4;
long magic = ByteBuffer.wrap(header, offset, 8).getLong();
if(!newFile && END_MAGIC != magic) {
throw new IOException("This is not an EncryptedRandomAccessBuffer!");
}
version = type.bitmask;
if(newFile) {
this.headerEncIV = KeyGenUtils.genIV(type.encryptType.ivSize).getIV();
this.unencryptedBaseKey = KeyGenUtils.genSecretKey(type.encryptKey);
writeHeader();
} else {
if(readVersion != version){
throw new IOException("Version of the underlying RandomAccessBuffer is "
+ "incompatible with this ERATType");
}
if(!verifyHeader()){
throw new GeneralSecurityException("MAC is incorrect");
}
}
ParametersWithIV tempPram = null;
try{
KeyParameter cipherKey = new KeyParameter(KeyGenUtils.deriveSecretKey(unencryptedBaseKey,
getClass(), kdfInput.underlyingKey.input,
type.encryptKey).getEncoded());
tempPram = new ParametersWithIV(cipherKey,
KeyGenUtils.deriveIvParameterSpec(unencryptedBaseKey, getClass(),
kdfInput.underlyingIV.input, type.encryptKey).getIV());
} catch(InvalidKeyException e) {
throw new IllegalStateException(e); // Must be a bug.
}
this.cipherParams = tempPram;
cipherRead.init(false, cipherParams);
cipherWrite.init(true, cipherParams);
}
@Override
public long size() {
return underlyingBuffer.size()-type.headerLen;
}
/**
* Reads the specified section of the underlying RAT and decrypts it. Decryption is thread-safe.
*/
@Override
public void pread(long fileOffset, byte[] buf, int bufOffset, int length)
throws IOException {
if(isClosed){
throw new IOException("This RandomAccessBuffer has already been closed. It can no longer"
+ " be read from.");
}
if(fileOffset < 0) throw new IllegalArgumentException("Cannot read before zero");
if(fileOffset+length > size()){
throw new IOException("Cannot read after end: trying to read from "+fileOffset+" to "+
(fileOffset+length)+" on block length "+size());
}
byte[] cipherText = new byte[length];
underlyingBuffer.pread(fileOffset+type.headerLen, cipherText, 0, length);
readLock.lock();
try{
//cipherRead.seekTo(fileOffset);
// seekTo() does reset() and then skip(). So it always skips from 0.
// This is ridiculously slow for big tempfiles.
// FIXME REVIEW CRYPTO: Is this safe? It should be, we're using the published skip() API...
long position = cipherRead.getPosition();
long delta = fileOffset - position;
cipherRead.skip(delta);
assert(cipherRead.getPosition() == fileOffset);
cipherRead.processBytes(cipherText, 0, length, buf, bufOffset);
assert(cipherRead.getPosition() == fileOffset+length);
}finally{
readLock.unlock();
}
}
/**
* Encrypts the given data and writes it to the underlying RAT. Encryption is thread-safe.
*/
@Override
public void pwrite(long fileOffset, byte[] buf, int bufOffset, int length)
throws IOException {
if(isClosed){
throw new IOException("This RandomAccessBuffer has already been closed. It can no longer"
+ " be written to.");
}
if(fileOffset < 0) throw new IllegalArgumentException("Cannot read before zero");
if(fileOffset+length > size()){
throw new IOException("Cannot write after end: trying to write from "+fileOffset+" to "+
(fileOffset+length)+" on block length "+size());
}
byte[] cipherText = new byte[length];
writeLock.lock();
try{
//cipherWrite.seekTo(fileOffset)
// seekTo() does reset() and then skip(). So it always skips from 0.
// This is ridiculously slow for big tempfiles.
// FIXME REVIEW CRYPTO: Is this safe? It should be, we're using the published skip() API...
long position = cipherWrite.getPosition();
long delta = fileOffset - position;
cipherWrite.skip(delta);
assert(cipherWrite.getPosition() == fileOffset);
cipherWrite.processBytes(buf, bufOffset, length, cipherText, 0);
assert(cipherWrite.getPosition() == fileOffset+length);
}finally{
writeLock.unlock();
}
underlyingBuffer.pwrite(fileOffset+type.headerLen, cipherText, 0, length);
}
@Override
public void close() {
if(!isClosed){
isClosed = true;
underlyingBuffer.close();
}
}
@Override
public void free() {
close();
underlyingBuffer.free();
}
/**
* Writes the footer to the end of the underlying RAT
* @throws IOException
* @throws GeneralSecurityException
*/
private void writeHeader() throws IOException, GeneralSecurityException{
if(isClosed){
throw new IOException("This RandomAccessBuffer has already been closed. This should not"
+ " happen.");
}
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);
underlyingBuffer.pwrite(0, header, 0, header.length);
}
/**
* Reads the iv, the encrypted key and the MAC from the footer. Then decrypts they key and
* verifies the MAC.
* @return Returns true if the MAC is verified. Otherwise false.
* @throws IOException
* @throws InvalidKeyException
*/
private boolean verifyHeader() throws IOException, InvalidKeyException {
if(isClosed){
throw new IOException("This RandomAccessBuffer has already been closed. This should not"
+ " happen.");
}
byte[] footer = new byte[type.headerLen-VERSION_AND_MAGIC_LENGTH];
int offset = 0;
underlyingBuffer.pread(0, footer, offset, type.headerLen-VERSION_AND_MAGIC_LENGTH);
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);
}
/**
* The Strings used to derive keys and ivs from the unencryptedBaseKey.
*/
enum kdfInput {
underlyingKey(),/** For deriving the key that will be used to encrypt the underlying RAT*/
underlyingIV();/** For deriving the iv that will be used to encrypt the underlying RAT*/
public final String input;
private kdfInput(){
this.input = name();
}
}
@Override
public RAFLock lockOpen() throws IOException {
return underlyingBuffer.lockOpen();
}
public static final int MAGIC = 0x39ea94c2;
@Override
public void onResume(ClientContext context) throws ResumeFailedException {
underlyingBuffer.onResume(context);
try {
setup(context.getPersistentMasterSecret(), false);
} catch (IOException e) {
Logger.error(this, "Disk I/O error resuming: "+e, e);
throw new ResumeFailedException(e);
} catch (GeneralSecurityException e) {
Logger.error(this, "Impossible security error resuming - maybe we lost a codec?: "+e, e);
throw new ResumeFailedException(e);
}
}
@Override
public void storeTo(DataOutputStream dos) throws IOException {
dos.writeInt(MAGIC);
dos.writeInt(type.bitmask);
underlyingBuffer.storeTo(dos);
}
public static LockableRandomAccessBuffer create(DataInputStream dis, FilenameGenerator fg, PersistentFileTracker persistentFileTracker, MasterSecret masterKey)
throws IOException, StorageFormatException, ResumeFailedException {
EncryptedRandomAccessBufferType type = EncryptedRandomAccessBufferType.getByBitmask(dis.readInt());
if(type == null)
throw new StorageFormatException("Unknown EncryptedRandomAccessBufferType");
LockableRandomAccessBuffer underlying = BucketTools.restoreRAFFrom(dis, fg, persistentFileTracker, masterKey);
try {
return new EncryptedRandomAccessBuffer(type, underlying, masterKey, false);
} catch (GeneralSecurityException e) {
Logger.error(EncryptedRandomAccessBuffer.class, "Crypto error resuming: "+e, e);
throw new ResumeFailedException(e);
}
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((underlyingBuffer == null) ? 0 : underlyingBuffer.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;
}
EncryptedRandomAccessBuffer other = (EncryptedRandomAccessBuffer) obj;
if (type != other.type) {
return false;
}
return underlyingBuffer.equals(other.underlyingBuffer);
}
}