/*
* EncFS Java Library
* Copyright (C) 2011
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*/
package org.mrpdaemon.sec.encfs;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FilterOutputStream extension that allows encrypted data to be written to a
* file on an EncFS volume.
*/
public class EncFSOutputStream extends FilterOutputStream {
private static final Logger LOG = LoggerFactory.getLogger(EncFSOutputStream.class);
// SecureRandom instance for random data generation
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
// Underlying volume
private final EncFSVolume volume;
// Volume configuration
private final EncFSConfig config;
// IV used for this file
private byte[] fileIv;
// Buffer to hold file header contents (uniqueIV)
private byte[] fileHeader;
// Buffer to hold the currently cached data contents to be written
private final byte dataBuf[];
// Count of the cached data bytes about to be written
private int dataBytes;
// Size of the block header for this file
private int blockHeaderSize;
// Number of random bytes per block header
private int blockMACRandLen;
// Number of MAC bytes per block header
private int blockMACLen;
// Index of the current block to be written
private int curBlockIndex;
/**
* Create a new EncFSOutputStream for writing encrypted data to a file on an
* EncFS volume
*
* @param volume
* Volume hosting the file to write
* @param out
* Output stream for writing the encrypted (raw) data
* @param volumePath
* Volume path of the file being encrypted (needed for
* externalIVChaining)
* <p/>
* <p/>
* File data is corrupt
* <p/>
* Unsupported EncFS configuration
*/
public EncFSOutputStream(EncFSVolume volume, OutputStream out, String volumePath)
throws EncFSUnsupportedException, EncFSCorruptDataException {
super(out);
this.volume = volume;
this.config = volume.getConfig();
this.blockHeaderSize = config.getNumberOfMACBytesForEachFileBlock()+config.getNumberOfRandomBytesInEachMACHeader();
this.dataBytes = this.blockHeaderSize;
this.blockMACLen = config.getNumberOfMACBytesForEachFileBlock();
this.blockMACRandLen = config.getNumberOfRandomBytesInEachMACHeader();
if (config.isUseUniqueIV()) {
// Compute file IV
this.fileHeader = new byte[8];
SECURE_RANDOM.nextBytes(fileHeader);
byte[] initIv;
if (config.isSupportedExternalIVChaining()) {
/*
* When using external IV chaining we compute initIv based on
* the file path.
*/
initIv = StreamCrypto.computeChainIv(volume, volumePath);
} else {
// When not using external IV chaining initIv is just zero's.
initIv = new byte[8];
}
try {
this.fileIv = StreamCrypto.streamDecrypt(volume, initIv, Arrays.copyOf(fileHeader, fileHeader.length));
} catch (InvalidAlgorithmParameterException e) {
LOG.error("()", e);
} catch (IllegalBlockSizeException|BadPaddingException e) {
throw new EncFSCorruptDataException(e);
}
} else {
// No unique IV per file, just use 0
this.fileIv = new byte[8];
}
Cipher blockCipher = BlockCrypto.newBlockCipher();
try {
EncFSCrypto.cipherInit(volume, Cipher.ENCRYPT_MODE, blockCipher, fileIv);
} catch (InvalidAlgorithmParameterException e) {
throw new EncFSCorruptDataException(e);
}
Cipher streamCipher = StreamCrypto.newStreamCipher();
try {
EncFSCrypto.cipherInit(volume, Cipher.ENCRYPT_MODE, streamCipher, fileIv);
} catch (InvalidAlgorithmParameterException e) {
throw new EncFSCorruptDataException(e);
}
int blockSize = config.getEncryptedFileBlockSizeInBytes();
// blockSize = blockHeaderSize + blockDataLen
dataBuf = new byte[blockSize];
}
// Flush the internal buffer
private void writeBuffer(boolean isFinal) throws IOException {
if (!isFinal&&dataBytes!=dataBuf.length) {
throw new IllegalStateException("Buffer not full");
}
if (curBlockIndex==0&&config.isUseUniqueIV()) {
out.write(this.fileHeader);
}
// Fill in the block header
if (blockHeaderSize>0) {
// Add random bytes to the buffer
if (blockMACRandLen>0) {
byte randomBytes[] = new byte[blockMACRandLen];
SECURE_RANDOM.nextBytes(randomBytes);
System.arraycopy(randomBytes, 0, dataBuf, blockMACLen,
blockMACRandLen);
}
// Compute MAC bytes and add them to the buffer
byte mac[] = EncFSCrypto.mac64(volume.getMAC(), dataBuf,
blockMACLen, dataBytes-blockMACLen);
for (int i = 0; i<blockMACLen; i++) {
dataBuf[i] = mac[7-i];
}
}
byte[] encBuffer;
try {
if (dataBytes==dataBuf.length) {
/*
* If allowHoles is configured, we scan the buffer to determine
* whether we should pass this block through as a zero block.
* Note that it is intended for the presence of a MAC header to
* cause this check to fail.
*/
boolean zeroBlock = false;
if (config.isHolesAllowedInFiles()) {
zeroBlock = true;
for (byte aDataBuf : dataBuf) {
if (aDataBuf!=0) {
zeroBlock = false;
break;
}
}
}
if (zeroBlock) {
encBuffer = dataBuf;
} else {
encBuffer = BlockCrypto.blockEncrypt(volume,
getBlockIV(), dataBuf);
}
} else {
encBuffer = StreamCrypto.streamEncrypt(volume,
getBlockIV(), dataBuf, 0, dataBytes);
}
} catch (IllegalBlockSizeException e) {
throw new IOException(e);
} catch (BadPaddingException e) {
throw new IOException(e);
} catch (InvalidAlgorithmParameterException e) {
throw new IOException(e);
} catch (EncFSUnsupportedException e) {
throw new IOException(e);
}
out.write(encBuffer);
dataBytes = blockHeaderSize;
curBlockIndex++;
}
// Return the block IV for the current block
private byte[] getBlockIV() {
long fileIvLong = EncFSUtil.convertByteArrayToLong(fileIv);
return EncFSUtil.convertLongToByteArrayBigEndian(curBlockIndex
^fileIvLong);
}
// Flush the internal buffer
private void writeBuffer() throws IOException {
writeBuffer(false);
}
/*
* (non-Javadoc)
*
* @see java.io.FilterOutputStream#write(int)
*/
@Override
public synchronized void write(int b) throws IOException {
dataBuf[dataBytes++] = (byte) b;
if (dataBytes==dataBuf.length) {
writeBuffer();
}
}
/*
* (non-Javadoc)
*
* @see java.io.FilterOutputStream#write(int)
*/
@Override
public synchronized void write(byte b[], int off, int len)
throws IOException {
if (dataBytes+len<=dataBuf.length) {
System.arraycopy(b, off, dataBuf, dataBytes, len);
dataBytes += len;
if (dataBytes==dataBuf.length) {
writeBuffer();
}
} else {
int tmpOff = off;
int remaining = len;
while (remaining>0) {
int chunk = Math.min(remaining, dataBuf.length-dataBytes);
write(b, tmpOff, chunk);
remaining -= chunk;
tmpOff += chunk;
}
}
}
/*
* (non-Javadoc)
*
* @see java.io.FilterOutputStream#write(int)
*/
@Override
public void close() throws IOException {
writeBuffer(true);
super.close();
}
}