Merge branch 'feature/external-keychain' into develop

This commit is contained in:
Sebastian Stenzel
2016-09-04 16:21:52 +02:00
27 changed files with 707 additions and 99 deletions

View File

@@ -17,16 +17,17 @@ public final class LazyInitializer {
* @return The initialized value
*/
public static <T> T initializeLazily(AtomicReference<T> reference, Supplier<T> factory) {
final T existingInstance = reference.get();
if (existingInstance != null) {
return existingInstance;
final T existing = reference.get();
if (existing != null) {
return existing;
} else {
final T newInstance = factory.get();
if (reference.compareAndSet(null, newInstance)) {
return newInstance;
} else {
return reference.get();
}
return reference.updateAndGet(currentValue -> {
if (currentValue == null) {
return factory.get();
} else {
return currentValue;
}
});
}
}

52
main/keychain/pom.xml Normal file
View File

@@ -0,0 +1,52 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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.2.0-SNAPSHOT</version>
</parent>
<artifactId>keychain</artifactId>
<name>System Keychain Access</name>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.7</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.54</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>commons-test</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,26 @@
package org.cryptomator.keychain;
public interface KeychainAccess {
/**
* Associates a passphrase with a given key.
*
* @param key Key used to retrieve the passphrase via {@link #loadPassphrase(String)}.
* @param passphrase The secret to store in this keychain.
*/
void storePassphrase(String key, CharSequence passphrase);
/**
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
* @return The stored passphrase for the given key or <code>null</code> if no value for the given key could be found.
*/
char[] loadPassphrase(String key);
/**
* Deletes a passphrase with a given key.
*
* @param key Unique key previously used while {@link #storePassphrase(String, CharSequence) storing a passphrase}.
*/
void deletePassphrase(String key);
}

View File

@@ -0,0 +1,10 @@
package org.cryptomator.keychain;
interface KeychainAccessStrategy extends KeychainAccess {
/**
* @return <code>true</code> if this KeychainAccessStrategy works on the current machine.
*/
boolean isSupported();
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.keychain;
import java.util.Optional;
import java.util.Set;
import org.cryptomator.jni.JniModule;
import com.google.common.collect.Sets;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.ElementsIntoSet;
@Module(includes = {JniModule.class})
public class KeychainModule {
@Provides
@ElementsIntoSet
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
return Sets.newHashSet(macKeychain, winKeychain);
}
@Provides
public Optional<KeychainAccess> provideSupportedKeychain(Set<KeychainAccessStrategy> keychainAccessStrategies) {
return keychainAccessStrategies.stream().filter(KeychainAccessStrategy::isSupported).map(KeychainAccess.class::cast).findFirst();
}
}

View File

@@ -0,0 +1,44 @@
package org.cryptomator.keychain;
import java.util.Optional;
import javax.inject.Inject;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.MacKeychainAccess;
class MacSystemKeychainAccess implements KeychainAccessStrategy {
private final MacKeychainAccess keychain;
@Inject
public MacSystemKeychainAccess(Optional<MacFunctions> macFunctions) {
if (macFunctions.isPresent()) {
this.keychain = macFunctions.get().keychainAccess();
} else {
this.keychain = null;
}
}
@Override
public void storePassphrase(String key, CharSequence passphrase) {
keychain.storePassword(key, passphrase);
}
@Override
public char[] loadPassphrase(String key) {
return keychain.loadPassword(key);
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_MAC_OSX && keychain != null;
}
@Override
public void deletePassphrase(String key) {
keychain.deletePassword(key);
}
}

View File

@@ -0,0 +1,191 @@
package org.cryptomator.keychain;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.SerializedName;
import com.google.gson.reflect.TypeToken;
class WindowsProtectedKeychainAccess implements KeychainAccessStrategy {
private static final Logger LOG = LoggerFactory.getLogger(WindowsProtectedKeychainAccess.class);
private static final Gson GSON = new GsonBuilder().setPrettyPrinting() //
.registerTypeHierarchyAdapter(byte[].class, new ByteArrayJsonAdapter()) //
.disableHtmlEscaping().create();
private final WinDataProtection dataProtection;
private final Path keychainPath;
private Map<String, KeychainEntry> keychainEntries;
@Inject
public WindowsProtectedKeychainAccess(Optional<WinFunctions> winFunctions) {
if (winFunctions.isPresent()) {
this.dataProtection = winFunctions.get().dataProtection();
} else {
this.dataProtection = null;
}
String keychainPathProperty = System.getProperty("cryptomator.keychainPath");
if (dataProtection != null && keychainPathProperty == null) {
LOG.warn("Windows DataProtection module loaded, but no cryptomator.keychainPath property found.");
}
if (keychainPathProperty != null) {
if (keychainPathProperty.startsWith("~/")) {
keychainPathProperty = SystemUtils.USER_HOME + keychainPathProperty.substring(1);
}
this.keychainPath = FileSystems.getDefault().getPath(keychainPathProperty);
} else {
this.keychainPath = null;
}
}
@Override
public void storePassphrase(String key, CharSequence passphrase) {
loadKeychainEntriesIfNeeded();
ByteBuffer buf = UTF_8.encode(CharBuffer.wrap(passphrase));
byte[] cleartext = new byte[buf.remaining()];
buf.get(cleartext);
KeychainEntry entry = new KeychainEntry();
entry.salt = generateSalt();
entry.ciphertext = dataProtection.protect(cleartext, entry.salt);
Arrays.fill(buf.array(), (byte) 0x00);
Arrays.fill(cleartext, (byte) 0x00);
keychainEntries.put(key, entry);
saveKeychainEntries();
}
@Override
public char[] loadPassphrase(String key) {
loadKeychainEntriesIfNeeded();
KeychainEntry entry = keychainEntries.get(key);
if (entry == null) {
return null;
}
byte[] cleartext = dataProtection.unprotect(entry.ciphertext, entry.salt);
if (cleartext == null) {
return null;
}
CharBuffer buf = UTF_8.decode(ByteBuffer.wrap(cleartext));
char[] passphrase = new char[buf.remaining()];
buf.get(passphrase);
Arrays.fill(cleartext, (byte) 0x00);
Arrays.fill(buf.array(), (char) 0x00);
return passphrase;
}
@Override
public void deletePassphrase(String key) {
loadKeychainEntriesIfNeeded();
keychainEntries.remove(key);
saveKeychainEntries();
}
@Override
public boolean isSupported() {
return SystemUtils.IS_OS_WINDOWS && dataProtection != null && keychainPath != null;
}
private byte[] generateSalt() {
byte[] result = new byte[2 * Long.BYTES];
UUID uuid = UUID.randomUUID();
ByteBuffer buf = ByteBuffer.wrap(result);
buf.putLong(uuid.getMostSignificantBits());
buf.putLong(uuid.getLeastSignificantBits());
return result;
}
private void loadKeychainEntriesIfNeeded() {
if (keychainEntries == null) {
loadKeychainEntries();
}
assert keychainEntries != null;
}
private void loadKeychainEntries() {
Type type = new TypeToken<Map<String, KeychainEntry>>() {
}.getType();
try (InputStream in = Files.newInputStream(keychainPath, StandardOpenOption.READ); //
Reader reader = new InputStreamReader(in, UTF_8)) {
keychainEntries = GSON.fromJson(reader, type);
} catch (JsonParseException | NoSuchFileException e) {
LOG.info("Creating new keychain at path {}", keychainPath);
} catch (IOException e) {
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
}
if (keychainEntries == null) {
keychainEntries = new HashMap<>();
}
}
private void saveKeychainEntries() {
try (OutputStream out = Files.newOutputStream(keychainPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); //
Writer writer = new OutputStreamWriter(out, UTF_8)) {
GSON.toJson(keychainEntries, writer);
} catch (IOException e) {
throw new UncheckedIOException("Could not read keychain from path " + keychainPath, e);
}
}
private static class KeychainEntry {
@SerializedName("ciphertext")
byte[] ciphertext;
@SerializedName("salt")
byte[] salt;
}
private static class ByteArrayJsonAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
private static final Base64 BASE64 = new Base64();
@Override
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return BASE64.decode(json.getAsString().getBytes(StandardCharsets.UTF_8));
}
@Override
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(new String(BASE64.encode(src), StandardCharsets.UTF_8));
}
}
}

View File

@@ -0,0 +1,17 @@
package org.cryptomator.keychain;
import java.util.Optional;
import org.junit.Assert;
import org.junit.Test;
public class KeychainModuleTest {
@Test
public void testGetKeychain() {
Optional<KeychainAccess> keychainAccess = DaggerTestKeychainComponent.builder().jniModule(new TestJniModule()).keychainModule(new TestKeychainModule()).build().keychainAccess();
Assert.assertTrue(keychainAccess.isPresent());
Assert.assertTrue(keychainAccess.get() instanceof MapKeychainAccess);
}
}

View File

@@ -0,0 +1,34 @@
package org.cryptomator.keychain;
import java.util.HashMap;
import java.util.Map;
class MapKeychainAccess implements KeychainAccessStrategy {
private final Map<String, char[]> map = new HashMap<>();
@Override
public void storePassphrase(String key, CharSequence passphrase) {
char[] pw = new char[passphrase.length()];
for (int i = 0; i < passphrase.length(); i++) {
pw[i] = passphrase.charAt(i);
}
map.put(key, pw);
}
@Override
public char[] loadPassphrase(String key) {
return map.get(key);
}
@Override
public void deletePassphrase(String key) {
map.remove(key);
}
@Override
public boolean isSupported() {
return true;
}
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.keychain;
import java.util.Optional;
import org.cryptomator.jni.JniModule;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.jni.WinFunctions;
import dagger.Lazy;
public class TestJniModule extends JniModule {
@Override
public Optional<WinFunctions> winFunctions(Lazy<WinFunctions> winFunction) {
return Optional.empty();
}
@Override
public Optional<MacFunctions> macFunctions(Lazy<MacFunctions> winFunction) {
return Optional.empty();
}
}

View File

@@ -0,0 +1,15 @@
package org.cryptomator.keychain;
import java.util.Optional;
import javax.inject.Singleton;
import dagger.Component;
@Singleton
@Component(modules = KeychainModule.class)
interface TestKeychainComponent {
Optional<KeychainAccess> keychainAccess();
}

View File

@@ -0,0 +1,14 @@
package org.cryptomator.keychain;
import java.util.Set;
import com.google.common.collect.Sets;
public class TestKeychainModule extends KeychainModule {
@Override
Set<KeychainAccessStrategy> provideKeychainAccessStrategies(MacSystemKeychainAccess macKeychain, WindowsProtectedKeychainAccess winKeychain) {
return Sets.newHashSet(new MapKeychainAccess());
}
}

View File

@@ -0,0 +1,60 @@
package org.cryptomator.keychain;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import org.cryptomator.jni.WinDataProtection;
import org.cryptomator.jni.WinFunctions;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
public class WindowsProtectedKeychainAccessTest {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private Path tmpFile;
private WindowsProtectedKeychainAccess keychain;
@Before
public void setup() throws IOException, ReflectiveOperationException {
tmpFile = Files.createTempFile("unit-tests", ".tmp");
System.setProperty("cryptomator.keychainPath", tmpFile.toAbsolutePath().normalize().toString());
WinFunctions winFunctions = Mockito.mock(WinFunctions.class);
WinDataProtection winDataProtection = Mockito.mock(WinDataProtection.class);
Mockito.when(winFunctions.dataProtection()).thenReturn(winDataProtection);
Answer<byte[]> answerReturningFirstArg = invocation -> invocation.getArgumentAt(0, byte[].class).clone();
Mockito.when(winDataProtection.protect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
Mockito.when(winDataProtection.unprotect(Mockito.any(), Mockito.any())).thenAnswer(answerReturningFirstArg);
keychain = new WindowsProtectedKeychainAccess(Optional.of(winFunctions));
}
@After
public void teardown() throws IOException {
Files.deleteIfExists(tmpFile);
}
@Test
public void testStoreAndLoad() {
String storedPw1 = "topSecret";
String storedPw2 = "bottomSecret";
keychain.storePassphrase("myPassword", storedPw1);
keychain.storePassphrase("myOtherPassword", storedPw2);
String loadedPw1 = new String(keychain.loadPassphrase("myPassword"));
String loadedPw2 = new String(keychain.loadPassphrase("myOtherPassword"));
Assert.assertEquals(storedPw1, loadedPw1);
Assert.assertEquals(storedPw2, loadedPw2);
keychain.deletePassphrase("myPassword");
Assert.assertNull(keychain.loadPassphrase("myPassword"));
Assert.assertNull(keychain.loadPassphrase("nonExistingPassword"));
}
}

View File

@@ -0,0 +1,33 @@
<?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>

View File

@@ -28,6 +28,7 @@
<!-- dependency versions -->
<cryptomator.cryptolib.version>1.0.0-SNAPSHOT</cryptomator.cryptolib.version>
<cryptomator.jni.version>1.0.0-SNAPSHOT</cryptomator.jni.version>
<log4j.version>2.1</log4j.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.12</junit.version>
@@ -40,7 +41,7 @@
<commons-httpclient.version>3.1</commons-httpclient.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
<dagger.version>2.4</dagger.version>
<dagger.version>2.6.1</dagger.version>
</properties>
<dependencyManagement>
@@ -57,7 +58,6 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>filesystem-api</artifactId>
@@ -106,7 +106,6 @@
<artifactId>filesystem-stats</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>frontend-api</artifactId>
@@ -117,19 +116,28 @@
<artifactId>frontend-webdav</artifactId>
<version>${project.version}</version>
</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>
<!-- Libs -->
<!-- Cryptomator Libs -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>${cryptomator.cryptolib.version}</version>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
<version>${cryptomator.jni.version}</version>
</dependency>
<!-- Logging -->
<dependency>
@@ -280,6 +288,7 @@
<module>filesystem-invariants-tests</module>
<module>frontend-api</module>
<module>frontend-webdav</module>
<module>keychain</module>
<module>ui</module>
</modules>

View File

@@ -54,6 +54,14 @@
<groupId>org.cryptomator</groupId>
<artifactId>frontend-webdav</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>jni</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>keychain</artifactId>
</dependency>
<!-- CryptoLib -->
<dependency>

View File

@@ -8,10 +8,12 @@
*******************************************************************************/
package org.cryptomator.ui;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
@@ -34,4 +36,7 @@ interface CryptomatorComponent {
Localization localization();
ExitUtil exitUtil();
}
Optional<MacFunctions> nativeMacFunctions();
}

View File

@@ -22,6 +22,8 @@ import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.FrontendId;
import org.cryptomator.frontend.webdav.WebDavModule;
import org.cryptomator.frontend.webdav.WebDavServer;
import org.cryptomator.jni.JniModule;
import org.cryptomator.keychain.KeychainModule;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.model.Vaults;
@@ -39,7 +41,7 @@ import javafx.application.Application;
import javafx.beans.Observable;
import javafx.stage.Stage;
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class})
@Module(includes = {CryptoEngineModule.class, CommonsModule.class, WebDavModule.class, KeychainModule.class, JniModule.class})
class CryptomatorModule {
private static final Logger LOG = LoggerFactory.getLogger(CryptomatorModule.class);
@@ -109,9 +111,7 @@ class CryptomatorModule {
private void setValidFrontendIds(WebDavServer webDavServer, Vaults vaults) {
webDavServer.setValidFrontendIds(vaults.stream() //
.map(Vault::getId)
.map(FrontendId::from)
.collect(toList()));
.map(Vault::getId).map(FrontendId::from).collect(toList()));
}
}

View File

@@ -21,6 +21,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
@@ -32,6 +33,9 @@ import javax.script.ScriptException;
import javax.swing.SwingUtilities;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.jni.JniException;
import org.cryptomator.jni.MacApplicationUiState;
import org.cryptomator.jni.MacFunctions;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
@@ -48,12 +52,14 @@ class ExitUtil {
private final Stage mainWindow;
private final Localization localization;
private final Settings settings;
private final Optional<MacFunctions> macFunctions;
@Inject
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings) {
public ExitUtil(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, Optional<MacFunctions> macFunctions) {
this.mainWindow = mainWindow;
this.localization = localization;
this.settings = settings;
this.macFunctions = macFunctions;
}
public void initExitHandler(Runnable exitCommand) {
@@ -88,6 +94,7 @@ class ExitUtil {
if (Platform.isImplicitExit()) {
exitCommand.run();
} else {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToAgentApplication));
mainWindow.close();
this.showTrayNotification(trayIcon);
}
@@ -189,6 +196,7 @@ class ExitUtil {
private void restoreFromTray(ActionEvent event) {
Platform.runLater(() -> {
macFunctions.map(MacFunctions::uiState).ifPresent(JniException.ignore(MacApplicationUiState::transformToForegroundApplication));
mainWindow.show();
mainWindow.requestFocus();
});

View File

@@ -12,6 +12,7 @@ package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -31,9 +32,7 @@ import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -49,9 +48,9 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private final Application app;
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<ChangePasswordListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<ChangePasswordListener> listener = Optional.empty();
private Vault vault;
@Inject
public ChangePasswordController(Application app, PasswordStrengthUtil strengthRater, Localization localization) {
@@ -101,7 +100,6 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer)));
passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate));
@@ -118,10 +116,11 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/change_password.fxml");
}
private void vaultDidChange(Vault newVault) {
oldPasswordField.clear();
newPasswordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
oldPasswordField.swipe();
newPasswordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
changePasswordButton.setDefaultButton(false);
changePasswordButton.setDefaultButton(true);
@@ -144,7 +143,7 @@ public class ChangePasswordController extends LocalizedFXMLViewController {
private void didClickChangePasswordButton(ActionEvent event) {
downloadsPageLink.setVisible(false);
try {
vault.get().changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
vault.changePassphrase(oldPasswordField.getCharacters(), newPasswordField.getCharacters());
messageText.setText(localization.getString("changePassword.infoMessage.success"));
listener.ifPresent(this::invokeListenerLater);
} catch (InvalidPassphraseException e) {

View File

@@ -10,9 +10,10 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -29,9 +30,7 @@ import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
@@ -44,9 +43,9 @@ public class InitializeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private final PasswordStrengthUtil strengthRater;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<InitializationListener> listener = Optional.empty();
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4
private Optional<InitializationListener> listener = Optional.empty();
private Vault vault;
@Inject
public InitializeController(Localization localization, PasswordStrengthUtil strengthRater) {
@@ -88,7 +87,6 @@ public class InitializeController extends LocalizedFXMLViewController {
public void initialize() {
BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty();
BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty());
EasyBind.subscribe(vault, this::vaultDidChange);
okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer));
passwordStrength.bind(EasyBind.map(passwordField.textProperty(), strengthRater::computeRate));
@@ -105,9 +103,10 @@ public class InitializeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/initialize.fxml");
}
private void vaultDidChange(Vault newVault) {
passwordField.clear();
retypePasswordField.clear();
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
retypePasswordField.swipe();
// trigger "default" change to refresh key bindings:
okButton.setDefaultButton(false);
okButton.setDefaultButton(true);
@@ -121,11 +120,13 @@ public class InitializeController extends LocalizedFXMLViewController {
protected void initializeVault(ActionEvent event) {
final CharSequence passphrase = passwordField.getCharacters();
try {
vault.get().create(passphrase);
vault.create(passphrase);
listener.ifPresent(this::invokeListenerLater);
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (UncheckedIOException | IOException ex) {
} catch (DirectoryNotEmptyException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.notEmpty"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed"));
} finally {

View File

@@ -315,7 +315,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showInitializeView() {
final InitializeController ctrl = initializeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didInitialize);
activeController.set(ctrl);
}
@@ -326,7 +327,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUpgradeView() {
final UpgradeController ctrl = upgradeController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUpgrade);
activeController.set(ctrl);
}
@@ -337,7 +339,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showUnlockView() {
final UnlockController ctrl = unlockController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didUnlock);
activeController.set(ctrl);
}
@@ -353,6 +356,7 @@ public class MainController extends LocalizedFXMLViewController {
final UnlockedController ctrl = unlockedVaults.computeIfAbsent(vault, k -> {
return unlockedControllerProvider.get();
});
ctrl.loadFxml();
ctrl.setVault(vault);
ctrl.setListener(this::didLock);
activeController.set(ctrl);
@@ -368,7 +372,8 @@ public class MainController extends LocalizedFXMLViewController {
private void showChangePasswordView() {
final ChangePasswordController ctrl = changePasswordController.get();
ctrl.vault.bind(selectedVault);
ctrl.loadFxml();
ctrl.setVault(selectedVault.get());
ctrl.setListener(this::didChangePassword);
activeController.set(ctrl);
}

View File

@@ -9,7 +9,9 @@
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Objects;
import java.util.Optional;
import javax.inject.Inject;
@@ -22,25 +24,24 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.frontend.FrontendCreationFailedException;
import org.cryptomator.frontend.FrontendFactory;
import org.cryptomator.frontend.webdav.mount.WindowsDriveLetters;
import org.cryptomator.keychain.KeychainAccess;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import dagger.Lazy;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
@@ -61,22 +62,34 @@ public class UnlockController extends LocalizedFXMLViewController {
private final Settings settings;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private final Optional<KeychainAccess> keychainAccess;
private Vault vault;
private Optional<UnlockListener> listener = Optional.empty();
@Inject
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters,
Optional<KeychainAccess> keychainAccess) {
super(localization);
this.app = app;
this.asyncTaskService = asyncTaskService;
this.frontendFactory = frontendFactory;
this.settings = settings;
this.driveLetters = driveLetters;
this.keychainAccess = keychainAccess;
}
@FXML
private SecPasswordField passwordField;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private CheckBox savePassword;
@FXML
private TextField mountName;
@@ -86,12 +99,6 @@ public class UnlockController extends LocalizedFXMLViewController {
@FXML
private ChoiceBox<Character> winDriveLetter;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@FXML
private ProgressIndicator progressIndicator;
@@ -107,8 +114,10 @@ public class UnlockController extends LocalizedFXMLViewController {
@Override
public void initialize() {
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
savePassword.setDisable(!keychainAccess.isPresent());
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
} else {
@@ -117,9 +126,6 @@ public class UnlockController extends LocalizedFXMLViewController {
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
unlockButton.disableProperty().bind(passwordField.textProperty().isEmpty());
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -127,11 +133,15 @@ public class UnlockController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/unlock.fxml");
}
private void vaultDidChange(Vault newVault) {
if (newVault == null) {
void setVault(Vault vault) {
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
if (vault.equals(this.vault)) {
return;
}
passwordField.clear();
this.vault = Objects.requireNonNull(vault);
passwordField.swipe();
advancedOptions.setVisible(false);
advancedOptionsButton.setText(localization.getString("unlock.button.advancedOptions.show"));
progressIndicator.setVisible(false);
@@ -145,13 +155,21 @@ public class UnlockController extends LocalizedFXMLViewController {
}
downloadsPageLink.setVisible(false);
messageText.setText(null);
mountName.setText(newVault.getMountName());
mountName.setText(vault.getMountName());
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
// trigger "default" change to refresh key bindings:
unlockButton.setDefaultButton(false);
unlockButton.setDefaultButton(true);
savePassword.setSelected(false);
// auto-fill pw from keychain:
if (keychainAccess.isPresent()) {
char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId());
if (storedPw != null) {
savePassword.setSelected(true);
passwordField.setText(new String(storedPw));
passwordField.selectRange(storedPw.length, storedPw.length);
Arrays.fill(storedPw, ' ');
}
}
}
// ****************************************
@@ -188,14 +206,11 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (vault.get() == null) {
return;
}
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.get().getMountName());
mountName.setText(vault.getMountName());
} else {
vault.get().setMountName(newValue);
vault.setMountName(newValue);
}
}
@@ -242,20 +257,17 @@ public class UnlockController extends LocalizedFXMLViewController {
}
private void winDriveLetterDidChange(ObservableValue<? extends Character> property, Character oldValue, Character newValue) {
if (vault.get() == null) {
return;
}
vault.get().setWinDriveLetter(newValue);
vault.setWinDriveLetter(newValue);
settings.save();
}
private void chooseSelectedDriveLetter() {
assert SystemUtils.IS_OS_WINDOWS;
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
if (driveLetters.getOccupiedDriveLetters().contains(vault.get().getWinDriveLetter())) {
vault.get().setWinDriveLetter(null);
if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
vault.setWinDriveLetter(null);
}
final Character letter = vault.get().getWinDriveLetter();
final Character letter = vault.getWinDriveLetter();
if (letter == null) {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
@@ -275,10 +287,10 @@ public class UnlockController extends LocalizedFXMLViewController {
progressIndicator.setVisible(true);
downloadsPageLink.setVisible(false);
CharSequence password = passwordField.getCharacters();
asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run();
asyncTaskService.asyncTaskOf(() -> this.unlock(password)).run();
}
private void unlock(Vault vault, CharSequence password) {
private void unlock(CharSequence password) {
try {
vault.activateFrontend(frontendFactory.get(), settings, password);
vault.reveal();
@@ -286,9 +298,15 @@ public class UnlockController extends LocalizedFXMLViewController {
messageText.setText(null);
listener.ifPresent(lstnr -> lstnr.didUnlock(vault));
});
if (keychainAccess.isPresent() && savePassword.isSelected()) {
keychainAccess.get().storePassphrase(vault.getId(), password);
} else {
passwordField.swipe();
}
} catch (InvalidPassphraseException e) {
Platform.runLater(() -> {
messageText.setText(localization.getString("unlock.errorMessage.wrongPassword"));
passwordField.selectAll();
passwordField.requestFocus();
});
} catch (UnsupportedVaultFormatException e) {
@@ -307,7 +325,6 @@ public class UnlockController extends LocalizedFXMLViewController {
});
} finally {
Platform.runLater(() -> {
passwordField.swipe();
mountName.setDisable(false);
advancedOptionsButton.setDisable(false);
progressIndicator.setVisible(false);

View File

@@ -27,11 +27,11 @@ import javafx.scene.control.ProgressIndicator;
public class UpgradeController extends LocalizedFXMLViewController {
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final AsyncTaskService asyncTaskService;
private Optional<UpgradeListener> listener = Optional.empty();
private Vault vault;
@Inject
public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) {
@@ -67,8 +67,6 @@ public class UpgradeController extends LocalizedFXMLViewController {
BooleanExpression passwordProvided = passwordField.textProperty().isNotEmpty().and(passwordField.disabledProperty().not());
BooleanExpression syncFinished = confirmationCheckbox.selectedProperty();
upgradeButton.disableProperty().bind(passwordProvided.not().or(syncFinished.not()));
EasyBind.subscribe(vault, this::vaultDidChange);
}
@Override
@@ -76,9 +74,10 @@ public class UpgradeController extends LocalizedFXMLViewController {
return getClass().getResource("/fxml/upgrade.fxml");
}
private void vaultDidChange(Vault newVault) {
void setVault(Vault vault) {
this.vault = Objects.requireNonNull(vault);
errorLabel.setText(null);
strategy.set(strategies.getUpgradeStrategy(newVault));
strategy.set(strategies.getUpgradeStrategy(vault));
// trigger "default" change to refresh key bindings:
upgradeButton.setDefaultButton(false);
upgradeButton.setDefaultButton(true);
@@ -89,7 +88,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
// ****************************************
private String upgradeNotification(UpgradeStrategy instruction) {
return instruction.getNotification(vault.get());
return instruction.getNotification(vault);
}
// ****************************************
@@ -102,15 +101,14 @@ public class UpgradeController extends LocalizedFXMLViewController {
}
private void upgrade(UpgradeStrategy instruction) {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
asyncTaskService //
.asyncTaskOf(() -> {
if (!instruction.isApplicable(v)) {
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
if (!instruction.isApplicable(vault)) {
throw new IllegalStateException("No ugprade needed for " + vault.path().getValue());
}
instruction.upgrade(v, passwordField.getCharacters());
instruction.upgrade(vault, passwordField.getCharacters());
}) //
.onSuccess(this::showNextUpgrade) //
.onError(UpgradeFailedException.class, e -> {
@@ -125,7 +123,7 @@ public class UpgradeController extends LocalizedFXMLViewController {
private void showNextUpgrade() {
errorLabel.setText(null);
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault);
if (nextStrategy.isPresent()) {
strategy.set(nextStrategy);
} else {

View File

@@ -12,6 +12,7 @@ import static org.apache.commons.lang3.StringUtils.stripStart;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -30,6 +31,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.LazyInitializer;
import org.cryptomator.common.Optionals;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.charsets.NormalizedNameFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
@@ -109,8 +111,10 @@ public class Vault implements CryptoFileSystemDelegate {
public void create(CharSequence passphrase) throws IOException {
try {
FileSystem fs = getNioFileSystem();
if (fs.children().count() > 0) {
throw new FileAlreadyExistsException(null, null, "Vault location not empty.");
if (fs.files().map(File::name).filter(s -> s.endsWith(VAULT_FILE_EXTENSION)).count() > 0) {
throw new FileAlreadyExistsException("masterkey.cryptomator", null, "Vault location not empty.");
} else if (fs.folders().count() > 0) {
throw new DirectoryNotEmptyException(fs.toString());
}
cryptoFileSystemFactory.initializeNew(fs, passphrase);
} catch (UncheckedIOException e) {

View File

@@ -68,12 +68,16 @@
</HBox>
<!-- Row 3.1 -->
<Label text="%unlock.label.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%unlock.label.savePassword" cacheShape="true" cache="true" />
<CheckBox GridPane.rowIndex="1" GridPane.columnIndex="1" fx:id="savePassword" cacheShape="true" cache="true" />
<!-- Row 3.2 -->
<Label fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
<ChoiceBox fx:id="winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%unlock.label.mountName" cacheShape="true" cache="true" />
<TextField GridPane.rowIndex="2" GridPane.columnIndex="1" fx:id="mountName" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
<!-- Row 3.3 -->
<Label GridPane.rowIndex="3" GridPane.columnIndex="0" fx:id="winDriveLetterLabel" text="%unlock.label.winDriveLetter" cacheShape="true" cache="true" />
<ChoiceBox GridPane.rowIndex="3" GridPane.columnIndex="1" fx:id="winDriveLetter" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
</GridPane>
<!-- Row 4 -->

View File

@@ -26,6 +26,7 @@ initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.messageLabel.alreadyInitialized=Vault already initialized
initialize.messageLabel.notEmpty=Vault not empty
initialize.messageLabel.initializationFailed=Could not initialize vault. See log file for details.
initialize.messageLabel.passwordStrength.0=Very weak
initialize.messageLabel.passwordStrength.1=Weak
@@ -49,6 +50,7 @@ upgrade.version3to4.err.io=Migration failed due to an I/O Exception. See log fil
# unlock.fxml
unlock.label.password=Password
unlock.label.savePassword=Save password
unlock.label.mountName=Drive name
unlock.label.winDriveLetter=Drive letter
unlock.label.downloadsPageLink=All Cryptomator versions