~exprez135/cryptomator-libre

efaf107b204aafc092247e3448c46ee6f9b309b2 — Sebastian Stenzel 11 months ago d24734e + 86cba5e
Merge pull request #1393 from cryptomator/feature/modular-keychains

Modularized Keychain Access, references #1301
41 files changed, 521 insertions(+), 1237 deletions(-)

M .github/workflows/build.yml
M main/buildkit/assembly-linux.xml
M main/buildkit/assembly-mac.xml
M main/buildkit/assembly-win.xml
M main/buildkit/pom.xml
M main/commons/pom.xml
M main/commons/src/main/java/org/cryptomator/common/CommonsModule.java
M main/commons/src/main/java/org/cryptomator/common/JniModule.java
R main/{keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java}
A main/commons/src/main/java/org/cryptomator/common/keychain/KeychainModule.java
A main/commons/src/main/java/org/cryptomator/common/keychain/NoKeychainAccessProviderException.java
M main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java
M main/commons/src/main/java/org/cryptomator/common/settings/Settings.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/i18n/strings.properties
M main/ui/src/main/resources/license/THIRD-PARTY.txt
M .github/workflows/build.yml => .github/workflows/build.yml +9 -7
@@ 7,7 7,6 @@ jobs:
  build:
    name: Build and Test
    runs-on: ubuntu-latest
    #This check is case insensitive
    if: "!contains(github.event.head_commit.message, '[ci skip]') && !contains(github.event.head_commit.message, '[skip ci]')"
    steps:
      - uses: actions/checkout@v2


@@ 29,28 28,31 @@ jobs:
        run: |
          curl -o ~/codacy-coverage-reporter.jar https://repo.maven.apache.org/maven2/com/codacy/codacy-coverage-reporter/7.1.0/codacy-coverage-reporter-7.1.0-assembly.jar
          $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/commons/target/site/jacoco/jacoco.xml --partial
          $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/keychain/target/site/jacoco/jacoco.xml --partial
          $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/ui/target/site/jacoco/jacoco.xml --partial
          $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar report -l Java -r main/launcher/target/site/jacoco/jacoco.xml --partial
          $JAVA_HOME/bin/java -jar ~/codacy-coverage-reporter.jar final
        env:
          CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }}
      - name: Assemble Buildkit
        run: mvn -B package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease
      - name: Assemble buildkit-linux.zip
        run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,linux
      - name: Upload buildkit-linux.zip
        uses: actions/upload-artifact@v1
        with:
          name: buildkit-linux.zip
          name: buildkit-linux
          path: main/buildkit/target/buildkit-linux.zip
      - name: Assemble buildkit-mac.zip
        run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,mac
      - name: Upload buildkit-mac.zip
        uses: actions/upload-artifact@v1
        with:
          name: buildkit-mac.zip
          name: buildkit-mac
          path: main/buildkit/target/buildkit-mac.zip
      - name: Assemble buildkit-win.zip
        run: mvn -B clean package -DskipTests --file main/pom.xml --resume-from=buildkit -Prelease,windows
      - name: Upload buildkit-win.zip
        uses: actions/upload-artifact@v1
        with:
          name: buildkit-win.zip
          name: buildkit-win
          path: main/buildkit/target/buildkit-win.zip
          
  release:

M main/buildkit/assembly-linux.xml => main/buildkit/assembly-linux.xml +0 -7
@@ 43,12 43,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/linux-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/assembly-mac.xml => main/buildkit/assembly-mac.xml +0 -7
@@ 43,12 43,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/mac-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/assembly-win.xml => main/buildkit/assembly-win.xml +0 -7
@@ 43,12 43,5 @@
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
		<fileSet>
			<directory>target/win-libs</directory>
			<includes>
				<include>*.jar</include>
			</includes>
			<outputDirectory>libs</outputDirectory>
		</fileSet>
	</fileSets>
</assembly>
\ No newline at end of file

M main/buildkit/pom.xml => main/buildkit/pom.xml +142 -100
@@ 24,7 24,6 @@
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-resources-plugin</artifactId>
				<version>3.1.0</version>
				<executions>
					<execution>
						<id>copy-resources</id>


@@ 55,8 54,8 @@

			<!-- copy libraries to target/libs/: -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<version>3.1.1</version>
				<executions>
					<execution>
						<id>copy-libs</id>


@@ 65,110 64,153 @@
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<includeScope>runtime</includeScope>
							<outputDirectory>${project.build.directory}/libs</outputDirectory>
							<excludeClassifiers>linux,mac,win</excludeClassifiers>
							<excludeArtifactIds>dbus-java,secret-service,kdewallet,hkdf,java-utils</excludeArtifactIds>
						</configuration>
					</execution>
					<execution>
						<id>copy-linux-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>linux</classifier>
						</configuration>
					</execution>
					<execution>
						<id>copy-linux-system-keychain-access</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/linux-libs</outputDirectory>
							<includeArtifactIds>dbus-java,secret-service,kdewallet,hkdf,java-utils</includeArtifactIds>
						</configuration>
					</execution>
					<execution>
						<id>copy-mac-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/mac-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>mac</classifier>
						</configuration>
					</execution>
					<execution>
						<id>copy-win-libs</id>
						<phase>prepare-package</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
						<configuration>
							<outputDirectory>${project.build.directory}/win-libs</outputDirectory>
							<includeGroupIds>org.openjfx</includeGroupIds>
							<classifier>win</classifier>
						</configuration>
					</execution>
				</executions>
			</plugin>

			<!-- create buildkit.zip: -->
			<plugin>
				<artifactId>maven-assembly-plugin</artifactId>
				<version>3.1.1</version>
				<executions>
					<execution>
						<id>assemble-linux</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-linux.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-linux</finalName>
						</configuration>
					</execution>
					<execution>
						<id>assemble-mac</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-mac.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-mac</finalName>
						</configuration>
					</execution>
					<execution>
						<id>assemble-win</id>
						<phase>package</phase>
						<goals>
							<goal>single</goal>
						</goals>
						<configuration>
							<descriptors>
								<descriptor>assembly-win.xml</descriptor>
							</descriptors>
							<appendAssemblyId>false</appendAssemblyId>
							<finalName>buildkit-win</finalName>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

	<profiles>
		<profile>
			<id>linux</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-linux</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-linux.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-linux</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-linux-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>linux</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>

		<profile>
			<id>mac</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-mac</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-mac.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-mac</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-mac-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>mac</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>

		<profile>
			<id>windows</id>
			<build>
				<plugins>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-assembly-plugin</artifactId>
						<executions>
							<execution>
								<id>assemble-win</id>
								<phase>package</phase>
								<goals>
									<goal>single</goal>
								</goals>
								<configuration>
									<descriptors>
										<descriptor>assembly-win.xml</descriptor>
									</descriptors>
									<appendAssemblyId>false</appendAssemblyId>
									<finalName>buildkit-win</finalName>
								</configuration>
							</execution>
						</executions>
					</plugin>
					<plugin>
						<groupId>org.apache.maven.plugins</groupId>
						<artifactId>maven-dependency-plugin</artifactId>
						<executions>
							<execution>
								<id>copy-win-libs</id>
								<phase>prepare-package</phase>
								<goals>
									<goal>copy-dependencies</goal>
								</goals>
								<configuration>
									<outputDirectory>${project.build.directory}/libs</outputDirectory>
									<includeGroupIds>org.openjfx</includeGroupIds>
									<classifier>win</classifier>
								</configuration>
							</execution>
						</executions>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>
</project>
\ No newline at end of file

M main/commons/pom.xml => main/commons/pom.xml +4 -0
@@ 29,6 29,10 @@
		</dependency>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>integrations-api</artifactId>
		</dependency>
		<dependency>
			<groupId>org.cryptomator</groupId>
			<artifactId>jni</artifactId>
		</dependency>


M main/commons/src/main/java/org/cryptomator/common/CommonsModule.java => main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +2 -1
@@ 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);

M main/commons/src/main/java/org/cryptomator/common/JniModule.java => main/commons/src/main/java/org/cryptomator/common/JniModule.java +1 -0
@@ 15,6 15,7 @@ import javax.inject.Singleton;
import java.util.Optional;

@Module
@Deprecated
public class JniModule {

	@Provides

R main/keychain/src/main/java/org/cryptomator/keychain/KeychainManager.java => main/commons/src/main/java/org/cryptomator/common/keychain/KeychainManager.java +28 -19
@@ 1,60 1,71 @@
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

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

	private static final Logger LOG = LoggerFactory.getLogger(KeychainManager.class);
	private final ObjectExpression<KeychainAccessProvider> keychain;
	private final LoadingCache<String, BooleanProperty> passphraseStoredProperties;

	private final KeychainAccessStrategy 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 80,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) {


@@ 84,7 95,6 @@ public class KeychainManager implements KeychainAccessStrategy {
			if (Platform.isFxApplicationThread()) {
				property.set(value);
			} else {
				LOG.warn("");
				Platform.runLater(() -> property.set(value));
			}
		}


@@ 107,7 117,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);

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

}

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.");
	}
}

M main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java => main/commons/src/main/java/org/cryptomator/common/settings/KeychainBackend.java +9 -29
@@ 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;

	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);
		}
	KeychainBackend(String providerClass) {
		this.providerClass = providerClass;
	}

	private final String configName;
	private final boolean isSupported;

	KeychainBackend(String configName, boolean isSupported) {
		this.configName = configName;
		this.isSupported = isSupported;
	public String getProviderClass() {
		return providerClass;
	}

	public String getDisplayName() {
		return configName;
	}

	public boolean isSupported() { return isSupported; }

}

M main/commons/src/main/java/org/cryptomator/common/settings/Settings.java => main/commons/src/main/java/org/cryptomator/common/settings/Settings.java +4 -2
@@ 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 = "";


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 +97 -32
@@ 25,6 25,10 @@

		<!-- cryptomator dependencies -->
		<cryptomator.cryptofs.version>1.9.12</cryptomator.cryptofs.version>
		<cryptomator.integrations.version>0.1.4</cryptomator.integrations.version>
		<cryptomator.integrations.win.version>0.1.0-beta1</cryptomator.integrations.win.version>
		<cryptomator.integrations.mac.version>0.1.0-beta1</cryptomator.integrations.mac.version>
		<cryptomator.integrations.linux.version>0.1.0-beta1</cryptomator.integrations.linux.version>
		<cryptomator.jni.version>2.2.3</cryptomator.jni.version>
		<cryptomator.fuse.version>1.2.5</cryptomator.fuse.version>
		<cryptomator.dokany.version>1.1.15</cryptomator.dokany.version>


@@ 66,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>


@@ 108,6 107,26 @@
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>integrations-api</artifactId>
				<version>${cryptomator.integrations.version}</version>
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>integrations-win</artifactId>
				<version>${cryptomator.integrations.win.version}</version>
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>integrations-mac</artifactId>
				<version>${cryptomator.integrations.mac.version}</version>
			</dependency>
			<dependency>
				<groupId>org.cryptomator</groupId>
				<artifactId>integrations-linux</artifactId>
				<version>${cryptomator.integrations.linux.version}</version>
			</dependency>
			<dependency> <!-- deprecated: will be replaced by integrations-api -->
				<groupId>org.cryptomator</groupId>
				<artifactId>jni</artifactId>
				<version>${cryptomator.jni.version}</version>
			</dependency>


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

			<!-- Linux System Keychain -->
			<dependency>
				<groupId>de.swiesend</groupId>
				<artifactId>secret-service</artifactId>
				<version>${secret-service.version}</version>
			</dependency>
			<dependency>
				<groupId>org.purejava</groupId>
				<artifactId>kdewallet</artifactId>
				<version>${kdewallet.version}</version>
			</dependency>

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


@@ 256,7 263,6 @@

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


@@ 279,26 285,87 @@
				</plugins>
			</build>
		</profile>
		<profile>
			<id>mac</id>
			<activation>
				<os>
					<family>mac</family>
				</os>
				<property>
					<name>idea.version</name>
				</property>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.cryptomator</groupId>
					<artifactId>integrations-mac</artifactId>
				</dependency>
			</dependencies>
		</profile>
		<profile>
			<id>linux</id>
			<activation>
				<os>
					<family>unix</family>
					<name>Linux</name>
				</os>
				<property>
					<name>idea.version</name>
				</property>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.cryptomator</groupId>
					<artifactId>integrations-linux</artifactId>
				</dependency>
			</dependencies>
		</profile>
		<profile>
			<id>windows</id>
			<activation>
				<os>
					<family>windows</family>
				</os>
				<property>
					<name>idea.version</name>
				</property>
			</activation>
			<dependencies>
				<dependency>
					<groupId>org.cryptomator</groupId>
					<artifactId>integrations-win</artifactId>
				</dependency>
			</dependencies>
		</profile>
	</profiles>

	<build>
		<pluginManagement>
			<plugins>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-compiler-plugin</artifactId>
					<version>3.8.1</version>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-resources-plugin</artifactId>
					<version>3.2.0</version>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-dependency-plugin</artifactId>
					<version>3.1.2</version>
					<executions>
						<execution>
							<id>copy-libs</id>
							<goals>
								<goal>copy-dependencies</goal>
							</goals>
							<configuration>
								<outputDirectory>${project.build.directory}/libs</outputDirectory>
								<includeScope>runtime</includeScope>
							</configuration>
						</execution>
					</executions>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-assembly-plugin</artifactId>
					<version>3.3.0</version>
				</plugin>
				<plugin>
					<groupId>org.apache.maven.plugins</groupId>
					<artifactId>maven-surefire-plugin</artifactId>
					<version>2.22.2</version>
				</plugin>
				<plugin>
					<groupId>org.codehaus.mojo</groupId>


@@ 348,7 415,6 @@
		<plugins>
			<plugin>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<release>14</release>
					<annotationProcessorPaths>


@@ 363,7 429,6 @@
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.22.2</version>
			</plugin>
		</plugins>
	</build>

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 +12 -28
@@ 1,13 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.keychain.KeychainAccessStrategy;
import org.cryptomator.keychain.LinuxSystemKeychainAccess;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@@ 27,12 25,12 @@ 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;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

@PreferencesScoped
public class GeneralPreferencesController implements FxController {


@@ 48,7 46,7 @@ public class GeneralPreferencesController implements FxController {
	private final ResourceBundle resourceBundle;
	private final Application application;
	private final Environment environment;
	private Optional<KeychainAccessStrategy> keychain;
	private final Set<KeychainAccessProvider> keychainAccessProviders;
	public ChoiceBox<UiTheme> themeChoiceBox;
	public ChoiceBox<KeychainBackend> keychainBackendChoiceBox;
	public CheckBox startHiddenCheckbox;


@@ 59,11 57,11 @@ public class GeneralPreferencesController implements FxController {
	public RadioButton nodeOrientationRtl;

	@Inject
	GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, Optional<KeychainAccessStrategy> keychain, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) {
	GeneralPreferencesController(Settings settings, @Named("trayMenuSupported") boolean trayMenuSupported, Optional<AutoStartStrategy> autoStartStrategy, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ExecutorService executor, ResourceBundle resourceBundle, Application application, Environment environment) {
		this.settings = settings;
		this.trayMenuSupported = trayMenuSupported;
		this.autoStartStrategy = autoStartStrategy;
		this.keychain = keychain;
		this.keychainAccessProviders = keychainAccessProviders;
		this.selectedTabProperty = selectedTabProperty;
		this.licenseHolder = licenseHolder;
		this.executor = executor;


@@ 96,16 94,15 @@ public class GeneralPreferencesController implements FxController {
		nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);

		keychainBackendChoiceBox.getItems().addAll(getAvailableBackends());
		if (keychain.isPresent() && SystemUtils.IS_OS_LINUX) {
			keychainBackendChoiceBox.setValue(LinuxSystemKeychainAccess.getBackendActivated());
		}
		if (keychain.isPresent() && (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS)) {
			keychainBackendChoiceBox.setValue(Arrays.stream(KeychainBackend.supportedBackends()).findFirst().orElseThrow(IllegalStateException::new));
		}
		keychainBackendChoiceBox.setConverter(new KeychainBackendConverter(resourceBundle));
		keychainBackendChoiceBox.valueProperty().bindBidirectional(settings.keychainBackend());
	}

	private KeychainBackend[] getAvailableBackends() {
		var namesOfAvailableProviders = keychainAccessProviders.stream().map(KeychainAccessProvider::getClass).map(Class::getName).collect(Collectors.toUnmodifiableSet());
		return Arrays.stream(KeychainBackend.values()).filter(value -> namesOfAvailableProviders.contains(value.getProviderClass())).toArray(KeychainBackend[]::new);
	}

	public boolean isTrayMenuSupported() {
		return this.trayMenuSupported;
	}


@@ 183,7 180,7 @@ public class GeneralPreferencesController implements FxController {

		@Override
		public String toString(KeychainBackend impl) {
			return resourceBundle.getString(impl.getDisplayName());
			return resourceBundle.getString("preferences.general.keychainBackend." + impl.getProviderClass());
		}

		@Override


@@ 215,17 212,4 @@ public class GeneralPreferencesController implements FxController {
		}
	}

	private KeychainBackend[] getAvailableBackends() {
		if (!keychain.isPresent()) {
			return new KeychainBackend[]{};
		}
		if (SystemUtils.IS_OS_LINUX) {
			EnumSet<KeychainBackend> backends = LinuxSystemKeychainAccess.getAvailableKeychainBackends();
			return backends.toArray(KeychainBackend[]::new);
		}
		if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {
			return KeychainBackend.supportedBackends();
		}
		return new KeychainBackend[]{};
	}
}

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/i18n/strings.properties => main/ui/src/main/resources/i18n/strings.properties +4 -4
@@ 144,10 144,10 @@ preferences.general.debugLogging=Enable debug logging
preferences.general.debugDirectory=Reveal log files
preferences.general.autoStart=Launch Cryptomator on system start
preferences.general.keychainBackend=Store passwords with
preferences.general.keychainBackend.gnome=Gnome Keyring
preferences.general.keychainBackend.kde=KDE KWallet
preferences.general.keychainBackend.macSystemKeychain=macOS Keychain Access
preferences.general.keychainBackend.winSystemKeychain=Windows Data Protection Keychain
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Gnome Keyring
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=KDE KWallet
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=macOS Keychain Access
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Windows Data Protection Keychain
preferences.general.interfaceOrientation=Interface Orientation
preferences.general.interfaceOrientation.ltr=Left to Right
preferences.general.interfaceOrientation.rtl=Right to Left

M main/ui/src/main/resources/license/THIRD-PARTY.txt => main/ui/src/main/resources/license/THIRD-PARTY.txt +70 -77
@@ 11,82 11,75 @@ 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:
	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)
		- error-prone annotations (com.google.errorprone:error_prone_annotations:2.3.4 - http://nexus.sonatype.org/oss-repository-hosting.html/error_prone_parent/error_prone_annotations)
		- Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
		- Guava: Google Core Libraries for Java (com.google.guava:guava:29.0-jre - https://github.com/google/guava/guava)
		- Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture)
		- J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/)
		- Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/)
		- Apache Commons IO (commons-io:commons-io:2.6 - http://commons.apache.org/proper/commons-io/)
		- javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
		- 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)
		- Apache Commons Lang (org.apache.commons:commons-lang3:3.11 - https://commons.apache.org/proper/commons-lang/)
		- Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.13 - http://hc.apache.org/httpcomponents-core-ga)
		- Jackrabbit WebDAV Library (org.apache.jackrabbit:jackrabbit-webdav:2.21.3 - http://jackrabbit.apache.org/jackrabbit-webdav/)
		- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.31.v20200723 - http://www.eclipse.org/jetty)
	BSD:
		- asm (org.ow2.asm:asm:7.1 - http://asm.ow2.org/)
		- asm-analysis (org.ow2.asm:asm-analysis:7.1 - http://asm.ow2.org/)
		- asm-commons (org.ow2.asm:asm-commons:7.1 - http://asm.ow2.org/)
		- asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
		- asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
	Eclipse Public License - Version 1.0:
		- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.31.v20200723 - http://www.eclipse.org/jetty)
		- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.31.v20200723 - http://www.eclipse.org/jetty)
	Eclipse Public License - v 2.0:
		- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
	GPLv2:
		- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
	GPLv2+CE:
		- Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net)
		- javafx-base (org.openjfx:javafx-base:14 - https://openjdk.java.net/projects/openjfx/javafx-base/)
		- javafx-controls (org.openjfx:javafx-controls:14 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
		- 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:2.11.1 - 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)
Cryptomator uses 46 third-party dependencies under the following licenses:
        Apache License v2.0:
			- 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-ffi (com.github.jnr:jnr-ffi:2.1.12 - http://github.com/jnr/jnr-ffi)
			- 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)
			- error-prone annotations (com.google.errorprone:error_prone_annotations:2.3.4 - http://nexus.sonatype.org/oss-repository-hosting.html/error_prone_parent/error_prone_annotations)
			- Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - https://github.com/google/guava/failureaccess)
			- Guava: Google Core Libraries for Java (com.google.guava:guava:30.0-jre - https://github.com/google/guava/guava)
			- Guava ListenableFuture only (com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava - https://github.com/google/guava/listenablefuture)
			- J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.3 - https://github.com/google/j2objc/)
			- Apache Commons CLI (commons-cli:commons-cli:1.4 - http://commons.apache.org/proper/commons-cli/)
			- Apache Commons IO (commons-io:commons-io:2.6 - http://commons.apache.org/proper/commons-io/)
			- javax.inject (javax.inject:javax.inject:1 - http://code.google.com/p/atinject/)
			- 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)
			- Apache Commons Lang (org.apache.commons:commons-lang3:3.11 - https://commons.apache.org/proper/commons-lang/)
			- Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.13 - http://hc.apache.org/httpcomponents-core-ga)
			- Jackrabbit WebDAV Library (org.apache.jackrabbit:jackrabbit-webdav:2.21.3 - http://jackrabbit.apache.org/jackrabbit-webdav/)
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.31.v20200723 - http://www.eclipse.org/jetty)
        BSD:
			- asm (org.ow2.asm:asm:7.1 - http://asm.ow2.org/)
			- asm-analysis (org.ow2.asm:asm-analysis:7.1 - http://asm.ow2.org/)
			- asm-commons (org.ow2.asm:asm-commons:7.1 - http://asm.ow2.org/)
			- asm-tree (org.ow2.asm:asm-tree:7.1 - http://asm.ow2.org/)
			- asm-util (org.ow2.asm:asm-util:7.1 - http://asm.ow2.org/)
        Eclipse Public License - Version 1.0:
			- Jetty :: Http Utility (org.eclipse.jetty:jetty-http:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: IO Utility (org.eclipse.jetty:jetty-io:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Security (org.eclipse.jetty:jetty-security:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Server Core (org.eclipse.jetty:jetty-server:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Utilities (org.eclipse.jetty:jetty-util:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: Webapp Application Support (org.eclipse.jetty:jetty-webapp:9.4.31.v20200723 - http://www.eclipse.org/jetty)
			- Jetty :: XML utilities (org.eclipse.jetty:jetty-xml:9.4.31.v20200723 - http://www.eclipse.org/jetty)
        Eclipse Public License - v 2.0:
			- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
        GPLv2:
			- jnr-posix (com.github.jnr:jnr-posix:3.0.54 - http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix)
        GPLv2+CE:
			- Java Servlet API (javax.servlet:javax.servlet-api:3.1.0 - http://servlet-spec.java.net)
			- javafx-base (org.openjfx:javafx-base:14 - https://openjdk.java.net/projects/openjfx/javafx-base/)
			- javafx-controls (org.openjfx:javafx-controls:14 - https://openjdk.java.net/projects/openjfx/javafx-controls/)
			- 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:
			- 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)
			- 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)
			- Checker Qual (org.checkerframework:checker-qual:3.5.0 - https://checkerframework.org)
			- 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)

Cryptomator uses other third-party assets under the following licenses:
	SIL OFL 1.1 License:
		- Font Awesome 5.12.0 (https://fontawesome.com/)
SIL OFL 1.1 License:
- Font Awesome 5.12.0 (https://fontawesome.com/)