Skip to content

Commit

Permalink
support for forced decryption ignoring failed authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
overheadhunter committed Jan 23, 2016
1 parent a6bbc0e commit 2e5264b
Show file tree
Hide file tree
Showing 16 changed files with 166 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ public interface FileContentCryptor {
* @param header The full fixed-length header of an encrypted file. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
* @param firstCiphertextByte Position of the first ciphertext byte passed to the decryptor. If the decryptor can not fast-forward to the requested byte, an exception is thrown.
* If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the decryptors internal block size, an IllegalArgumentException will be thrown.
* @param authenticate Skip authentication by setting this flag to <code>false</code>. Should be <code>true</code> by default.
* @return A possibly new FileContentDecryptor instance which is capable of decrypting ciphertexts associated with the given file header.
*/
FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) throws IllegalArgumentException;
FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) throws IllegalArgumentException;

/**
* @param header The full fixed-length header of an encrypted file or {@link Optional#empty()}. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ public long toCiphertextPos(long cleartextPos) {
}

@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header.");
}
if (firstCiphertextByte % (CHUNK_SIZE + MAC_SIZE) != 0) {
throw new IllegalArgumentException("Invalid starting point for decryption.");
}
return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte);
return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte, authenticate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD);
private final ThreadLocal<Mac> hmacSha256;
private final FileHeader header;
private final boolean authenticate;
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
private long chunkNumber = 0;

public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte) {
public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
final ThreadLocalMac hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
this.hmacSha256 = hmacSha256;
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
this.authenticate = authenticate;
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
}

Expand Down Expand Up @@ -141,10 +143,12 @@ public DecryptionJob(ByteBuffer ciphertextChunk, long chunkNumber) {
@Override
public ByteBuffer call() {
try {
Mac mac = hmacSha256.get();
mac.update(ciphertextChunk.asReadOnlyBuffer());
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
throw new AuthenticationFailedException();
if (authenticate) {
Mac mac = hmacSha256.get();
mac.update(ciphertextChunk.asReadOnlyBuffer());
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
throw new AuthenticationFailedException();
}
}

Cipher cipher = ThreadLocalAesCtrCipher.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public Instant lastModified() throws UncheckedIOException {

@Override
public ReadableFile openReadable() {
return new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalFile().openReadable());
boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
return new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalFile().openReadable(), authenticate);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";

private final Folder physicalRoot;
private final CryptoFileSystemDelegate delegate;

public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) throws InvalidPassphraseException {
public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CryptoFileSystemDelegate delegate, CharSequence passphrase) throws InvalidPassphraseException {
super(null, "", cryptor);
this.physicalRoot = physicalRoot;
this.delegate = delegate;
final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
if (masterkeyFile.exists()) {
final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
Expand Down Expand Up @@ -68,6 +70,10 @@ private static void encryptMasterKeyFile(Cryptor cryptor, File masterkeyFile, Ch
}
}

CryptoFileSystemDelegate delegate() {
return delegate;
}

@Override
protected File physicalFile() {
return physicalDataRoot().file(ROOT_DIR_FILE);
Expand Down Expand Up @@ -107,7 +113,7 @@ public void create() {

@Override
public String toString() {
return physicalRoot + ":::/";
return "/";
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.cryptomator.filesystem.crypto;

public interface CryptoFileSystemDelegate {

/**
* Reports the path for resources, that could not be decrypted due to authentication errors.
*
* @param cleartextPath Unix-style vault-relative path
*/
void authenticationFailed(String cleartextPath);

/**
* Allows the delegate to deactivate authentication during decryption.
* This bears the risk of CCAs, thus this method should only return <code>true</code> for data recovery purposes.
*
* @param cleartextPath Unix-style vault-relative path
* @return Must always <b>default to <code>false</code></b>, except when authentication should be skipped.
*/
boolean shouldSkipAuthentication(String cleartextPath);

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ public CryptoFileSystemFactory(Provider<Cryptor> cryptorProvider, ShorteningFile
this.blockAlignedFileSystemFactory = blockAlignedFileSystemFactory;
}

public FileSystem get(Folder root, CharSequence passphrase) {
public FileSystem get(Folder root, CharSequence passphrase, CryptoFileSystemDelegate delegate) {
final FileSystem nameShorteningFs = shorteningFileSystemFactory.get(root);
final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptorProvider.get(), passphrase);
final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptorProvider.get(), delegate, passphrase);
return blockAlignedFileSystemFactory.get(cryptoFs);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ protected File physicalFile() {
return parent.physicalFolder().file(encryptedName());
}

@Override
public CryptoFileSystem fileSystem() {
return (CryptoFileSystem) Node.super.fileSystem();
}

@Override
public Optional<CryptoFolder> parent() {
return Optional.of(parent);
Expand Down Expand Up @@ -74,4 +79,12 @@ public boolean equals(Object obj) {
}
}

/**
* Unix-style cleartext path rooted at the vault's top-level directory.
*
* @return Vault-relative cleartext path.
*/
@Override
public abstract String toString();

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,16 @@ class CryptoReadableFile implements ReadableFile {
private final ByteBuffer header;
private final FileContentCryptor cryptor;
private final ReadableFile file;
private final boolean authenticate;
private FileContentDecryptor decryptor;
private Future<Void> readAheadTask;
private ByteBuffer bufferedCleartext = EMPTY_BUFFER;

public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file) {
public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file, boolean authenticate) {
this.header = ByteBuffer.allocate(cryptor.getHeaderSize());
this.cryptor = cryptor;
this.file = file;
this.authenticate = authenticate;
file.position(0);
file.read(header);
header.flip();
Expand Down Expand Up @@ -73,7 +75,7 @@ public void position(long position) throws UncheckedIOException {
bufferedCleartext = EMPTY_BUFFER;
}
long ciphertextPos = cryptor.toCiphertextPos(position);
decryptor = cryptor.createFileContentDecryptor(header.asReadOnlyBuffer(), ciphertextPos);
decryptor = cryptor.createFileContentDecryptor(header.asReadOnlyBuffer(), ciphertextPos, authenticate);
readAheadTask = executorService.submit(new CiphertextReader(file, decryptor, header.remaining() + ciphertextPos));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public long toCiphertextPos(long cleartextPos) {
}

@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header size.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public void testShortHeaderInDecryptor() throws InterruptedException {
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);

ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
cryptor.createFileContentDecryptor(tooShortHeader, 0);
cryptor.createFileContentDecryptor(tooShortHeader, 0, true);
}

@Test(expected = IllegalArgumentException.class)
Expand All @@ -75,7 +75,7 @@ public void testInvalidStartingPointInDecryptor() throws InterruptedException {
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);

ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
cryptor.createFileContentDecryptor(header, 3);
cryptor.createFileContentDecryptor(header, 3, true);
}

@Test(expected = IllegalArgumentException.class)
Expand Down Expand Up @@ -110,7 +110,7 @@ public void testEncryptionAndDecryption() throws InterruptedException {
ciphertext.flip();

ByteBuffer plaintext = ByteBuffer.allocate(100);
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
decryptor.append(ciphertext);
decryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
Expand Down Expand Up @@ -163,7 +163,7 @@ public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOEx

final Thread fileReader;
final long decStart = System.nanoTime();
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
fileReader = new Thread(() -> {
try (FileChannel fc = FileChannel.open(tmpFile, StandardOpenOption.READ)) {
ByteBuffer ciphertext = ByteBuffer.allocate(654321);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import javax.crypto.spec.SecretKeySpec;

import org.bouncycastle.util.encoders.Base64;
import org.cryptomator.crypto.engine.AuthenticationFailedException;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
Expand Down Expand Up @@ -47,7 +48,51 @@ public void testDecryption() throws InterruptedException {
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHQ==");

try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0)) {
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
decryptor.append(FileContentCryptor.EOF);

ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
ByteBuffer buf;
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, result);
}

Assert.assertArrayEquals("hello world".getBytes(), result.array());
}
}

@Test(expected = AuthenticationFailedException.class)
public void testManipulatedDecryption() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHq==");

try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
decryptor.append(FileContentCryptor.EOF);

ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
ByteBuffer buf;
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
ByteBuffers.copy(buf, result);
}
}
}

@Test
public void testManipulatedDecryptionWithSuppressedAuthentication() throws InterruptedException {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHq==");

try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
decryptor.append(FileContentCryptor.EOF);
Expand All @@ -69,7 +114,7 @@ public void testPassthroughException() throws InterruptedException {
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");

try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0)) {
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
decryptor.cancelWithException(new IOException("can not do"));
decryptor.cleartext();
}
Expand Down Expand Up @@ -113,7 +158,7 @@ public void testPartialDecryption() throws InterruptedException {

for (int i = 3; i >= 0; i--) {
final int ciphertextPos = (int) cryptor.toCiphertextPos(i * 32768);
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, ciphertextPos)) {
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, ciphertextPos, true)) {
final Thread ciphertextReader = new Thread(() -> {
try {
ciphertext.position(ciphertextPos);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -24,13 +25,15 @@ public class CryptoFileSystemComponentIntegrationTest {

private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemComponentIntegrationTest.class);

private CryptoFileSystemDelegate cryptoDelegate;
private FileSystem ciphertextFs;
private FileSystem cleartextFs;

@Before
public void setupFileSystems() {
cryptoDelegate = Mockito.mock(CryptoFileSystemDelegate.class);
ciphertextFs = new InMemoryFileSystem();
cleartextFs = cryptoFsComp.cryptoFileSystemFactory().get(ciphertextFs, "TopSecret");
cleartextFs = cryptoFsComp.cryptoFileSystemFactory().get(ciphertextFs, "TopSecret", cryptoDelegate);
cleartextFs.create();
}

Expand Down Expand Up @@ -77,7 +80,38 @@ public void testEncryptionAndDecryptionOfFiles() {
}
}

@Test(timeout = 2000000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
@Test
public void testForcedDecryptionOfManipulatedFile() {
// write test content to encrypted file
try (WritableFile writable = cleartextFs.file("test1.txt").openWritable()) {
writable.write(ByteBuffer.wrap("Hello World".getBytes()));
}

File physicalFile = ciphertextFs.folder("d").folders().findAny().get().folders().findAny().get().files().findAny().get();
Assert.assertTrue(physicalFile.exists());

// toggle last bit
try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
readable.read(buf);
buf.array()[buf.limit() - 1] ^= 0x01;
buf.flip();
writable.write(buf);
}

// whitelist
Mockito.when(cryptoDelegate.shouldSkipAuthentication("/test1.txt")).thenReturn(true);

// read test content from decrypted file
try (ReadableFile readable = cleartextFs.file("test1.txt").openReadable()) {
ByteBuffer buf = ByteBuffer.allocate(11);
readable.read(buf);
buf.flip();
Assert.assertArrayEquals("Hello World".getBytes(), buf.array());
}
}

@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
File file = cleartextFs.file("benchmark.test");

Expand Down
Loading

0 comments on commit 2e5264b

Please sign in to comment.