bugfix: correct decryption of looooooong filenames (>255 chars)

This commit is contained in:
Sebastian Stenzel 2014-12-08 22:25:45 +01:00
parent ebb3207854
commit 884b894e04
10 changed files with 99 additions and 32 deletions

View File

@ -118,6 +118,9 @@ public class WebDavLocatorFactory extends AbstractLocatorFactory implements Sens
@Override
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
final Path metaDataFile = fsRoot.resolve(encryptedPath);
if (!Files.isReadable(metaDataFile)) {
return null;
}
final long metaDataFileSize = Files.size(metaDataFile);
final SeekableByteChannel channel = Files.newByteChannel(metaDataFile, StandardOpenOption.READ);
try {

View File

@ -27,6 +27,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@ -51,6 +52,7 @@ import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
@ -256,6 +258,12 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private long crc32Sum(byte[] source) {
final CRC32 crc32 = new CRC32();
crc32.update(source);
return crc32.getValue();
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
@ -272,17 +280,33 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
/**
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
* Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file
* systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in
* {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
* cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
* with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
* alternative names are stored.<br/>
* <br/>
* These alternative names consist of the checksum, a unique id and a special file extension defined in
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE);
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
final String encrypted = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes) + BASIC_FILE_EXT;
if (cleartext.length() > PLAINTEXT_FILENAME_LENGTH_LIMIT) {
final LongFilenameMetadata metadata = new LongFilenameMetadata();
metadata.setEncryptedFilename(encrypted);
final String alternativeFileName = UUID.randomUUID().toString() + LONG_NAME_FILE_EXT;
ioSupport.writePathSpecificMetadata(alternativeFileName + METADATA_FILE_EXT, objectMapper.writeValueAsBytes(metadata));
if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = String.valueOf(crc32Sum(encrypted.getBytes()));
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(encrypted).toString() + LONG_NAME_FILE_EXT;
this.storeMetadata(ioSupport, metadataFilename, metadata);
return alternativeFileName;
} else {
return encrypted;
@ -305,11 +329,18 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
/**
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
*/
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final String ciphertext;
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
final LongFilenameMetadata metadata = objectMapper.readValue(ioSupport.readPathSpecificMetadata(encrypted + METADATA_FILE_EXT), LongFilenameMetadata.class);
ciphertext = metadata.getEncryptedFilename();
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
} else {
@ -322,6 +353,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
return new String(cleartextBytes, Charsets.UTF_8);
}
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
if (fileContent == null) {
return new LongFilenameMetadata();
} else {
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
}
}
private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
final ByteBuffer sizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);

View File

@ -28,25 +28,28 @@ interface FileNamingConventions {
/**
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
* 144 and 160 are multiples of 16 (128bit aes block size).<br/>
* 144 * 8/5 (base32) = 230,..<br/>
* 160 * 8/5 = 256<br/>
* Base 64 isn't supported on case-insensitive file systems.<br/>
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int PLAINTEXT_FILENAME_LENGTH_LIMIT = 144;
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
/**
* For plaintext file names <= {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String BASIC_FILE_EXT = ".aes";
/**
* For plaintext file names > {@value #PLAINTEXT_FILENAME_LENGTH_LIMIT} chars.
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_NAME_FILE_EXT = ".lng.aes";
/**
* For file-related metadata.
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
String LONG_NAME_PREFIX_SEPARATOR = "_";
/**
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
* kind of uniform distribution for better load balancing.
*/
String METADATA_FILE_EXT = ".meta";

View File

@ -9,20 +9,41 @@
package org.cryptomator.crypto.aes256;
import java.io.Serializable;
import java.util.UUID;
import org.apache.commons.collections4.BidiMap;
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
class LongFilenameMetadata implements Serializable {
private static final long serialVersionUID = 6214509403824421320L;
private String encryptedFilename;
@JsonDeserialize(as = DualHashBidiMap.class)
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
/* Getter/Setter */
public String getEncryptedFilename() {
return encryptedFilename;
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
return encryptedFilenames.get(uuid);
}
public void setEncryptedFilename(String encryptedFilename) {
this.encryptedFilename = encryptedFilename;
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
if (uuid == null) {
uuid = UUID.randomUUID();
encryptedFilenames.put(uuid, encryptedFilename);
}
return uuid;
}
public BidiMap<UUID, String> getEncryptedFilenames() {
return encryptedFilenames;
}
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
this.encryptedFilenames = encryptedFilenames;
}
}

View File

@ -57,7 +57,7 @@ function toggleAllBoxes() {
<tr>
<td>Tests passed/Failed/Skipped:</td><td>0/0/0</td>
</tr><tr>
<td>Started on:</td><td>Sat Dec 06 14:26:26 CET 2014</td>
<td>Started on:</td><td>Mon Dec 08 22:07:11 CET 2014</td>
</tr>
<tr><td>Total time:</td><td>0 seconds (5 ms)</td>
</tr><tr>

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by org.testng.reporters.JUnitXMLReporter -->
<testsuite hostname="Sebastians-iMac.local" tests="0" failures="0" timestamp="6 Dec 2014 13:26:27 GMT" time="0.005" errors="0">
<testsuite hostname="Sebastians-iMac.local" tests="0" failures="0" timestamp="8 Dec 2014 21:07:12 GMT" time="0.005" errors="0">
</testsuite>

View File

@ -105,7 +105,7 @@
</div> <!-- panel Default_suite -->
<div panel-name="test-xml-Default_suite" class="panel">
<div class="main-panel-header rounded-window-top">
<span class="header-content">/private/var/folders/t_/sydpw2q97yj_fh3p7jp6jx8w0000gn/T/testng-eclipse-1690973351/testng-customsuite.xml</span>
<span class="header-content">/private/var/folders/t_/sydpw2q97yj_fh3p7jp6jx8w0000gn/T/testng-eclipse--34592626/testng-customsuite.xml</span>
</div> <!-- main-panel-header rounded-window-top -->
<div class="main-panel-content rounded-window-bottom">
<pre>
@ -114,11 +114,7 @@
&lt;suite name=&quot;Default suite&quot;&gt;
&lt;test verbose=&quot;2&quot; name=&quot;Default test&quot;&gt;
&lt;classes&gt;
&lt;class name=&quot;org.cryptomator.crypto.aes256.Aes256CryptorTest&quot;&gt;
&lt;methods&gt;
&lt;include name=&quot;testEncryptionOfLongFilenames&quot;/&gt;
&lt;/methods&gt;
&lt;/class&gt; &lt;!-- org.cryptomator.crypto.aes256.Aes256CryptorTest --&gt;
&lt;class name=&quot;org.cryptomator.crypto.aes256.Aes256CryptorTest&quot;/&gt;
&lt;/classes&gt;
&lt;/test&gt; &lt;!-- Default test --&gt;
&lt;/suite&gt; &lt;!-- Default suite --&gt;

View File

@ -1 +1 @@
<html><head><title>testng.xml for Default suite</title></head><body><tt>&lt;?xml&nbsp;version="1.0"&nbsp;encoding="UTF-8"?&gt;<br/>&lt;!DOCTYPE&nbsp;suite&nbsp;SYSTEM&nbsp;"http://testng.org/testng-1.0.dtd"&gt;<br/>&lt;suite&nbsp;name="Default&nbsp;suite"&gt;<br/>&nbsp;&nbsp;&lt;test&nbsp;verbose="2"&nbsp;name="Default&nbsp;test"&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;classes&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;class&nbsp;name="org.cryptomator.crypto.aes256.Aes256CryptorTest"&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;methods&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;include&nbsp;name="testEncryptionOfLongFilenames"/&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/methods&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;/class&gt;&nbsp;&lt;!--&nbsp;org.cryptomator.crypto.aes256.Aes256CryptorTest&nbsp;--&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;/classes&gt;<br/>&nbsp;&nbsp;&lt;/test&gt;&nbsp;&lt;!--&nbsp;Default&nbsp;test&nbsp;--&gt;<br/>&lt;/suite&gt;&nbsp;&lt;!--&nbsp;Default&nbsp;suite&nbsp;--&gt;<br/></tt></body></html>
<html><head><title>testng.xml for Default suite</title></head><body><tt>&lt;?xml&nbsp;version="1.0"&nbsp;encoding="UTF-8"?&gt;<br/>&lt;!DOCTYPE&nbsp;suite&nbsp;SYSTEM&nbsp;"http://testng.org/testng-1.0.dtd"&gt;<br/>&lt;suite&nbsp;name="Default&nbsp;suite"&gt;<br/>&nbsp;&nbsp;&lt;test&nbsp;verbose="2"&nbsp;name="Default&nbsp;test"&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;classes&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&lt;class&nbsp;name="org.cryptomator.crypto.aes256.Aes256CryptorTest"/&gt;<br/>&nbsp;&nbsp;&nbsp;&nbsp;&lt;/classes&gt;<br/>&nbsp;&nbsp;&lt;/test&gt;&nbsp;&lt;!--&nbsp;Default&nbsp;test&nbsp;--&gt;<br/>&lt;/suite&gt;&nbsp;&lt;!--&nbsp;Default&nbsp;suite&nbsp;--&gt;<br/></tt></body></html>

View File

@ -2,10 +2,10 @@
<testng-results skipped="0" failed="0" total="0" passed="0">
<reporter-output>
</reporter-output>
<suite name="Default suite" duration-ms="5" started-at="2014-12-06T13:26:26Z" finished-at="2014-12-06T13:26:26Z">
<suite name="Default suite" duration-ms="5" started-at="2014-12-08T21:07:11Z" finished-at="2014-12-08T21:07:11Z">
<groups>
</groups>
<test name="Default test" duration-ms="5" started-at="2014-12-06T13:26:26Z" finished-at="2014-12-06T13:26:26Z">
<test name="Default test" duration-ms="5" started-at="2014-12-08T21:07:11Z" finished-at="2014-12-08T21:07:11Z">
</test> <!-- Default test -->
</suite> <!-- Default suite -->
</testng-results>

View File

@ -16,7 +16,7 @@ public interface CryptorIOSupport {
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
/**
* @return Previously written encryptedMetadata stored at the given encryptedPath.
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
*/
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;