diff --git a/main/filesystem-charsets/pom.xml b/main/filesystem-charsets/pom.xml
index 49060e25f..e156412e5 100644
--- a/main/filesystem-charsets/pom.xml
+++ b/main/filesystem-charsets/pom.xml
@@ -15,7 +15,7 @@
1.2.0-SNAPSHOT
filesystem-charsets
- Cryptomator filesystem: Filename charset compatibility layer
+ Cryptomator filesystem: Charset compatibility layer
diff --git a/main/filesystem-crypto/pom.xml b/main/filesystem-crypto/pom.xml
index 1897ef0c9..403777a29 100644
--- a/main/filesystem-crypto/pom.xml
+++ b/main/filesystem-crypto/pom.xml
@@ -19,7 +19,7 @@
1.51
- 1.0.2
+ 1.0.4
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/UnsupportedVaultFormatException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/UnsupportedVaultFormatException.java
index b3d06b9f2..1f087b080 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/UnsupportedVaultFormatException.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/UnsupportedVaultFormatException.java
@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
public class UnsupportedVaultFormatException extends CryptoException {
private final Integer detectedVersion;
- private final Integer supportedVersion;
+ private final Integer latestSupportedVersion;
- public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
- super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
+ public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
+ super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
this.detectedVersion = detectedVersion;
- this.supportedVersion = supportedVersion;
+ this.latestSupportedVersion = latestSupportedVersion;
}
public Integer getDetectedVersion() {
return detectedVersion;
}
- public Integer getSupportedVersion() {
- return supportedVersion;
+ public Integer getLatestSupportedVersion() {
+ return latestSupportedVersion;
}
public boolean isVaultOlderThanSoftware() {
- return detectedVersion == null || detectedVersion < supportedVersion;
+ return detectedVersion == null || detectedVersion < latestSupportedVersion;
}
public boolean isSoftwareOlderThanVault() {
- return detectedVersion > supportedVersion;
+ return detectedVersion > latestSupportedVersion;
}
}
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java
index dc0f0c9d4..efbe84bfc 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java
@@ -1,11 +1,16 @@
package org.cryptomator.crypto.engine.impl;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
public final class Constants {
private Constants() {
}
- static final Integer CURRENT_VAULT_VERSION = 3;
+ static final Collection SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
+ static final Integer CURRENT_VAULT_VERSION = 4;
public static final int PAYLOAD_SIZE = 32 * 1024;
public static final int NONCE_SIZE = 16;
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
index ef88c4a8d..836e6c134 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java
@@ -9,6 +9,7 @@
package org.cryptomator.crypto.engine.impl;
import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
+import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -109,7 +110,7 @@ class CryptorImpl implements Cryptor {
assert keyFile != null;
// check version
- if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
+ if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
}
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
index 4b977d44d..6833d1b9a 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java
@@ -26,7 +26,8 @@ import org.cryptomator.siv.SivMode;
class FilenameCryptorImpl implements FilenameCryptor {
private static final BaseNCodec BASE32 = new Base32();
- private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z0-9]{8})*[A-Z0-9=]{8}");
+ // https://tools.ietf.org/html/rfc4648#section-6
+ private static final Pattern BASE32_PATTERN = Pattern.compile("([A-Z2-7]{8})*[A-Z2-7=]{8}");
private static final ThreadLocal SHA1 = new ThreadLocalSha1();
private static final ThreadLocal AES_SIV = new ThreadLocal() {
@Override
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java
index 11bc14f77..19dceb326 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/ConflictResolver.java
@@ -1,6 +1,6 @@
package org.cryptomator.filesystem.crypto;
-import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
+import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
import java.util.Optional;
import java.util.UUID;
@@ -32,7 +32,7 @@ final class ConflictResolver {
}
public File resolveIfNecessary(File file) {
- Matcher m = encryptedNamePattern.matcher(StringUtils.removeEnd(file.name(), DIR_SUFFIX));
+ Matcher m = encryptedNamePattern.matcher(StringUtils.removeStart(file.name(), DIR_PREFIX));
if (m.matches()) {
// full match, use file as is
return file;
@@ -47,11 +47,11 @@ final class ConflictResolver {
private File resolveConflict(File conflictingFile, MatchResult matchResult) {
String ciphertext = matchResult.group();
- boolean isDirectory = conflictingFile.name().substring(matchResult.end()).startsWith(DIR_SUFFIX);
+ boolean isDirectory = conflictingFile.name().startsWith(DIR_PREFIX);
Optional cleartext = nameDecryptor.apply(ciphertext);
if (cleartext.isPresent()) {
Folder folder = conflictingFile.parent().get();
- File canonicalFile = folder.file(isDirectory ? ciphertext + DIR_SUFFIX : ciphertext);
+ File canonicalFile = folder.file(isDirectory ? DIR_PREFIX + ciphertext : ciphertext);
if (canonicalFile.exists()) {
// there must not be two directories pointing to the same directory id. In this case no human interaction is needed to resolve this conflict:
if (isDirectory && FileContents.UTF_8.readContents(canonicalFile).equals(FileContents.UTF_8.readContents(conflictingFile))) {
@@ -66,7 +66,7 @@ final class ConflictResolver {
conflictId = createConflictId();
String alternativeCleartext = cleartext.get() + " (Conflict " + conflictId + ")";
String alternativeCiphertext = nameEncryptor.apply(alternativeCleartext).get();
- alternativeFile = folder.file(isDirectory ? alternativeCiphertext + DIR_SUFFIX : alternativeCiphertext);
+ alternativeFile = folder.file(isDirectory ? DIR_PREFIX + alternativeCiphertext : alternativeCiphertext);
} while (alternativeFile.exists());
LOG.info("Detected conflict {}:\n{}\n{}", conflictId, canonicalFile, conflictingFile);
conflictingFile.moveTo(alternativeFile);
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Constants.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Constants.java
index 7e6d31af0..eb552aad7 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Constants.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Constants.java
@@ -8,9 +8,9 @@ public final class Constants {
static final String DATA_ROOT_DIR = "d";
static final String ROOT_DIRECOTRY_ID = "";
- static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
- static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
+ public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
+ public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
- static final String DIR_SUFFIX = "_";
+ static final String DIR_PREFIX = "0";
}
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java
index 10e4289a6..b66721ca2 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java
@@ -10,7 +10,7 @@ package org.cryptomator.filesystem.crypto;
import static java.lang.String.format;
import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.cryptomator.filesystem.crypto.Constants.DIR_SUFFIX;
+import static org.cryptomator.filesystem.crypto.Constants.DIR_PREFIX;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
@@ -54,9 +54,9 @@ class CryptoFolder extends CryptoNode implements Folder {
@Override
protected Optional encryptedName() {
if (parent().isPresent()) {
- return parent().get().encryptChildName(name()).map(s -> s + DIR_SUFFIX);
+ return parent().get().encryptChildName(name()).map(s -> DIR_PREFIX + s);
} else {
- return Optional.of(cryptor.getFilenameCryptor().encryptFilename(name()) + DIR_SUFFIX);
+ return Optional.of(DIR_PREFIX + cryptor.getFilenameCryptor().encryptFilename(name()));
}
}
@@ -121,20 +121,20 @@ class CryptoFolder extends CryptoNode implements Folder {
@Override
public Stream files() {
- return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
+ return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix().negate()).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::file);
}
@Override
public Stream folders() {
- return nonConflictingFiles().map(File::name).filter(endsWithDirSuffix()).map(this::removeDirSuffix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
+ return nonConflictingFiles().map(File::name).filter(startsWithDirPrefix()).map(this::removeDirPrefix).map(this::decryptChildName).filter(Optional::isPresent).map(Optional::get).map(this::folder);
}
- private Predicate endsWithDirSuffix() {
- return (String encryptedFolderName) -> StringUtils.endsWith(encryptedFolderName, DIR_SUFFIX);
+ private Predicate startsWithDirPrefix() {
+ return (String encryptedFolderName) -> StringUtils.startsWith(encryptedFolderName, DIR_PREFIX);
}
- private String removeDirSuffix(String encryptedFolderName) {
- return StringUtils.removeEnd(encryptedFolderName, DIR_SUFFIX);
+ private String removeDirPrefix(String encryptedFolderName) {
+ return StringUtils.removeStart(encryptedFolderName, DIR_PREFIX);
}
@Override
diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java
index de299c452..8bd890a76 100644
--- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java
@@ -21,20 +21,20 @@ public class CryptorImplTest {
@Test
public void testMasterkeyDecryptionWithCorrectPassphrase() throws IOException {
- final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
- + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test(expected = InvalidPassphraseException.class)
public void testMasterkeyDecryptionWithWrongPassphrase() throws IOException {
- final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
- + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe");
}
@@ -44,7 +44,7 @@ public class CryptorImplTest {
final String testMasterKey = "{\"version\":-1,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
- + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@@ -62,23 +62,24 @@ public class CryptorImplTest {
@Ignore
@Test(expected = UnsupportedVaultFormatException.class)
public void testMasterkeyDecryptionWithWrongVersionMac() throws IOException {
- final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ final String testMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
+ "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," //
- + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLa=\"}";
+ + "\"versionMac\":\"z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfoK=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd");
}
@Test
public void testMasterkeyEncryption() throws IOException {
- final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ final String expectedMasterKey = "{\"version\":4,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," //
+ "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
+ "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," //
- + "\"versionMac\":\"iUmRRHITuyJsJbVNqGNw+82YQ4A3Rma7j/y1v0DCVLA=\"}";
+ + "\"versionMac\":\"Z9J8Uc5K1f7YKckLUFpXG39NHK1qUjzadw5nvOqvfok=\"}";
final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
cryptor.randomizeMasterkey();
final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd");
+ System.out.println(new String(masterkeyFile));
Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile);
}
diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java
index b99a000b4..bd1cf3b51 100644
--- a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/ConflictResolverTest.java
@@ -46,7 +46,7 @@ public class ConflictResolverTest {
unrelatedFile = Mockito.mock(File.class);
String canonicalFileName = encode.apply("test name").get();
- String canonicalFolderName = canonicalFileName + Constants.DIR_SUFFIX;
+ String canonicalFolderName = Constants.DIR_PREFIX + canonicalFileName;
String conflictingFileName = canonicalFileName + " (version 2)";
String conflictingFolderName = canonicalFolderName + " (version 2)";
String unrelatedName = "notBa$e32!";
@@ -70,6 +70,7 @@ public class ConflictResolverTest {
Mockito.doReturn(Optional.of(folder)).when(unrelatedFile).parent();
Mockito.when(folder.file(Mockito.startsWith(canonicalFileName.substring(0, 8)))).thenReturn(resolved);
+ Mockito.when(folder.file(Mockito.startsWith(canonicalFolderName.substring(0, 8)))).thenReturn(resolved);
Mockito.when(folder.file(canonicalFileName)).thenReturn(canonicalFile);
Mockito.when(folder.file(canonicalFolderName)).thenReturn(canonicalFolder);
Mockito.when(folder.file(conflictingFileName)).thenReturn(conflictingFile);
diff --git a/main/pom.xml b/main/pom.xml
index 321c0122e..cfb5ace1a 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -43,13 +43,6 @@
2.4
-
-
- jitpack.io
- https://jitpack.io
-
-
-
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java
index b74b88e80..c9dee3248 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java
@@ -26,6 +26,7 @@ import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.controls.DirectoryListCell;
+import org.cryptomator.ui.model.UpgradeStrategies;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Localization;
@@ -78,6 +79,7 @@ public class MainController extends LocalizedFXMLViewController {
private final Provider unlockedControllerProvider;
private final Lazy changePasswordController;
private final Lazy settingsController;
+ private final Lazy upgradeStrategies;
private final ObjectProperty activeController = new SimpleObjectProperty<>();
private final ObservableList vaults;
private final ObjectProperty selectedVault = new SimpleObjectProperty<>();
@@ -90,7 +92,7 @@ public class MainController extends LocalizedFXMLViewController {
@Inject
public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy welcomeController,
Lazy initializeController, Lazy notFoundController, Lazy upgradeController, Lazy unlockController,
- Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController) {
+ Provider unlockedControllerProvider, Lazy changePasswordController, Lazy settingsController, Lazy upgradeStrategies) {
super(localization);
this.mainWindow = mainWindow;
this.vaultFactoy = vaultFactoy;
@@ -102,6 +104,7 @@ public class MainController extends LocalizedFXMLViewController {
this.unlockedControllerProvider = unlockedControllerProvider;
this.changePasswordController = changePasswordController;
this.settingsController = settingsController;
+ this.upgradeStrategies = upgradeStrategies;
this.vaults = FXCollections.observableList(settings.getDirectories());
this.vaults.addListener((Change extends Vault> c) -> {
settings.save();
@@ -288,7 +291,7 @@ public class MainController extends LocalizedFXMLViewController {
this.showUnlockedView(newValue);
} else if (!newValue.doesVaultDirectoryExist()) {
this.showNotFoundView();
- } else if (newValue.isValidVaultDirectory() && newValue.needsUpgrade()) {
+ } else if (newValue.isValidVaultDirectory() && upgradeStrategies.get().getUpgradeStrategy(newValue).isPresent()) {
this.showUpgradeView();
} else if (newValue.isValidVaultDirectory()) {
this.showUnlockView();
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java
index f559fa50d..db92d7df4 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java
@@ -7,8 +7,10 @@ import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
-import org.cryptomator.ui.model.UpgradeInstruction;
-import org.cryptomator.ui.model.UpgradeInstruction.UpgradeFailedException;
+import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.model.UpgradeStrategies;
+import org.cryptomator.ui.model.UpgradeStrategy;
+import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.fxmisc.easybind.EasyBind;
@@ -16,7 +18,6 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
-import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
@@ -30,19 +31,24 @@ public class UpgradeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
final ObjectProperty vault = new SimpleObjectProperty<>();
+ final ObjectProperty> strategy = new SimpleObjectProperty<>();
+ private final UpgradeStrategies strategies;
private final ExecutorService exec;
- private final Binding> upgradeInstruction = EasyBind.monadic(vault).map(Vault::availableUpgrade);
private Optional listener = Optional.empty();
@Inject
- public UpgradeController(Localization localization, ExecutorService exec) {
+ public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
super(localization);
+ this.strategies = strategies;
this.exec = exec;
}
@FXML
private Label upgradeLabel;
+ @FXML
+ private SecPasswordField passwordField;
+
@FXML
private Button upgradeButton;
@@ -54,10 +60,12 @@ public class UpgradeController extends LocalizedFXMLViewController {
@Override
protected void initialize() {
- upgradeLabel.textProperty().bind(EasyBind.monadic(upgradeInstruction).map(instruction -> {
+ upgradeLabel.textProperty().bind(EasyBind.monadic(strategy).map(instruction -> {
return instruction.map(this::upgradeNotification).orElse("");
}).orElse(""));
+ upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
+
EasyBind.subscribe(vault, this::vaultDidChange);
}
@@ -68,14 +76,18 @@ public class UpgradeController extends LocalizedFXMLViewController {
private void vaultDidChange(Vault newVault) {
errorLabel.setText(null);
+ strategy.set(strategies.getUpgradeStrategy(newVault));
+ // trigger "default" change to refresh key bindings:
+ upgradeButton.setDefaultButton(false);
+ upgradeButton.setDefaultButton(true);
}
// ****************************************
// Upgrade label
// ****************************************
- private String upgradeNotification(UpgradeInstruction instruction) {
- return instruction.getNotification(vault.get(), localization);
+ private String upgradeNotification(UpgradeStrategy instruction) {
+ return instruction.getNotification(vault.get());
}
// ****************************************
@@ -84,36 +96,45 @@ public class UpgradeController extends LocalizedFXMLViewController {
@FXML
private void didClickUpgradeButton(ActionEvent event) {
- upgradeInstruction.getValue().ifPresent(this::upgrade);
+ strategy.getValue().ifPresent(this::upgrade);
}
- private void upgrade(UpgradeInstruction instruction) {
- Vault v = vault.getValue();
- Objects.requireNonNull(v);
+ private void upgrade(UpgradeStrategy instruction) {
+ Vault v = Objects.requireNonNull(vault.getValue());
+ passwordField.setDisable(true);
progressIndicator.setVisible(true);
- upgradeButton.setDisable(true);
exec.submit(() -> {
if (!instruction.isApplicable(v)) {
LOG.error("No upgrade needed for " + v.path().getValue());
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
try {
- instruction.upgrade(v, localization);
- Platform.runLater(() -> {
- progressIndicator.setVisible(false);
- upgradeButton.setDisable(false);
- listener.ifPresent(UpgradeListener::didUpgrade);
- });
+ instruction.upgrade(v, passwordField.getCharacters());
+ Platform.runLater(this::showNextUpgrade);
} catch (UpgradeFailedException e) {
Platform.runLater(() -> {
errorLabel.setText(e.getLocalizedMessage());
+ });
+ } finally {
+ Platform.runLater(() -> {
progressIndicator.setVisible(false);
- upgradeButton.setDisable(false);
+ passwordField.setDisable(false);
+ passwordField.swipe();
});
}
});
}
+ private void showNextUpgrade() {
+ errorLabel.setText(null);
+ Optional nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
+ if (nextStrategy.isPresent()) {
+ strategy.set(nextStrategy);
+ } else {
+ listener.ifPresent(UpgradeListener::didUpgrade);
+ }
+ }
+
/* callback */
public void setListener(UpgradeListener listener) {
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeInstruction.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeInstruction.java
deleted file mode 100644
index a903c1369..000000000
--- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeInstruction.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.cryptomator.ui.model;
-
-import org.cryptomator.ui.settings.Localization;
-
-public interface UpgradeInstruction {
-
- static UpgradeInstruction[] AVAILABLE_INSTRUCTIONS = {new UpgradeVersion3DropBundleExtension()};
-
- /**
- * @return Localized string to display to the user when an upgrade is needed.
- */
- String getNotification(Vault vault, Localization localization);
-
- /**
- * Upgrades a vault. Might take a moment, should be run in a background thread.
- */
- void upgrade(Vault vault, Localization localization) throws UpgradeFailedException;
-
- /**
- * Determines in O(1), if an upgrade can be applied to a vault.
- *
- * @return true if and only if the vault can be migrated to a newer version without the risk of data losses.
- */
- boolean isApplicable(Vault vault);
-
- /**
- * Thrown when data migration failed.
- */
- public class UpgradeFailedException extends Exception {
-
- UpgradeFailedException() {
- }
-
- UpgradeFailedException(String message) {
- super(message);
- }
-
- }
-
-}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java
new file mode 100644
index 000000000..8a8ec3444
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java
@@ -0,0 +1,27 @@
+package org.cryptomator.ui.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class UpgradeStrategies {
+
+ private final Collection strategies;
+
+ @Inject
+ public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
+ strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
+ }
+
+ public Optional getUpgradeStrategy(Vault vault) {
+ return strategies.stream().filter(strategy -> {
+ return strategy.isApplicable(vault);
+ }).findFirst();
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java
new file mode 100644
index 000000000..6f7f147ce
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java
@@ -0,0 +1,87 @@
+package org.cryptomator.ui.model;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+
+import javax.inject.Provider;
+
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
+import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
+import org.cryptomator.filesystem.crypto.Constants;
+import org.cryptomator.ui.settings.Localization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class UpgradeStrategy {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
+
+ protected final Provider cryptorProvider;
+ protected final Localization localization;
+
+ UpgradeStrategy(Provider cryptorProvider, Localization localization) {
+ this.cryptorProvider = cryptorProvider;
+ this.localization = localization;
+ }
+
+ /**
+ * @return Localized string to display to the user when an upgrade is needed.
+ */
+ public abstract String getNotification(Vault vault);
+
+ /**
+ * Upgrades a vault. Might take a moment, should be run in a background thread.
+ */
+ public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
+ final Cryptor cryptor = cryptorProvider.get();
+ try {
+ final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+ final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
+ cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
+ // create backup, as soon as we know the password was correct:
+ final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
+ Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
+ // do stuff:
+ upgrade(vault, cryptor);
+ // write updated masterkey file:
+ final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
+ final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
+ Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
+ } catch (InvalidPassphraseException e) {
+ throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
+ } catch (IOException | UnsupportedVaultFormatException e) {
+ LOG.warn("Upgrade failed.", e);
+ throw new UpgradeFailedException("Upgrade failed. Details in log message.");
+ } finally {
+ cryptor.destroy();
+ }
+ }
+
+ protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException;
+
+ /**
+ * Determines in O(1), if an upgrade can be applied to a vault.
+ *
+ * @return true if and only if the vault can be migrated to a newer version without the risk of data losses.
+ */
+ public abstract boolean isApplicable(Vault vault);
+
+ /**
+ * Thrown when data migration failed.
+ */
+ public class UpgradeFailedException extends Exception {
+
+ UpgradeFailedException() {
+ }
+
+ UpgradeFailedException(String message) {
+ super(message);
+ }
+
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java
index 36a21b1fb..125646069 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java
@@ -4,19 +4,36 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
import org.apache.commons.lang3.StringUtils;
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
+import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
+import org.cryptomator.filesystem.crypto.Constants;
import org.cryptomator.ui.settings.Localization;
+import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
-class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
+@Singleton
+class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
+ private final Settings settings;
+
+ @Inject
+ public UpgradeVersion3DropBundleExtension(Provider cryptorProvider, Localization localization, Settings settings) {
+ super(cryptorProvider, localization);
+ this.settings = settings;
+ }
@Override
- public String getNotification(Vault vault, Localization localization) {
+ public String getNotification(Vault vault) {
String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
@@ -25,7 +42,26 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
}
@Override
- public void upgrade(Vault vault, Localization localization) throws UpgradeFailedException {
+ public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
+ final Cryptor cryptor = cryptorProvider.get();
+ try {
+ final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+ final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
+ cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
+ upgrade(vault, cryptor);
+ // don't write new masterkey. this is a special case, as we were stupid and didn't increase the vault version with this upgrade...
+ } catch (InvalidPassphraseException e) {
+ throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
+ } catch (IOException | UnsupportedVaultFormatException e) {
+ LOG.warn("Upgrade failed.", e);
+ throw new UpgradeFailedException("Upgrade failed. Details in log message.");
+ } finally {
+ cryptor.destroy();
+ }
+ }
+
+ @Override
+ protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
Path path = vault.path().getValue();
String oldVaultName = path.getFileName().toString();
String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
@@ -39,6 +75,7 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
Files.move(path, path.resolveSibling(newVaultName));
Platform.runLater(() -> {
vault.setPath(newPath);
+ settings.save();
});
} catch (IOException e) {
LOG.error("Vault migration failed", e);
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java
new file mode 100644
index 000000000..45c3a9ff6
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java
@@ -0,0 +1,116 @@
+package org.cryptomator.ui.model;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.filesystem.crypto.Constants;
+import org.cryptomator.ui.settings.Localization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class UpgradeVersion3to4 extends UpgradeStrategy {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
+ private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_");
+ private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all)
+
+ @Inject
+ public UpgradeVersion3to4(Provider cryptorProvider, Localization localization) {
+ super(cryptorProvider, localization);
+ }
+
+ @Override
+ public String getNotification(Vault vault) {
+ return localization.getString("upgrade.version3to4.msg");
+ }
+
+ @Override
+ protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
+ Path dataDir = vault.path().get().resolve("d");
+ if (!Files.isDirectory(dataDir)) {
+ return; // empty vault. no migration needed.
+ }
+ try {
+ Files.walkFileTree(dataDir, new FileVisitor() {
+
+ @Override
+ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+ migrate(file, attrs);
+ return FileVisitResult.CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
+ throw exc;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ return FileVisitResult.CONTINUE;
+ }
+
+ });
+ } catch (IOException e) {
+ LOG.error("Migration failed.", e);
+ throw new UpgradeFailedException(localization.getString("upgrade.version3to4.err.io"));
+ }
+ LOG.info("Migration finished.");
+ }
+
+ private void migrate(Path file, BasicFileAttributes attrs) throws IOException {
+ String name = file.getFileName().toString();
+ long size = attrs.size();
+ Matcher m = BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN.matcher(name);
+ if (m.find(0) && size < FILE_MIN_SIZE) {
+ String base32 = m.group(1);
+ String suffix = name.substring(m.end());
+ String renamed = "0" + base32 + (suffix.isEmpty() ? "" : " " + suffix);
+ renameWithoutOverwriting(file, renamed);
+ }
+ }
+
+ private void renameWithoutOverwriting(Path path, String newName) throws IOException {
+ Path newPath = path.resolveSibling(newName);
+ for (int i = 2; Files.exists(newPath); i++) {
+ newPath = path.resolveSibling(newName + " " + i);
+ }
+ Files.move(path, newPath);
+ LOG.info("Renaming {} to {}", path, newPath.getFileName());
+ }
+
+ @Override
+ public boolean isApplicable(Vault vault) {
+ final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+ try {
+ if (Files.isRegularFile(masterkeyFile)) {
+ final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
+ return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
+ } else {
+ LOG.warn("Not a file: {}", masterkeyFile);
+ return false;
+ }
+ } catch (IOException e) {
+ LOG.warn("Could not determine, whether upgrade is applicable.", e);
+ return false;
+ }
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
index 3cbc4bbc1..d38f9152c 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.Normalizer;
import java.text.Normalizer.Form;
-import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
@@ -64,9 +63,9 @@ public class Vault implements CryptoFileSystemDelegate {
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
private final ObjectProperty path;
- private final DeferredCloser closer;
private final ShorteningFileSystemFactory shorteningFileSystemFactory;
private final CryptoFileSystemFactory cryptoFileSystemFactory;
+ private final DeferredCloser closer;
private final BooleanProperty unlocked = new SimpleBooleanProperty();
private final ObservableList namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set whitelistedResourcesWithInvalidMac = new HashSet<>();
@@ -82,9 +81,9 @@ public class Vault implements CryptoFileSystemDelegate {
*/
Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
this.path = new SimpleObjectProperty(vaultDirectoryPath);
- this.closer = closer;
this.shorteningFileSystemFactory = shorteningFileSystemFactory;
this.cryptoFileSystemFactory = cryptoFileSystemFactory;
+ this.closer = closer;
try {
setMountName(name().getValue());
@@ -167,16 +166,6 @@ public class Vault implements CryptoFileSystemDelegate {
Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount);
}
- public boolean needsUpgrade() {
- return availableUpgrade().isPresent();
- }
-
- public Optional availableUpgrade() {
- return Arrays.stream(UpgradeInstruction.AVAILABLE_INSTRUCTIONS).filter(instruction -> {
- return instruction.isApplicable(this);
- }).findAny();
- }
-
// ******************************************************************************
// Delegate methods
// ********************************************************************************/
diff --git a/main/ui/src/main/resources/fxml/upgrade.fxml b/main/ui/src/main/resources/fxml/upgrade.fxml
index b153c4f6a..5d54014a3 100644
--- a/main/ui/src/main/resources/fxml/upgrade.fxml
+++ b/main/ui/src/main/resources/fxml/upgrade.fxml
@@ -11,17 +11,38 @@
-
+
+
+
+
+
+
-
+
+
+
+
-
+
+
+
+
-
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/ui/src/main/resources/localization/en.txt b/main/ui/src/main/resources/localization/en.txt
index 668313168..a108831f7 100644
--- a/main/ui/src/main/resources/localization/en.txt
+++ b/main/ui/src/main/resources/localization/en.txt
@@ -26,7 +26,7 @@ initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.messageLabel.alreadyInitialized=Vault already initialized
-initialize.messageLabel.initializationFailed=Could not initialize vault. See logfile for details.
+initialize.messageLabel.initializationFailed=Could not initialize vault. See log file for details.
initialize.messageLabel.passwordStrength.0=Very weak
initialize.messageLabel.passwordStrength.1=Weak
initialize.messageLabel.passwordStrength.2=Fair
@@ -43,6 +43,9 @@ upgrade.button=Upgrade vault
upgrade.version3dropBundleExtension.msg=This vault needs to be migrated to a newer format.\n"%1$s" will be renamed to "%2$s".\nPlease make sure synchronization has finished before proceeding.
upgrade.version3dropBundleExtension.err.alreadyExists=Automatic migration failed.\n"%s" already exists.
+upgrade.version3to4.msg=This vault needs to be migrated to a newer format.\nEncrypted folder names will be updated.\nPlease make sure synchronization has finished before proceeding.
+upgrade.version3to4.err.io=Migration failed due to an I/O Exception. See log file for details.
+
# unlock.fxml
unlock.label.password=Password
unlock.label.mountName=Drive name
@@ -54,7 +57,7 @@ unlock.button.advancedOptions.show=More options
unlock.button.advancedOptions.hide=Less options
unlock.choicebox.winDriveLetter.auto=Assign automatically
unlock.errorMessage.wrongPassword=Wrong password
-unlock.errorMessage.mountingFailed=Mounting failed. See logfile for details.
+unlock.errorMessage.mountingFailed=Mounting failed. See log file for details.
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.