Browse Source

added possibility to add associated data to filename encryption (references #128, #119)

Sebastian Stenzel 9 years ago
parent
commit
b2d425e11f

+ 4 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java

@@ -23,13 +23,15 @@ public interface FilenameCryptor {
 
 	/**
 	 * @param cleartextName original filename including cleartext file extension
+	 * @param associatedData optional associated data, that will not get encrypted but needs to be provided during decryption
 	 * @return encrypted filename without any file extension
 	 */
-	String encryptFilename(String cleartextName);
+	String encryptFilename(String cleartextName, byte[]... associatedData);
 
 	/**
 	 * @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first.
+	 * @param associatedData the same associated data used during encryption, otherwise and {@link AuthenticationFailedException} will be thrown
 	 * @return cleartext filename, probably including its cleartext file extension.
 	 */
-	String decryptFilename(String ciphertextName) throws AuthenticationFailedException;
+	String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException;
 }

+ 9 - 8
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java

@@ -8,7 +8,8 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine.impl;
 
-import java.nio.charset.StandardCharsets;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -37,25 +38,25 @@ class FilenameCryptorImpl implements FilenameCryptor {
 
 	@Override
 	public String hashDirectoryId(String cleartextDirectoryId) {
-		final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
+		final byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
 		byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
 		final byte[] hashedBytes = SHA1.get().digest(encryptedBytes);
 		return BASE32.encodeAsString(hashedBytes);
 	}
 
 	@Override
-	public String encryptFilename(String cleartextName) {
-		final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
-		final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes);
+	public String encryptFilename(String cleartextName, byte[]... associatedData) {
+		final byte[] cleartextBytes = cleartextName.getBytes(UTF_8);
+		final byte[] encryptedBytes = AES_SIV.encrypt(encryptionKey, macKey, cleartextBytes, associatedData);
 		return BASE32.encodeAsString(encryptedBytes);
 	}
 
 	@Override
-	public String decryptFilename(String ciphertextName) throws AuthenticationFailedException {
+	public String decryptFilename(String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException {
 		final byte[] encryptedBytes = BASE32.decode(ciphertextName);
 		try {
-			final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes);
-			return new String(cleartextBytes, StandardCharsets.UTF_8);
+			final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes, associatedData);
+			return new String(cleartextBytes, UTF_8);
 		} catch (AEADBadTagException e) {
 			throw new AuthenticationFailedException("Authentication failed.", e);
 		}

+ 3 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Scrypt.java

@@ -8,9 +8,10 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine.impl;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
-import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 
 import org.bouncycastle.crypto.generators.SCrypt;
@@ -34,7 +35,7 @@ final class Scrypt {
 	 */
 	public static byte[] scrypt(CharSequence passphrase, byte[] salt, int costParam, int blockSize, int keyLengthInBytes) {
 		// This is an attempt to get the password bytes without copies of the password being created in some dark places inside the JVM:
-		final ByteBuffer buf = StandardCharsets.UTF_8.encode(CharBuffer.wrap(passphrase));
+		final ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
 		final byte[] pw = new byte[buf.remaining()];
 		buf.get(pw);
 		try {

+ 3 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java

@@ -8,12 +8,13 @@
  *******************************************************************************/
 package org.cryptomator.filesystem.crypto;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
 import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
-import java.nio.charset.StandardCharsets;
 import java.time.Instant;
 import java.util.Optional;
 import java.util.UUID;
@@ -52,7 +53,7 @@ class CryptoFolder extends CryptoNode implements Folder {
 		if (directoryId.get() == null) {
 			File dirFile = physicalFile();
 			if (dirFile.exists()) {
-				try (Reader reader = Channels.newReader(dirFile.openReadable(), StandardCharsets.UTF_8.newDecoder(), -1)) {
+				try (Reader reader = Channels.newReader(dirFile.openReadable(), UTF_8.newDecoder(), -1)) {
 					directoryId.set(IOUtils.toString(reader));
 				} catch (IOException e) {
 					throw new UncheckedIOException(e);

+ 5 - 4
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFilenameCryptor.java

@@ -8,7 +8,8 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine;
 
-import java.nio.charset.StandardCharsets;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -22,18 +23,18 @@ class NoFilenameCryptor implements FilenameCryptor {
 
 	@Override
 	public String hashDirectoryId(String cleartextDirectoryId) {
-		final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
+		final byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8);
 		final byte[] hashedBytes = SHA1.get().digest(cleartextBytes);
 		return BASE32.encodeAsString(hashedBytes);
 	}
 
 	@Override
-	public String encryptFilename(String cleartextName) {
+	public String encryptFilename(String cleartextName, byte[]... associatedData) {
 		return cleartextName;
 	}
 
 	@Override
-	public String decryptFilename(String ciphertextName) {
+	public String decryptFilename(String ciphertextName, byte[]... associatedData) {
 		return ciphertextName;
 	}
 

+ 27 - 3
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImplTest.java

@@ -8,8 +8,9 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine.impl;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.util.UUID;
 
 import javax.crypto.SecretKey;
@@ -71,9 +72,32 @@ public class FilenameCryptorImplTest {
 		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
 		final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
 
-		final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(StandardCharsets.UTF_8);
+		final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8);
 		encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte
-		filenameCryptor.decryptFilename(new String(encrypted, StandardCharsets.UTF_8));
+		filenameCryptor.decryptFilename(new String(encrypted, UTF_8));
+	}
+
+	@Test
+	public void testDeterministicEncryptionOfFilenamesWithAssociatedData() {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+		final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
+
+		final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8));
+		final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8));
+		Assert.assertEquals("test", decrypted);
+	}
+
+	@Test(expected = AuthenticationFailedException.class)
+	public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+		final FilenameCryptor filenameCryptor = new FilenameCryptorImpl(encryptionKey, macKey);
+
+		final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8));
+		filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8));
 	}
 
 }

+ 4 - 3
main/filesystem-nameshortening/src/main/java/org/cryptomator/filesystem/shortening/FilenameShortener.java

@@ -1,12 +1,13 @@
 package org.cryptomator.filesystem.shortening;
 
+import static java.nio.charset.StandardCharsets.UTF_8;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.UncheckedIOException;
 import java.io.Writer;
 import java.nio.channels.Channels;
-import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 
@@ -55,7 +56,7 @@ class FilenameShortener {
 		final File mappingFile = mappingFile(shortName);
 		if (!mappingFile.exists()) {
 			mappingFile.parent().get().create();
-			try (Writer writer = Channels.newWriter(mappingFile.openWritable(), StandardCharsets.UTF_8.newEncoder(), -1)) {
+			try (Writer writer = Channels.newWriter(mappingFile.openWritable(), UTF_8.newEncoder(), -1)) {
 				writer.write(longName);
 			} catch (IOException e) {
 				throw new UncheckedIOException(e);
@@ -73,7 +74,7 @@ class FilenameShortener {
 		if (!mappingFile.exists()) {
 			throw new UncheckedIOException(new FileNotFoundException("Mapping file not found " + mappingFile));
 		} else {
-			try (Reader reader = Channels.newReader(mappingFile.openReadable(), StandardCharsets.UTF_8.newDecoder(), -1)) {
+			try (Reader reader = Channels.newReader(mappingFile.openReadable(), UTF_8.newDecoder(), -1)) {
 				return IOUtils.toString(reader);
 			} catch (IOException e) {
 				throw new UncheckedIOException(e);