/*
* EncFS Java Library
* Copyright (C) 2011 Mark R. Pariente
*
* 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.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* FilterInputStream extension that allows decrypted data to be read from a file
* on an EncFS volume.
*/
public class EncFSInputStream extends FilterInputStream {
private static final Logger LOG = LoggerFactory.getLogger(EncFSInputStream.class);
// Volume that underlying file belongs to
private final EncFSVolume volume;
// Volume configuration for this file
private final EncFSConfig config;
// Cached block size for this volume
private final int blockSize;
// Number of MAC bytes for each block
private final int numMACBytes;
// Size of the block header for each block
private final int blockHeaderSize;
// Current block number for generating block IV
private int blockNum;
// Buffer containing decrypted data from the current block
private byte[] blockBuf;
// Cursor into blockBuf denoting current stream position
private int bufCursor;
// File IV computed from the first 8 bytes of the file
private byte[] fileIv;
/**
* Create a new EncFSInputStream for reading decrypted data off a file on an
* EncFS volume
*
* @param volume
* Volume hosting the file to read
* @param in
* Input stream to access the raw (encrypted) file contents
* @param volumePath
* Volume path of the file being decrypted (needed for
* externalIVChaining)
*/
public EncFSInputStream(EncFSVolume volume, InputStream in, String volumePath) throws EncFSCorruptDataException, EncFSUnsupportedException {
super(in);
this.volume = volume;
this.config = volume.getConfig();
this.blockSize = config.getEncryptedFileBlockSizeInBytes();
this.numMACBytes = config.getNumberOfMACBytesForEachFileBlock();
int numRandBytes = config.getNumberOfRandomBytesInEachMACHeader();
this.blockHeaderSize = this.numMACBytes+numRandBytes;
this.blockBuf = null;
this.bufCursor = 0;
this.blockNum = 0;
if (config.isUseUniqueIV()) {
// Compute file IV
byte[] fileHeader = new byte[EncFSFile.HEADER_SIZE];
try {
in.read(fileHeader);
} catch (IOException e) {
throw new EncFSCorruptDataException("Could't read file IV");
}
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,
fileHeader);
} 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[EncFSFile.HEADER_SIZE];
}
}
/*
* (non-Javadoc)
*
* @see java.io.FileInputStream#read()
*/
@Override
public int read() throws IOException {
byte[] oneByte = new byte[1];
int ret = this.read(oneByte, 0, 1);
if (ret==1) {
return oneByte[0];
}
return ret;
}
/*
* (non-Javadoc)
*
* @see java.io.FileInputStream#read(byte[])
*/
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
/*
* (non-Javadoc)
*
* @see java.io.InputStream#read(byte[], int, int)
*/
@Override
public int read(byte[] output, int offset, int size) throws IOException {
int bytesRead = 0;
int destOffset = offset;
int bytesToCopy;
int ret;
while (bytesRead<size) {
// Read more data if the data buffer is out
if ((blockBuf==null)||(bufCursor==(blockBuf.length))) {
try {
ret = readBlock();
} catch (EncFSCorruptDataException|EncFSUnsupportedException e) {
throw new IOException(e);
}
if (ret<0) {
if (bytesRead==0) {
return -1;
} else {
return bytesRead;
}
}
}
bytesToCopy = Math.min(blockBuf.length-bufCursor, size
-bytesRead);
System.arraycopy(blockBuf, bufCursor, output, destOffset,
bytesToCopy);
bufCursor += bytesToCopy;
bytesRead += bytesToCopy;
destOffset += bytesToCopy;
}
return bytesRead;
}
/*
* (non-Javadoc)
*
* @see java.io.FileInputStream#skip(long)
*/
@Override
public long skip(long n) throws IOException {
if (n<0) {
throw new IOException("Negative skip count");
}
long bytesSkipped = 0;
int toSkip;
int bytesRead;
byte[] skipBuf = new byte[config.getEncryptedFileBlockSizeInBytes()];
while (bytesSkipped<n) {
toSkip = (int) Math.min(n-bytesSkipped,
config.getEncryptedFileBlockSizeInBytes());
bytesRead = this.read(skipBuf, 0, toSkip);
bytesSkipped += bytesRead;
if (bytesRead==-1) {
return -1; // Already at EOF
} else if (bytesRead<toSkip) {
return bytesSkipped; // Hit EOF now
}
}
return bytesSkipped;
}
/*
* (non-Javadoc)
*
* @see java.io.InputStream#markSupported()
*/
@Override
public boolean markSupported() {
return false;
}
// Return the block IV for the current block
private byte[] getBlockIV() {
long fileIvLong = EncFSUtil.convertByteArrayToLong(fileIv);
return EncFSUtil.convertLongToByteArrayBigEndian(blockNum^fileIvLong);
}
/*
* Read one block (blockSize bytes) of data from the underlying
* FileInputStream, decrypt it and store it in blockBuf for consumption via
* read() methods
*/
private int readBlock() throws IOException, EncFSCorruptDataException,
EncFSUnsupportedException {
byte[] cipherBuf = new byte[blockSize];
boolean zeroBlock = false;
int bytesRead = 0;
int lastBytesRead;
// Read until we read a whole block or we reach the end of the input
while (bytesRead<blockSize) {
lastBytesRead = in
.read(cipherBuf, bytesRead, blockSize-bytesRead);
if (lastBytesRead>0) {
bytesRead += lastBytesRead;
} else if (lastBytesRead<0) {
/*
* If we read some bytes return that, if not then we're at the
* end of the stream
*/
if (bytesRead==0) {
bytesRead = -1;
}
break;
}
}
if (bytesRead==blockSize) { // block decode
/*
* If file holes are allowed then we need to test whether the whole
* block is made up of 0's. If not (which is going to be the case
* for MAC header by default), we will do block decryption.
*/
if (config.isHolesAllowedInFiles()) {
zeroBlock = true;
for (byte aCipherBuf : cipherBuf) {
if (aCipherBuf!=0) {
zeroBlock = false;
break;
}
}
}
try {
if (zeroBlock) {
blockBuf = cipherBuf;
} else {
blockBuf = BlockCrypto.blockDecrypt(volume,
getBlockIV(), cipherBuf);
}
} catch (InvalidAlgorithmParameterException e) {
LOG.error("readBlock", e);
} catch (IllegalBlockSizeException|BadPaddingException e) {
throw new EncFSCorruptDataException(e);
}
bufCursor = blockHeaderSize;
blockNum++;
} else if (bytesRead>0) { // stream decode
try {
blockBuf = StreamCrypto.streamDecrypt(volume,
getBlockIV(), cipherBuf, 0, bytesRead);
} catch (InvalidAlgorithmParameterException e) {
LOG.error("readBlock", e);
} catch (IllegalBlockSizeException|BadPaddingException e) {
throw new EncFSCorruptDataException(e);
}
bufCursor = blockHeaderSize;
blockNum++;
}
// Verify the block header
if ((bytesRead>0)&&(blockHeaderSize>0)&&(!zeroBlock)) {
byte mac[] = EncFSCrypto.mac64(volume.getMAC(), blockBuf,
numMACBytes);
for (int i = 0; i<numMACBytes; i++) {
if (mac[7-i]!=blockBuf[i]) {
throw new EncFSCorruptDataException("Block MAC mismatch");
}
}
}
return bytesRead;
}
}