- preparation for http range requests: cryptor supports partial decryption now

This commit is contained in:
Sebastian Stenzel
2014-12-20 10:47:26 +01:00
parent 3cdda99c67
commit 6d98442f7e
10 changed files with 416 additions and 4 deletions

View File

@@ -90,7 +90,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
private final byte[] masterKey = new byte[MASTER_KEY_LENGTH];
private static final int SIZE_OF_LONG = Long.SIZE / Byte.SIZE;
private static final int SIZE_OF_INT = Integer.SIZE / Byte.SIZE;
static {
try {
@@ -403,14 +402,48 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
return IOUtils.copyLarge(in, cipheredOut);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
// read iv:
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int read = encryptedFile.read(countingIv);
if (read != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read encrypted file header.");
}
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long numberOfRelevantBlocks = 1 + length / AES_BLOCK_LENGTH;
long numberOfRelevantBytes = numberOfRelevantBlocks * AES_BLOCK_LENGTH;
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
// fast forward stream:
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);
final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final OutputStream rangedOut = new RangeFilterOutputStream(plaintextFile, offsetInsideFirstRelevantBlock, length);
final OutputStream cipheredOut = new CipherOutputStream(rangedOut, cipher);
return IOUtils.copyLarge(in, cipheredOut, 0, numberOfRelevantBytes);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
// truncate file
encryptedFile.truncate(0);
// use an IV, whose last 4 bytes store an integer used in counter mode and write initial value to file.
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
countingIv.putInt(AES_BLOCK_LENGTH - SIZE_OF_INT, 0);
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
// derive secret key and generate cipher:
final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH);

View File

@@ -0,0 +1,46 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.OutputStream;
class LimitFilterOutputStream extends java.io.FilterOutputStream {
private final long limit;
private long bytesWritten;
LimitFilterOutputStream(OutputStream out, long limit) {
super(out);
if (limit < 0) {
throw new IllegalArgumentException("Limit must be greater than or equal 0.");
}
this.limit = limit;
}
@Override
public void write(int b) throws IOException {
this.write(new byte[] {(byte) b});
}
@Override
public void write(byte[] b) throws IOException {
this.write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
final long adjustedLength = Math.min(bytesRemainingUntilReachingLimit(), len);
// adjustedLength is <= len, so it must be INT and we can safely cast:
out.write(b, off, (int) adjustedLength);
bytesWritten += adjustedLength;
}
private long bytesRemainingUntilReachingLimit() {
if (bytesWritten < limit) {
return limit - bytesWritten;
} else {
return 0l;
}
}
}

View File

@@ -0,0 +1,49 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.OutputStream;
class OffsetFilterOutputStream extends java.io.FilterOutputStream {
private final long offset;
private long bytesWritten;
OffsetFilterOutputStream(OutputStream out, long offset) {
super(out);
if (offset < 0) {
throw new IllegalArgumentException("Offset must be greater than or equal 0.");
}
this.offset = offset;
}
@Override
public void write(int b) throws IOException {
this.write(new byte[] {(byte) b});
}
@Override
public void write(byte[] b) throws IOException {
this.write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
final long adjustedOffset = remainingOffset() + off;
final long adjustedLength = len - remainingOffset();
if (adjustedOffset < b.length && adjustedLength <= b.length) {
// b.length is INT, so by definition adjustedOffset and adjustedLength must be INT too and we can safely cast:
out.write(b, (int) adjustedOffset, (int) adjustedLength);
}
bytesWritten += len;
}
private long remainingOffset() {
if (bytesWritten < offset) {
return offset - bytesWritten;
} else {
return 0l;
}
}
}

View File

@@ -0,0 +1,21 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Passthrough of all bytes except for certain bytes at the begin and end of the stream, which will get cut off.
*/
class RangeFilterOutputStream extends FilterOutputStream {
RangeFilterOutputStream(OutputStream out, long offset, long limit) {
super(new OffsetFilterOutputStream(new LimitFilterOutputStream(out, limit), offset));
}
@Override
public void write(byte b[], int off, int len) throws IOException {
out.write(b, off, len);
}
}

View File

@@ -8,9 +8,13 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -33,10 +37,11 @@ import org.junit.Test;
public class Aes256CryptorTest {
private static final Random TEST_PRNG = new Random();
private static final Random TEST_PRNG = new NotReallyRandom();
private Path tmpDir;
private Path masterKey;
private Path encryptedFile;
@Before
public void prepareTmpDir() throws IOException {
@@ -44,6 +49,7 @@ public class Aes256CryptorTest {
final Path path = FileSystems.getDefault().getPath(tmpDirName);
tmpDir = Files.createTempDirectory(path, "oce-crypto-test");
masterKey = tmpDir.resolve("test" + Aes256Cryptor.MASTERKEY_FILE_EXT);
encryptedFile = tmpDir.resolve("test" + Aes256Cryptor.BASIC_FILE_EXT);
}
@After
@@ -98,6 +104,39 @@ public class Aes256CryptorTest {
decryptor.decryptMasterKey(in, pw);
}
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = new byte[500 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 500; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final SeekableByteChannel fileOut = Files.newByteChannel(encryptedFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
cryptor.encryptFile(plaintextIn, fileOut);
fileOut.close();
// decrypt:
final SeekableByteChannel fileIn = Files.newByteChannel(encryptedFile, StandardOpenOption.READ);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(fileIn, plaintextOut, 313 * Integer.BYTES, 50 * Integer.BYTES);
Assert.assertTrue(numDecryptedBytes > 0);
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = new byte[50 * Integer.BYTES];
final ByteBuffer bbOut = ByteBuffer.wrap(expected);
for (int i = 313; i < 363; i++) {
bbOut.putInt(i);
}
Assert.assertArrayEquals(expected, result);
}
@Test(expected = FileAlreadyExistsException.class)
public void testReInitialization() throws IOException {
final String pw = "asd";
@@ -146,4 +185,13 @@ public class Aes256CryptorTest {
}
private static class NotReallyRandom extends Random {
private static final long serialVersionUID = 6080187127141721369L;
@Override
protected int next(int bits) {
return 4; // http://xkcd.com/221/
}
}
}

View File

@@ -0,0 +1,63 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class LimitFilterOutputStreamTest {
@Test
public void testNoLimit() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testLimit43() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, 43l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 43);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testLimit307() throws IOException {
final byte[] testData = createTestData(512);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, 307l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 307);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@@ -0,0 +1,63 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class OffsetFilterOutputStreamTest {
@Test
public void testNoOffset() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 0l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffset43() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 43l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffset307() throws IOException {
final byte[] testData = createTestData(512);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 307l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 307, 512);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@@ -0,0 +1,76 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class RangeFilterOutputStreamTest {
@Test
public void testNoOffsetUnlimited() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 0l, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testNoOffsetButLimit() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 0l, 97l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 97);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testNoLimitButOffset() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 43l, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffsettedAndLimited() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 43l, 57l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 100);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@@ -79,6 +79,13 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
/**
* @param pos First byte (inclusive)
* @param length Number of requested bytes beginning at pos.
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
*/

View File

@@ -87,6 +87,12 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
return cryptor.decryptedFile(encryptedFile, countingInputStream);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
}
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);