mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-27 05:50:26 +00:00
bugfix: correct decryption of looooooong filenames (>255 chars)
This commit is contained in:
parent
ebb3207854
commit
884b894e04
@ -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 {
|
||||
|
@ -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);
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,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>
|
||||
|
@ -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 @@
|
||||
<suite name="Default suite">
|
||||
<test verbose="2" name="Default test">
|
||||
<classes>
|
||||
<class name="org.cryptomator.crypto.aes256.Aes256CryptorTest">
|
||||
<methods>
|
||||
<include name="testEncryptionOfLongFilenames"/>
|
||||
</methods>
|
||||
</class> <!-- org.cryptomator.crypto.aes256.Aes256CryptorTest -->
|
||||
<class name="org.cryptomator.crypto.aes256.Aes256CryptorTest"/>
|
||||
</classes>
|
||||
</test> <!-- Default test -->
|
||||
</suite> <!-- Default suite -->
|
||||
|
@ -1 +1 @@
|
||||
<html><head><title>testng.xml for Default suite</title></head><body><tt><?xml version="1.0" encoding="UTF-8"?><br/><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><br/><suite name="Default suite"><br/> <test verbose="2" name="Default test"><br/> <classes><br/> <class name="org.cryptomator.crypto.aes256.Aes256CryptorTest"><br/> <methods><br/> <include name="testEncryptionOfLongFilenames"/><br/> </methods><br/> </class> <!-- org.cryptomator.crypto.aes256.Aes256CryptorTest --><br/> </classes><br/> </test> <!-- Default test --><br/></suite> <!-- Default suite --><br/></tt></body></html>
|
||||
<html><head><title>testng.xml for Default suite</title></head><body><tt><?xml version="1.0" encoding="UTF-8"?><br/><!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"><br/><suite name="Default suite"><br/> <test verbose="2" name="Default test"><br/> <classes><br/> <class name="org.cryptomator.crypto.aes256.Aes256CryptorTest"/><br/> </classes><br/> </test> <!-- Default test --><br/></suite> <!-- Default suite --><br/></tt></body></html>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user