diff --git a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java index eb7a9400f..1316c50d1 100644 --- a/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java +++ b/main/commons/src/main/java/org/cryptomator/common/LazyInitializer.java @@ -17,16 +17,17 @@ public final class LazyInitializer { * @return The initialized value */ public static T initializeLazily(AtomicReference reference, Supplier factory) { - final T existingInstance = reference.get(); - if (existingInstance != null) { - return existingInstance; + final T existing = reference.get(); + if (existing != null) { + return existing; } else { - final T newInstance = factory.get(); - if (reference.compareAndSet(null, newInstance)) { - return newInstance; - } else { - return reference.get(); - } + return reference.updateAndGet(currentValue -> { + if (currentValue == null) { + return factory.get(); + } else { + return currentValue; + } + }); } } diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml new file mode 100644 index 000000000..5c96a8aad --- /dev/null +++ b/main/keychain/pom.xml @@ -0,0 +1,52 @@ + + 4.0.0 + + org.cryptomator + main + 1.2.0-SNAPSHOT + + keychain + System Keychain Access + + + + org.apache.commons + commons-lang3 + + + com.google.code.gson + gson + 2.7 + + + commons-codec + commons-codec + + + org.bouncycastle + bcprov-jdk15on + 1.54 + + + org.cryptomator + jni + + + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + + + + org.cryptomator + commons-test + + + \ No newline at end of file diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java new file mode 100644 index 000000000..abd7a3ee8 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java @@ -0,0 +1,26 @@ +package org.cryptomator.keychain; + +public interface KeychainAccess { + + /** + * Associates a passphrase with a given key. + * + * @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}. + * @param passphrase The secret to store in this keychain. + */ + void storePassphrase(String key, CharSequence passphrase); + + /** + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + * @return The stored passphrase for the given key or null if no value for the given key could be found. + */ + char[] loadPassphrase(String key); + + /** + * Deletes a passphrase with a given key. + * + * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}. + */ + void deletePassphrase(String key); + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java new file mode 100644 index 000000000..b304d6edf --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java @@ -0,0 +1,10 @@ +package org.cryptomator.keychain; + +interface KeychainAccessStrategy extends KeychainAccess { + + /** + * @return true if this KeychainAccessStrategy works on the current machine. + */ + boolean isSupported(); + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java new file mode 100644 index 000000000..3f2cf6abc --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java @@ -0,0 +1,28 @@ +package org.cryptomator.keychain; + +import java.util.Optional; +import java.util.Set; + +import org.cryptomator.jni.JniModule; + +import com.google.common.collect.Sets; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +@Module(includes = {JniModule.class}) +public class KeychainModule { + + @Provides + @ElementsIntoSet + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) { + return Sets.newHashSet(macKeychain, winKeychain); + } + + @Provides + public Optional provideSupportedKeychain(Set keychainAccessStrategies) { + return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst(); + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java new file mode 100644 index 000000000..5e8b27b6c --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -0,0 +1,44 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import javax.inject.Inject; + +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.MacFunctions; +import org.cryptomator.jni.MacKeychainAccess; + +class MacSystemKeychainAccess implements KeychainAccessStrategy { + + private final MacKeychainAccess keychain; + + @Inject + public MacSystemKeychainAccess(Optional macFunctions) { + if (macFunctions.isPresent()) { + this.keychain = macFunctions.get().keychainAccess(); + } else { + this.keychain = null; + } + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + keychain.storePassword(key, passphrase); + } + + @Override + public char[] loadPassphrase(String key) { + return keychain.loadPassword(key); + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_MAC_OSX && keychain != null; + } + + @Override + public void deletePassphrase(String key) { + keychain.deletePassword(key); + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java new file mode 100644 index 000000000..a0ced12eb --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java @@ -0,0 +1,191 @@ +package org.cryptomator.keychain; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import javax.inject.Inject; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.WinDataProtection; +import org.cryptomator.jni.WinFunctions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.annotations.SerializedName; +import com.google.gson.reflect.TypeToken; + +class WindowsProtectedKeychainAccess implements KeychainAccessStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class); + private static final Gson GSON = new GsonBuilder().setPrettyPrinting() // + .registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) // + .disableHtmlEscaping().create(); + + private final WinDataProtection dataProtection; + private final Path keychainPath; + private Map keychainEntries; + + @Inject + public WindowsProtectedKeychainAccess(Optional winFunctions) { + if (winFunctions.isPresent()) { + this.dataProtection = winFunctions.get().dataProtection(); + } else { + this.dataProtection = null; + } + String keychainPathProperty = System.getProperty("cryptomator.keychainPath"); + if (dataProtection != null && keychainPathProperty == null) { + LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found."); + } + if (keychainPathProperty != null) { + if (keychainPathProperty.startsWith("~/")) { + keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1); + } + this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty); + } else { + this.keychainPath = null; + } + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + loadKeychainEntriesIfNeeded(); + ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase)); + byte[] cleartext = new byte[buf.remaining()]; + buf.get(cleartext); + KeychainEntry entry = new KeychainEntry(); + entry.salt = generateSalt(); + entry.ciphertext = dataProtection.protect(cleartext, entry.salt); + Arrays.fill(buf.array(), (byte) 0x00); + Arrays.fill(cleartext, (byte) 0x00); + keychainEntries.put(key, entry); + saveKeychainEntries(); + } + + @Override + public char[] loadPassphrase(String key) { + loadKeychainEntriesIfNeeded(); + KeychainEntry entry = keychainEntries.get(key); + if (entry == null) { + return null; + } + byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt); + if (cleartext == null) { + return null; + } + CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext)); + char[] passphrase = new char[buf.remaining()]; + buf.get(passphrase); + Arrays.fill(cleartext, (byte) 0x00); + Arrays.fill(buf.array(), (char) 0x00); + return passphrase; + } + + @Override + public void deletePassphrase(String key) { + loadKeychainEntriesIfNeeded(); + keychainEntries.remove(key); + saveKeychainEntries(); + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null; + } + + private byte[] generateSalt() { + byte[] result = new byte[2 * Long.BYTES]; + UUID uuid = UUID.randomUUID(); + ByteBuffer buf = ByteBuffer.wrap(result); + buf.putLong(uuid.getMostSignificantBits()); + buf.putLong(uuid.getLeastSignificantBits()); + return result; + } + + private void loadKeychainEntriesIfNeeded() { + if (keychainEntries == null) { + loadKeychainEntries(); + } + assert keychainEntries != null; + } + + private void loadKeychainEntries() { + Type type = new TypeToken>() { + }.getType(); + try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); // + Reader reader = new InputStreamReader(in, UTF_8)) { + keychainEntries = GSON.fromJson(reader, type); + } catch (JsonParseException | NoSuchFileException e) { + LOG.info("Creating new keychain at path {}", keychainPath); + } catch (IOException e) { + throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e); + } + if (keychainEntries == null) { + keychainEntries = new HashMap<>(); + } + } + + private void saveKeychainEntries() { + try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); // + Writer writer = new OutputStreamWriter(out, UTF_8)) { + GSON.toJson(keychainEntries, writer); + } catch (IOException e) { + throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e); + } + } + + private static class KeychainEntry { + @SerializedName("ciphertext") + byte[] ciphertext; + @SerializedName("salt") + byte[] salt; + } + + private static class ByteArrayJsonAdapter implements JsonSerializer, JsonDeserializer { + + private static final Base64 BASE64 = new Base64(); + + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8)); + } + + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8)); + } + + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java new file mode 100644 index 000000000..2d8fe4e9e --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java @@ -0,0 +1,17 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import org.junit.Assert; +import org.junit.Test; + +public class KeychainModuleTest { + + @Test + public void testGetKeychain() { + Optional keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess(); + Assert.assertTrue(keychainAccess.isPresent()); + Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java new file mode 100644 index 000000000..8b3bb3e07 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -0,0 +1,34 @@ +package org.cryptomator.keychain; + +import java.util.HashMap; +import java.util.Map; + +class MapKeychainAccess implements KeychainAccessStrategy { + + private final Map map = new HashMap<>(); + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + char[] pw = new char[passphrase.length()]; + for (int i = 0; i < passphrase.length(); i++) { + pw[i] = passphrase.charAt(i); + } + map.put(key, pw); + } + + @Override + public char[] loadPassphrase(String key) { + return map.get(key); + } + + @Override + public void deletePassphrase(String key) { + map.remove(key); + } + + @Override + public boolean isSupported() { + return true; + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java new file mode 100644 index 000000000..79d770b57 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestJniModule.java @@ -0,0 +1,23 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import org.cryptomator.jni.JniModule; +import org.cryptomator.jni.MacFunctions; +import org.cryptomator.jni.WinFunctions; + +import dagger.Lazy; + +public class TestJniModule extends JniModule { + + @Override + public Optional winFunctions(Lazy winFunction) { + return Optional.empty(); + } + + @Override + public Optional macFunctions(Lazy winFunction) { + return Optional.empty(); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java new file mode 100644 index 000000000..30e42ee52 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java @@ -0,0 +1,15 @@ +package org.cryptomator.keychain; + +import java.util.Optional; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = KeychainModule.class) +interface TestKeychainComponent { + + Optional keychainAccess(); + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java new file mode 100644 index 000000000..4bb403906 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java @@ -0,0 +1,14 @@ +package org.cryptomator.keychain; + +import java.util.Set; + +import com.google.common.collect.Sets; + +public class TestKeychainModule extends KeychainModule { + + @Override + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) { + return Sets.newHashSet(new MapKeychainAccess()); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java new file mode 100644 index 000000000..9b3fd94e8 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java @@ -0,0 +1,60 @@ +package org.cryptomator.keychain; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +import org.cryptomator.jni.WinDataProtection; +import org.cryptomator.jni.WinFunctions; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +public class WindowsProtectedKeychainAccessTest { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private Path tmpFile; + private WindowsProtectedKeychainAccess keychain; + + @Before + public void setup() throws IOException, ReflectiveOperationException { + tmpFile = Files.createTempFile("unit-tests", ".tmp"); + System.setProperty("cryptomator.keychainPath", tmpFile.toAbsolutePath().normalize().toString()); + WinFunctions winFunctions = Mockito.mock(WinFunctions.class); + WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class); + Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection); + Answer answerReturningFirstArg = invocation -> invocation.getArgumentAt(0, byte[].class).clone(); + Mockito.when(winDataProtection.protect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg); + Mockito.when(winDataProtection.unprotect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg); + keychain = new WindowsProtectedKeychainAccess(Optional.of(winFunctions)); + } + + @After + public void teardown() throws IOException { + Files.deleteIfExists(tmpFile); + } + + @Test + public void testStoreAndLoad() { + String storedPw1 = "topSecret"; + String storedPw2 = "bottomSecret"; + keychain.storePassphrase("myPassword", storedPw1); + keychain.storePassphrase("myOtherPassword", storedPw2); + String loadedPw1 = new String(keychain.loadPassphrase("myPassword")); + String loadedPw2 = new String(keychain.loadPassphrase("myOtherPassword")); + Assert.assertEquals(storedPw1, loadedPw1); + Assert.assertEquals(storedPw2, loadedPw2); + keychain.deletePassphrase("myPassword"); + Assert.assertNull(keychain.loadPassphrase("myPassword")); + Assert.assertNull(keychain.loadPassphrase("nonExistingPassword")); + } + +} diff --git a/main/keychain/src/test/resources/log4j2.xml b/main/keychain/src/test/resources/log4j2.xml new file mode 100644 index 000000000..39c2f8545 --- /dev/null +++ b/main/keychain/src/test/resources/log4j2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/main/pom.xml b/main/pom.xml index 8707743d7..4716aed5b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -28,6 +28,7 @@ 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT 2.1 1.7.7 4.12 @@ -40,7 +41,7 @@ 3.1 2.4.4 1.10.19 - 2.4 + 2.6.1 @@ -57,7 +58,6 @@ ${project.version} test - org.cryptomator filesystem-api @@ -106,7 +106,6 @@ filesystem-stats ${project.version} - org.cryptomator frontend-api @@ -117,19 +116,28 @@ frontend-webdav ${project.version} - + + org.cryptomator + keychain + ${project.version} + org.cryptomator ui ${project.version} - + org.cryptomator cryptolib ${cryptomator.cryptolib.version} + + org.cryptomator + jni + ${cryptomator.jni.version} + @@ -280,6 +288,7 @@ filesystem-invariants-tests frontend-api frontend-webdav + keychain ui diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 5515e6d26..a0e7a0749 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -54,6 +54,14 @@ org.cryptomator frontend-webdav + + org.cryptomator + jni + + + org.cryptomator + keychain + diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java index 095caeb89..e82e006e9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -8,10 +8,12 @@ *******************************************************************************/ package org.cryptomator.ui; +import java.util.Optional; import java.util.concurrent.ExecutorService; import javax.inject.Singleton; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; @@ -34,4 +36,7 @@ interface CryptomatorComponent { Localization localization(); ExitUtil exitUtil(); -} \ No newline at end of file + + Optional nativeMacFunctions(); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java index f460dc133..d4f5f7b38 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -22,6 +22,8 @@ import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.FrontendId; import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; +import org.cryptomator.jni.JniModule; +import org.cryptomator.keychain.KeychainModule; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.model.Vaults; @@ -39,7 +41,7 @@ import javafx.application.Application; import javafx.beans.Observable; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); @@ -109,9 +111,7 @@ class CryptomatorModule { private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) { webDavServer.setValidFrontendIds(vaults.stream() // - .map(Vault::getId) - .map(FrontendId::from) - .collect(toList())); + .map(Vault::getId).map(FrontendId::from).collect(toList())); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java index ff0cbe0dd..7352d149c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -21,6 +21,7 @@ import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.IOException; +import java.util.Optional; import java.util.concurrent.TimeUnit; import javax.inject.Inject; @@ -32,6 +33,9 @@ import javax.script.ScriptException; import javax.swing.SwingUtilities; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.JniException; +import org.cryptomator.jni.MacApplicationUiState; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.slf4j.Logger; @@ -48,12 +52,14 @@ class ExitUtil { private final Stage mainWindow; private final Localization localization; private final Settings settings; + private final Optional macFunctions; @Inject - public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings) { + public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional macFunctions) { this.mainWindow = mainWindow; this.localization = localization; this.settings = settings; + this.macFunctions = macFunctions; } public void initExitHandler(Runnable exitCommand) { @@ -88,6 +94,7 @@ class ExitUtil { if (Platform.isImplicitExit()) { exitCommand.run(); } else { + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication)); mainWindow.close(); this.showTrayNotification(trayIcon); } @@ -189,6 +196,7 @@ class ExitUtil { private void restoreFromTray(ActionEvent event) { Platform.runLater(() -> { + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication)); mainWindow.show(); mainWindow.requestFocus(); }); 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 b1a76529b..c78ac51f0 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 @@ -12,6 +12,7 @@ package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -31,9 +32,7 @@ import javafx.application.Application; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -49,9 +48,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private final Application app; private final PasswordStrengthUtil strengthRater; - final ObjectProperty vault = new SimpleObjectProperty<>(); - private Optional listener = Optional.empty(); private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private Optional listener = Optional.empty(); + private Vault vault; @Inject public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) { @@ -101,7 +100,6 @@ public class ChangePasswordController extends LocalizedFXMLViewController { BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty(); BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - EasyBind.subscribe(vault, this::vaultDidChange); changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate)); @@ -118,10 +116,11 @@ public class ChangePasswordController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/change_password.fxml"); } - private void vaultDidChange(Vault newVault) { - oldPasswordField.clear(); - newPasswordField.clear(); - retypePasswordField.clear(); + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); + oldPasswordField.swipe(); + newPasswordField.swipe(); + retypePasswordField.swipe(); // trigger "default" change to refresh key bindings: changePasswordButton.setDefaultButton(false); changePasswordButton.setDefaultButton(true); @@ -144,7 +143,7 @@ public class ChangePasswordController extends LocalizedFXMLViewController { private void didClickChangePasswordButton(ActionEvent event) { downloadsPageLink.setVisible(false); try { - vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); + vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters()); messageText.setText(localization.getString("changePassword.infoMessage.success")); listener.ifPresent(this::invokeListenerLater); } catch (InvalidPassphraseException e) { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 2989e9eaf..d45cce37a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -10,9 +10,10 @@ package org.cryptomator.ui.controllers; import java.io.IOException; -import java.io.UncheckedIOException; import java.net.URL; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -29,9 +30,7 @@ import org.slf4j.LoggerFactory; import javafx.application.Platform; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; @@ -44,9 +43,9 @@ public class InitializeController extends LocalizedFXMLViewController { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); private final PasswordStrengthUtil strengthRater; - final ObjectProperty vault = new SimpleObjectProperty<>(); - private Optional listener = Optional.empty(); private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private Optional listener = Optional.empty(); + private Vault vault; @Inject public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) { @@ -88,7 +87,6 @@ public class InitializeController extends LocalizedFXMLViewController { public void initialize() { BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty(); BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - EasyBind.subscribe(vault, this::vaultDidChange); okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate)); @@ -105,9 +103,10 @@ public class InitializeController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/initialize.fxml"); } - private void vaultDidChange(Vault newVault) { - passwordField.clear(); - retypePasswordField.clear(); + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); + passwordField.swipe(); + retypePasswordField.swipe(); // trigger "default" change to refresh key bindings: okButton.setDefaultButton(false); okButton.setDefaultButton(true); @@ -121,11 +120,13 @@ public class InitializeController extends LocalizedFXMLViewController { protected void initializeVault(ActionEvent event) { final CharSequence passphrase = passwordField.getCharacters(); try { - vault.get().create(passphrase); + vault.create(passphrase); listener.ifPresent(this::invokeListenerLater); } catch (FileAlreadyExistsException ex) { messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); - } catch (UncheckedIOException | IOException ex) { + } catch (DirectoryNotEmptyException ex) { + messageLabel.setText(localization.getString("initialize.messageLabel.notEmpty")); + } catch (IOException ex) { LOG.error("I/O Exception", ex); messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed")); } finally { 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 478c6be6b..9c9c194ba 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 @@ -315,7 +315,8 @@ public class MainController extends LocalizedFXMLViewController { private void showInitializeView() { final InitializeController ctrl = initializeController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didInitialize); activeController.set(ctrl); } @@ -326,7 +327,8 @@ public class MainController extends LocalizedFXMLViewController { private void showUpgradeView() { final UpgradeController ctrl = upgradeController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didUpgrade); activeController.set(ctrl); } @@ -337,7 +339,8 @@ public class MainController extends LocalizedFXMLViewController { private void showUnlockView() { final UnlockController ctrl = unlockController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didUnlock); activeController.set(ctrl); } @@ -353,6 +356,7 @@ public class MainController extends LocalizedFXMLViewController { final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> { return unlockedControllerProvider.get(); }); + ctrl.loadFxml(); ctrl.setVault(vault); ctrl.setListener(this::didLock); activeController.set(ctrl); @@ -368,7 +372,8 @@ public class MainController extends LocalizedFXMLViewController { private void showChangePasswordView() { final ChangePasswordController ctrl = changePasswordController.get(); - ctrl.vault.bind(selectedVault); + ctrl.loadFxml(); + ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didChangePassword); activeController.set(ctrl); } 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 d17175dc7..e5fe09fe4 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 @@ -9,7 +9,9 @@ package org.cryptomator.ui.controllers; import java.net.URL; +import java.util.Arrays; import java.util.Comparator; +import java.util.Objects; import java.util.Optional; import javax.inject.Inject; @@ -22,25 +24,24 @@ import org.cryptomator.frontend.CommandFailedException; import org.cryptomator.frontend.FrontendCreationFailedException; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters; +import org.cryptomator.keychain.KeychainAccess; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.util.AsyncTaskService; -import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import dagger.Lazy; import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; @@ -61,22 +62,34 @@ public class UnlockController extends LocalizedFXMLViewController { private final Settings settings; private final WindowsDriveLetters driveLetters; private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; - final ObjectProperty vault = new SimpleObjectProperty<>(); + private final Optional keychainAccess; + private Vault vault; private Optional listener = Optional.empty(); @Inject - public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters) { + public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy frontendFactory, Settings settings, WindowsDriveLetters driveLetters, + Optional keychainAccess) { super(localization); this.app = app; this.asyncTaskService = asyncTaskService; this.frontendFactory = frontendFactory; this.settings = settings; this.driveLetters = driveLetters; + this.keychainAccess = keychainAccess; } @FXML private SecPasswordField passwordField; + @FXML + private Button advancedOptionsButton; + + @FXML + private Button unlockButton; + + @FXML + private CheckBox savePassword; + @FXML private TextField mountName; @@ -86,12 +99,6 @@ public class UnlockController extends LocalizedFXMLViewController { @FXML private ChoiceBox winDriveLetter; - @FXML - private Button advancedOptionsButton; - - @FXML - private Button unlockButton; - @FXML private ProgressIndicator progressIndicator; @@ -107,8 +114,10 @@ public class UnlockController extends LocalizedFXMLViewController { @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); + unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); + savePassword.setDisable(!keychainAccess.isPresent()); if (SystemUtils.IS_OS_WINDOWS) { winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); } else { @@ -117,9 +126,6 @@ public class UnlockController extends LocalizedFXMLViewController { winDriveLetter.setVisible(false); winDriveLetter.setManaged(false); } - unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty()); - - EasyBind.subscribe(vault, this::vaultDidChange); } @Override @@ -127,11 +133,15 @@ public class UnlockController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/unlock.fxml"); } - private void vaultDidChange(Vault newVault) { - if (newVault == null) { + void setVault(Vault vault) { + // trigger "default" change to refresh key bindings: + unlockButton.setDefaultButton(false); + unlockButton.setDefaultButton(true); + if (vault.equals(this.vault)) { return; } - passwordField.clear(); + this.vault = Objects.requireNonNull(vault); + passwordField.swipe(); advancedOptions.setVisible(false); advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show")); progressIndicator.setVisible(false); @@ -145,13 +155,21 @@ public class UnlockController extends LocalizedFXMLViewController { } downloadsPageLink.setVisible(false); messageText.setText(null); - mountName.setText(newVault.getMountName()); + mountName.setText(vault.getMountName()); if (SystemUtils.IS_OS_WINDOWS) { chooseSelectedDriveLetter(); } - // trigger "default" change to refresh key bindings: - unlockButton.setDefaultButton(false); - unlockButton.setDefaultButton(true); + savePassword.setSelected(false); + // auto-fill pw from keychain: + if (keychainAccess.isPresent()) { + char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + if (storedPw != null) { + savePassword.setSelected(true); + passwordField.setText(new String(storedPw)); + passwordField.selectRange(storedPw.length, storedPw.length); + Arrays.fill(storedPw, ' '); + } + } } // **************************************** @@ -188,14 +206,11 @@ public class UnlockController extends LocalizedFXMLViewController { } private void mountNameDidChange(ObservableValue property, String oldValue, String newValue) { - if (vault.get() == null) { - return; - } // newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents if (newValue.isEmpty()) { - mountName.setText(vault.get().getMountName()); + mountName.setText(vault.getMountName()); } else { - vault.get().setMountName(newValue); + vault.setMountName(newValue); } } @@ -242,20 +257,17 @@ public class UnlockController extends LocalizedFXMLViewController { } private void winDriveLetterDidChange(ObservableValue property, Character oldValue, Character newValue) { - if (vault.get() == null) { - return; - } - vault.get().setWinDriveLetter(newValue); + vault.setWinDriveLetter(newValue); settings.save(); } private void chooseSelectedDriveLetter() { assert SystemUtils.IS_OS_WINDOWS; // if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this: - if (driveLetters.getOccupiedDriveLetters().contains(vault.get().getWinDriveLetter())) { - vault.get().setWinDriveLetter(null); + if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) { + vault.setWinDriveLetter(null); } - final Character letter = vault.get().getWinDriveLetter(); + final Character letter = vault.getWinDriveLetter(); if (letter == null) { // first option is known to be 'auto-assign' due to #WinDriveLetterComparator. this.winDriveLetter.getSelectionModel().selectFirst(); @@ -275,10 +287,10 @@ public class UnlockController extends LocalizedFXMLViewController { progressIndicator.setVisible(true); downloadsPageLink.setVisible(false); CharSequence password = passwordField.getCharacters(); - asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run(); + asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run(); } - private void unlock(Vault vault, CharSequence password) { + private void unlock(CharSequence password) { try { vault.activateFrontend(frontendFactory.get(), settings, password); vault.reveal(); @@ -286,9 +298,15 @@ public class UnlockController extends LocalizedFXMLViewController { messageText.setText(null); listener.ifPresent(lstnr -> lstnr.didUnlock(vault)); }); + if (keychainAccess.isPresent() && savePassword.isSelected()) { + keychainAccess.get().storePassphrase(vault.getId(), password); + } else { + passwordField.swipe(); + } } catch (InvalidPassphraseException e) { Platform.runLater(() -> { messageText.setText(localization.getString("unlock.errorMessage.wrongPassword")); + passwordField.selectAll(); passwordField.requestFocus(); }); } catch (UnsupportedVaultFormatException e) { @@ -307,7 +325,6 @@ public class UnlockController extends LocalizedFXMLViewController { }); } finally { Platform.runLater(() -> { - passwordField.swipe(); mountName.setDisable(false); advancedOptionsButton.setDisable(false); progressIndicator.setVisible(false); 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 a34e2aeb6..8e6a4e97f 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 @@ -27,11 +27,11 @@ import javafx.scene.control.ProgressIndicator; public class UpgradeController extends LocalizedFXMLViewController { - final ObjectProperty vault = new SimpleObjectProperty<>(); - final ObjectProperty> strategy = new SimpleObjectProperty<>(); + private final ObjectProperty> strategy = new SimpleObjectProperty<>(); private final UpgradeStrategies strategies; private final AsyncTaskService asyncTaskService; private Optional listener = Optional.empty(); + private Vault vault; @Inject public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) { @@ -67,8 +67,6 @@ public class UpgradeController extends LocalizedFXMLViewController { BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not()); BooleanExpression syncFinished = confirmationCheckbox.selectedProperty(); upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not())); - - EasyBind.subscribe(vault, this::vaultDidChange); } @Override @@ -76,9 +74,10 @@ public class UpgradeController extends LocalizedFXMLViewController { return getClass().getResource("/fxml/upgrade.fxml"); } - private void vaultDidChange(Vault newVault) { + void setVault(Vault vault) { + this.vault = Objects.requireNonNull(vault); errorLabel.setText(null); - strategy.set(strategies.getUpgradeStrategy(newVault)); + strategy.set(strategies.getUpgradeStrategy(vault)); // trigger "default" change to refresh key bindings: upgradeButton.setDefaultButton(false); upgradeButton.setDefaultButton(true); @@ -89,7 +88,7 @@ public class UpgradeController extends LocalizedFXMLViewController { // **************************************** private String upgradeNotification(UpgradeStrategy instruction) { - return instruction.getNotification(vault.get()); + return instruction.getNotification(vault); } // **************************************** @@ -102,15 +101,14 @@ public class UpgradeController extends LocalizedFXMLViewController { } private void upgrade(UpgradeStrategy instruction) { - Vault v = Objects.requireNonNull(vault.getValue()); passwordField.setDisable(true); progressIndicator.setVisible(true); asyncTaskService // .asyncTaskOf(() -> { - if (!instruction.isApplicable(v)) { - throw new IllegalStateException("No ugprade needed for " + v.path().getValue()); + if (!instruction.isApplicable(vault)) { + throw new IllegalStateException("No ugprade needed for " + vault.path().getValue()); } - instruction.upgrade(v, passwordField.getCharacters()); + instruction.upgrade(vault, passwordField.getCharacters()); }) // .onSuccess(this::showNextUpgrade) // .onError(UpgradeFailedException.class, e -> { @@ -125,7 +123,7 @@ public class UpgradeController extends LocalizedFXMLViewController { private void showNextUpgrade() { errorLabel.setText(null); - Optional nextStrategy = strategies.getUpgradeStrategy(vault.getValue()); + Optional nextStrategy = strategies.getUpgradeStrategy(vault); if (nextStrategy.isPresent()) { strategy.set(nextStrategy); } else { 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 07c83530d..8c0c00fc3 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 @@ -12,6 +12,7 @@ import static org.apache.commons.lang3.StringUtils.stripStart; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; @@ -30,6 +31,7 @@ import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.LazyInitializer; import org.cryptomator.common.Optionals; import org.cryptomator.crypto.engine.InvalidPassphraseException; +import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem; import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate; @@ -109,8 +111,10 @@ public class Vault implements CryptoFileSystemDelegate { public void create(CharSequence passphrase) throws IOException { try { FileSystem fs = getNioFileSystem(); - if (fs.children().count() > 0) { - throw new FileAlreadyExistsException(null, null, "Vault location not empty."); + if (fs.files().map(File::name).filter(s -> s.endsWith(VAULT_FILE_EXTENSION)).count() > 0) { + throw new FileAlreadyExistsException("masterkey.cryptomator", null, "Vault location not empty."); + } else if (fs.folders().count() > 0) { + throw new DirectoryNotEmptyException(fs.toString()); } cryptoFileSystemFactory.initializeNew(fs, passphrase); } catch (UncheckedIOException e) { diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 851d33fc9..0a8ea2fb8 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -68,12 +68,16 @@ -