diff --git a/.github/workflows/mac-dmg-x64.yml b/.github/workflows/mac-dmg-x64.yml index 28aa29f13..69c15a29e 100644 --- a/.github/workflows/mac-dmg-x64.yml +++ b/.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' diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index ac23b67a8..16e2ba876 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -83,6 +83,15 @@ + + https://github.com/cryptomator/cryptomator/releases/1.16.2 + + + https://github.com/cryptomator/cryptomator/releases/1.16.1 + + + https://github.com/cryptomator/cryptomator/releases/1.16.0 + https://github.com/cryptomator/cryptomator/releases/1.15.3 diff --git a/pom.xml b/pom.xml index 174bb1306..5e4641944 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator cryptomator - 1.16.0-SNAPSHOT + 1.17.0-SNAPSHOT Cryptomator Desktop App @@ -33,10 +33,10 @@ org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents - 2.9.0-beta2 + 2.9.0 1.5.1 1.3.0 - 1.3.0 + 1.3.2 1.5.3 5.0.5 2.0.10 @@ -75,6 +75,17 @@ + + + + + org.cryptomator + webdav-nio-adapter-servlet + 1.2.9 + + + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 4dd4242b3..459d3c52d 100644 --- a/src/main/java/module-info.java +++ b/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; diff --git a/src/main/java/org/cryptomator/common/EventMap.java b/src/main/java/org/cryptomator/common/EventMap.java new file mode 100644 index 000000000..2e8dbf035 --- /dev/null +++ b/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 + *

+ * Use {@link EventMap#put(VaultEvent)} to add an element and {@link EventMap#remove(VaultEvent)} to remove it. + *

+ * 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 { + + private static final int MAX_SIZE = 300; + + public record EventKey(Path ciphertextPath, Class c) {} + + private final ObservableMap delegate; + + @Inject + public EventMap() { + delegate = FXCollections.observableHashMap(); + } + + @Override + public void addListener(MapChangeListener mapChangeListener) { + delegate.addListener(mapChangeListener); + } + + @Override + public void removeListener(MapChangeListener 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 m) { + delegate.putAll(m); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public @NotNull Set keySet() { + return delegate.keySet(); + } + + @Override + public @NotNull Collection values() { + return delegate.values(); + } + + @Override + public @NotNull Set> 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()); + } +} diff --git a/src/main/java/org/cryptomator/common/vaults/Vault.java b/src/main/java/org/cryptomator/common/vaults/Vault.java index 8c0b68e80..2e1ae4bba 100644 --- a/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/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; diff --git a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index c362ca0c0..f66f976dc 100644 --- a/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/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) { diff --git a/src/main/java/org/cryptomator/event/Answer.java b/src/main/java/org/cryptomator/event/Answer.java new file mode 100644 index 000000000..bfb780e52 --- /dev/null +++ b/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(); + } + } +} diff --git a/src/main/java/org/cryptomator/event/NotificationHandler.java b/src/main/java/org/cryptomator/event/NotificationHandler.java new file mode 100644 index 000000000..983a5d2dd --- /dev/null +++ b/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 loadAll() { + return IntegrationsLoader.loadAll(ServiceLoader.load(NotificationHandler.class), NotificationHandler.class); + } +} diff --git a/src/main/java/org/cryptomator/event/VaultEvent.java b/src/main/java/org/cryptomator/event/VaultEvent.java new file mode 100644 index 000000000..8b31747cf --- /dev/null +++ b/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 { + + 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); + } +} diff --git a/src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java b/src/main/java/org/cryptomator/networking/CombinedKeyStoreSpi.java new file mode 100644 index 000000000..426dd5304 --- /dev/null +++ b/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 engineAliases() { + var aliases = new LinkedHashSet(); + 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. + } +} diff --git a/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java b/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java index 0078fdfd3..db52481e2 100644 --- a/src/main/java/org/cryptomator/networking/SSLContextWithMacKeychain.java +++ b/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); + } } } diff --git a/src/main/java/org/cryptomator/ui/error/ErrorComponent.java b/src/main/java/org/cryptomator/ui/error/ErrorComponent.java index 47798c920..5d287942d 100644 --- a/src/main/java/org/cryptomator/ui/error/ErrorComponent.java +++ b/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; } diff --git a/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java b/src/main/java/org/cryptomator/ui/eventview/UpdateEventViewController.java new file mode 100644 index 000000000..19c475447 --- /dev/null +++ b/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() { + + } +} diff --git a/src/main/resources/fxml/vault_detail_unlocked.fxml b/src/main/resources/fxml/vault_detail_unlocked.fxml index d80d8b7b1..289814347 100644 --- a/src/main/resources/fxml/vault_detail_unlocked.fxml +++ b/src/main/resources/fxml/vault_detail_unlocked.fxml @@ -74,7 +74,6 @@ -