diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index a7f6e94b3..c0ec1a3b4 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -89,7 +89,7 @@ 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_LONG = Long.BYTES; static { try { @@ -444,15 +444,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo // 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.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l); + countingIv.position(0); // 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.ENCRYPT_MODE); - // skip 8 bytes (reserved for file size): - encryptedFile.position(SIZE_OF_LONG); + // 8 bytes (file size: temporarily -1): + final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG); + fileSize.putLong(-1L); + fileSize.position(0); + encryptedFile.write(fileSize); - // write iv: + // 16 bytes (iv): encryptedFile.write(countingIv); // write content: @@ -461,11 +465,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut); // write filesize - final ByteBuffer actualSizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG); - actualSizeBuffer.putLong(actualSize); - actualSizeBuffer.position(0); + fileSize.position(0); + fileSize.putLong(actualSize); + fileSize.position(0); encryptedFile.position(0); - encryptedFile.write(actualSizeBuffer); + encryptedFile.write(fileSize); return actualSize; } diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index 781224daf..a40157bb7 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -12,96 +12,84 @@ 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; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; import java.util.HashMap; import java.util.Map; import java.util.Random; -import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.cryptomator.crypto.CryptorIOSupport; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; public class Aes256CryptorTest { private static final Random TEST_PRNG = new Random(); - private Path tmpDir; - private Path masterKey; - private Path encryptedFile; - - @Before - public void prepareTmpDir() throws IOException { - final String tmpDirName = (String) System.getProperties().get("java.io.tmpdir"); - 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 - public void dropTmpDir() { - try { - FileUtils.deleteDirectory(tmpDir.toFile()); - } catch (IOException e) { - // ignore - } - } - - /* ------------------------------------------------------------------------------- */ - @Test public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); - final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); cryptor.encryptMasterKey(out, pw); cryptor.swipeSensitiveData(); final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG); - final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ); + final InputStream in = new ByteArrayInputStream(out.toByteArray()); decryptor.decryptMasterKey(in, pw); + + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); } @Test(expected = WrongPasswordException.class) public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); - final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); cryptor.encryptMasterKey(out, pw); cryptor.swipeSensitiveData(); final String wrongPw = "foo"; final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG); - final InputStream in = Files.newInputStream(masterKey, StandardOpenOption.READ); + final InputStream in = new ByteArrayInputStream(out.toByteArray()); decryptor.decryptMasterKey(in, wrongPw); + + IOUtils.closeQuietly(out); + IOUtils.closeQuietly(in); } - @Test(expected = NoSuchFileException.class) - public void testWrongLocation() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { - final String pw = "asd"; - final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); - final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - cryptor.encryptMasterKey(out, pw); - cryptor.swipeSensitiveData(); + @Test + public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + // our test plaintext data: + final byte[] plaintextData = "Hello World".getBytes(); + final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); - final Path wrongMasterKey = tmpDir.resolve("notExistingMasterKey.json"); - final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG); - final InputStream in = Files.newInputStream(wrongMasterKey, StandardOpenOption.READ); - decryptor.decryptMasterKey(in, pw); + // init cryptor: + final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); + + // encrypt: + final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200); + final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); + cryptor.encryptFile(plaintextIn, encryptedOut); + IOUtils.closeQuietly(plaintextIn); + IOUtils.closeQuietly(encryptedOut); + + // decrypt: + final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData); + final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); + final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut); + IOUtils.closeQuietly(encryptedIn); + IOUtils.closeQuietly(plaintextOut); + Assert.assertTrue(numDecryptedBytes > 0); + + // check decrypted data: + final byte[] result = plaintextOut.toByteArray(); + Assert.assertArrayEquals(plaintextData, result); } @Test @@ -118,16 +106,21 @@ public class Aes256CryptorTest { 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(); + final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200); + final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData); + cryptor.encryptFile(plaintextIn, encryptedOut); + IOUtils.closeQuietly(plaintextIn); + IOUtils.closeQuietly(encryptedOut); // decrypt: - final SeekableByteChannel fileIn = Files.newByteChannel(encryptedFile, StandardOpenOption.READ); + final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData); final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream(); - final Long numDecryptedBytes = cryptor.decryptRange(fileIn, plaintextOut, 313 * Integer.BYTES, 50 * Integer.BYTES); + final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 313 * Integer.BYTES, 50 * Integer.BYTES); + IOUtils.closeQuietly(encryptedIn); + IOUtils.closeQuietly(plaintextOut); Assert.assertTrue(numDecryptedBytes > 0); + // check decrypted data: final byte[] result = plaintextOut.toByteArray(); final byte[] expected = new byte[50 * Integer.BYTES]; final ByteBuffer bbOut = ByteBuffer.wrap(expected); @@ -137,19 +130,6 @@ public class Aes256CryptorTest { Assert.assertArrayEquals(expected, result); } - @Test(expected = FileAlreadyExistsException.class) - public void testReInitialization() throws IOException { - final String pw = "asd"; - final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG); - final OutputStream out = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); - cryptor.encryptMasterKey(out, pw); - cryptor.swipeSensitiveData(); - - final OutputStream outAgain = Files.newOutputStream(masterKey, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); - cryptor.encryptMasterKey(outAgain, pw); - cryptor.swipeSensitiveData(); - } - @Test public void testEncryptionOfFilenames() throws IOException { final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock(); diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/ByteBufferBackedSeekableChannel.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/ByteBufferBackedSeekableChannel.java new file mode 100644 index 000000000..d037327e8 --- /dev/null +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/ByteBufferBackedSeekableChannel.java @@ -0,0 +1,79 @@ +package org.cryptomator.crypto.aes256; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; + +class ByteBufferBackedSeekableChannel implements SeekableByteChannel { + + private final ByteBuffer buffer; + private boolean open = true; + + ByteBufferBackedSeekableChannel(ByteBuffer buffer) { + this.buffer = buffer; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public void close() throws IOException { + open = false; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (buffer.remaining() == 0) { + return -1; + } + int num = Math.min(dst.remaining(), buffer.remaining()); + byte[] bytes = new byte[num]; + buffer.get(bytes); + dst.put(bytes); + return num; + } + + @Override + public int write(ByteBuffer src) throws IOException { + int num = src.remaining(); + if (buffer.remaining() < src.remaining()) { + buffer.limit(buffer.limit() + src.remaining()); + } + buffer.put(src); + return num; + } + + @Override + public long position() throws IOException { + return buffer.position(); + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + if (newPosition > Integer.MAX_VALUE) { + throw new UnsupportedOperationException(); + } + if (newPosition > buffer.limit()) { + buffer.limit((int) newPosition); + } + buffer.position((int) newPosition); + return this; + } + + @Override + public long size() throws IOException { + return buffer.limit(); + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + if (size > Integer.MAX_VALUE) { + throw new UnsupportedOperationException(); + } + buffer.limit((int) size); + return this; + } + +}