From d8c9279f6f9c29c650c805c3f141daad584bd248 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 30 Dec 2014 17:38:57 +0100 Subject: [PATCH] - fixes #7 - removes any use of CBC mode (might affect issue #9) --- .../crypto/aes256/Aes256Cryptor.java | 51 +++++++++++-------- .../aes256/AesCryptographicConfiguration.java | 14 +++-- .../crypto/aes256/FileNamingConventions.java | 5 ++ 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index 372017e4a..d63f63309 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -74,8 +74,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo private static final SecretKeyFactory PBKDF2_FACTORY; /** - * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE isn't installed. JCE can be - * installed from here: http://www.oracle.com/technetwork/java/javase/downloads/. + * Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction + * Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/. */ private static final int AES_KEY_LENGTH; @@ -227,11 +227,11 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo return result; } - private SecretKey pbkdf2(byte[] password, byte[] salt, int iterations, int keyLength) { - final char[] pw = new char[password.length]; + private SecretKey deriveSecretKeyFromMasterKey() { + final char[] pw = new char[masterKey.length]; try { - byteToChar(password, pw); - return pbkdf2(CharBuffer.wrap(pw), salt, iterations, keyLength); + byteToChar(masterKey, pw); + return pbkdf2(CharBuffer.wrap(pw), EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); } finally { Arrays.fill(pw, (char) 0); } @@ -271,7 +271,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo @Override public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) { try { - final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final SecretKey key = this.deriveSecretKeyFromMasterKey(); final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep); final List encryptedPathComps = new ArrayList<>(cleartextPathComps.length); for (final String cleartext : cleartextPathComps) { @@ -300,27 +300,30 @@ 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 Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.ENCRYPT_MODE); + final byte[] ivRandomPart = randomData(FILE_NAME_IV_LENGTH); + final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH); + iv.put(ivRandomPart); + final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, iv.array(), 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; + final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(ivRandomPart) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes); - if (encrypted.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) { - final String crc32 = Long.toHexString(crc32Sum(encrypted.getBytes())); + if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) { + final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.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; + final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT; this.storeMetadata(ioSupport, metadataFilename, metadata); return alternativeFileName; } else { - return encrypted; + return ivAndCiphertext + BASIC_FILE_EXT; } } @Override public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) { try { - final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final SecretKey key = this.deriveSecretKeyFromMasterKey(); final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep); final List cleartextPathComps = new ArrayList<>(encryptedPathComps.length); for (final String encrypted : encryptedPathComps) { @@ -337,21 +340,27 @@ 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; + final String ivAndCiphertext; if (encrypted.endsWith(LONG_NAME_FILE_EXT)) { 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)); + ivAndCiphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid)); } else if (encrypted.endsWith(BASIC_FILE_EXT)) { - ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT); + ivAndCiphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT); } else { throw new IllegalArgumentException("Unsupported path component: " + encrypted); } - final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, EMPTY_IV, Cipher.DECRYPT_MODE); + final String ivRandomPartStr = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR); + final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR); + final byte[] ivRandomPart = ENCRYPTED_FILENAME_CODEC.decode(ivRandomPartStr); + final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH); + iv.put(ivRandomPart); + + final Cipher cipher = this.cipher(FILE_NAME_CIPHER, key, iv.array(), Cipher.DECRYPT_MODE); final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext); final byte[] cleartextBytes = cipher.doFinal(encryptedBytes); return new String(cleartextBytes, Charsets.UTF_8); @@ -394,7 +403,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } // derive secret key and generate cipher: - final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final SecretKey key = this.deriveSecretKeyFromMasterKey(); final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE); // read content @@ -425,7 +434,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock); // derive secret key and generate cipher: - final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final SecretKey key = this.deriveSecretKeyFromMasterKey(); final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.DECRYPT_MODE); // read content @@ -445,7 +454,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo countingIv.position(0); // derive secret key and generate cipher: - final SecretKey key = this.pbkdf2(masterKey, EMPTY_SALT, PBKDF2_MASTERKEY_ITERATIONS, AES_KEY_LENGTH); + final SecretKey key = this.deriveSecretKeyFromMasterKey(); final Cipher cipher = this.cipher(FILE_CONTENT_CIPHER, key, countingIv.array(), Cipher.ENCRYPT_MODE); // 8 bytes (file size: temporarily -1): diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java index dea1992c4..9f7aa1cfd 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java @@ -31,7 +31,10 @@ interface AesCryptographicConfiguration { byte[] EMPTY_SALT = new byte[SALT_LENGTH]; /** - * Algorithm used for key derivation. + * Algorithm used for key derivation as defined in RFC 2898 / PKCS #5. + * + * SHA1 will deprecate soon, but the main purpose of PBKDF2 is waisting CPU cycles, so cryptographically strong hash algorithms are not + * necessary here. See also http://crypto.stackexchange.com/a/11017 */ String KEY_FACTORY_ALGORITHM = "PBKDF2WithHmacSHA1"; @@ -52,14 +55,14 @@ interface AesCryptographicConfiguration { * * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher */ - String MASTERKEY_CIPHER = "AES/CBC/PKCS5Padding"; + String MASTERKEY_CIPHER = "AES/CTR/NoPadding"; /** * Cipher specs for file name encryption. * * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher */ - String FILE_NAME_CIPHER = "AES/CBC/PKCS5Padding"; + String FILE_NAME_CIPHER = "AES/CTR/NoPadding"; /** * Cipher specs for content encryption. Using CTR-mode for random access. @@ -74,9 +77,10 @@ interface AesCryptographicConfiguration { int AES_BLOCK_LENGTH = 16; /** - * 0-filled initialization vector. + * Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy. + * Maximum length is {@value #AES_BLOCK_LENGTH}. */ - byte[] EMPTY_IV = new byte[AES_BLOCK_LENGTH]; + int FILE_NAME_IV_LENGTH = 4; /** * Number of iterations for key derived from user pw. High iteration count for better resistance to bruteforcing. diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java index fd2866b06..6d3b5bef6 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java @@ -37,6 +37,11 @@ interface FileNamingConventions { */ String BASIC_FILE_EXT = ".aes"; + /** + * Prefix in front of the actual encrypted file name used as IV. + */ + String IV_PREFIX_SEPARATOR = "_"; + /** * For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */