~exprez135/cryptomator-libre

82a42ea183d0f6de48f5483b33d2213ba335fa69 — Sebastian Stenzel 1 year, 1 month ago bfde26b
Removed keychain module. Implemented new KeychainManager in various UI controller classes
30 files changed, 126 insertions(+), 962 deletions(-)

R main/{keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java}
M main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
A main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java
R main/{keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java => commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java}
R main/{keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java => commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java}
D main/keychain/pom.xml
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java
D main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java
D main/keychain/src/main/java/org/cryptomator/keychain/LinuxKDEWalletKeychainAccessImpl.java
D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java
D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java
D main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java
D main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java
D main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java
D main/keychain/src/test/resources/log4j2.xml
M main/pom.xml
M main/ui/pom.xml
M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java
M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java
M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncherModule.java
M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java
M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java
M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java
M main/ui/src/main/resources/license/THIRD-PARTY.txt
R main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +27 -13
@@ 1,60 1,75 @@
package org.cryptomator.keychain;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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;

public class KeychainManager implements KeychainAccessStrategy {
@Singleton
public class KeychainManager implements KeychainAccessProvider {

	private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class);

	private final KeychainAccessStrategy keychain;
	private final ObjectExpression<KeychainAccessProvider> keychain;
	private LoadingCache<String, BooleanProperty> passphraseStoredProperties;

	KeychainManager(KeychainAccessStrategy keychain) {
		assert keychain.isSupported();
		this.keychain = keychain;
	@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 {
		keychain.storePassphrase(key, passphrase);
		getKeychainOrFail().storePassphrase(key, passphrase);
		setPassphraseStored(key, true);
	}

	@Override
	public char[] loadPassphrase(String key) throws KeychainAccessException {
		char[] passphrase = keychain.loadPassphrase(key);
		char[] passphrase = getKeychainOrFail().loadPassphrase(key);
		setPassphraseStored(key, passphrase != null);
		return passphrase;
	}

	@Override
	public void deletePassphrase(String key) throws KeychainAccessException {
		keychain.deletePassphrase(key);
		getKeychainOrFail().deletePassphrase(key);
		setPassphraseStored(key, false);
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		keychain.changePassphrase(key, passphrase);
		getKeychainOrFail().changePassphrase(key, passphrase);
		setPassphraseStored(key, true);
	}

	@Override
	public boolean isSupported() {
		return true;
		return keychain.getValue() != null;
	}

	/**


@@ 69,7 84,7 @@ public class KeychainManager implements KeychainAccessStrategy {
	public boolean isPassphraseStored(String key) throws KeychainAccessException {
		char[] storedPw = null;
		try {
			storedPw = keychain.loadPassphrase(key);
			storedPw = getKeychainOrFail().loadPassphrase(key);
			return storedPw != null;
		} finally {
			if (storedPw != null) {


@@ 107,7 122,6 @@ public class KeychainManager implements KeychainAccessStrategy {

	private BooleanProperty createStoredPassphraseProperty(String key) {
		try {
			LOG.warn("LOAD"); // TODO remove
			return new SimpleBooleanProperty(isPassphraseStored(key));
		} catch (KeychainAccessException e) {
			return new SimpleBooleanProperty(false);

M main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java => main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java +3 -1
@@ 8,6 8,8 @@ import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import javax.inject.Singleton;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.value.ObservableValue;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;


@@ 29,7 31,7 @@ public class KeychainModule {

	@Provides
	@Singleton
	static ObjectBinding<KeychainAccessProvider> provideKeychainAccessProvider(Settings settings, Set<KeychainAccessProvider> providers) {
	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();

A main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java => main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java +13 -0
@@ 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.");
	}
}

R main/keychain/src/test/java/org/cryptomator/keychain/KeychainManagerTest.java => main/commons/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java +7 -4
@@ 1,23 1,26 @@
package org.cryptomator.keychain;
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;


class KeychainManagerTest {
public class KeychainManagerTest {

	@Test
	public void testStoreAndLoad() throws KeychainAccessException {
		KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
		KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
		keychainManager.storePassphrase("test", "asd");
		Assertions.assertArrayEquals("asd".toCharArray(), keychainManager.loadPassphrase("test"));
	}


@@ 34,7 37,7 @@ class KeychainManagerTest {

		@Test
		public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
			KeychainManager keychainManager = new KeychainManager(new MapKeychainAccess());
			KeychainManager keychainManager = new KeychainManager(new SimpleObjectProperty<>(new MapKeychainAccess()));
			ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
			Assertions.assertEquals(false, property.get());


R main/keychain/src/test/java/org/cryptomator/keychain/MapKeychainAccess.java => main/commons/src/test/java/org/cryptomator/common/keychain/MapKeychainAccess.java +4 -2
@@ 3,12 3,14 @@
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the accompanying LICENSE file.
 *******************************************************************************/
package org.cryptomator.keychain;
package org.cryptomator.common.keychain;

import org.cryptomator.integrations.keychain.KeychainAccessProvider;

import java.util.HashMap;
import java.util.Map;

class MapKeychainAccess implements KeychainAccessStrategy {
class MapKeychainAccess implements KeychainAccessProvider {

	private final Map<String, char[]> map = new HashMap<>();


D main/keychain/pom.xml => main/keychain/pom.xml +0 -69
@@ 1,69 0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.cryptomator</groupId>
		<artifactId>main</artifactId>
		<version>1.6.0-SNAPSHOT</version>
	</parent>
	<artifactId>keychain</artifactId>
	<name>System Keychain Access</name>

	<dependencies>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>commons</artifactId>
		</dependency>

		<!-- JavaFx -->
		<dependency>
			<groupId>org.openjfx</groupId>
			<artifactId>javafx-base</artifactId>
		</dependency>
		<dependency>
			<groupId>org.openjfx</groupId>
			<artifactId>javafx-graphics</artifactId>
		</dependency>

		<!-- Apache -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-lang3</artifactId>
		</dependency>

		<!-- Google -->
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
		</dependency>
		<dependency>
			<groupId>com.google.guava</groupId>
			<artifactId>guava</artifactId>
		</dependency>

		<!-- DI -->
		<dependency>
			<groupId>com.google.dagger</groupId>
			<artifactId>dagger</artifactId>
		</dependency>

		<!-- secret-service lib -->
		<dependency>
			<groupId>de.swiesend</groupId>
			<artifactId>secret-service</artifactId>
		</dependency>

		<!-- kdewallet lib -->
		<dependency>
			<groupId>org.purejava</groupId>
			<artifactId>kdewallet</artifactId>
		</dependency>

		<!-- Logging -->
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-simple</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
</project>
\ No newline at end of file

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessException.java +0 -12
@@ 1,12 0,0 @@
package org.cryptomator.keychain;

/**
 * Indicates an error during communication with the operating system's keychain.
 */
public class KeychainAccessException extends Exception {

	KeychainAccessException(Throwable cause) {
		super(cause);
	}

}

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainAccessStrategy.java +0 -46
@@ 1,46 0,0 @@
/*******************************************************************************
 * 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.keychain;

public interface KeychainAccessStrategy {

	/**
	 * 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) throws KeychainAccessException;

	/**
	 * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
	 * @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
	 */
	char[] loadPassphrase(String key) throws KeychainAccessException;

	/**
	 * 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) throws KeychainAccessException;

	/**
	 * Updates a passphrase with a given key. Noop, if there is no item for the given key.
	 *
	 * @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
	 * @param passphrase The secret to be updated in this keychain.
	 */
	void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException;

	/**
	 * @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
	 * @implNote This method must not throw any exceptions and should fail fast
	 * returning <code>false</code> if it can't determine availability of the checked strategy
	 */
	boolean isSupported();

}

D main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java => main/keychain/src/main/java/org/cryptomator/keychain/KeychainModule.java +0 -45
@@ 1,45 0,0 @@
/*******************************************************************************
 * 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.keychain;

import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import org.cryptomator.common.JniModule;

import javax.inject.Singleton;
import java.util.Optional;
import java.util.Set;

@Module(includes = {JniModule.class})
public abstract class KeychainModule {

	@Binds
	@IntoSet
	abstract KeychainAccessStrategy bindMacSystemKeychainAccess(MacSystemKeychainAccess keychainAccessStrategy);

	@Binds
	@IntoSet
	abstract KeychainAccessStrategy bindWindowsProtectedKeychainAccess(WindowsProtectedKeychainAccess keychainAccessStrategy);

	@Binds
	@IntoSet
	abstract KeychainAccessStrategy bindLinuxSystemKeychainAccess(LinuxSystemKeychainAccess keychainAccessStrategy);

	@Provides
	@Singleton
	static Optional<KeychainAccessStrategy> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
		return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).findFirst();
	}

	@Provides
	@Singleton
	public static Optional<KeychainManager> provideKeychainManager(Optional<KeychainAccessStrategy> keychainAccess) {
		return keychainAccess.map(KeychainManager::new);
	}

}

D main/keychain/src/main/java/org/cryptomator/keychain/LinuxKDEWalletKeychainAccessImpl.java => main/keychain/src/main/java/org/cryptomator/keychain/LinuxKDEWalletKeychainAccessImpl.java +0 -126
@@ 1,126 0,0 @@
package org.cryptomator.keychain;

import org.freedesktop.dbus.connections.impl.DBusConnection;
import org.freedesktop.dbus.exceptions.DBusException;
import org.kde.KWallet;
import org.kde.Static;
import org.purejava.KDEWallet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LinuxKDEWalletKeychainAccessImpl implements KeychainAccessStrategy {

	private static final Logger LOG = LoggerFactory.getLogger(LinuxKDEWalletKeychainAccessImpl.class);

	private final String FOLDER_NAME = "Cryptomator";
	private final String APP_NAME = "Cryptomator";
	private DBusConnection connection;
	private KDEWallet wallet;
	private int handle = -1;

	public LinuxKDEWalletKeychainAccessImpl() throws KeychainAccessException {
		try {
			connection = DBusConnection.getConnection(DBusConnection.DBusBusType.SESSION);
		} catch (DBusException e) {
			LOG.error("Connecting to D-Bus failed:", e);
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public boolean isSupported() {
		try {
			wallet = new KDEWallet(connection);
			return wallet.isEnabled();
		} catch (Exception e) {
			LOG.error("A KDEWallet could not be created:", e);
			return false;
		}
	}

	@Override
	public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		try {
			if (walletIsOpen() && //
					!(wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) //
							&& wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1) //
					&& wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) {
				LOG.debug("Passphrase successfully stored.");
			} else {
				LOG.debug("Passphrase was not stored.");
			}
		} catch (Exception e) {
			LOG.error("Storing the passphrase failed:", e);
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public char[] loadPassphrase(String key) throws KeychainAccessException {
		String password = "";
		try {
			if (walletIsOpen()) {
				password = wallet.readPassword(handle, FOLDER_NAME, key, APP_NAME);
				LOG.debug("loadPassphrase: wallet is open.");
			} else {
				LOG.debug("loadPassphrase: wallet is closed.");
			}
			return (password.equals("")) ? null : password.toCharArray();
		} catch (Exception e) {
			LOG.error("Loading the passphrase failed:", e);
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public void deletePassphrase(String key) throws KeychainAccessException {
		try {
			if (walletIsOpen() //
					&& wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) //
					&& wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 //
					&& wallet.removeEntry(handle, FOLDER_NAME, key, APP_NAME) == 0) {
				LOG.debug("Passphrase successfully deleted.");
			} else {
				LOG.debug("Passphrase was not deleted.");
			}
		} catch (Exception e) {
			LOG.error("Deleting the passphrase failed:", e);
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		try {
			if (walletIsOpen() //
					&& wallet.hasEntry(handle, FOLDER_NAME, key, APP_NAME) //
					&& wallet.entryType(handle, FOLDER_NAME, key, APP_NAME) == 1 //
					&& wallet.writePassword(handle, FOLDER_NAME, key, passphrase.toString(), APP_NAME) == 0) {
				LOG.debug("Passphrase successfully changed.");
			} else {
				LOG.debug("Passphrase could not be changed.");
			}
		} catch (Exception e) {
			LOG.error("Changing the passphrase failed:", e);
			throw new KeychainAccessException(e);
		}
	}

	private boolean walletIsOpen() throws KeychainAccessException {
		try {
			if (wallet.isOpen(Static.DEFAULT_WALLET)) {
				// This is needed due to KeechainManager loading the passphase directly
				if (handle == -1) handle = wallet.open(Static.DEFAULT_WALLET, 0, APP_NAME);
				return true;
			}
			wallet.openAsync(Static.DEFAULT_WALLET, 0, APP_NAME, false);
			wallet.getSignalHandler().await(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5, () -> null);
			handle = wallet.getSignalHandler().getLastHandledSignal(KWallet.walletAsyncOpened.class, Static.ObjectPaths.KWALLETD5).handle;
			LOG.debug("Wallet successfully initialized.");
			return handle != -1;
		} catch (Exception e) {
			LOG.error("Asynchronous opening the wallet failed:", e);
			throw new KeychainAccessException(e);
		}
	}
}

D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java => main/keychain/src/main/java/org/cryptomator/keychain/LinuxSecretServiceKeychainAccessImpl.java +0 -81
@@ 1,81 0,0 @@
package org.cryptomator.keychain;

import org.freedesktop.secret.simple.SimpleCollection;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

class LinuxSecretServiceKeychainAccessImpl implements KeychainAccessStrategy {

	private final String LABEL_FOR_SECRET_IN_KEYRING = "Cryptomator";

	@Override
	public boolean isSupported() {
		try (@SuppressWarnings("unused") SimpleCollection keyring = new SimpleCollection()) {
			// seems like we're able to access the keyring.
			return true;
		} catch (IOException | RuntimeException e) {
			return false;
		}
	}

	@Override
	public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list == null) {
				keyring.createItem(LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
			} else {
				changePassphrase(key, passphrase);
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public char[] loadPassphrase(String key) throws KeychainAccessException {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list != null) {
				return keyring.getSecret(list.get(0));
			} else {
				return null;
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public void deletePassphrase(String key) throws KeychainAccessException {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list != null) {
				keyring.deleteItem(list.get(0));
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);
		}
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		try (SimpleCollection keyring = new SimpleCollection()) {
			List<String> list = keyring.getItems(createAttributes(key));
			if (list != null) {
				keyring.updateItem(list.get(0), LABEL_FOR_SECRET_IN_KEYRING, passphrase, createAttributes(key));
			}
		} catch (IOException e) {
			throw new KeychainAccessException(e);
		}
	}

	private Map<String, String> createAttributes(String key) {
		Map<String, String> attributes = new HashMap();
		attributes.put("Vault", key);
		return attributes;
	}
}

D main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/LinuxSystemKeychainAccess.java +0 -106
@@ 1,106 0,0 @@
package org.cryptomator.keychain;

import javafx.beans.property.ObjectProperty;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.settings.KeychainBackend;
import org.cryptomator.common.settings.Settings;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.EnumSet;
import java.util.Optional;

/**
 * A facade to LinuxSecretServiceKeychainAccessImpl and LinuxKDEWalletKeychainAccessImpl
 * that depend on libraries that are unavailable on Mac and Windows.
 */
@Singleton
public class LinuxSystemKeychainAccess implements KeychainAccessStrategy {

	// the actual implementation is hidden in this delegate objects which are loaded via reflection,
	// as it depends on libraries that aren't necessarily available:
	private final Optional<KeychainAccessStrategy> delegate;
	private final Settings settings;
	private static EnumSet<KeychainBackend> availableKeychainBackends = EnumSet.noneOf(KeychainBackend.class);
	private static KeychainBackend backendActivated = null;
	private static boolean isGnomeKeyringAvailable;
	private static boolean isKdeWalletAvailable;

	@Inject
	public LinuxSystemKeychainAccess(Settings settings) {
		this.settings = settings;
		this.delegate = constructKeychainAccess();
	}

	private Optional<KeychainAccessStrategy> constructKeychainAccess() {
		try { // find out which backends are available
			Class<?> clazz = Class.forName("org.cryptomator.keychain.LinuxSecretServiceKeychainAccessImpl");
			KeychainAccessStrategy gnomeKeyring = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance();
			if (gnomeKeyring.isSupported()) {
				LinuxSystemKeychainAccess.availableKeychainBackends.add(KeychainBackend.GNOME);
				LinuxSystemKeychainAccess.isGnomeKeyringAvailable = true;
			}
			clazz = Class.forName("org.cryptomator.keychain.LinuxKDEWalletKeychainAccessImpl");
			KeychainAccessStrategy kdeWallet = (KeychainAccessStrategy) clazz.getDeclaredConstructor().newInstance();
			if (kdeWallet.isSupported()) {
				LinuxSystemKeychainAccess.availableKeychainBackends.add(KeychainBackend.KDE);
				LinuxSystemKeychainAccess.isKdeWalletAvailable = true;
			}

			// load password backend setting as the preferred backend
			ObjectProperty<KeychainBackend> pwSetting =  settings.keychainBackend();

			// check for GNOME keyring first, as this gets precedence over
			// KDE wallet as the former was implemented first
			if (isGnomeKeyringAvailable && pwSetting.get().equals(KeychainBackend.GNOME)) {
					pwSetting.setValue(KeychainBackend.GNOME);
					LinuxSystemKeychainAccess.backendActivated = KeychainBackend.GNOME;
					return Optional.of(gnomeKeyring);
			}

			if (isKdeWalletAvailable && pwSetting.get().equals(KeychainBackend.KDE)) {
					pwSetting.setValue(KeychainBackend.KDE);
					LinuxSystemKeychainAccess.backendActivated = KeychainBackend.KDE;
					return Optional.of(kdeWallet);
			}
			return Optional.empty();
		} catch (Exception e) {
			return Optional.empty();
		}
	}

	/* Getter/Setter */

	public static EnumSet<KeychainBackend> getAvailableKeychainBackends() {
		return availableKeychainBackends;
	}

	public static KeychainBackend getBackendActivated() {
		return backendActivated;
	}

	@Override
	public boolean isSupported() {
		return SystemUtils.IS_OS_LINUX && delegate.map(KeychainAccessStrategy::isSupported).orElse(false);
	}

	@Override
	public void storePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		delegate.orElseThrow(IllegalStateException::new).storePassphrase(key, passphrase);
	}

	@Override
	public char[] loadPassphrase(String key) throws KeychainAccessException {
		return delegate.orElseThrow(IllegalStateException::new).loadPassphrase(key);
	}

	@Override
	public void deletePassphrase(String key) throws KeychainAccessException {
		delegate.orElseThrow(IllegalStateException::new).deletePassphrase(key);
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) throws KeychainAccessException {
		delegate.orElseThrow(IllegalStateException::new).changePassphrase(key, passphrase);
	}
}

D main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/MacSystemKeychainAccess.java +0 -57
@@ 1,57 0,0 @@
/*******************************************************************************
 * 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.keychain;

import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.MacKeychainAccess;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Optional;

@Singleton
class MacSystemKeychainAccess implements KeychainAccessStrategy {

	private final Optional<MacFunctions> macFunctions;

	@Inject
	public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
		this.macFunctions = macFunctions;
	}

	private MacKeychainAccess keychain() {
		return macFunctions.orElseThrow(IllegalStateException::new).keychainAccess();
	}

	@Override
	public void storePassphrase(String key, CharSequence passphrase) {
		keychain().storePassword(key, passphrase);
	}

	@Override
	public char[] loadPassphrase(String key) {
		return keychain().loadPassword(key);
	}

	@Override
	public boolean isSupported() {
		return SystemUtils.IS_OS_MAC_OSX && macFunctions.isPresent();
	}

	@Override
	public void deletePassphrase(String key) {
		keychain().deletePassword(key);
	}

	@Override
	public void changePassphrase(String key, CharSequence passphrase) {
		if (keychain().deletePassword(key)) {
			keychain().storePassword(key, passphrase);
		}
	}

}

D main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java => main/keychain/src/main/java/org/cryptomator/keychain/WindowsProtectedKeychainAccess.java +0 -209
@@ 1,209 0,0 @@
/*******************************************************************************
 * 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.keychain;

import com.google.common.io.BaseEncoding;
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;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javax.inject.Singleton;
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.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.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

import static java.nio.charset.StandardCharsets.UTF_8;

@Singleton
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 Optional<WinFunctions> winFunctions;
	private final List<Path> keychainPaths;
	private Map<String, KeychainEntry> keychainEntries;

	@Inject
	public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions, Environment environment) {
		this.winFunctions = winFunctions;
		this.keychainPaths = environment.getKeychainPath().collect(Collectors.toList());
	}

	private WinDataProtection dataProtection() {
		return winFunctions.orElseThrow(IllegalStateException::new).dataProtection();
	}

	@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 void changePassphrase(String key, CharSequence passphrase) {
		loadKeychainEntriesIfNeeded();
		if (keychainEntries.remove(key) != null) {
			storePassphrase(key, passphrase);
		}
	}

	@Override
	public boolean isSupported() {
		return SystemUtils.IS_OS_WINDOWS && winFunctions.isPresent() && !keychainPaths.isEmpty();
	}

	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) {
			for (Path keychainPath : keychainPaths) {
				Optional<Map<String, KeychainEntry>> keychain = loadKeychainEntries(keychainPath);
				if (keychain.isPresent()) {
					keychainEntries = keychain.get();
					break;
				}
			}
		}
		if (keychainEntries == null) {
			LOG.info("Unable to load existing keychain file, creating new keychain.");
			keychainEntries = new HashMap<>();
		}
	}

	private Optional<Map<String, KeychainEntry>> loadKeychainEntries(Path keychainPath) {
		LOG.debug("Attempting to load keychain from {}", keychainPath);
		Type type = new TypeToken<Map<String, KeychainEntry>>() {
		}.getType();
		try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
			 Reader reader = new InputStreamReader(in, UTF_8)) {
			return Optional.of(GSON.fromJson(reader, type));
		} catch (NoSuchFileException | JsonParseException e) {
			return Optional.empty();
		} catch (IOException e) {
			throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
		}
	}

	private void saveKeychainEntries() {
		if (keychainPaths.isEmpty()) {
			throw new IllegalStateException("Can't save keychain if no keychain path is specified.");
		}
		saveKeychainEntries(keychainPaths.get(0));
	}

	private void saveKeychainEntries(Path keychainPath) {
		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<byte[]>, JsonDeserializer<byte[]> {

		private static final BaseEncoding BASE64 = BaseEncoding.base64();

		@Override
		public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
			return BASE64.decode(json.getAsString());
		}

		@Override
		public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
			return new JsonPrimitive(BASE64.encode(src));
		}

	}

}

D main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java => main/keychain/src/test/java/org/cryptomator/keychain/WindowsProtectedKeychainAccessTest.java +0 -56
@@ 1,56 0,0 @@
/*******************************************************************************
 * 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.keychain;

import org.cryptomator.common.Environment;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;

import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;

public class WindowsProtectedKeychainAccessTest {

	private WindowsProtectedKeychainAccess keychain;

	@BeforeEach
	public void setup(@TempDir Path tempDir) {
		Path keychainPath = tempDir.resolve("keychainfile.tmp");
		Environment env = Mockito.mock(Environment.class);
		Mockito.when(env.getKeychainPath()).thenReturn(Stream.of(keychainPath));
		WinFunctions winFunctions = Mockito.mock(WinFunctions.class);
		WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class);
		Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection);
		Answer<byte[]> answerReturningFirstArg = invocation -> ((byte[]) invocation.getArgument(0)).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), env);
	}

	@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"));
		Assertions.assertEquals(storedPw1, loadedPw1);
		Assertions.assertEquals(storedPw2, loadedPw2);
		keychain.deletePassphrase("myPassword");
		Assertions.assertNull(keychain.loadPassphrase("myPassword"));
		Assertions.assertNotNull(keychain.loadPassphrase("myOtherPassword"));
		Assertions.assertNull(keychain.loadPassphrase("nonExistingPassword"));
	}

}

D main/keychain/src/test/resources/log4j2.xml => main/keychain/src/test/resources/log4j2.xml +0 -33
@@ 1,33 0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
  Copyright (c) 2014 Markus Kreusch
  This file is licensed under the terms of the MIT license.
  See the LICENSE.txt file for more info.
  
  Contributors:
      Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<Configuration status="WARN">

	<Appenders>
		<Console name="Console" target="SYSTEM_OUT">
			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n"/>
			<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT"/>
		</Console>
		<Console name="StdErr" target="SYSTEM_ERR">
			<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n"/>
			<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY"/>
		</Console>
	</Appenders>

	<Loggers>
		<!-- show our own debug messages: -->
		<Logger name="org.cryptomator" level="DEBUG"/>
		<!-- mute dependencies: -->
		<Root level="INFO">
			<AppenderRef ref="Console"/>
			<AppenderRef ref="StdErr"/>
		</Root>
	</Loggers>

</Configuration>

M main/pom.xml => main/pom.xml +0 -18
@@ 70,11 70,6 @@
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>keychain</artifactId>
				<version>${project.version}</version>
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>ui</artifactId>
				<version>${project.version}</version>
			</dependency>


@@ 187,18 182,6 @@
				<version>${commons-lang3.version}</version>
			</dependency>

			<!-- Linux System Keychain -->
			<dependency> <!-- deprecated: will be replaced by integrations-api -->
				<groupId>de.swiesend</groupId>
				<artifactId>secret-service</artifactId>
				<version>${secret-service.version}</version>
			</dependency>
			<dependency> <!-- deprecated: will be replaced by integrations-api -->
				<groupId>org.purejava</groupId>
				<artifactId>kdewallet</artifactId>
				<version>${kdewallet.version}</version>
			</dependency>

			<!-- JWT -->
			<dependency>
				<groupId>com.auth0</groupId>


@@ 280,7 263,6 @@

	<modules>
		<module>commons</module>
		<module>keychain</module>
		<module>ui</module>
		<module>launcher</module>
	</modules>

M main/ui/pom.xml => main/ui/pom.xml +0 -4
@@ 12,10 12,6 @@
	<dependencies>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>keychain</artifactId>
		</dependency>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>commons</artifactId>
		</dependency>
		<dependency>

M main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java => main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +6 -6
@@ 1,10 1,10 @@
package org.cryptomator.ui.changepassword;

import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;


@@ 36,14 36,14 @@ public class ChangePasswordController implements FxController {
	private final Vault vault;
	private final ObjectProperty<CharSequence> newPassword;
	private final ErrorComponent.Builder errorComponent;
	private final Optional<KeychainManager> keychain;
	private final KeychainManager keychain;

	public NiceSecurePasswordField oldPasswordField;
	public CheckBox finalConfirmationCheckbox;
	public Button finishButton;

	@Inject
	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, Optional<KeychainManager> keychain) {
	public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
		this.window = window;
		this.vault = vault;
		this.newPassword = newPassword;


@@ 82,9 82,9 @@ public class ChangePasswordController implements FxController {
	}

	private void updatePasswordInSystemkeychain() {
		if (keychain.isPresent()) {
		if (keychain.isSupported()) {
			try {
				keychain.get().changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
				keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
				LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
			} catch (KeychainAccessException e) {
				LOG.error("Failed to update password in system keychain.", e);

M main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java => main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java +1 -5
@@ 3,7 3,6 @@ package org.cryptomator.ui.common;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.fxapp.FxApplicationScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@@ 14,7 13,6 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;


@@ 25,12 23,10 @@ public class VaultService {
	private static final Logger LOG = LoggerFactory.getLogger(VaultService.class);

	private final ExecutorService executorService;
	private final Optional<KeychainManager> keychain;

	@Inject
	public VaultService(ExecutorService executorService, Optional<KeychainManager> keychain) {
	public VaultService(ExecutorService executorService) {
		this.executorService = executorService;
		this.keychain = keychain;
	}

	public void reveal(Vault vault) {

M main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java => main/ui/src/main/java/org/cryptomator/ui/forgetPassword/ForgetPasswordController.java +6 -7
@@ 1,8 1,8 @@
package org.cryptomator.ui.forgetPassword;

import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@@ 11,7 11,6 @@ import javax.inject.Inject;
import javafx.beans.property.BooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;

@ForgetPasswordScoped
public class ForgetPasswordController implements FxController {


@@ 20,11 19,11 @@ public class ForgetPasswordController implements FxController {

	private final Stage window;
	private final Vault vault;
	private final Optional<KeychainManager> keychain;
	private final KeychainManager keychain;
	private final BooleanProperty confirmedResult;

	@Inject
	public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, Optional<KeychainManager> keychain, @ForgetPasswordWindow BooleanProperty confirmedResult) {
	public ForgetPasswordController(@ForgetPasswordWindow Stage window, @ForgetPasswordWindow Vault vault, KeychainManager keychain, @ForgetPasswordWindow BooleanProperty confirmedResult) {
		this.window = window;
		this.vault = vault;
		this.keychain = keychain;


@@ 38,9 37,9 @@ public class ForgetPasswordController implements FxController {

	@FXML
	public void finish() {
		if (keychain.isPresent()) {
		if (keychain.isSupported()) {
			try {
				keychain.get().deletePassphrase(vault.getId());
				keychain.deletePassphrase(vault.getId());
				LOG.debug("Forgot password for vault {}.", vault.getDisplayName());
				confirmedResult.setValue(true);
			} catch (KeychainAccessException e) {

M main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncherModule.java => main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncherModule.java +1 -2
@@ 3,7 3,6 @@ package org.cryptomator.ui.launcher;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.JniModule;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.fxapp.FxApplicationComponent;
import org.cryptomator.ui.traymenu.TrayMenuComponent;



@@ 13,7 12,7 @@ import java.util.ResourceBundle;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

@Module(includes = {JniModule.class, KeychainModule.class}, subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
@Module(includes = {JniModule.class}, subcomponents = {TrayMenuComponent.class, FxApplicationComponent.class})
public abstract class UiLauncherModule {

	@Provides

M main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java => main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +8 -8
@@ 1,8 1,8 @@
package org.cryptomator.ui.mainwindow;

import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;


@@ 22,19 22,19 @@ public class VaultDetailLockedController implements FxController {
	private final ReadOnlyObjectProperty<Vault> vault;
	private final FxApplication application;
	private final VaultOptionsComponent.Builder vaultOptionsWindow;
	private final Optional<KeychainManager> keychainManagerOptional;
	private final KeychainManager keychain;
	private final Stage mainWindow;
	private final BooleanExpression passwordSaved;

	@Inject
	VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, Optional<KeychainManager> keychainManagerOptional, @MainWindow Stage mainWindow) {
	VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
		this.vault = vault;
		this.application = application;
		this.vaultOptionsWindow = vaultOptionsWindow;
		this.keychainManagerOptional = keychainManagerOptional;
		this.keychain = keychain;
		this.mainWindow = mainWindow;
		if (keychainManagerOptional.isPresent()) {
			this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychainManagerOptional.get().getPassphraseStoredProperty(v.getId())));
		if (keychain.isSupported()) {
			this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychain.getPassphraseStoredProperty(v.getId())));
		} else {
			this.passwordSaved = new SimpleBooleanProperty(false);
		}


@@ 65,8 65,8 @@ public class VaultDetailLockedController implements FxController {
	}

	public boolean isPasswordSaved() {
		if (keychainManagerOptional.isPresent() && vault.get() != null) {
			return keychainManagerOptional.get().getPassphraseStoredProperty(vault.get().getId()).get();
		if (keychain.isSupported() && vault.get() != null) {
			return keychain.getPassphraseStoredProperty(vault.get().getId()).get();
		} else return false;
	}
}

M main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java => main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +7 -9
@@ 1,6 1,7 @@
package org.cryptomator.ui.migration;

import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.cryptofs.FileNameTooLongException;


@@ 9,8 10,7 @@ import org.cryptomator.cryptofs.migration.Migrators;
import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;


@@ 37,7 37,6 @@ import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.stage.Stage;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;


@@ 55,7 54,7 @@ public class MigrationRunController implements FxController {
	private final Vault vault;
	private final ExecutorService executor;
	private final ScheduledExecutorService scheduler;
	private final Optional<KeychainManager> keychain;
	private final KeychainManager keychain;
	private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
	private final ErrorComponent.Builder errorComponent;
	private final Lazy<Scene> startScene;


@@ 69,8 68,7 @@ public class MigrationRunController implements FxController {
	public NiceSecurePasswordField passwordField;

	@Inject
	public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, Optional<KeychainManager> keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {

	public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, KeychainManager keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
		this.window = window;
		this.vault = vault;
		this.executor = executor;


@@ 88,7 86,7 @@ public class MigrationRunController implements FxController {
	}

	public void initialize() {
		if (keychain.isPresent()) {
		if (keychain.isSupported()) {
			loadStoredPassword();
		}
		migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));


@@ 167,10 165,10 @@ public class MigrationRunController implements FxController {
	}

	private void loadStoredPassword() {
		assert keychain.isPresent();
		assert keychain.isSupported();
		char[] storedPw = null;
		try {
			storedPw = keychain.get().loadPassphrase(vault.getId());
			storedPw = keychain.loadPassphrase(vault.getId());
			if (storedPw != null) {
				passwordField.setPassword(storedPw);
				passwordField.selectRange(storedPw.length, storedPw.length);

M main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java => main/ui/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java +0 -5
@@ 1,14 1,11 @@
package org.cryptomator.ui.preferences;

import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.KeychainBackend;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.keychain.KeychainAccessStrategy;
import org.cryptomator.keychain.LinuxSystemKeychainAccess;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@@ 28,9 25,7 @@ import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.util.StringConverter;

import java.util.Arrays;
import java.util.EnumSet;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Set;

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +4 -4
@@ 1,7 1,7 @@
package org.cryptomator.ui.unlock;

import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings;


@@ 49,7 49,7 @@ public class UnlockController implements FxController {
	private final Optional<char[]> savedPassword;
	private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
	private final ForgetPasswordComponent.Builder forgetPassword;
	private final Optional<KeychainManager> keychain;
	private final KeychainManager keychain;
	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
	private final BooleanBinding userInteractionDisabled;
	private final BooleanProperty unlockButtonDisabled;


@@ 65,7 65,7 @@ public class UnlockController implements FxController {
	public Animation unlockAnimation;

	@Inject
	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainManager> keychain) {
	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
		this.window = window;
		this.vault = vault;
		this.password = password;


@@ 214,6 214,6 @@ public class UnlockController implements FxController {
	}

	public boolean isKeychainAccessAvailable() {
		return keychain.isPresent();
		return keychain.isSupported();
	}
}

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +9 -7
@@ 4,9 4,9 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FXMLLoaderFactory;
import org.cryptomator.ui.common.FxController;


@@ 49,15 49,17 @@ abstract class UnlockModule {
	@Provides
	@Named("savedPassword")
	@UnlockScoped
	static Optional<char[]> provideStoredPassword(Optional<KeychainManager> keychain, @UnlockWindow Vault vault) {
		return keychain.map(k -> {
	static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow Vault vault) {
		if (keychain.isSupported()) {
			return Optional.empty();
		} else {
			try {
				return k.loadPassphrase(vault.getId());
				return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
			} catch (KeychainAccessException e) {
				LOG.error("Failed to load entry from system keychain.", e);
				return null;
				return Optional.empty();
			}
		});
		}
	}

	@Provides

M main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java => main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +6 -6
@@ 1,14 1,14 @@
package org.cryptomator.ui.unlock;

import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile;


@@ 53,14 53,14 @@ public class UnlockWorkflow extends Task<Boolean> {
	private final AtomicBoolean savePassword;
	private final Optional<char[]> savedPassword;
	private final UserInteractionLock<PasswordEntry> passwordEntryLock;
	private final Optional<KeychainManager> keychain;
	private final KeychainManager keychain;
	private final Lazy<Scene> unlockScene;
	private final Lazy<Scene> successScene;
	private final Lazy<Scene> invalidMountPointScene;
	private final ErrorComponent.Builder errorComponent;

	@Inject
	UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, Optional<KeychainManager> keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
	UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
		this.window = window;
		this.vault = vault;
		this.vaultService = vaultService;


@@ 150,9 150,9 @@ public class UnlockWorkflow extends Task<Boolean> {
	}

	private void savePasswordToSystemkeychain() {
		if (keychain.isPresent()) {
		if (keychain.isSupported()) {
			try {
				keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
				keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
			} catch (KeychainAccessException e) {
				LOG.error("Failed to store passphrase in system keychain.", e);
			}

M main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java => main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java +23 -13
@@ 1,11 1,13 @@
package org.cryptomator.ui.vaultoptions;

import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.keychain.KeychainAccessException;
import org.cryptomator.keychain.KeychainManager;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.changepassword.ChangePasswordComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.inject.Inject;
import javafx.beans.binding.Bindings;


@@ 13,29 15,32 @@ import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;

@VaultOptionsScoped
public class MasterkeyOptionsController implements FxController {

	private static final Logger LOG = LoggerFactory.getLogger(MasterkeyOptionsController.class);

	private final Vault vault;
	private final Stage window;
	private final ChangePasswordComponent.Builder changePasswordWindow;
	private final RecoveryKeyComponent.Builder recoveryKeyWindow;
	private final Optional<KeychainManager> keychainManagerOptional;
	private final KeychainManager keychain;
	private final BooleanExpression passwordSaved;


	@Inject
	MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow, Optional<KeychainManager> keychainManagerOptional) {
	MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Builder recoveryKeyWindow, KeychainManager keychain) {
		this.vault = vault;
		this.window = window;
		this.changePasswordWindow = changePasswordWindow;
		this.recoveryKeyWindow = recoveryKeyWindow;
		this.keychainManagerOptional = keychainManagerOptional;
		if (keychainManagerOptional.isPresent()) {
			this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()));
		} else this.passwordSaved = new SimpleBooleanProperty(false);
		this.keychain = keychain;
		if (keychain.isSupported()) {
			this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychain.getPassphraseStoredProperty(vault.getId()));
		} else {
			this.passwordSaved = new SimpleBooleanProperty(false);
		}
	}

	@FXML


@@ 54,8 59,13 @@ public class MasterkeyOptionsController implements FxController {
	}

	@FXML
	public void removePasswordFromKeychain() throws KeychainAccessException {
		keychainManagerOptional.get().deletePassphrase(vault.getId());
	public void removePasswordFromKeychain() {
		assert keychain.isSupported();
		try {
			keychain.deletePassphrase(vault.getId());
		} catch (KeychainAccessException e) {
			LOG.error("Failed to delete passphrase from system keychain.", e);
		}
		window.close();
	}



@@ 64,8 74,8 @@ public class MasterkeyOptionsController implements FxController {
	}

	public boolean isPasswordSaved() {
		if (keychainManagerOptional.isPresent() && vault != null) {
			return keychainManagerOptional.get().getPassphraseStoredProperty(vault.getId()).get();
		if (keychain.isSupported() && vault != null) {
			return keychain.getPassphraseStoredProperty(vault.getId()).get();
		} else return false;
	}
}

M main/ui/src/main/resources/license/THIRD-PARTY.txt => main/ui/src/main/resources/license/THIRD-PARTY.txt +1 -8
@@ 11,15 11,12 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program.  If not, see http://www.gnu.org/licenses/.

Cryptomator uses 53 third-party dependencies under the following licenses:
Cryptomator uses 46 third-party dependencies under the following licenses:
        Apache License v2.0:
			- HKDF-RFC5869 (at.favre.lib:hkdf:1.1.0 - https://github.com/patrickfav/hkdf)
			- jffi (com.github.jnr:jffi:1.2.23 - http://github.com/jnr/jffi)
			- jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm)
			- jnr-constants (com.github.jnr:jnr-constants:0.9.15 - http://github.com/jnr/jnr-constants)
			- jnr-enxio (com.github.jnr:jnr-enxio:0.28 - http://github.com/jnr/jnr-enxio)
			- jnr-ffi (com.github.jnr:jnr-ffi:2.1.12 - http://github.com/jnr/jnr-ffi)
			- jnr-unixsocket (com.github.jnr:jnr-unixsocket:0.33 - http://github.com/jnr/jnr-unixsocket)
			- FindBugs-jsr305 (com.google.code.findbugs:jsr305:3.0.2 - http://findbugs.sourceforge.net/)
			- Gson (com.google.code.gson:gson:2.8.6 - https://github.com/google/gson/gson)
			- Dagger (com.google.dagger:dagger:2.22 - https://github.com/google/dagger)


@@ 70,19 67,15 @@ Cryptomator uses 53 third-party dependencies under the following licenses:
			- javafx-fxml (org.openjfx:javafx-fxml:14 - https://openjdk.java.net/projects/openjfx/javafx-fxml/)
			- javafx-graphics (org.openjfx:javafx-graphics:14 - https://openjdk.java.net/projects/openjfx/javafx-graphics/)
        LGPL 2.1:
			- dbus-java (com.github.hypfvieh:dbus-java:3.2.3 - https://github.com/hypfvieh/dbus-java/dbus-java)
			- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
			- Java Native Access (net.java.dev.jna:jna:5.1.0 - https://github.com/java-native-access/jna)
			- Java Native Access Platform (net.java.dev.jna:jna-platform:5.1.0 - https://github.com/java-native-access/jna)
        MIT License:
			- java jwt (com.auth0:java-jwt:3.10.3 - https://github.com/auth0/java-jwt)
			- java-utils (com.github.hypfvieh:java-utils:1.0.6 - https://github.com/hypfvieh/java-utils)
			- jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
			- jnr-fuse (com.github.serceman:jnr-fuse:0.5.4 - no url defined)
			- zxcvbn4j (com.nulab-inc:zxcvbn:1.3.0 - https://github.com/nulab/zxcvbn4j)
			- secret-service (de.swiesend:secret-service:1.1.0 - https://github.com/swiesend/secret-service)
			- Checker Qual (org.checkerframework:checker-qual:3.5.0 - https://checkerframework.org)
			- kdewallet (org.purejava:kdewallet:1.1.1 - https://github.com/purejava/kdewallet)
			- SLF4J API Module (org.slf4j:slf4j-api:1.7.30 - http://www.slf4j.org)
        The BSD 2-Clause License:
			- EasyBind (com.tobiasdiez:easybind:2.1.0 - https://github.com/tobiasdiez/EasyBind)