瀏覽代碼

Merge branch 'develop' into feature/jdk-24

Armin Schrenk 2 月之前
父節點
當前提交
cba6ed9875
共有 29 個文件被更改,包括 558 次插入30 次删除
  1. 12 3
      .github/workflows/mac-dmg-x64.yml
  2. 9 0
      dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml
  3. 14 3
      pom.xml
  4. 1 0
      src/main/java/module-info.java
  5. 160 0
      src/main/java/org/cryptomator/common/EventMap.java
  6. 2 0
      src/main/java/org/cryptomator/common/vaults/Vault.java
  7. 8 4
      src/main/java/org/cryptomator/common/vaults/VaultListManager.java
  8. 14 0
      src/main/java/org/cryptomator/event/Answer.java
  9. 15 0
      src/main/java/org/cryptomator/event/NotificationHandler.java
  10. 27 0
      src/main/java/org/cryptomator/event/VaultEvent.java
  11. 185 0
      src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java
  12. 12 1
      src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java
  13. 2 2
      src/main/java/org/cryptomator/ui/error/ErrorComponent.java
  14. 14 0
      src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java
  15. 0 1
      src/main/resources/fxml/vault_detail_unlocked.fxml
  16. 11 13
      src/main/resources/fxml/vault_list_cell.fxml
  17. 1 0
      src/main/resources/i18n/strings_es.properties
  18. 1 1
      src/main/resources/i18n/strings_fa.properties
  19. 3 0
      src/main/resources/i18n/strings_fr.properties
  20. 1 0
      src/main/resources/i18n/strings_he.properties
  21. 1 0
      src/main/resources/i18n/strings_it.properties
  22. 1 0
      src/main/resources/i18n/strings_lv.properties
  23. 1 0
      src/main/resources/i18n/strings_nl.properties
  24. 1 0
      src/main/resources/i18n/strings_pt.properties
  25. 1 0
      src/main/resources/i18n/strings_pt_BR.properties
  26. 1 0
      src/main/resources/i18n/strings_ru.properties
  27. 1 0
      src/main/resources/i18n/strings_sk.properties
  28. 37 0
      src/main/resources/i18n/strings_sv.properties
  29. 22 2
      src/main/resources/i18n/strings_zh_TW.properties

+ 12 - 3
.github/workflows/mac-dmg-x64.yml

@@ -2,16 +2,25 @@ name: Build macOS .dmg for x64
 
 #######################################
 # STOP! DO NOT EDIT THIS FILE!
-# 
+#
 # It is a copy of mac-dmg.yml with tiny adjustements (mainly lines 42 to 47)
 # It was made necessary, since Github does not offer free macos intel runners for macos 15 and above.
-# This workflow can only be triggered by a release.
-# 
+#
 #######################################
 
 on:
   release:
     types: [published]
+  workflow_dispatch:
+    inputs:
+      version:
+        description: 'Version'
+        required: false
+      notarize:
+        description: 'Notarize'
+        required: true
+        default: false
+        type: boolean
 
 env:
   JAVA_DIST: 'temurin'

+ 9 - 0
dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml

@@ -83,6 +83,15 @@
 	</content_rating>
 
 	<releases>
+		<release date="2025-05-15" version="1.16.2">
+			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.2</url>
+		</release>
+		<release date="2025-04-30" version="1.16.1">
+			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.1</url>
+		</release>
+		<release date="2025-04-29" version="1.16.0">
+			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.16.0</url>
+		</release>
 		<release date="2025-04-09" version="1.15.3">
 			<url type="details">https://github.com/cryptomator/cryptomator/releases/1.15.3</url>
 		</release>

+ 14 - 3
pom.xml

@@ -3,7 +3,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptomator</artifactId>
-	<version>1.16.0-SNAPSHOT</version>
+	<version>1.17.0-SNAPSHOT</version>
 	<name>Cryptomator Desktop App</name>
 
 	<organization>
@@ -33,10 +33,10 @@
 		<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
 
 		<!-- cryptomator dependencies -->
-		<cryptomator.cryptofs.version>2.9.0-beta2</cryptomator.cryptofs.version>
+		<cryptomator.cryptofs.version>2.9.0</cryptomator.cryptofs.version>
 		<cryptomator.integrations.version>1.5.1</cryptomator.integrations.version>
 		<cryptomator.integrations.win.version>1.3.0</cryptomator.integrations.win.version>
-		<cryptomator.integrations.mac.version>1.3.0</cryptomator.integrations.mac.version>
+		<cryptomator.integrations.mac.version>1.3.2</cryptomator.integrations.mac.version>
 		<cryptomator.integrations.linux.version>1.5.3</cryptomator.integrations.linux.version>
 		<cryptomator.fuse.version>5.0.5</cryptomator.fuse.version>
 		<cryptomator.webdav.version>2.0.10</cryptomator.webdav.version>
@@ -75,6 +75,17 @@
 		<surefire.jacoco.args></surefire.jacoco.args>
 	</properties>
 
+	<!-- TODO: Remove once webdav version 2.0.11 is released -->
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>org.cryptomator</groupId>
+				<artifactId>webdav-nio-adapter-servlet</artifactId>
+				<version>1.2.9</version>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
 	<dependencies>
 		<!-- Cryptomator Libs -->
 		<dependency>

+ 1 - 0
src/main/java/module-info.java

@@ -59,6 +59,7 @@ open module org.cryptomator.desktop {
 
 	uses org.cryptomator.common.locationpresets.LocationPresetsProvider;
 	uses SSLContextProvider;
+	uses org.cryptomator.event.NotificationHandler;
 
 	provides TrayMenuController with AwtTrayMenuController;
 	provides Configurator with LogbackConfiguratorFactory;

+ 160 - 0
src/main/java/org/cryptomator/common/EventMap.java

@@ -0,0 +1,160 @@
+package org.cryptomator.common;
+
+import org.cryptomator.cryptofs.event.BrokenDirFileEvent;
+import org.cryptomator.cryptofs.event.BrokenFileNodeEvent;
+import org.cryptomator.cryptofs.event.ConflictResolutionFailedEvent;
+import org.cryptomator.cryptofs.event.ConflictResolvedEvent;
+import org.cryptomator.cryptofs.event.DecryptionFailedEvent;
+import org.cryptomator.cryptofs.event.FilesystemEvent;
+import org.cryptomator.event.VaultEvent;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javafx.beans.InvalidationListener;
+import javafx.collections.FXCollections;
+import javafx.collections.MapChangeListener;
+import javafx.collections.ObservableMap;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Map containing {@link VaultEvent}s.
+ * The map is keyed by the ciphertext path of the affected resource _and_ the {@link FilesystemEvent}s class in order to group same events
+ * <p>
+ * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it.
+ * <p>
+ * The map is size restricted to {@value MAX_SIZE} elements. If a _new_ element (i.e. not already present) is added, the least recently added is removed.
+ */
+@Singleton
+public class EventMap implements ObservableMap<EventMap.EventKey, VaultEvent> {
+
+	private static final int MAX_SIZE = 300;
+
+	public record EventKey(Path ciphertextPath, Class<? extends FilesystemEvent> c) {}
+
+	private final ObservableMap<EventMap.EventKey, VaultEvent> delegate;
+
+	@Inject
+	public EventMap() {
+		delegate = FXCollections.observableHashMap();
+	}
+
+	@Override
+	public void addListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
+		delegate.addListener(mapChangeListener);
+	}
+
+	@Override
+	public void removeListener(MapChangeListener<? super EventKey, ? super VaultEvent> mapChangeListener) {
+		delegate.removeListener(mapChangeListener);
+	}
+
+	@Override
+	public int size() {
+		return delegate.size();
+	}
+
+	@Override
+	public boolean isEmpty() {
+		return delegate.isEmpty();
+	}
+
+	@Override
+	public boolean containsKey(Object key) {
+		return delegate.containsKey(key);
+	}
+
+	@Override
+	public boolean containsValue(Object value) {
+		return delegate.containsValue(value);
+	}
+
+	@Override
+	public VaultEvent get(Object key) {
+		return delegate.get(key);
+	}
+
+	@Override
+	public @Nullable VaultEvent put(EventKey key, VaultEvent value) {
+		return delegate.put(key, value);
+	}
+
+	@Override
+	public VaultEvent remove(Object key) {
+		return delegate.remove(key);
+	}
+
+	@Override
+	public void putAll(@NotNull Map<? extends EventKey, ? extends VaultEvent> m) {
+		delegate.putAll(m);
+	}
+
+	@Override
+	public void clear() {
+		delegate.clear();
+	}
+
+	@Override
+	public @NotNull Set<EventKey> keySet() {
+		return delegate.keySet();
+	}
+
+	@Override
+	public @NotNull Collection<VaultEvent> values() {
+		return delegate.values();
+	}
+
+	@Override
+	public @NotNull Set<Entry<EventKey, VaultEvent>> entrySet() {
+		return delegate.entrySet();
+	}
+
+	@Override
+	public void addListener(InvalidationListener invalidationListener) {
+		delegate.addListener(invalidationListener);
+	}
+
+	@Override
+	public void removeListener(InvalidationListener invalidationListener) {
+		delegate.removeListener(invalidationListener);
+	}
+
+	public synchronized void put(VaultEvent e) {
+		//compute key
+		var key = computeKey(e.actualEvent());
+		//if-else
+		var nullOrEntry = delegate.get(key);
+		if (nullOrEntry == null) {
+			if (size() == MAX_SIZE) {
+				delegate.entrySet().stream() //
+						.min(Comparator.comparing(entry -> entry.getValue().actualEvent().getTimestamp())) //
+						.ifPresent(oldestEntry -> delegate.remove(oldestEntry.getKey()));
+			}
+			delegate.put(key, e);
+		} else {
+			delegate.put(key, nullOrEntry.incrementCount(e.actualEvent()));
+		}
+	}
+
+	public synchronized VaultEvent remove(VaultEvent similar) {
+		//compute key
+		var key = computeKey(similar.actualEvent());
+		return this.remove(key);
+	}
+
+	private EventKey computeKey(FilesystemEvent e) {
+		var p = switch (e) {
+			case DecryptionFailedEvent(_, Path ciphertextPath, _) -> ciphertextPath;
+			case ConflictResolvedEvent(_, _, _, _, Path resolvedCiphertext) -> resolvedCiphertext;
+			case ConflictResolutionFailedEvent(_, _, Path conflictingCiphertext, _) -> conflictingCiphertext;
+			case BrokenDirFileEvent(_, Path ciphertext) -> ciphertext;
+			case BrokenFileNodeEvent(_, _, Path ciphertext) -> ciphertext;
+		};
+		return new EventKey(p, e.getClass());
+	}
+}

+ 2 - 0
src/main/java/org/cryptomator/common/vaults/Vault.java

@@ -23,6 +23,7 @@ import org.cryptomator.cryptofs.event.FilesystemEvent;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
+import org.cryptomator.event.VaultEvent;
 import org.cryptomator.integrations.mount.MountFailedException;
 import org.cryptomator.integrations.mount.Mountpoint;
 import org.cryptomator.integrations.mount.UnmountFailedException;
@@ -34,6 +35,7 @@ import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Named;
+import javafx.application.Platform;
 import javafx.beans.Observable;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.BooleanBinding;

+ 8 - 4
src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -9,6 +9,7 @@
 package org.cryptomator.common.vaults;
 
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.Constants;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
@@ -35,6 +36,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
 import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
 import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
+import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
 
 @Singleton
 public class VaultListManager {
@@ -115,13 +117,15 @@ public class VaultListManager {
 	private Vault create(VaultSettings vaultSettings) {
 		var wrapper = new VaultConfigCache(vaultSettings);
 		try {
-			if (Objects.isNull(vaultSettings.lastKnownKeyLoader.get())) {
-				var keyIdScheme = wrapper.get().getKeyId().getScheme();
-				vaultSettings.lastKnownKeyLoader.set(keyIdScheme);
-			}
 			var vaultState = determineVaultState(vaultSettings.path.get());
 			if (vaultState == LOCKED) { //for legacy reasons: pre v8 vault do not have a config, but they are in the NEEDS_MIGRATION state
 				wrapper.reloadConfig();
+				if (Objects.isNull(vaultSettings.lastKnownKeyLoader.get())) {
+					var keyIdScheme = wrapper.get().getKeyId().getScheme();
+					vaultSettings.lastKnownKeyLoader.set(keyIdScheme);
+				}
+			} else if (vaultState == NEEDS_MIGRATION) {
+				vaultSettings.lastKnownKeyLoader.set(Constants.DEFAULT_KEY_ID.toString());
 			}
 			return vaultComponentFactory.create(vaultSettings, wrapper, vaultState, null).vault();
 		} catch (IOException e) {

+ 14 - 0
src/main/java/org/cryptomator/event/Answer.java

@@ -0,0 +1,14 @@
+package org.cryptomator.event;
+
+public sealed interface Answer permits Answer.DoNothing, Answer.DoSomething {
+
+
+	record DoNothing() implements Answer {}
+
+	record DoSomething(Runnable action) implements Answer {
+
+		void run() {
+			action.run();
+		}
+	}
+}

+ 15 - 0
src/main/java/org/cryptomator/event/NotificationHandler.java

@@ -0,0 +1,15 @@
+package org.cryptomator.event;
+
+import org.cryptomator.integrations.common.IntegrationsLoader;
+
+import java.util.ServiceLoader;
+import java.util.stream.Stream;
+
+public interface NotificationHandler {
+
+	Answer handle(VaultEvent e);
+
+	static Stream<NotificationHandler> loadAll() {
+		return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class);
+	}
+}

+ 27 - 0
src/main/java/org/cryptomator/event/VaultEvent.java

@@ -0,0 +1,27 @@
+package org.cryptomator.event;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.event.FilesystemEvent;
+
+import java.time.Instant;
+
+public record VaultEvent(Vault v, FilesystemEvent actualEvent, int count) implements Comparable<VaultEvent> {
+
+	public VaultEvent(Vault v, FilesystemEvent actualEvent) {
+		this(v, actualEvent, 1);
+	}
+
+	@Override
+	public int compareTo(VaultEvent other) {
+		var timeResult = actualEvent.getTimestamp().compareTo(other.actualEvent().getTimestamp());
+		if(timeResult != 0) {
+			return timeResult;
+		} else {
+			return this.equals(other) ? 0 : this.actualEvent.getClass().getName().compareTo(other.actualEvent.getClass().getName());
+		}
+	}
+
+	public VaultEvent incrementCount(FilesystemEvent update) {
+		return new VaultEvent(v, update, count+1);
+	}
+}

+ 185 - 0
src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java

@@ -0,0 +1,185 @@
+package org.cryptomator.networking;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.KeyStoreSpi;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.LinkedHashSet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class CombinedKeyStoreSpi extends KeyStoreSpi {
+
+	private final KeyStore primary;
+	private final KeyStore fallback;
+
+	public static CombinedKeyStoreSpi create(KeyStore primary, KeyStore fallback) {
+		checkIfLoaded(primary);
+		checkIfLoaded(fallback);
+		return new CombinedKeyStoreSpi(primary, fallback);
+	}
+
+	private static void checkIfLoaded(KeyStore s) {
+		try {
+			s.aliases();
+		} catch (KeyStoreException e) {
+			throw new IllegalArgumentException("Keystore %s is not loaded.".formatted(s.getType()));
+		}
+	}
+
+	private CombinedKeyStoreSpi(KeyStore primary, KeyStore fallback) {
+		this.primary = primary;
+		this.fallback = fallback;
+	}
+
+	@Override
+	public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException {
+		try {
+			Key key = primary.getKey(alias, password);
+			if (key == null) {
+				key = fallback.getKey(alias, password);
+			}
+			return key;
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public Certificate[] engineGetCertificateChain(String alias) {
+		try {
+			Certificate[] chain = primary.getCertificateChain(alias);
+			if (chain == null) {
+				chain = fallback.getCertificateChain(alias);
+			}
+			return chain;
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public Certificate engineGetCertificate(String alias) {
+		try {
+			Certificate cert = primary.getCertificate(alias);
+			if (cert == null) {
+				cert = fallback.getCertificate(alias);
+			}
+			return cert;
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public Date engineGetCreationDate(String alias) {
+		try {
+			Date date = primary.getCreationDate(alias);
+			if (date == null) {
+				date = fallback.getCreationDate(alias);
+			}
+			return date;
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain) throws KeyStoreException {
+		throw new UnsupportedOperationException("Read-only KeyStore");
+	}
+
+	@Override
+	public void engineSetKeyEntry(String alias, byte[] key, Certificate[] chain) throws KeyStoreException {
+		throw new UnsupportedOperationException("Read-only KeyStore");
+	}
+
+	@Override
+	public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
+		throw new UnsupportedOperationException("Read-only KeyStore");
+	}
+
+	@Override
+	public void engineDeleteEntry(String alias) throws KeyStoreException {
+		throw new UnsupportedOperationException("Read-only KeyStore");
+	}
+
+	@Override
+	public Enumeration<String> engineAliases() {
+		var aliases = new LinkedHashSet<String>();
+		try {
+			primary.aliases().asIterator().forEachRemaining(aliases::add);
+			fallback.aliases().asIterator().forEachRemaining(aliases::add);
+			return Collections.enumeration(aliases);
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public boolean engineContainsAlias(String alias) {
+		try {
+			return primary.containsAlias(alias) || fallback.containsAlias(alias);
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public int engineSize() {
+		var aliases = engineAliases();
+		var i = new AtomicInteger(0);
+		aliases.asIterator().forEachRemaining(_ -> i.incrementAndGet());
+		return i.get();
+	}
+
+	@Override
+	public boolean engineIsKeyEntry(String alias) {
+		try {
+			return primary.isKeyEntry(alias) || fallback.isKeyEntry(alias);
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public boolean engineIsCertificateEntry(String alias) {
+		try {
+			return primary.isCertificateEntry(alias) || fallback.isCertificateEntry(alias);
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public String engineGetCertificateAlias(Certificate cert) {
+		try {
+			String alias = primary.getCertificateAlias(cert);
+			if (alias == null) {
+				alias = fallback.getCertificateAlias(cert);
+			}
+			return alias;
+		} catch (KeyStoreException e) {
+			throw new IllegalStateException("At least one keystore of [%s, %s] is not initialized.".formatted(primary.getType(), fallback.getType()), e);
+		}
+	}
+
+	@Override
+	public void engineStore(OutputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
+		throw new UnsupportedOperationException("Read-only KeyStore");
+	}
+
+	@Override
+	public void engineLoad(InputStream stream, char[] password) throws IOException, NoSuchAlgorithmException, CertificateException {
+		// Nothing to do; the real keystores are already loaded.
+	}
+}

+ 12 - 1
src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java

@@ -6,6 +6,7 @@ import java.io.IOException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.NoSuchAlgorithmException;
+import java.security.Provider;
 import java.security.cert.CertificateException;
 
 /**
@@ -16,6 +17,16 @@ public class SSLContextWithMacKeychain extends SSLContextDifferentTrustStoreBase
 
 	@Override
 	KeyStore getTruststore() throws KeyStoreException, CertificateException, IOException, NoSuchAlgorithmException {
-		return KeyStore.getInstance("KeychainStore-ROOT");
+		var userKeyStore = KeyStore.getInstance("KeychainStore");
+		var systemRootKeyStore = KeyStore.getInstance("KeychainStore-ROOT");
+		userKeyStore.load(null);
+		systemRootKeyStore.load(null);
+		try {
+			CombinedKeyStoreSpi spi = CombinedKeyStoreSpi.create(userKeyStore, systemRootKeyStore);
+			Provider dummyProvider = new Provider("CombinedKeyStoreProvider", "1.0", "Provides a combined, read-only KeyStore") {};
+			return new KeyStore(spi, dummyProvider, "CombinedKeyStoreProvider") {};
+		} catch (IllegalArgumentException e) {
+			throw new KeyStoreException(e);
+		}
 	}
 }

+ 2 - 2
src/main/java/org/cryptomator/ui/error/ErrorComponent.java

@@ -20,8 +20,8 @@ public interface ErrorComponent {
 	default Stage show() {
 		Stage stage = window();
 		stage.setScene(scene());
-		stage.setMinWidth(420);
-		stage.setMinHeight(300);
+		stage.setMinWidth(450);
+		stage.setMinHeight(450);
 		stage.show();
 		return stage;
 	}

+ 14 - 0
src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java

@@ -0,0 +1,14 @@
+package org.cryptomator.ui.eventview;
+
+import org.cryptomator.ui.common.FxController;
+
+import javax.inject.Inject;
+
+@EventViewScoped
+public class UpdateEventViewController implements FxController {
+
+	@Inject
+	public UpdateEventViewController() {
+
+	}
+}

+ 0 - 1
src/main/resources/fxml/vault_detail_unlocked.fxml

@@ -74,7 +74,6 @@
 				<Tooltip text="%main.vaultDetail.decryptName.tooltip"/>
 			</tooltip>
 		</Button>
-
 		<Region HBox.hgrow="ALWAYS"/>
 
 		<Button text="%main.vaultDetail.stats" minWidth="120" onAction="#showVaultStatistics" contentDisplay="BOTTOM" prefHeight="72">

+ 11 - 13
src/main/resources/fxml/vault_list_cell.fxml

@@ -14,17 +14,15 @@
 	  spacing="12"
 	  alignment="CENTER_LEFT">
 	<!-- Remark Check the containing list view for a fixed cell size before editing height properties -->
-	<children>
-		<VBox alignment="CENTER" minWidth="20">
-			<FontAwesome5IconView fx:id="vaultStateView" glyph="${controller.glyph}" HBox.hgrow="NEVER" glyphSize="16"/>
-		</VBox>
-		<VBox spacing="4" HBox.hgrow="ALWAYS">
-			<Label styleClass="header-label" text="${controller.vault.displayName}"/>
-			<Label styleClass="detail-label" text="${controller.vault.displayablePath}" textOverrun="CENTER_ELLIPSIS" visible="${!controller.compactMode}" managed="${!controller.compactMode}">
-				<tooltip>
-					<Tooltip text="${controller.vault.displayablePath}"/>
-				</tooltip>
-			</Label>
-		</VBox>
-	</children>
+	<VBox alignment="CENTER" minWidth="20">
+		<FontAwesome5IconView fx:id="vaultStateView" glyph="${controller.glyph}" HBox.hgrow="NEVER" glyphSize="16"/>
+	</VBox>
+	<VBox spacing="4" HBox.hgrow="ALWAYS">
+		<Label styleClass="header-label" text="${controller.vault.displayName}"/>
+		<Label styleClass="detail-label" text="${controller.vault.displayablePath}" textOverrun="CENTER_ELLIPSIS" visible="${!controller.compactMode}" managed="${!controller.compactMode}">
+			<tooltip>
+				<Tooltip text="${controller.vault.displayablePath}"/>
+			</tooltip>
+		</Label>
+	</VBox>
 </HBox>

+ 1 - 0
src/main/resources/i18n/strings_es.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Error al descifrar nombre de archivos
 # Event View
 eventView.title=Eventos
 eventView.filter.allVaults=Todos
+eventView.clearListButton.tooltip=Borrar lista
 ## event list entries
 eventView.entry.vaultLocked.description=Desbloquear "%s" para más detalles
 eventView.entry.conflictResolved.message=Conflicto resuelto

+ 1 - 1
src/main/resources/i18n/strings_fa.properties

@@ -3,7 +3,7 @@
 # Generics
 generic.action.dismiss=لغو
 ## Button
-generic.button.apply=درخواست
+generic.button.apply=اعمال
 generic.button.back=بازگشت
 generic.button.cancel=انصراف
 generic.button.change=تغییر

+ 3 - 0
src/main/resources/i18n/strings_fr.properties

@@ -424,7 +424,9 @@ main.vaultDetail.stats=Statistiques du volume chiffré
 main.vaultDetail.locateEncryptedFileBtn=Localiser le fichier chiffré
 main.vaultDetail.locateEncryptedFileBtn.tooltip=Choisissez un fichier dans votre coffre pour localiser sa version chiffrée
 main.vaultDetail.encryptedPathsCopied=Chemins d'accès copiés dans le presse-papier !
+main.vaultDetail.locateEncrypted.filePickerTitle=Sélectionner le fichier dans le coffre
 main.vaultDetail.decryptName.buttonLabel=Déchiffrer le nom d'un fichier
+main.vaultDetail.decryptName.tooltip=Choisir un fichier de coffre chiffré pour déchiffrer son nom
 ### Missing
 main.vaultDetail.missing.info=Cryptomator n'a pas pu trouver de volume chiffré dans ce chemin d'accès.
 main.vaultDetail.missing.recheck=Revérifier
@@ -596,6 +598,7 @@ decryptNames.dropZone.error.generic=Impossible de déchiffrer les noms de fichie
 # Event View
 eventView.title=Événements
 eventView.filter.allVaults=Tous
+eventView.clearListButton.tooltip=Effacer la liste
 ## event list entries
 eventView.entry.vaultLocked.description=Déverrouillez "%s" pour plus de détails
 eventView.entry.conflictResolved.message=Conflit résolu

+ 1 - 0
src/main/resources/i18n/strings_he.properties

@@ -49,6 +49,7 @@ addvaultwizard.new.nameInstruction=בחירת שם עבור הכספת
 addvaultwizard.new.namePrompt=שם הכספת
 ### Location
 addvaultwizard.new.locationInstruction=היכן Cryptomator צריך לשמור את הקבצים המוצפנים של הכספת שלך?
+addvaultwizard.new.locationLoading=בודק מערכת קבצים מקומית עבור ספריות ברירת מחדל לאחסון ענן…
 addvaultwizard.new.locationLabel=מיקום אחסון
 addvaultwizard.new.locationPrompt=…
 addvaultwizard.new.directoryPickerLabel=מיקום מותאם אישית

+ 1 - 0
src/main/resources/i18n/strings_it.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Decifratura nomi file non riuscita
 # Event View
 eventView.title=Eventi
 eventView.filter.allVaults=Tutti
+eventView.clearListButton.tooltip=Cancella elenco
 ## event list entries
 eventView.entry.vaultLocked.description=Sblocca "%s" per i dettagli
 eventView.entry.conflictResolved.message=Conflitto risolto

+ 1 - 0
src/main/resources/i18n/strings_lv.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Neizdevās atšifrēt datņu nosaukumus
 # Event View
 eventView.title=Notikumi
 eventView.filter.allVaults=Viss
+eventView.clearListButton.tooltip=Notīrīt sarakstu
 ## event list entries
 eventView.entry.vaultLocked.description=Atslēgt "%s", lai redzētu informāciju
 eventView.entry.conflictResolved.message=Atrisināta nesaderība

+ 1 - 0
src/main/resources/i18n/strings_nl.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Kan bestandsnamen niet decoderen
 # Event View
 eventView.title=Activiteiten
 eventView.filter.allVaults=Alle
+eventView.clearListButton.tooltip=Wis lijst
 ## event list entries
 eventView.entry.vaultLocked.description=Ontgrendel "%s" voor details
 eventView.entry.conflictResolved.message=Opgelost conflict

+ 1 - 0
src/main/resources/i18n/strings_pt.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Falha ao desencriptar nomes de ficheiros
 # Event View
 eventView.title=Eventos
 eventView.filter.allVaults=Todos
+eventView.clearListButton.tooltip=Limpar lista
 ## event list entries
 eventView.entry.vaultLocked.description=Desbloquear "%s" para detalhes
 eventView.entry.conflictResolved.message=Conflito resolvido

+ 1 - 0
src/main/resources/i18n/strings_pt_BR.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Falha ao descriptografar nomes de arquivos
 # Event View
 eventView.title=Eventos
 eventView.filter.allVaults=Todos
+eventView.clearListButton.tooltip=Limpar lista
 ## event list entries
 eventView.entry.vaultLocked.description=Desbloquear "%s" para detalhes
 eventView.entry.conflictResolved.message=Conflito resolvido

+ 1 - 0
src/main/resources/i18n/strings_ru.properties

@@ -599,6 +599,7 @@ decryptNames.dropZone.error.generic=Не удалось расшифровать
 # Event View
 eventView.title=События
 eventView.filter.allVaults=Все
+eventView.clearListButton.tooltip=Очистить список
 ## event list entries
 eventView.entry.vaultLocked.description=Разблокируйте "%s" для деталей
 eventView.entry.conflictResolved.message=Решённый конфликт

+ 1 - 0
src/main/resources/i18n/strings_sk.properties

@@ -577,6 +577,7 @@ decryptNames.dropZone.error.generic=Nepodarilo sa dešifrovať názvy súborov
 # Event View
 eventView.title=Udalosti
 eventView.filter.allVaults=Všetko
+eventView.clearListButton.tooltip=Vymazať zoznam
 ## event list entries
 eventView.entry.vaultLocked.description=Podrobnosti získate odomknutím „%s“
 eventView.entry.conflictResolved.message=Vyriešený konflikt

+ 37 - 0
src/main/resources/i18n/strings_sv.properties

@@ -394,6 +394,7 @@ main.vaultlist.contextMenu.vaultoptions=Visa inställningar för valv
 main.vaultlist.contextMenu.reveal=Visa enhet
 main.vaultlist.addVaultBtn.menuItemNew=Skapa nytt valv...
 main.vaultlist.addVaultBtn.menuItemExisting=Öppna befintligt valv...
+main.vaultlist.showEventsButton.tooltip=Öppna händelsevy
 ##Notificaition
 main.notification.updateAvailable=Uppdatering tillgänglig.
 main.notification.support=Stöd Cryptomator.
@@ -422,6 +423,9 @@ main.vaultDetail.stats=Valv Statistik
 main.vaultDetail.locateEncryptedFileBtn=Leta upp krypterad fil
 main.vaultDetail.locateEncryptedFileBtn.tooltip=Välj en fil från ditt valv för att hitta dess krypterade motsvarighet
 main.vaultDetail.encryptedPathsCopied=Sökvägar kopierade till klippbordet!
+main.vaultDetail.locateEncrypted.filePickerTitle=Välj fil inuti valvet
+main.vaultDetail.decryptName.buttonLabel=Dekryptera filnamn
+main.vaultDetail.decryptName.tooltip=Välj en krypterad valvfil för att dekryptera dess namn
 ### Missing
 main.vaultDetail.missing.info=Cryptomator kunde inte hitta någt valv i denna sökväg.
 main.vaultDetail.missing.recheck=Kontrollera igen
@@ -578,7 +582,40 @@ shareVault.hub.instruction.2=2. Ge åtkomst till gruppmedlemmen i Cryptomatornav
 shareVault.hub.openHub=Öppna kryptomatornav
 
 # Decrypt File Names
+decryptNames.title=Dekryptera filnamn
+decryptNames.filePicker.title=Välj krypterad fil
+decryptNames.filePicker.extensionDescription=Cryptomator krypterad fil
+decryptNames.copyTable.tooltip=Kopiera tabell
+decryptNames.clearTable.tooltip=Rensa tabell
+decryptNames.copyHint=Kopiera cellinnehåll med %s
+decryptNames.dropZone.message=Släpp filer eller klicka för att välja
+decryptNames.dropZone.error.vaultInternalFiles=Interna valvfiler utan dekrypterbart namn valdes
+decryptNames.dropZone.error.foreignFiles=Filer tillhör inte valvet "%s"
+decryptNames.dropZone.error.noDirIdBackup=Katalog med valda filer innehåller inte dirId.c9r fil
+decryptNames.dropZone.error.generic=Det gick inte att dekryptera filnamn
 
 
 # Event View
+eventView.title=Händelser
+eventView.filter.allVaults=Samtliga
+eventView.clearListButton.tooltip=Rensa listan
 ## event list entries
+eventView.entry.vaultLocked.description=Lås upp "%s" för detaljer
+eventView.entry.conflictResolved.message=Löst konflikt
+eventView.entry.conflictResolved.showDecrypted=Visa dekrypterad fil
+eventView.entry.conflictResolved.copyDecrypted=Kopiera dekrypterad sökväg
+eventView.entry.conflict.message=Konfliktlösning misslyckades
+eventView.entry.conflict.showDecrypted=Visa dekrypterad, originalfil
+eventView.entry.conflict.copyDecrypted=Kopiera dekrypterad, ursprunglig sökväg
+eventView.entry.conflict.showEncrypted=Visa krypterad fil som inte kunde synkroniseras
+eventView.entry.conflict.copyEncrypted=Kopiera krypterad sökväg som inte kunde synkroniseras
+eventView.entry.decryptionFailed.message=Dekryptering misslyckades
+eventView.entry.decryptionFailed.showEncrypted=Visa krypterad fil
+eventView.entry.decryptionFailed.copyEncrypted=Kopiera krypterad sökväg
+eventView.entry.brokenDirFile.message=Trasig kataloglänk
+eventView.entry.brokenDirFile.showEncrypted=Visa trasig, krypterad länk
+eventView.entry.brokenDirFile.copyEncrypted=Kopiera sökväg för trasig länk
+eventView.entry.brokenFileNode.message=Trasig filsystemsnod
+eventView.entry.brokenFileNode.showEncrypted=Visa trasig krypterad nod
+eventView.entry.brokenFileNode.copyEncrypted=Kopiera sökväg för trasig, krypterad nod
+eventView.entry.brokenFileNode.copyDecrypted=Kopiera dekrypterad sökväg

+ 22 - 2
src/main/resources/i18n/strings_zh_TW.properties

@@ -94,7 +94,7 @@ addvault.new.readme.accessLocation.2=這是您加密檔案庫的存取位置。
 addvault.new.readme.accessLocation.3=所有被加進這個磁區的檔案都將被 Cryptomator 加密。你可以把它當做磁碟或資料夾使用。這裡式顯示出解密後內容,您的檔案總是以被加密的狀態儲存在磁碟中。
 addvault.new.readme.accessLocation.4=您可以放心移除這個檔案。
 ## Existing
-addvaultwizard.existing.title=添加現有的加密檔案庫
+addvaultwizard.existing.title=開啟現有加密檔案庫
 addvaultwizard.existing.instruction=請選擇現有加密檔案庫中名為「vault.cryptomator」的檔案。如果只有一個名為「masterkey.cryptomator」的檔案,則選擇該檔案。
 addvaultwizard.existing.chooseBtn=選取…
 addvaultwizard.existing.filePickerTitle=選取加密檔案庫的檔案
@@ -127,7 +127,7 @@ unlock.unlockBtn=解鎖
 ## Select
 unlock.chooseMasterkey.message=未找到主金鑰文件
 unlock.chooseMasterkey.description=無法在其預期位置找到加密檔案庫「%s」的主密鑰檔案。請手動選擇密鑰文件。
-unlock.chooseMasterkey.filePickerTitle=选择主金鑰文件
+unlock.chooseMasterkey.filePickerTitle=選擇主金鑰檔案
 unlock.chooseMasterkey.filePickerMimeDesc=Cryptomator 主密鑰
 ## Success
 unlock.success.message=解鎖成功
@@ -177,6 +177,7 @@ hub.registerFailed.description.generic=註冊過程發生錯誤。更多細節
 hub.registerFailed.description.deviceAlreadyExists=其他使用者已在此裝置上註冊。請切換至該使用者帳戶或使用其他裝置進行註冊。
 ### Unauthorized
 hub.unauthorized.message=拒絕存取
+hub.unauthorized.description=您沒有被授權開啟這個加密檔案庫。請聯絡檔案庫擁有者取得權限。
 ### Requires Account Initialization
 hub.requireAccountInit.message=需進一步操作
 hub.requireAccountInit.description.0=請完成您的
@@ -282,6 +283,7 @@ preferences.title=偏好
 ## General
 preferences.general=一般
 preferences.general.startHidden=啟動 Cryptomator 時隱藏視窗
+preferences.general.autoCloseVaults=當離開應用程式的時候直接鎖定檔案庫而不詢問
 preferences.general.debugLogging=啟用除錯日誌
 preferences.general.debugDirectory=顯示日誌檔
 preferences.general.autoStart=系統啟動時同時啟動 Cryptomator
@@ -393,6 +395,7 @@ main.vaultlist.contextMenu.vaultoptions=顯示加密檔案庫選項
 main.vaultlist.contextMenu.reveal=顯示磁碟
 main.vaultlist.addVaultBtn.menuItemNew=新建加密檔案庫...
 main.vaultlist.addVaultBtn.menuItemExisting=開啟現有的加密檔案庫...
+main.vaultlist.showEventsButton.tooltip=打開事件檢視
 ##Notificaition
 main.notification.updateAvailable=有可用更新
 main.notification.support=贊助 Cryptomator.
@@ -421,6 +424,9 @@ main.vaultDetail.stats=加密檔案庫統計
 main.vaultDetail.locateEncryptedFileBtn=顯示加密檔案路徑
 main.vaultDetail.locateEncryptedFileBtn.tooltip=選擇要顯示對應加密檔案路徑的加密檔案庫檔案
 main.vaultDetail.encryptedPathsCopied=路徑已複製到剪貼簿
+main.vaultDetail.locateEncrypted.filePickerTitle=從加密檔案庫中選擇檔案
+main.vaultDetail.decryptName.buttonLabel=解密檔案名稱
+main.vaultDetail.decryptName.tooltip=選擇加密的檔案庫檔案以解密其名稱
 ### Missing
 main.vaultDetail.missing.info=Cryptomator 無法在指定位置找到加密檔案庫。
 main.vaultDetail.missing.recheck=重新檢查
@@ -577,7 +583,21 @@ shareVault.hub.instruction.2=2. 在Cryptomator Hub中允許團隊成員對加密
 shareVault.hub.openHub=打開 Cryptomator Hub
 
 # Decrypt File Names
+decryptNames.filePicker.title=選擇已加密的檔案
+decryptNames.filePicker.extensionDescription=Cryptomator 加密檔案
+decryptNames.copyTable.tooltip=複製表格
+decryptNames.clearTable.tooltip=清除表格
+decryptNames.dropZone.error.foreignFiles=檔案不屬於加密檔案庫「%s」
+decryptNames.dropZone.error.generic=解密檔案名稱失敗
 
 
 # Event View
+eventView.title=事件
+eventView.filter.allVaults=全部
 ## event list entries
+eventView.entry.decryptionFailed.message=解密失敗
+eventView.entry.decryptionFailed.showEncrypted=顯示加密的檔案
+eventView.entry.decryptionFailed.copyEncrypted=複製加密路徑
+eventView.entry.brokenDirFile.message=損壞的目錄連結
+eventView.entry.brokenDirFile.showEncrypted=顯示損壞的加密路徑
+eventView.entry.brokenDirFile.copyEncrypted=複製損壞的路徑連結