Merge branch 'develop' into feature/#1013-#1061-cleanupAndInformation

This commit is contained in:
Sebastian Stenzel
2020-11-06 15:17:45 +01:00
committed by GitHub
52 changed files with 647 additions and 1376 deletions
@@ -9,6 +9,7 @@ import com.tobiasdiez.easybind.EasyBind;
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.keychain.KeychainModule;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault;
@@ -33,7 +34,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Module(subcomponents = {VaultComponent.class})
@Module(subcomponents = {VaultComponent.class}, includes = {KeychainModule.class})
public abstract class CommonsModule {
private static final Logger LOG = LoggerFactory.getLogger(CommonsModule.class);
@@ -1,32 +0,0 @@
/*******************************************************************************
* Copyright (c) 2019 Skymatic GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.jni.JniFunctions;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import javax.inject.Singleton;
import java.util.Optional;
@Module
public class JniModule {
@Provides
@Singleton
Optional<MacFunctions> provideOptionalMacFunctions() {
return JniFunctions.macFunctions();
}
@Provides
@Singleton
Optional<WinFunctions> provideOptionalWinFunctions() {
return JniFunctions.winFunctions();
}
}
@@ -0,0 +1,126 @@
package org.cryptomator.common.keychain;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.application.Platform;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.Arrays;
@Singleton
public class KeychainManager implements KeychainAccessProvider {
private final ObjectExpression<KeychainAccessProvider> keychain;
private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;
@Inject
KeychainManager(ObjectExpression<KeychainAccessProvider> selectedKeychain) {
this.keychain = selectedKeychain;
this.passphraseStoredProperties = CacheBuilder.newBuilder() //
.weakValues() //
.build(CacheLoader.from(this::createStoredPassphraseProperty));
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
}
private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
var result = keychain.getValue();
if (result == null) {
throw new NoKeychainAccessProviderException();
}
return result;
}
@Override
public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
getKeychainOrFail().storePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public char[] loadPassphrase(String key) throws KeychainAccessException {
char[] passphrase = getKeychainOrFail().loadPassphrase(key);
setPassphraseStored(key, passphrase != null);
return passphrase;
}
@Override
public void deletePassphrase(String key) throws KeychainAccessException {
getKeychainOrFail().deletePassphrase(key);
setPassphraseStored(key, false);
}
@Override
public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
getKeychainOrFail().changePassphrase(key, passphrase);
setPassphraseStored(key, true);
}
@Override
public boolean isSupported() {
return keychain.getValue() != null;
}
/**
* Checks if the keychain knows a passphrase for the given key.
* <p>
* Expensive operation. If possible, use {@link #getPassphraseStoredProperty(String)} instead.
*
* @param key The key to look up
* @return <code>true</code> if a password for <code>key</code> is stored.
* @throws KeychainAccessException
*/
public boolean isPassphraseStored(String key) throws KeychainAccessException {
char[] storedPw = null;
try {
storedPw = getKeychainOrFail().loadPassphrase(key);
return storedPw != null;
} finally {
if (storedPw != null) {
Arrays.fill(storedPw, ' ');
}
}
}
private void setPassphraseStored(String key, boolean value) {
BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
if (property != null) {
if (Platform.isFxApplicationThread()) {
property.set(value);
} else {
Platform.runLater(() -> property.set(value));
}
}
}
/**
* Returns an observable property for use in the UI that tells whether a passphrase is stored for the given key.
* <p>
* Assuming that this process is the only process modifying Cryptomator-related items in the system keychain, this
* property stays in memory in an attempt to avoid unnecessary calls to the system keychain. Note that due to this
* fact the value stored in the returned property is not 100% reliable. Code defensively!
*
* @param key The key to look up
* @return An observable property which is <code>true</code> when it almost certain that a password for <code>key</code> is stored.
* @see #isPassphraseStored(String)
*/
public ReadOnlyBooleanProperty getPassphraseStoredProperty(String key) {
return passphraseStoredProperties.getUnchecked(key);
}
private BooleanProperty createStoredPassphraseProperty(String key) {
try {
return new SimpleBooleanProperty(isPassphraseStored(key));
} catch (KeychainAccessException e) {
return new SimpleBooleanProperty(false);
}
}
}
@@ -0,0 +1,44 @@
package org.cryptomator.common.keychain;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import javax.inject.Singleton;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectExpression;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
@Module
public class KeychainModule {
@Provides
@Singleton
static Set<ServiceLoader.Provider<KeychainAccessProvider>> provideAvailableKeychainAccessProviderFactories() {
return ServiceLoader.load(KeychainAccessProvider.class).stream().collect(Collectors.toUnmodifiableSet());
}
@Provides
@Singleton
static Set<KeychainAccessProvider> provideSupportedKeychainAccessProviders(Set<ServiceLoader.Provider<KeychainAccessProvider>> availableFactories) {
return availableFactories.stream() //
.map(ServiceLoader.Provider::get) //
.filter(KeychainAccessProvider::isSupported) //
.collect(Collectors.toUnmodifiableSet());
}
@Provides
@Singleton
static ObjectExpression<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> providers) {
return Bindings.createObjectBinding(() -> {
var selectedProviderClass = settings.keychainBackend().get().getProviderClass();
var selectedProvider = providers.stream().filter(provider -> provider.getClass().getName().equals(selectedProviderClass)).findAny();
var fallbackProvider = providers.stream().findAny().orElse(null);
return selectedProvider.orElse(fallbackProvider);
}, settings.keychainBackend());
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.common.keychain;
import org.cryptomator.integrations.keychain.KeychainAccessException;
/**
* Thrown by {@link KeychainManager} if attempted to access a keychain despite no supported keychain access provider being available.
*/
public class NoKeychainAccessProviderException extends KeychainAccessException {
public NoKeychainAccessProviderException() {
super("Did not find any supported keychain access provider.");
}
}
@@ -1,39 +1,19 @@
package org.cryptomator.common.settings;
import org.apache.commons.lang3.SystemUtils;
import java.util.Arrays;
public enum KeychainBackend {
GNOME("preferences.general.keychainBackend.gnome", SystemUtils.IS_OS_LINUX), //
KDE("preferences.general.keychainBackend.kde", SystemUtils.IS_OS_LINUX), //
MAC_SYSTEM_KEYCHAIN("preferences.general.keychainBackend.macSystemKeychain", SystemUtils.IS_OS_MAC), //
WIN_SYSTEM_KEYCHAIN("preferences.general.keychainBackend.winSystemKeychain", SystemUtils.IS_OS_WINDOWS);
GNOME("org.cryptomator.linux.keychain.SecretServiceKeychainAccess"),
KDE("org.cryptomator.linux.keychain.KDEWalletKeychainAccess"),
MAC_SYSTEM_KEYCHAIN("org.cryptomator.macos.keychain.MacSystemKeychainAccess"),
WIN_SYSTEM_KEYCHAIN("org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess");
public static KeychainBackend[] supportedBackends() {
return Arrays.stream(values()).filter(KeychainBackend::isSupported).toArray(KeychainBackend[]::new);
private final String providerClass;
KeychainBackend(String providerClass) {
this.providerClass = providerClass;
}
public static KeychainBackend defaultBackend() {
if (SystemUtils.IS_OS_LINUX) {
return KeychainBackend.GNOME;
} else { // SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS
return Arrays.stream(KeychainBackend.supportedBackends()).findFirst().orElseThrow(IllegalStateException::new);
}
public String getProviderClass() {
return providerClass;
}
private final String configName;
private final boolean isSupported;
KeychainBackend(String configName, boolean isSupported) {
this.configName = configName;
this.isSupported = isSupported;
}
public String getDisplayName() {
return configName;
}
public boolean isSupported() { return isSupported; }
}
@@ -8,6 +8,8 @@
******************************************************************************/
package org.cryptomator.common.settings;
import org.apache.commons.lang3.SystemUtils;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
@@ -33,9 +35,9 @@ public class Settings {
public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
public static final WebDavUrlScheme DEFAULT_GVFS_SCHEME = WebDavUrlScheme.DAV;
public static final boolean DEFAULT_DEBUG_MODE = false;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = System.getProperty("os.name").toLowerCase().contains("windows") ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final VolumeImpl DEFAULT_PREFERRED_VOLUME_IMPL = SystemUtils.IS_OS_WINDOWS ? VolumeImpl.DOKANY : VolumeImpl.FUSE;
public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = KeychainBackend.defaultBackend();
public static final KeychainBackend DEFAULT_KEYCHAIN_BACKEND = SystemUtils.IS_OS_WINDOWS ? KeychainBackend.WIN_SYSTEM_KEYCHAIN : SystemUtils.IS_OS_MAC ? KeychainBackend.MAC_SYSTEM_KEYCHAIN : KeychainBackend.GNOME;
public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
private static final String DEFAULT_LICENSE_KEY = "";
@@ -0,0 +1,58 @@
package org.cryptomator.common.keychain;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class KeychainManagerTest {
@Test
public void testStoreAndLoad() throws KeychainAccessException {
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
keychainManager.storePassphrase("test", "asd");
Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
}
@Nested
public static class WhenObservingProperties {
@BeforeAll
public static void startup() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
Platform.startup(latch::countDown);
latch.await(5, TimeUnit.SECONDS);
}
@Test
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
Assertions.assertEquals(false, property.get());
keychainManager.storePassphrase("test", "bar");
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
result.set(property.get());
latch.countDown();
});
latch.await(1, TimeUnit.SECONDS);
Assertions.assertEquals(true, result.get());
}
}
}
@@ -0,0 +1,47 @@
/*******************************************************************************
* Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the accompanying LICENSE file.
*******************************************************************************/
package org.cryptomator.common.keychain;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import java.util.HashMap;
import java.util.Map;
class MapKeychainAccess implements KeychainAccessProvider {
private final Map<String, char[]> 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 void changePassphrase(String key, CharSequence passphrase) {
map.get(key);
storePassphrase(key, passphrase);
}
@Override
public boolean isSupported() {
return true;
}
}