mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-23 21:21:31 +00:00
- preparation for http range requests: cryptor supports partial decryption now
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user