浏览代码

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

Sebastian Stenzel 10 年之前
父节点
当前提交
884b894e04

+ 3 - 0
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavLocatorFactory.java

@@ -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 {

+ 51 - 7
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java

@@ -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);

+ 11 - 8
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java

@@ -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";
 

+ 26 - 5
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LongFilenameMetadata.java

@@ -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 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 setEncryptedFilename(String encryptedFilename) {
-		this.encryptedFilename = encryptedFilename;
+	public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
+		this.encryptedFilenames = encryptedFilenames;
 	}
 
 }

+ 1 - 1
main/crypto-aes/test-output/Default suite/Default test.html

@@ -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>

+ 1 - 1
main/crypto-aes/test-output/Default suite/Default test.xml

@@ -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>

+ 2 - 6
main/crypto-aes/test-output/index.html

@@ -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;

文件差异内容过多而无法显示
+ 1 - 1
main/crypto-aes/test-output/old/Default suite/testng.xml.html


+ 2 - 2
main/crypto-aes/test-output/testng-results.xml

@@ -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>

+ 1 - 1
main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java

@@ -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;