From a07efc52090fce26e099365ae0cfb570ccc04603 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 5 May 2015 17:29:51 +0200 Subject: [PATCH] Proper error handling for outdated vault formats --- .../crypto/aes256/Aes256Cryptor.java | 76 +++++++++++-------- .../aes256/AesCryptographicConfiguration.java | 18 ++--- .../crypto/aes256/FileNamingConventions.java | 2 +- .../cryptomator/crypto/aes256/KeyFile.java | 13 +++- .../crypto/aes256/Aes256CryptorTest.java | 5 +- .../crypto/AbstractCryptorDecorator.java | 3 +- .../java/org/cryptomator/crypto/Cryptor.java | 4 +- .../exceptions/UnsupportedVaultException.java | 32 ++++++++ .../controllers/ChangePasswordController.java | 9 +++ .../ui/controllers/UnlockController.java | 9 +++ .../main/resources/localization.properties | 4 + 11 files changed, 126 insertions(+), 49 deletions(-) create mode 100644 main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java 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 3b627fb59..3178e4f14 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 @@ -50,6 +50,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.crypto.io.SeekableByteChannelInputStream; import org.cryptomator.crypto.io.SeekableByteChannelOutputStream; @@ -133,6 +134,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi // save encrypted masterkey: final KeyFile keyfile = new KeyFile(); + keyfile.setVersion(KeyFile.CURRENT_VERSION); keyfile.setScryptSalt(kekSalt); keyfile.setScryptCostParam(SCRYPT_COST_PARAM); keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE); @@ -151,13 +153,19 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown. * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed. + * @throws UnsupportedVaultException If the masterkey file is too old or too modern. */ @Override - public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { + public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException { try { // load encrypted masterkey: final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class); + // check version + if (keyfile.getVersion() != KeyFile.CURRENT_VERSION) { + throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION); + } + // check, whether the key length is supported: final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM); if (keyfile.getKeyLength() > maxKeyLen) { @@ -225,17 +233,16 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi } } - private Cipher aesEcbCipher(SecretKey key, int cipherMode) { + private Cipher aesCbcCipher(SecretKey key, byte[] iv, int cipherMode) { try { - final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER); - cipher.init(cipherMode, key); + final Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER); + cipher.init(cipherMode, key, new IvParameterSpec(iv)); return cipher; } catch (InvalidKeyException ex) { throw new IllegalArgumentException("Invalid key.", ex); - } catch (NoSuchAlgorithmException | NoSuchPaddingException ex) { - throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) { + throw new AssertionError("Every implementation of the Java platform is required to support AES/CBC/PKCS5Padding, which accepts an IV", ex); } - } private Mac hmacSha256(SecretKey key) { @@ -291,7 +298,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi /** * Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.
- * 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.
+ * Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*
* 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 @@ -364,15 +371,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi return null; } + // read iv: + final byte[] iv = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(0); + headerBuf.get(iv); + // read content length: - headerBuf.position(16); final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(16); headerBuf.get(encryptedContentLengthBytes); - final Long fileSize = decryptContentLength(encryptedContentLengthBytes); + final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv); // read stored header mac: - headerBuf.position(32); final byte[] storedHeaderMac = new byte[32]; + headerBuf.position(32); headerBuf.get(storedHeaderMac); // calculate mac over first 32 bytes of header: @@ -389,9 +401,9 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi return fileSize; } - private long decryptContentLength(byte[] encryptedContentLengthBytes) { + private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) { try { - final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE); + final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE); final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes); final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize); return fileSizeBuffer.getLong(); @@ -400,11 +412,11 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi } } - private byte[] encryptContentLength(long contentLength) { + private byte[] encryptContentLength(long contentLength, byte[] iv) { try { final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES); fileSizeBuffer.putLong(contentLength); - final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE); + final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); return sizeCipher.doFinal(fileSizeBuffer.array()); } catch (IllegalBlockSizeException | BadPaddingException e) { throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e); @@ -414,7 +426,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi @Override public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException { // read header: - encryptedFile.position(0); + encryptedFile.position(0l); final ByteBuffer headerBuf = ByteBuffer.allocate(96); final int headerBytesRead = encryptedFile.read(headerBuf); if (headerBytesRead != headerBuf.capacity()) { @@ -422,23 +434,23 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi } // read header mac: - headerBuf.position(32); final byte[] storedHeaderMac = new byte[32]; + headerBuf.position(32); headerBuf.get(storedHeaderMac); // read content mac: - headerBuf.position(64); final byte[] storedContentMac = new byte[32]; + headerBuf.position(64); headerBuf.get(storedContentMac); // calculate mac over first 32 bytes of header: final Mac headerMac = this.hmacSha256(hMacMasterKey); - headerBuf.rewind(); + headerBuf.position(0); headerBuf.limit(32); headerMac.update(headerBuf); // calculate mac over content: - encryptedFile.position(96); + encryptedFile.position(96l); final Mac contentMac = this.hmacSha256(hMacMasterKey); final InputStream in = new SeekableByteChannelInputStream(encryptedFile); final InputStream macIn = new MacInputStream(in, contentMac); @@ -453,29 +465,32 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException { // read header: - encryptedFile.position(0); + encryptedFile.position(0l); final ByteBuffer headerBuf = ByteBuffer.allocate(96); final int headerBytesRead = encryptedFile.read(headerBuf); if (headerBytesRead != headerBuf.capacity()) { throw new IOException("Failed to read file header."); } - headerBuf.rewind(); // read iv: final byte[] iv = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(0); headerBuf.get(iv); // read content length: final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH]; + headerBuf.position(16); headerBuf.get(encryptedContentLengthBytes); - final Long fileSize = decryptContentLength(encryptedContentLengthBytes); + final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv); // read header mac: final byte[] headerMac = new byte[32]; + headerBuf.position(32); headerBuf.get(headerMac); // read content mac: final byte[] contentMac = new byte[32]; + headerBuf.position(64); headerBuf.get(contentMac); // decrypt content @@ -507,7 +522,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi @Override public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException { // read iv: - encryptedFile.position(0); + encryptedFile.position(0l); final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH); final int numIvBytesRead = encryptedFile.read(countingIv); @@ -540,23 +555,24 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi @Override public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { // truncate file - encryptedFile.truncate(0); + encryptedFile.truncate(0l); // use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file. - final ByteBuffer iv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); - iv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); + final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); + ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); + final byte[] iv = ivBuf.array(); // 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac) // prefilled with "zero" content length for impatient processes, which want to know the size, before file has been completely written: final ByteBuffer headerBuf = ByteBuffer.allocate(96); headerBuf.position(16); - headerBuf.put(encryptContentLength(0l)); + headerBuf.put(encryptContentLength(0l, iv)); headerBuf.flip(); headerBuf.limit(96); encryptedFile.write(headerBuf); // content encryption: - final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv.array(), Cipher.ENCRYPT_MODE); + final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE); final Mac contentMac = this.hmacSha256(hMacMasterKey); final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile); final OutputStream macOut = new MacOutputStream(out, contentMac); @@ -586,7 +602,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi // create and write header: headerBuf.clear(); headerBuf.put(iv); - headerBuf.put(encryptContentLength(plaintextSize)); + headerBuf.put(encryptContentLength(plaintextSize, iv)); headerBuf.flip(); final Mac headerMac = this.hmacSha256(hMacMasterKey); headerMac.update(headerBuf); 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 4bd81f93f..852248b9b 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 @@ -26,14 +26,14 @@ interface AesCryptographicConfiguration { int SCRYPT_BLOCK_SIZE = 8; /** - * Number of bytes of the master key. Should be the maximum possible AES key length to provide best security. + * Preferred number of bytes of the master key. */ int PREF_MASTER_KEY_LENGTH_IN_BITS = 256; /** * Number of bytes used as seed for the PRNG. */ - int PRNG_SEED_LENGTH = 32; + int PRNG_SEED_LENGTH = 16; /** * Algorithm used for random number generation. @@ -60,30 +60,22 @@ interface AesCryptographicConfiguration { String AES_KEYWRAP_CIPHER = "AESWrap"; /** - * Cipher specs for file name and file content encryption. Using CTR-mode for random access.
- * Important: As JCE doesn't support a padding, input must be a multiple of the block size. + * Cipher specs for file content encryption. Using CTR-mode for random access.
* * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher */ String AES_CTR_CIPHER = "AES/CTR/NoPadding"; /** - * Cipher specs for single block encryption (like file size). + * Cipher specs for file header encryption (fixed-length block cipher).
* * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl */ - String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding"; + String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding"; /** * AES block size is 128 bit or 16 bytes. */ int AES_BLOCK_LENGTH = 16; - /** - * 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}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC}) - * encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length. - */ - int FILE_NAME_IV_LENGTH = 5; - } 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 deb00f70a..9a6f4b771 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 @@ -22,7 +22,7 @@ interface FileNamingConventions { BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32(); /** - * Maximum length possible on file systems with a filename limit of 255 chars.
+ * Maximum length possible on file systems with a filename or even path length limit of 255 chars.
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}. */ int ENCRYPTED_FILENAME_LENGTH_LIMIT = 128; diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java index 330730d6f..dec5d477a 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/KeyFile.java @@ -4,10 +4,13 @@ import java.io.Serializable; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"}) +@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"}) public class KeyFile implements Serializable { + static final Integer CURRENT_VERSION = 1; private static final long serialVersionUID = 8578363158959619885L; + + private Integer version; private byte[] scryptSalt; private int scryptCostParam; private int scryptBlockSize; @@ -15,6 +18,14 @@ public class KeyFile implements Serializable { private byte[] primaryMasterKey; private byte[] hMacMasterKey; + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + public byte[] getScryptSalt() { return scryptSalt; } diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index 39fc568f8..a04303fb2 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -25,6 +25,7 @@ import org.cryptomator.crypto.CryptorMetadataSupport; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.junit.Assert; import org.junit.Test; @@ -32,7 +33,7 @@ import org.junit.Test; public class Aes256CryptorTest { @Test - public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException { + public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -48,7 +49,7 @@ public class Aes256CryptorTest { } @Test - public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException { + public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException { final String pw = "asd"; final Aes256Cryptor cryptor = new Aes256Cryptor(); final ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java index 56eea4313..ab44886f0 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java @@ -13,6 +13,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; public class AbstractCryptorDecorator implements Cryptor { @@ -29,7 +30,7 @@ public class AbstractCryptorDecorator implements Cryptor { } @Override - public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException { + public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException { cryptor.decryptMasterKey(in, password); } diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java index 1eecf0f21..bf7a5f8ba 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java @@ -21,6 +21,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; /** @@ -39,8 +40,9 @@ public interface Cryptor extends Destroyable { * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). * @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown. * @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed. + * @throws UnsupportedVaultException If the masterkey file is too old or too modern. */ - void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException; + void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException; /** * Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories. diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java new file mode 100644 index 000000000..b2a69ac54 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/UnsupportedVaultException.java @@ -0,0 +1,32 @@ +package org.cryptomator.crypto.exceptions; + +public class UnsupportedVaultException extends Exception { + + private static final long serialVersionUID = -5147549533387945622L; + + private final Integer detectedVersion; + private final Integer supportedVersion; + + public UnsupportedVaultException(Integer detectedVersion, Integer supportedVersion) { + super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion); + this.detectedVersion = detectedVersion; + this.supportedVersion = supportedVersion; + } + + public Integer getDetectedVersion() { + return detectedVersion; + } + + public Integer getSupportedVersion() { + return supportedVersion; + } + + public boolean isVaultOlderThanSoftware() { + return detectedVersion == null || detectedVersion < supportedVersion; + } + + public boolean isSoftwareOlderThanVault() { + return detectedVersion > supportedVersion; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 91fa9df66..7215b39a8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -20,6 +20,7 @@ import javafx.scene.control.Label; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; @@ -108,6 +109,14 @@ public class ChangePasswordController implements Initializable { newPasswordField.swipe(); retypePasswordField.swipe(); return; + } catch (UnsupportedVaultException e) { + if (e.isVaultOlderThanSoftware()) { + messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware")); + } else if (e.isSoftwareOlderThanVault()) { + messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault")); + } + newPasswordField.swipe(); + retypePasswordField.swipe(); } finally { oldPasswordField.swipe(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 877d18a39..6aab80736 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -35,6 +35,7 @@ import javax.security.auth.DestroyFailedException; import org.apache.commons.lang3.CharUtils; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; @@ -131,6 +132,14 @@ public class UnlockController implements Initializable { progressIndicator.setVisible(false); messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE")); LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); + } catch (UnsupportedVaultException e) { + setControlsDisabled(false); + progressIndicator.setVisible(false); + if (e.isVaultOlderThanSoftware()) { + messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware")); + } else if (e.isSoftwareOlderThanVault()) { + messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault")); + } } catch (DestroyFailedException e) { setControlsDisabled(false); progressIndicator.setVisible(false); diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties index 11b6c5abd..03fc09d2a 100644 --- a/main/ui/src/main/resources/localization.properties +++ b/main/ui/src/main/resources/localization.properties @@ -32,6 +32,8 @@ unlock.button.unlock=Unlock vault unlock.errorMessage.wrongPassword=Wrong password. unlock.errorMessage.decryptionFailed=Decryption failed. unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy. +unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator. +unlock.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator. unlock.messageLabel.startServerFailed=Starting WebDAV server failed. # change_password.fxml @@ -42,6 +44,8 @@ changePassword.button.unlock=Change password changePassword.errorMessage.wrongPassword=Wrong password. changePassword.errorMessage.decryptionFailed=Decryption failed. changePassword.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy. +changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator. +changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator. changePassword.infoMessage.success=Password changed. # unlocked.fxml