- Changed file layout, added MAC (see #17)

- Obfuscates file size (fixes #18)
This commit is contained in:
Sebastian Stenzel
2015-01-06 01:23:16 +01:00
parent 55e758315d
commit e19cf1c942
10 changed files with 260 additions and 66 deletions

View File

@@ -29,6 +29,7 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
@@ -78,9 +79,8 @@ public class EncryptedFile extends AbstractEncryptedNode {
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}

View File

@@ -21,7 +21,7 @@ import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -128,9 +128,8 @@ public class EncryptedFilePart extends EncryptedFile {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}

View File

@@ -86,8 +86,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
*/
private SecretKey hMacMasterKey;
private static final int SIZE_OF_LONG = Long.BYTES;
static {
try {
SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
@@ -240,6 +238,31 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private Cipher aesEcbCipher(SecretKey key, int cipherMode) {
try {
final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
cipher.init(cipherMode, key);
return cipher;
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex);
}
}
private Mac hmacSha256(SecretKey key) {
try {
final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
mac.init(key);
return mac;
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
private byte[] randomData(int length) {
final byte[] result = new byte[length];
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
@@ -269,18 +292,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
return crc32.getValue();
}
private byte[] hmacSha256(byte[] data) {
try {
final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
mac.init(hMacMasterKey);
return mac.doFinal(data);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("Invalid key", e);
}
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
@@ -312,7 +323,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final byte[] mac = hmacSha256(cleartext.getBytes());
final byte[] mac = hmacSha256(hMacMasterKey).doFinal(cleartext.getBytes());
final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(partialIv);
@@ -390,58 +401,83 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
throw new UnsupportedOperationException("Not yet implemented.");
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
final ByteBuffer sizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
final int read = encryptedFile.read(sizeBuffer);
if (read == SIZE_OF_LONG) {
return sizeBuffer.getLong(0);
} else {
// skip 128bit IV + 256 bit MAC:
encryptedFile.position(48);
// read encrypted value:
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);
// return "unknown" value, if EOF
if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
return null;
}
// decrypt size:
try {
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
return fileSizeBuffer.getLong();
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
// read iv:
encryptedFile.position(0);
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.");
final int numIvBytesRead = encryptedFile.read(countingIv);
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
throw new IOException("Failed to read file header.");
}
// go to begin of content:
encryptedFile.position(64);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile);
return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
// skip content size:
encryptedFile.position(SIZE_OF_LONG);
// read iv:
encryptedFile.position(0);
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.");
final int numIvBytesRead = encryptedFile.read(countingIv);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH) {
throw new IOException("Failed to read file header.");
}
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);
// fast forward stream:
encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
encryptedFile.position(64 + beginOfFirstRelevantBlock);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
@@ -459,32 +495,58 @@ 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.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
countingIv.position(0);
encryptedFile.write(countingIv);
// generate cipher:
// init crypto stuff:
final Mac mac = this.hmacSha256(hMacMasterKey);
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
// 8 bytes (file size: temporarily -1):
final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
fileSize.putLong(-1L);
fileSize.position(0);
encryptedFile.write(fileSize);
// init mac buffer and skip 32 bytes
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
encryptedFile.write(macBuffer);
// 16 bytes (iv):
encryptedFile.write(countingIv);
// init filesize buffer and skip 16 bytes
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
encryptedFile.write(encryptedFileSizeBuffer);
// write content:
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream cipheredOut = new CipherOutputStream(out, cipher);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
// write filesize
fileSize.position(0);
fileSize.putLong(actualSize);
fileSize.position(0);
encryptedFile.position(0);
encryptedFile.write(fileSize);
// append fake content:
final int randomContentLength = (int) Math.ceil(Math.random() * actualSize / 10.0);
final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
for (int i = 0; i < randomContentLength; i += AES_BLOCK_LENGTH) {
cipheredOut.write(emptyBytes);
}
cipheredOut.flush();
// copy MAC:
macBuffer.position(0);
macBuffer.put(mac.doFinal());
// encrypt actualSize
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(actualSize);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer.position(0);
encryptedFileSizeBuffer.put(encryptedFileSize);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
// write file header
encryptedFile.position(16); // skip already written 128 bit IV
macBuffer.position(0);
encryptedFile.write(macBuffer); // 256 bit MAC
encryptedFileSizeBuffer.position(0);
encryptedFile.write(encryptedFileSizeBuffer); // 128 bit encrypted file size
return actualSize;
}

View File

@@ -66,6 +66,13 @@ interface AesCryptographicConfiguration {
*/
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
/**
* Cipher specs for single block encryption (like file size).
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
*/
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
/**
* AES block size is 128 bit or 16 bytes.
*/

View File

@@ -0,0 +1,39 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
*/
class MacInputStream extends FilterInputStream {
private final Mac mac;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public MacInputStream(InputStream in, Mac mac) {
super(in);
this.mac = mac;
}
@Override
public int read() throws IOException {
int b = in.read();
mac.update((byte) b);
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
mac.update(b);
return read;
}
}

View File

@@ -0,0 +1,37 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes written to this stream.
*/
class MacOutputStream extends FilterOutputStream {
private final Mac mac;
/**
* @param out Stream to redirect contents to after updating the mac.
* @param mac Mac to be updated during writes.
*/
public MacOutputStream(OutputStream out, Mac mac) {
super(out);
this.mac = mac;
}
@Override
public void write(int b) throws IOException {
mac.update((byte) b);
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
mac.update(b, off, len);
out.write(b, off, len);
}
}

View File

@@ -25,6 +25,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
public class Aes256CryptorTest {
@@ -72,6 +73,31 @@ public class Aes256CryptorTest {
}
}
@Ignore
@Test
public void testIntegrityAuthentication() throws IOException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// authenticate unmodified content:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean unmodifiedContent = cryptor.authenticateContent(encryptedIn);
Assert.assertTrue(unmodifiedContent);
}
@Test
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
@@ -82,19 +108,25 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
// decrypt:
encryptedData.position(0);
// decrypt file size:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final Long filesize = cryptor.decryptedContentLength(encryptedIn);
Assert.assertEquals(plaintextData.length, filesize.longValue());
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
@@ -115,12 +147,14 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();

View File

@@ -68,6 +68,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
/**
* @return <code>true</code> If the integrity of the file can be assured.
*/
boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Content length of the decrypted file or <code>null</code> if unknown.
@@ -76,15 +81,17 @@ public interface Cryptor extends SensitiveDataSwipeListener {
/**
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
/**
* @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.
* @throws DecryptFailedException If decryption failed
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException;
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.

View File

@@ -76,19 +76,24 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.authenticateContent(encryptedFile);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
}

View File

@@ -6,4 +6,8 @@ public class DecryptFailedException extends StorageCryptingException {
public DecryptFailedException(Throwable t) {
super("Decryption failed.", t);
}
protected DecryptFailedException(String msg) {
super(msg);
}
}