mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
Merge branch 'feature/external-keychain' into develop
This commit is contained in:
@@ -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
52
main/keychain/pom.xml
Normal 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>
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
33
main/keychain/src/test/resources/log4j2.xml
Normal file
33
main/keychain/src/test/resources/log4j2.xml
Normal 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>
|
||||
19
main/pom.xml
19
main/pom.xml
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user