From 34af30630926a391af4fd8c7eb45c7e0b71bdf61 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 29 Aug 2016 17:16:56 +0200 Subject: [PATCH 01/10] defined keychain access interfaces --- main/keychain/pom.xml | 39 ++++++++++++++++ .../cryptomator/keychain/KeychainAccess.java | 19 ++++++++ .../keychain/KeychainAccessStrategy.java | 10 +++++ .../keychain/KeychainComponent.java | 15 +++++++ .../cryptomator/keychain/KeychainModule.java | 26 +++++++++++ .../keychain/MacSystemKeychainAccess.java | 45 +++++++++++++++++++ .../keychain/WindowsSystemKeychainAccess.java | 45 +++++++++++++++++++ .../keychain/KeychainModuleTest.java | 17 +++++++ .../keychain/KeychainTestModule.java | 14 ++++++ .../keychain/MapKeychainAccess.java | 25 +++++++++++ main/pom.xml | 1 + 11 files changed, 256 insertions(+) create mode 100644 main/keychain/pom.xml create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.java create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java create mode 100644 main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java create mode 100644 main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java create mode 100644 main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java create mode 100644 main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml new file mode 100644 index 000000000..4fab0ebc6 --- /dev/null +++ b/main/keychain/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + org.cryptomator + main + 1.2.0-SNAPSHOT + + keychain + System Keychain Access + + + + org.apache.commons + commons-lang3 + + + org.bouncycastle + bcprov-jdk15on + 1.54 + + + + + 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..af2152ce6 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java @@ -0,0 +1,19 @@ +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. + */ + CharSequence loadPassphrase(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/KeychainComponent.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.java new file mode 100644 index 000000000..0264aa3fc --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.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) +public interface KeychainComponent { + + Optional keychainAccess(); + +} 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..ab593f674 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java @@ -0,0 +1,26 @@ +package org.cryptomator.keychain; + +import java.util.Optional; +import java.util.Set; + +import com.google.common.collect.Sets; + +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.ElementsIntoSet; + +@Module +public class KeychainModule { + + @Provides + @ElementsIntoSet + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsSystemKeychainAccess 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..55a5e6ce9 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -0,0 +1,45 @@ +package org.cryptomator.keychain; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; + +@Singleton +class MacSystemKeychainAccess implements KeychainAccessStrategy { + + private final KeyStore keyStore; + + @Inject + public MacSystemKeychainAccess() { + KeyStore ks; + try { + ks = KeyStore.getInstance("KeychainStore", "Apple"); + ks.load(null); + } catch (GeneralSecurityException | IOException e) { + ks = null; + } + this.keyStore = ks; + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + // TODO Auto-generated method stub + } + + @Override + public CharSequence loadPassphrase(String key) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_MAC_OSX && keyStore != null; + } + +} diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java new file mode 100644 index 000000000..61aed5af9 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java @@ -0,0 +1,45 @@ +package org.cryptomator.keychain; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; + +@Singleton +class WindowsSystemKeychainAccess implements KeychainAccessStrategy { + + private final KeyStore keyStore; + + @Inject + public WindowsSystemKeychainAccess() { + KeyStore ks; + try { + ks = KeyStore.getInstance("Windows-MY", "SunMSCAPI"); + ks.load(null); + } catch (GeneralSecurityException | IOException e) { + ks = null; + } + this.keyStore = ks; + } + + @Override + public void storePassphrase(String key, CharSequence passphrase) { + // TODO Auto-generated method stub + } + + @Override + public CharSequence loadPassphrase(String key) { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean isSupported() { + return SystemUtils.IS_OS_WINDOWS && keyStore != null; + } + +} 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..c15c8c1f1 --- /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 = DaggerKeychainComponent.builder().keychainModule(new KeychainTestModule()).build().keychainAccess(); + Assert.assertTrue(keychainAccess.isPresent()); + Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess); + } + +} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java new file mode 100644 index 000000000..e2636981b --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java @@ -0,0 +1,14 @@ +package org.cryptomator.keychain; + +import java.util.Set; + +import com.google.common.collect.Sets; + +public class KeychainTestModule extends KeychainModule { + + @Override + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsSystemKeychainAccess winKeychain) { + return Sets.newHashSet(new 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..0af7f0a51 --- /dev/null +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -0,0 +1,25 @@ +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) { + map.put(key, passphrase); + } + + @Override + public CharSequence loadPassphrase(String key) { + return map.get(key); + } + + @Override + public boolean isSupported() { + return true; + } + +} diff --git a/main/pom.xml b/main/pom.xml index 6640becc7..ef7cf60fb 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -273,6 +273,7 @@ frontend-webdav ui filesystem-charsets + keychain From dc58ba434a061b0ac6a759edb31d6b6e8f6c5b3f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 29 Aug 2016 20:14:48 +0200 Subject: [PATCH 02/10] Make Cryptomator a foreground app when restoring from status bar icon --- .../cryptomator/ui/CryptomatorComponent.java | 4 ++ .../org/cryptomator/ui/CryptomatorModule.java | 3 +- .../java/org/cryptomator/ui/ExitUtil.java | 9 ++++- .../org/cryptomator/ui/MainApplication.java | 3 ++ .../org/cryptomator/ui/jni/JniException.java | 24 ++++++++++++ .../org/cryptomator/ui/jni/JniModule.java | 33 +++++++++++++++++ .../org/cryptomator/ui/jni/MacFunctions.java | 37 +++++++++++++++++++ 7 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java 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..7c047b213 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -8,11 +8,13 @@ *******************************************************************************/ package org.cryptomator.ui; +import java.util.Optional; import java.util.concurrent.ExecutorService; import javax.inject.Singleton; import org.cryptomator.ui.controllers.MainController; +import org.cryptomator.ui.jni.MacFunctions; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DeferredCloser; @@ -34,4 +36,6 @@ interface CryptomatorComponent { Localization localization(); ExitUtil exitUtil(); + + Optional nativeMacFunctions(); } \ No newline at end of file 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 b62f631c4..7d8d86d0c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -19,6 +19,7 @@ import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; +import org.cryptomator.ui.jni.JniModule; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; @@ -33,7 +34,7 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, JniModule.class}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); 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..8bbac3ea6 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,8 @@ import javax.script.ScriptException; import javax.swing.SwingUtilities; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.jni.JniException; +import org.cryptomator.ui.jni.MacFunctions; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.settings.Settings; import org.slf4j.Logger; @@ -48,12 +51,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 +93,7 @@ class ExitUtil { if (Platform.isImplicitExit()) { exitCommand.run(); } else { + macFunctions.ifPresent(JniException.ignore(MacFunctions::transformToAgentApplication)); mainWindow.close(); this.showTrayNotification(trayIcon); } @@ -189,6 +195,7 @@ class ExitUtil { private void restoreFromTray(ActionEvent event) { Platform.runLater(() -> { + macFunctions.ifPresent(JniException.ignore(MacFunctions::transformToForegroundApplication)); mainWindow.show(); mainWindow.requestFocus(); }); diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index d28721ffa..a6c4509de 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -16,6 +16,8 @@ import java.util.concurrent.ExecutionException; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.controllers.MainController; +import org.cryptomator.ui.jni.JniException; +import org.cryptomator.ui.jni.MacFunctions; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.DeferredCloser; import org.cryptomator.ui.util.SingleInstanceManager; @@ -67,6 +69,7 @@ public class MainApplication extends Application { } // show window and start observing its focus: + comp.nativeMacFunctions().ifPresent(JniException.ignore(MacFunctions::transformToForegroundApplication)); primaryStage.show(); ActiveWindowStyleSupport.startObservingFocus(primaryStage); comp.exitUtil().initExitHandler(this::quit); diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java b/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java new file mode 100644 index 000000000..2fd06eb07 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java @@ -0,0 +1,24 @@ +package org.cryptomator.ui.jni; + +import java.util.function.Consumer; + +/** + * Thrown to indicate that a JNI call didn't succeed, i.e. returned an unexpected return value. + */ +public class JniException extends RuntimeException { + + protected JniException(String message) { + super(message); + } + + public static Consumer ignore(Consumer consumer) { + return value -> { + try { + consumer.accept(value); + } catch (RuntimeException e) { + // no-op + } + }; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java b/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java new file mode 100644 index 000000000..de72d0228 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java @@ -0,0 +1,33 @@ +package org.cryptomator.ui.jni; + +import java.util.Optional; + +import javax.inject.Singleton; + +import org.apache.commons.lang3.SystemUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dagger.Module; +import dagger.Provides; + +@Module +public class JniModule { + + private static final Logger LOG = LoggerFactory.getLogger(JniModule.class); + + @Provides + @Singleton + Optional provideMacFunctions(MacFunctions macFunctions) { + if (SystemUtils.IS_OS_MAC) { + try { + System.loadLibrary("MacFunctions"); + return Optional.of(macFunctions); + } catch (UnsatisfiedLinkError e) { + LOG.error("Could not load JNI lib from path {}", System.getProperty("java.library.path")); + } + } + return Optional.empty(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java b/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java new file mode 100644 index 000000000..3cc58d7db --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java @@ -0,0 +1,37 @@ +package org.cryptomator.ui.jni; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class MacFunctions { + + @Inject + MacFunctions() { + } + + /** + * Makes the current application a foreground application, which appears in the Dock and the Application Switcher. + */ + public void transformToForegroundApplication() { + int errorCode = transformToForegroundApplication0(); + if (errorCode != 0) { + throw new JniException("Failed to make app a foreground app. Error code " + errorCode); + } + } + + private native int transformToForegroundApplication0(); + + /** + * Makes the current application an agent app. Agent apps do not appear in the Dock or in the Force Quit window. + */ + public void transformToAgentApplication() { + int errorCode = transformToAgentApplication0(); + if (errorCode != 0) { + throw new JniException("Failed to make app an agent app. Error code " + errorCode); + } + } + + private native int transformToAgentApplication0(); + +} From e0ae50378fac717d2615bb64d7f127b800913bdd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 30 Aug 2016 17:19:45 +0200 Subject: [PATCH 03/10] externalized JNI bindings --- .../cryptomator/common/LazyInitializer.java | 19 +++++----- main/ui/pom.xml | 5 +++ .../cryptomator/ui/CryptomatorComponent.java | 5 ++- .../org/cryptomator/ui/CryptomatorModule.java | 12 +++++- .../java/org/cryptomator/ui/ExitUtil.java | 9 +++-- .../org/cryptomator/ui/MainApplication.java | 7 ++-- .../org/cryptomator/ui/jni/JniException.java | 24 ------------ .../org/cryptomator/ui/jni/JniModule.java | 33 ----------------- .../org/cryptomator/ui/jni/MacFunctions.java | 37 ------------------- 9 files changed, 37 insertions(+), 114 deletions(-) delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java 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/ui/pom.xml b/main/ui/pom.xml index 2c261d3ad..6ceb70390 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -54,6 +54,11 @@ org.cryptomator frontend-webdav + + org.cryptomator + jni + 1.0.0-SNAPSHOT + 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 7c047b213..e82e006e9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorComponent.java @@ -13,8 +13,8 @@ import java.util.concurrent.ExecutorService; import javax.inject.Singleton; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.controllers.MainController; -import org.cryptomator.ui.jni.MacFunctions; import org.cryptomator.ui.settings.Localization; import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DeferredCloser; @@ -38,4 +38,5 @@ interface CryptomatorComponent { ExitUtil exitUtil(); Optional nativeMacFunctions(); -} \ No newline at end of file + +} 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 7d8d86d0c..f4cc072ae 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -8,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.ui; +import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -19,7 +20,8 @@ import org.cryptomator.crypto.engine.impl.CryptoEngineModule; import org.cryptomator.frontend.FrontendFactory; import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; -import org.cryptomator.ui.jni.JniModule; +import org.cryptomator.jni.JniModule; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.settings.Settings; import org.cryptomator.ui.settings.SettingsProvider; @@ -34,7 +36,7 @@ import dagger.Provides; import javafx.application.Application; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, JniModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); @@ -100,4 +102,10 @@ class CryptomatorModule { return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new); } + @Provides + @Singleton + Optional provideMacFunctions() { + return JniModule.macFunctions(); + } + } 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 8bbac3ea6..7352d149c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/ExitUtil.java @@ -33,8 +33,9 @@ import javax.script.ScriptException; import javax.swing.SwingUtilities; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.ui.jni.JniException; -import org.cryptomator.ui.jni.MacFunctions; +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; @@ -93,7 +94,7 @@ class ExitUtil { if (Platform.isImplicitExit()) { exitCommand.run(); } else { - macFunctions.ifPresent(JniException.ignore(MacFunctions::transformToAgentApplication)); + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication)); mainWindow.close(); this.showTrayNotification(trayIcon); } @@ -195,7 +196,7 @@ class ExitUtil { private void restoreFromTray(ActionEvent event) { Platform.runLater(() -> { - macFunctions.ifPresent(JniException.ignore(MacFunctions::transformToForegroundApplication)); + macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication)); mainWindow.show(); mainWindow.requestFocus(); }); diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index a6c4509de..8e5f60b06 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -15,9 +15,10 @@ import java.nio.file.Path; import java.util.concurrent.ExecutionException; 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.controllers.MainController; -import org.cryptomator.ui.jni.JniException; -import org.cryptomator.ui.jni.MacFunctions; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.DeferredCloser; import org.cryptomator.ui.util.SingleInstanceManager; @@ -69,7 +70,7 @@ public class MainApplication extends Application { } // show window and start observing its focus: - comp.nativeMacFunctions().ifPresent(JniException.ignore(MacFunctions::transformToForegroundApplication)); + comp.nativeMacFunctions().map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication)); primaryStage.show(); ActiveWindowStyleSupport.startObservingFocus(primaryStage); comp.exitUtil().initExitHandler(this::quit); diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java b/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java deleted file mode 100644 index 2fd06eb07..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/jni/JniException.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.cryptomator.ui.jni; - -import java.util.function.Consumer; - -/** - * Thrown to indicate that a JNI call didn't succeed, i.e. returned an unexpected return value. - */ -public class JniException extends RuntimeException { - - protected JniException(String message) { - super(message); - } - - public static Consumer ignore(Consumer consumer) { - return value -> { - try { - consumer.accept(value); - } catch (RuntimeException e) { - // no-op - } - }; - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java b/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java deleted file mode 100644 index de72d0228..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/jni/JniModule.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.cryptomator.ui.jni; - -import java.util.Optional; - -import javax.inject.Singleton; - -import org.apache.commons.lang3.SystemUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import dagger.Module; -import dagger.Provides; - -@Module -public class JniModule { - - private static final Logger LOG = LoggerFactory.getLogger(JniModule.class); - - @Provides - @Singleton - Optional provideMacFunctions(MacFunctions macFunctions) { - if (SystemUtils.IS_OS_MAC) { - try { - System.loadLibrary("MacFunctions"); - return Optional.of(macFunctions); - } catch (UnsatisfiedLinkError e) { - LOG.error("Could not load JNI lib from path {}", System.getProperty("java.library.path")); - } - } - return Optional.empty(); - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java b/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java deleted file mode 100644 index 3cc58d7db..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/jni/MacFunctions.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.cryptomator.ui.jni; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class MacFunctions { - - @Inject - MacFunctions() { - } - - /** - * Makes the current application a foreground application, which appears in the Dock and the Application Switcher. - */ - public void transformToForegroundApplication() { - int errorCode = transformToForegroundApplication0(); - if (errorCode != 0) { - throw new JniException("Failed to make app a foreground app. Error code " + errorCode); - } - } - - private native int transformToForegroundApplication0(); - - /** - * Makes the current application an agent app. Agent apps do not appear in the Dock or in the Force Quit window. - */ - public void transformToAgentApplication() { - int errorCode = transformToAgentApplication0(); - if (errorCode != 0) { - throw new JniException("Failed to make app an agent app. Error code " + errorCode); - } - } - - private native int transformToAgentApplication0(); - -} From 72e52df4e0712582f4fcab34ae48c956da186b25 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 30 Aug 2016 19:12:20 +0200 Subject: [PATCH 04/10] implemented keychain access on OS X --- main/keychain/pom.xml | 4 +++ .../keychain/MacSystemKeychainAccess.java | 26 ++++++++----------- main/pom.xml | 8 ++++++ main/ui/pom.xml | 1 - 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index 4fab0ebc6..1983d5873 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -18,6 +18,10 @@ bcprov-jdk15on 1.54 + + org.cryptomator + jni + diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java index 55a5e6ce9..465a17ec1 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -1,45 +1,41 @@ package org.cryptomator.keychain; -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; +import java.nio.CharBuffer; import javax.inject.Inject; import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.jni.JniModule; +import org.cryptomator.jni.MacKeychainAccess; @Singleton class MacSystemKeychainAccess implements KeychainAccessStrategy { - private final KeyStore keyStore; + private final MacKeychainAccess keychain; @Inject public MacSystemKeychainAccess() { - KeyStore ks; - try { - ks = KeyStore.getInstance("KeychainStore", "Apple"); - ks.load(null); - } catch (GeneralSecurityException | IOException e) { - ks = null; + if (JniModule.macFunctions().isPresent()) { + this.keychain = JniModule.macFunctions().get().getKeychainAccess(); + } else { + this.keychain = null; } - this.keyStore = ks; } @Override public void storePassphrase(String key, CharSequence passphrase) { - // TODO Auto-generated method stub + keychain.storePassword(key, passphrase); } @Override public CharSequence loadPassphrase(String key) { - // TODO Auto-generated method stub - return null; + return CharBuffer.wrap(keychain.loadPassword(key)); } @Override public boolean isSupported() { - return SystemUtils.IS_OS_MAC_OSX && keyStore != null; + return SystemUtils.IS_OS_MAC_OSX && keychain != null; } } diff --git a/main/pom.xml b/main/pom.xml index ef7cf60fb..e4ddb32c9 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -27,6 +27,7 @@ UTF-8 + 1.0.0-SNAPSHOT 2.1 1.7.7 4.12 @@ -122,6 +123,13 @@ ui ${project.version} + + + + org.cryptomator + jni + ${cryptomator.jni.version} + diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 6ceb70390..885a875bc 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -57,7 +57,6 @@ org.cryptomator jni - 1.0.0-SNAPSHOT From 6a15fa132a5c33cdd2d38c32b8ef3b2c1e5a60d9 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Tue, 30 Aug 2016 22:41:01 +0200 Subject: [PATCH 05/10] app launches as foreground app on mac --- main/ui/src/main/java/org/cryptomator/ui/MainApplication.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 8e5f60b06..d28721ffa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -15,9 +15,6 @@ import java.nio.file.Path; import java.util.concurrent.ExecutionException; 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.controllers.MainController; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.DeferredCloser; @@ -70,7 +67,6 @@ public class MainApplication extends Application { } // show window and start observing its focus: - comp.nativeMacFunctions().map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication)); primaryStage.show(); ActiveWindowStyleSupport.startObservingFocus(primaryStage); comp.exitUtil().initExitHandler(this::quit); From d61e5c5a0888c2a72e4aad6410f7621d979ef362 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Wed, 31 Aug 2016 01:08:58 +0200 Subject: [PATCH 06/10] added "delete passphrase" method to keychain access --- .../main/java/org/cryptomator/keychain/KeychainAccess.java | 7 +++++++ .../org/cryptomator/keychain/MacSystemKeychainAccess.java | 5 +++++ .../cryptomator/keychain/WindowsSystemKeychainAccess.java | 6 ++++++ .../java/org/cryptomator/keychain/MapKeychainAccess.java | 6 ++++++ 4 files changed, 24 insertions(+) diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java index af2152ce6..35e406752 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java @@ -16,4 +16,11 @@ public interface KeychainAccess { */ CharSequence 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/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java index 465a17ec1..c8ab83c8c 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -38,4 +38,9 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy { 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/WindowsSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java index 61aed5af9..cf8163141 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java @@ -37,6 +37,12 @@ class WindowsSystemKeychainAccess implements KeychainAccessStrategy { return null; } + @Override + public void deletePassphrase(String key) { + // TODO Auto-generated method stub + + } + @Override public boolean isSupported() { return SystemUtils.IS_OS_WINDOWS && keyStore != null; diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java index 0af7f0a51..dd06319cd 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -17,6 +17,12 @@ class MapKeychainAccess implements KeychainAccessStrategy { return map.get(key); } + @Override + public void deletePassphrase(String key) { + // TODO Auto-generated method stub + + } + @Override public boolean isSupported() { return true; From ce12af84955073597fe8639c6f19df9820c22e06 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 2 Sep 2016 15:49:09 +0200 Subject: [PATCH 07/10] Added save password functionality to UI --- .../cryptomator/keychain/KeychainAccess.java | 2 +- .../keychain/MacSystemKeychainAccess.java | 6 +- .../keychain/WindowsSystemKeychainAccess.java | 2 +- .../keychain/KeychainComponent.java | 2 +- .../keychain/MapKeychainAccess.java | 10 ++- main/pom.xml | 8 +- main/ui/pom.xml | 4 + .../org/cryptomator/ui/CryptomatorModule.java | 3 +- .../controllers/ChangePasswordController.java | 19 ++-- .../ui/controllers/InitializeController.java | 17 ++-- .../ui/controllers/MainController.java | 13 ++- .../ui/controllers/UnlockController.java | 89 +++++++++++-------- .../ui/controllers/UpgradeController.java | 22 +++-- main/ui/src/main/resources/fxml/unlock.fxml | 12 ++- .../ui/src/main/resources/localization/en.txt | 1 + 15 files changed, 121 insertions(+), 89 deletions(-) rename main/keychain/src/{main => test}/java/org/cryptomator/keychain/KeychainComponent.java (85%) diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java index 35e406752..abd7a3ee8 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccess.java @@ -14,7 +14,7 @@ public interface KeychainAccess { * @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. */ - CharSequence loadPassphrase(String key); + char[] loadPassphrase(String key); /** * Deletes a passphrase with a given key. diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java index c8ab83c8c..a80d084ef 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -1,7 +1,5 @@ package org.cryptomator.keychain; -import java.nio.CharBuffer; - import javax.inject.Inject; import javax.inject.Singleton; @@ -29,8 +27,8 @@ class MacSystemKeychainAccess implements KeychainAccessStrategy { } @Override - public CharSequence loadPassphrase(String key) { - return CharBuffer.wrap(keychain.loadPassword(key)); + public char[] loadPassphrase(String key) { + return keychain.loadPassword(key); } @Override diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java index cf8163141..47d56953c 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java @@ -32,7 +32,7 @@ class WindowsSystemKeychainAccess implements KeychainAccessStrategy { } @Override - public CharSequence loadPassphrase(String key) { + public char[] loadPassphrase(String key) { // TODO Auto-generated method stub return null; } diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainComponent.java similarity index 85% rename from main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.java rename to main/keychain/src/test/java/org/cryptomator/keychain/KeychainComponent.java index 0264aa3fc..44301ed7f 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainComponent.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainComponent.java @@ -8,7 +8,7 @@ import dagger.Component; @Singleton @Component(modules = KeychainModule.class) -public interface KeychainComponent { +interface KeychainComponent { Optional keychainAccess(); diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java index dd06319cd..081a0b0e6 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -5,15 +5,19 @@ import java.util.Map; class MapKeychainAccess implements KeychainAccessStrategy { - private final Map map = new HashMap<>(); + private final Map map = new HashMap<>(); @Override public void storePassphrase(String key, CharSequence passphrase) { - map.put(key, 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 CharSequence loadPassphrase(String key) { + public char[] loadPassphrase(String key) { return map.get(key); } diff --git a/main/pom.xml b/main/pom.xml index bb9d47a8b..1a412a88a 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -58,7 +58,6 @@ ${project.version} test - org.cryptomator filesystem-api @@ -107,7 +106,6 @@ filesystem-stats ${project.version} - org.cryptomator frontend-api @@ -118,7 +116,11 @@ frontend-webdav ${project.version} - + + org.cryptomator + keychain + ${project.version} + org.cryptomator ui diff --git a/main/ui/pom.xml b/main/ui/pom.xml index bf6ab3fec..a0e7a0749 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -58,6 +58,10 @@ org.cryptomator jni + + org.cryptomator + keychain + 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 473e213d0..3de672b47 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -25,6 +25,7 @@ import org.cryptomator.frontend.webdav.WebDavModule; import org.cryptomator.frontend.webdav.WebDavServer; import org.cryptomator.jni.JniModule; import org.cryptomator.jni.MacFunctions; +import org.cryptomator.keychain.KeychainModule; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultObjectMapperProvider; import org.cryptomator.ui.model.Vaults; @@ -42,7 +43,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}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); 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..9a61f28c9 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 @@ -13,6 +13,7 @@ import java.io.IOException; import java.io.UncheckedIOException; import java.net.URL; 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,7 +120,7 @@ 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")); 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/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 @@ - + + com.google.code.gson + gson + 2.7 + + + commons-codec + commons-codec + org.bouncycastle bcprov-jdk15on diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java index ab593f674..3f2cf6abc 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java @@ -3,18 +3,20 @@ 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 +@Module(includes = {JniModule.class}) public class KeychainModule { @Provides @ElementsIntoSet - Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsSystemKeychainAccess winKeychain) { + Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) { return Sets.newHashSet(macKeychain, winKeychain); } diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java index a80d084ef..5e8b27b6c 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java @@ -1,21 +1,21 @@ package org.cryptomator.keychain; +import java.util.Optional; + import javax.inject.Inject; -import javax.inject.Singleton; import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.jni.JniModule; +import org.cryptomator.jni.MacFunctions; import org.cryptomator.jni.MacKeychainAccess; -@Singleton class MacSystemKeychainAccess implements KeychainAccessStrategy { private final MacKeychainAccess keychain; @Inject - public MacSystemKeychainAccess() { - if (JniModule.macFunctions().isPresent()) { - this.keychain = JniModule.macFunctions().get().getKeychainAccess(); + public MacSystemKeychainAccess(Optional macFunctions) { + if (macFunctions.isPresent()) { + this.keychain = macFunctions.get().keychainAccess(); } else { this.keychain = null; } 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..205772d15 --- /dev/null +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java @@ -0,0 +1,188 @@ +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; + } + final String keychainPathProperty = System.getProperty("cryptomator.keychainPath"); + if (dataProtection != null && keychainPathProperty == null) { + LOG.warn("Windows DataProtection module loaded, but no keychainPath configured."); + } + if (keychainPathProperty != null) { + 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/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java deleted file mode 100644 index 47d56953c..000000000 --- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsSystemKeychainAccess.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.cryptomator.keychain; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.security.KeyStore; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import org.apache.commons.lang3.SystemUtils; - -@Singleton -class WindowsSystemKeychainAccess implements KeychainAccessStrategy { - - private final KeyStore keyStore; - - @Inject - public WindowsSystemKeychainAccess() { - KeyStore ks; - try { - ks = KeyStore.getInstance("Windows-MY", "SunMSCAPI"); - ks.load(null); - } catch (GeneralSecurityException | IOException e) { - ks = null; - } - this.keyStore = ks; - } - - @Override - public void storePassphrase(String key, CharSequence passphrase) { - // TODO Auto-generated method stub - } - - @Override - public char[] loadPassphrase(String key) { - // TODO Auto-generated method stub - return null; - } - - @Override - public void deletePassphrase(String key) { - // TODO Auto-generated method stub - - } - - @Override - public boolean isSupported() { - return SystemUtils.IS_OS_WINDOWS && keyStore != null; - } - -} diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java index c15c8c1f1..2d8fe4e9e 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/KeychainModuleTest.java @@ -9,7 +9,7 @@ public class KeychainModuleTest { @Test public void testGetKeychain() { - Optional keychainAccess = DaggerKeychainComponent.builder().keychainModule(new KeychainTestModule()).build().keychainAccess(); + 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 index 081a0b0e6..8b3bb3e07 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java @@ -23,8 +23,7 @@ class MapKeychainAccess implements KeychainAccessStrategy { @Override public void deletePassphrase(String key) { - // TODO Auto-generated method stub - + map.remove(key); } @Override 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/KeychainComponent.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java similarity index 86% rename from main/keychain/src/test/java/org/cryptomator/keychain/KeychainComponent.java rename to main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java index 44301ed7f..30e42ee52 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainComponent.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainComponent.java @@ -8,7 +8,7 @@ import dagger.Component; @Singleton @Component(modules = KeychainModule.class) -interface KeychainComponent { +interface TestKeychainComponent { Optional keychainAccess(); diff --git a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java similarity index 62% rename from main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java rename to main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java index e2636981b..4bb403906 100644 --- a/main/keychain/src/test/java/org/cryptomator/keychain/KeychainTestModule.java +++ b/main/keychain/src/test/java/org/cryptomator/keychain/TestKeychainModule.java @@ -4,10 +4,10 @@ import java.util.Set; import com.google.common.collect.Sets; -public class KeychainTestModule extends KeychainModule { +public class TestKeychainModule extends KeychainModule { @Override - Set provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsSystemKeychainAccess winKeychain) { + 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 1a412a88a..4716aed5b 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -41,7 +41,7 @@ 3.1 2.4.4 1.10.19 - 2.4 + 2.6.1 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 3de672b47..d4f5f7b38 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java @@ -10,7 +10,6 @@ package org.cryptomator.ui; import static java.util.stream.Collectors.toList; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -24,7 +23,6 @@ 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.jni.MacFunctions; import org.cryptomator.keychain.KeychainModule; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultObjectMapperProvider; @@ -43,7 +41,7 @@ import javafx.application.Application; import javafx.beans.Observable; import javafx.stage.Stage; -@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class}) +@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class}) class CryptomatorModule { private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class); @@ -111,12 +109,6 @@ class CryptomatorModule { return closer.closeLater(webDavServer, WebDavServer::stop).get().orElseThrow(IllegalStateException::new); } - @Provides - @Singleton - Optional provideMacFunctions() { - return JniModule.macFunctions(); - } - private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) { webDavServer.setValidFrontendIds(vaults.stream() // .map(Vault::getId).map(FrontendId::from).collect(toList())); From 5c4bf2a207add4aeb9ccc94f2287dc69c1c5dd76 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 4 Sep 2016 12:27:23 +0200 Subject: [PATCH 09/10] support home-relative paths for cryptomator.keychainPath --- .../keychain/WindowsProtectedKeychainAccess.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java index 205772d15..a0ced12eb 100644 --- a/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java +++ b/main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java @@ -64,11 +64,14 @@ class WindowsProtectedKeychainAccess implements KeychainAccessStrategy { } else { this.dataProtection = null; } - final String keychainPathProperty = System.getProperty("cryptomator.keychainPath"); + String keychainPathProperty = System.getProperty("cryptomator.keychainPath"); if (dataProtection != null && keychainPathProperty == null) { - LOG.warn("Windows DataProtection module loaded, but no keychainPath configured."); + 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; From a63bcfbaa269627887c553a8ddc1b77b033db7ae Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 4 Sep 2016 16:04:16 +0200 Subject: [PATCH 10/10] relaxed "vault not empty" check --- .../cryptomator/ui/controllers/InitializeController.java | 6 ++++-- main/ui/src/main/java/org/cryptomator/ui/model/Vault.java | 8 ++++++-- main/ui/src/main/resources/localization/en.txt | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) 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 9a61f28c9..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,8 +10,8 @@ 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; @@ -124,7 +124,9 @@ public class InitializeController extends LocalizedFXMLViewController { 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/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/localization/en.txt b/main/ui/src/main/resources/localization/en.txt index 1e72c083e..984884574 100644 --- a/main/ui/src/main/resources/localization/en.txt +++ b/main/ui/src/main/resources/localization/en.txt @@ -26,6 +26,7 @@ initialize.label.password=Password initialize.label.retypePassword=Retype password initialize.button.ok=Create vault initialize.messageLabel.alreadyInitialized=Vault already initialized +initialize.messageLabel.notEmpty=Vault not empty initialize.messageLabel.initializationFailed=Could not initialize vault. See log file for details. initialize.messageLabel.passwordStrength.0=Very weak initialize.messageLabel.passwordStrength.1=Weak