From 0bece0f59124231176473c790fdb157e635c079c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 13:56:59 +0100 Subject: [PATCH 001/141] move passphrase entry to subcomponent and use CompletableFuture instead of UserInteractionLock + AtomicReference --- .../org/cryptomator/ui/common/FxmlFile.java | 2 +- .../ui/keyloading/KeyLoadingModule.java | 2 +- .../MasterkeyFileLoadingFinisher.java | 62 -------------- .../MasterkeyFileLoadingModule.java | 39 +-------- .../MasterkeyFileLoadingStrategy.java | 81 ++++++++++++------- .../PassphraseEntryComponent.java | 35 ++++++++ .../PassphraseEntryController.java | 80 +++++++----------- .../masterkeyfile/PassphraseEntryModule.java | 41 ++++++++++ .../masterkeyfile/PassphraseEntryResult.java | 6 ++ .../masterkeyfile/PassphraseEntryScoped.java | 13 +++ 10 files changed, 179 insertions(+), 182 deletions(-) delete mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index b8d5bbff0..bc952f9d1 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -42,7 +42,7 @@ public enum FxmlFile { this.ressourcePathString = ressourcePathString; } - String getRessourcePathString() { + public String getRessourcePathString() { return ressourcePathString; } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java index bff757b1a..616e7e5e0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -26,7 +26,7 @@ abstract class KeyLoadingModule { @Provides @KeyLoading @KeyLoadingScoped - static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> strategies) { + static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map> strategies) { try { String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme(); var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme)); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java deleted file mode 100644 index 44d7ebfb0..000000000 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.cryptomator.ui.keyloading.masterkeyfile; - -import org.cryptomator.common.keychain.KeychainManager; -import org.cryptomator.common.vaults.Vault; -import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Inject; -import javax.inject.Named; -import java.nio.CharBuffer; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - -@KeyLoadingScoped -class MasterkeyFileLoadingFinisher { - - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class); - - private final Vault vault; - private final Optional storedPassword; - private final AtomicReference enteredPassword; - private final AtomicBoolean shouldSavePassword; - private final KeychainManager keychain; - - @Inject - MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) { - this.vault = vault; - this.storedPassword = storedPassword; - this.enteredPassword = enteredPassword; - this.shouldSavePassword = shouldSavePassword; - this.keychain = keychain; - } - - public void cleanup(boolean successfullyUnlocked) { - if (successfullyUnlocked && shouldSavePassword.get()) { - savePasswordToSystemkeychain(); - } - wipePassword(storedPassword.orElse(null)); - wipePassword(enteredPassword.getAndSet(null)); - } - - private void savePasswordToSystemkeychain() { - if (keychain.isSupported()) { - try { - keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(enteredPassword.get())); - } catch (KeychainAccessException e) { - LOG.error("Failed to store passphrase in system keychain.", e); - } - } - } - - private void wipePassword(char[] pw) { - if (pw != null) { - Arrays.fill(pw, ' '); - } - } -} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java index 901eacfb9..7f55de898 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -25,30 +25,18 @@ import javax.inject.Named; import javafx.scene.Scene; import java.nio.file.Path; import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -@Module(subcomponents = {ForgetPasswordComponent.class}) +@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class}) public abstract class MasterkeyFileLoadingModule { private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class); - public enum PasswordEntry { - PASSWORD_ENTERED, - CANCELED - } - public enum MasterkeyFileProvision { MASTERKEYFILE_PROVIDED, CANCELED } - @Provides - @KeyLoadingScoped - static UserInteractionLock providePasswordEntryLock() { - return new UserInteractionLock<>(null); - } - @Provides @KeyLoadingScoped static UserInteractionLock provideMasterkeyFileProvisionLock() { @@ -77,26 +65,6 @@ public abstract class MasterkeyFileLoadingModule { return new AtomicReference<>(); } - @Provides - @KeyLoadingScoped - static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { - return new AtomicReference<>(storedPassword.orElse(null)); - } - - @Provides - @Named("savePassword") - @KeyLoadingScoped - static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional storedPassword) { - return new AtomicBoolean(storedPassword.isPresent()); - } - - @Provides - @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) - @KeyLoadingScoped - static Scene provideUnlockScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.UNLOCK_ENTER_PASSWORD); - } - @Provides @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) @KeyLoadingScoped @@ -104,11 +72,6 @@ public abstract class MasterkeyFileLoadingModule { return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE); } - @Binds - @IntoMap - @FxControllerKey(PassphraseEntryController.class) - abstract FxController bindUnlockController(PassphraseEntryController controller); - @Binds @IntoMap @FxControllerKey(SelectMasterkeyFileController.class) diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 1fa7dd986..2ee17a744 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -2,12 +2,14 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import com.google.common.base.Preconditions; import dagger.Lazy; +import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.common.BackupHelper; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; +import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; @@ -17,6 +19,7 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.unlock.UnlockCancelledException; import javax.inject.Inject; +import javax.inject.Named; import javafx.application.Platform; import javafx.scene.Scene; import javafx.stage.Stage; @@ -26,6 +29,10 @@ import java.net.URI; import java.nio.CharBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; @KeyLoading @@ -36,28 +43,28 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final Vault vault; private final MasterkeyFileAccess masterkeyFileAccess; private final Stage window; - private final Lazy passphraseEntryScene; private final Lazy selectMasterkeyFileScene; - private final UserInteractionLock passwordEntryLock; + private final PassphraseEntryComponent.Builder passphraseEntry; private final UserInteractionLock masterkeyFileProvisionLock; - private final AtomicReference password; private final AtomicReference filePath; - private final MasterkeyFileLoadingFinisher finisher; + private final KeychainManager keychain; - private boolean wrongPassword; + private char[] passphrase; + private boolean savePassphrase; + private boolean wrongPassphrase; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference filePath, MasterkeyFileLoadingFinisher finisher) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, UserInteractionLock masterkeyFileProvisionLock, AtomicReference filePath, KeychainManager keychain) { this.vault = vault; this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; - this.passphraseEntryScene = passphraseEntryScene; this.selectMasterkeyFileScene = selectMasterkeyFileScene; - this.passwordEntryLock = passwordEntryLock; + this.passphraseEntry = passphraseEntry; this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; - this.password = password; this.filePath = filePath; - this.finisher = finisher; + this.keychain = keychain; + this.passphrase = savedPassphrase.orElse(null); + this.savePassphrase = savedPassphrase.isPresent(); } @Override @@ -68,8 +75,10 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { if (!Files.exists(filePath)) { filePath = getAlternateMasterkeyFilePath(); } - CharSequence passphrase = getPassphrase(); - var masterkey = masterkeyFileAccess.load(filePath, passphrase); + if (passphrase == null) { + askForPassphrase(); + } + var masterkey = masterkeyFileAccess.load(filePath, CharBuffer.wrap(passphrase)); //backup if (filePath.startsWith(vault.getPath())) { try { @@ -90,8 +99,8 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @Override public boolean recoverFromException(MasterkeyLoadingFailedException exception) { if (exception instanceof InvalidPassphraseException) { - this.wrongPassword = true; - password.set(null); + this.wrongPassphrase = true; + this.passphrase = null; return true; // reattempting key load } else { return false; // nothing we can do @@ -100,7 +109,20 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @Override public void cleanup(boolean unlockedSuccessfully) { - finisher.cleanup(unlockedSuccessfully); + if (unlockedSuccessfully && savePassphrase) { + savePasswordToSystemkeychain(passphrase); + } + Arrays.fill(passphrase, '\0'); + } + + private void savePasswordToSystemkeychain(char[] passphrase) { + if (keychain.isSupported()) { + try { + keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(passphrase)); + } catch (KeychainAccessException e) { + LOG.error("Failed to store passphrase in system keychain.", e); + } + } } private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException { @@ -129,21 +151,10 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { return masterkeyFileProvisionLock.awaitInteraction(); } - private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException { - if (password.get() == null) { - return switch (askForPassphrase()) { - case PASSWORD_ENTERED -> CharBuffer.wrap(password.get()); - case CANCELED -> throw new UnlockCancelledException("Password entry cancelled."); - }; - } else { - // e.g. pre-filled from keychain or previous unlock attempt - return CharBuffer.wrap(password.get()); - } - } - - private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException { + private void askForPassphrase() throws InterruptedException { + var comp = passphraseEntry.savedPassword(passphrase).build(); Platform.runLater(() -> { - window.setScene(passphraseEntryScene.get()); + window.setScene(comp.passphraseEntryScene()); window.show(); Window owner = window.getOwner(); if (owner != null) { @@ -152,11 +163,19 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } else { window.centerOnScreen(); } - if (wrongPassword) { + if (wrongPassphrase) { Animations.createShakeWindowAnimation(window).play(); } }); - return passwordEntryLock.awaitInteraction(); + try { + var result = comp.result().get(); + this.passphrase = result.passphrase(); + this.savePassphrase = result.savePassphrase(); + } catch (CancellationException e) { + throw new UnlockCancelledException("Password entry cancelled."); + } catch (ExecutionException e) { + throw new MasterkeyLoadingFailedException("Failed to ask for password.", e); + } } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java new file mode 100644 index 000000000..637ffd5c6 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java @@ -0,0 +1,35 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.BindsInstance; +import dagger.Subcomponent; +import org.cryptomator.common.Nullable; +import org.cryptomator.ui.common.Animations; + +import javax.inject.Named; +import javafx.scene.Scene; +import javafx.stage.Stage; +import javafx.stage.Window; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +@PassphraseEntryScoped +@Subcomponent(modules = {PassphraseEntryModule.class}) +public interface PassphraseEntryComponent { + + @PassphraseEntryScoped + Scene passphraseEntryScene(); + + @PassphraseEntryScoped + CompletableFuture result(); + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") char[] savedPassword); + + PassphraseEntryComponent build(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index f6ce79e51..d048cb776 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -1,16 +1,13 @@ package org.cryptomator.ui.keyloading.masterkeyfile; +import org.cryptomator.common.Nullable; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.common.WeakBindings; -import org.cryptomator.ui.controls.FontAwesome5IconView; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,8 +18,8 @@ import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; +import javafx.application.Platform; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.StringBinding; import javafx.beans.property.BooleanProperty; @@ -37,33 +34,27 @@ import javafx.scene.transform.Translate; import javafx.stage.Stage; import javafx.stage.WindowEvent; import javafx.util.Duration; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CompletableFuture; -@KeyLoadingScoped +@PassphraseEntryScoped public class PassphraseEntryController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class); private final Stage window; private final Vault vault; - private final AtomicReference password; - private final AtomicBoolean savePassword; - private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; + private final CompletableFuture result; + private final char[] savedPassword; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; - private final ObjectBinding unlockButtonContentDisplay; - private final BooleanBinding userInteractionDisabled; - private final BooleanProperty unlockButtonDisabled; private final StringBinding vaultName; + private final BooleanProperty unlockInProgress = new SimpleBooleanProperty(); + private final ObjectBinding unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, unlockInProgress); + private final BooleanProperty unlockButtonDisabled = new SimpleBooleanProperty(); /* FXML */ public NiceSecurePasswordField passwordField; public CheckBox savePasswordCheckbox; - public FontAwesome5IconView unlockInProgressView; public ImageView face; public ImageView leftArm; public ImageView rightArm; @@ -72,29 +63,25 @@ public class PassphraseEntryController implements FxController { public Animation unlockAnimation; @Inject - public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") char[] savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { this.window = window; this.vault = vault; - this.password = password; - this.savePassword = savePassword; + this.result = result; this.savedPassword = savedPassword; - this.passwordEntryLock = passwordEntryLock; this.forgetPassword = forgetPassword; this.keychain = keychain; - this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction()); - this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not(); - this.unlockButtonDisabled = new SimpleBooleanProperty(); this.vaultName = WeakBindings.bindString(vault.displayNameProperty()); - this.window.setOnHiding(this::windowClosed); + window.setOnHiding(this::windowClosed); + result.whenCompleteAsync((r, t) -> unlockInProgress.set(false), Platform::runLater); } @FXML public void initialize() { - savePasswordCheckbox.setSelected(savedPassword.isPresent()); - if (password.get() != null) { - passwordField.setPassword(password.get()); + if (savedPassword != null) { + savePasswordCheckbox.setSelected(true); + passwordField.setPassword(savedPassword); } - unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty())); + unlockButtonDisabled.bind(unlockInProgress.or(passwordField.textProperty().isEmpty())); var leftArmTranslation = new Translate(24, 0); var leftArmRotation = new Rotate(60, 16, 30, 0); @@ -132,7 +119,7 @@ public class PassphraseEntryController implements FxController { new KeyFrame(Duration.millis(1000), faceVisible) // ); - passwordEntryLock.awaitingInteraction().addListener(observable -> stopUnlockAnimation()); + result.whenCompleteAsync((r, t) -> stopUnlockAnimation()); } @FXML @@ -141,26 +128,20 @@ public class PassphraseEntryController implements FxController { } private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, mark this workflow as cancelled: - if (passwordEntryLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - passwordEntryLock.interacted(PasswordEntry.CANCELED); - } + LOG.debug("Unlock canceled by user."); + result.cancel(true); } @FXML public void unlock() { LOG.trace("UnlockController.unlock()"); + unlockInProgress.set(true); CharSequence pwFieldContents = passwordField.getCharacters(); - char[] newPw = new char[pwFieldContents.length()]; + char[] pw = new char[pwFieldContents.length()]; for (int i = 0; i < pwFieldContents.length(); i++) { - newPw[i] = pwFieldContents.charAt(i); + pw[i] = pwFieldContents.charAt(i); } - char[] oldPw = password.getAndSet(newPw); - if (oldPw != null) { - Arrays.fill(oldPw, ' '); - } - passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED); + result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected())); startUnlockAnimation(); } @@ -184,8 +165,7 @@ public class PassphraseEntryController implements FxController { @FXML private void didClickSavePasswordCheckbox() { - savePassword.set(savePasswordCheckbox.isSelected()); - if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) { + if (!savePasswordCheckbox.isSelected() && savedPassword != null) { forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten)); } } @@ -205,15 +185,15 @@ public class PassphraseEntryController implements FxController { } public ContentDisplay getUnlockButtonContentDisplay() { - return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT; + return unlockInProgress.get() ? ContentDisplay.LEFT : ContentDisplay.TEXT_ONLY; } - public BooleanBinding userInteractionDisabledProperty() { - return userInteractionDisabled; + public ReadOnlyBooleanProperty userInteractionDisabledProperty() { + return unlockInProgress; } public boolean isUserInteractionDisabled() { - return userInteractionDisabled.get(); + return unlockInProgress.get(); } public ReadOnlyBooleanProperty unlockButtonDisabledProperty() { @@ -227,4 +207,6 @@ public class PassphraseEntryController implements FxController { public boolean isKeychainAccessAvailable() { return keychain.isSupported(); } + + } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java new file mode 100644 index 000000000..aec32e5e6 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -0,0 +1,41 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Module; +import dagger.Provides; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +@Module +abstract class PassphraseEntryModule { + + @Provides + @PassphraseEntryScoped + static CompletableFuture provideResult() { + return new CompletableFuture<>(); + } + + @Provides + @PassphraseEntryScoped + static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + // TODO: simplify FxmlLoaderFactory + try { + var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_ENTER_PASSWORD.getRessourcePathString()); + var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); + Parent root = loader.load(); + return sceneFactory.apply(root); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load UnlockScene", e); + } + } + + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java new file mode 100644 index 000000000..2c55c203e --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java @@ -0,0 +1,6 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +// TODO needs to be public due to Dagger -.- +public record PassphraseEntryResult(char[] passphrase, boolean savePassphrase) { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java new file mode 100644 index 000000000..a077bcf81 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface PassphraseEntryScoped { + +} From 85a5146d4bce1ea6671913f3448ad93f30a92c5f Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 13:59:10 +0100 Subject: [PATCH 002/141] wipe pw before losing reference [ci skip] --- .../keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 2ee17a744..3a5c7548b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -100,6 +100,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { public boolean recoverFromException(MasterkeyLoadingFailedException exception) { if (exception instanceof InvalidPassphraseException) { this.wrongPassphrase = true; + Arrays.fill(passphrase, '\0'); this.passphrase = null; return true; // reattempting key load } else { From d72d9f533c0ed6a8dd6c67ed628ccf841f31ba38 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 16:41:07 +0100 Subject: [PATCH 003/141] Replace CharBuffer and char[] instances by destroyable Passphrase --- .../org/cryptomator/common/Passphrase.java | 115 ++++++++++++++++ .../ui/controls/NiceSecurePasswordField.java | 4 +- .../ui/controls/SecurePasswordField.java | 6 +- .../MasterkeyFileLoadingStrategy.java | 19 +-- .../PassphraseEntryComponent.java | 8 +- .../PassphraseEntryController.java | 10 +- .../masterkeyfile/PassphraseEntryResult.java | 4 +- .../cryptomator/common/PassphraseTest.java | 130 ++++++++++++++++++ 8 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 src/main/java/org/cryptomator/common/Passphrase.java create mode 100644 src/test/java/org/cryptomator/common/PassphraseTest.java diff --git a/src/main/java/org/cryptomator/common/Passphrase.java b/src/main/java/org/cryptomator/common/Passphrase.java new file mode 100644 index 000000000..d57252507 --- /dev/null +++ b/src/main/java/org/cryptomator/common/Passphrase.java @@ -0,0 +1,115 @@ +package org.cryptomator.common; + +import org.cryptomator.cryptolib.common.MessageDigestSupplier; + +import javax.security.auth.Destroyable; +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * A destroyable CharSequence. + */ +public class Passphrase implements Destroyable, CharSequence { + + private final char[] data; + private final int offset; + private final int length; + private boolean destroyed; + + /** + * Wraps (doesn't copy) the given data. + * + * @param data The wrapped data. Any changes to this will be reflected in this passphrase + */ + public Passphrase(char[] data) { + this(data, 0, data.length); + } + + /** + * Wraps (doesn't copy) a subarray of the given data. + * + * @param data The wrapped data. Any changes to this will be reflected in this passphrase + * @param offset The subarray offset, i.e. the first character of this passphrase + * @param length The subarray length, i.e. the length of this passphrase + */ + public Passphrase(char[] data, int offset, int length) { + if (offset < 0 || length < 0 || offset + length > data.length) { + throw new IndexOutOfBoundsException("[%1$d %1$d + %2$d[ not within [0, %3$d[".formatted(offset, length, data.length)); + } + this.data = data; + this.offset = offset; + this.length = length; + } + + public static Passphrase copyOf(CharSequence cs) { + char[] result = new char[cs.length()]; + for (int i = 0; i < cs.length(); i++) { + result[i] = cs.charAt(i); + } + return new Passphrase(result); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Passphrase that = (Passphrase) o; + // time-constant comparison + int diff = 0; + for (int i = 0; i < length; i++) { + diff |= charAt(i) ^ that.charAt(i); + } + return diff == 0; + } + + @Override + public int hashCode() { + // TODO: do we really need to a secure hashcode? toString leaks the pw anyway + var md = MessageDigestSupplier.SHA256.get(); + ByteBuffer buf = ByteBuffer.allocate(Character.BYTES * length); + for (int i = 0; i < length; i++) { + char c = charAt(i); + buf.putChar(i * Character.BYTES, c); + } + buf.flip(); + md.update(buf); + return Arrays.hashCode(md.digest()); + } + + @Override + public String toString() { + return new String(data, offset, length); + } + + @Override + public int length() { + return length; + } + + @Override + public char charAt(int index) { + if (index < 0 || index >= length) { + throw new IndexOutOfBoundsException("%d not within [0, %d[".formatted(index, length)); + } + return data[offset + index]; + } + + @Override + public Passphrase subSequence(int start, int end) { + if (start < 0 || end < 0 || end > length || start > end) { + throw new IndexOutOfBoundsException("[%d, %d[ not within [0, %d[".formatted(start, end, length)); + } + return new Passphrase(Arrays.copyOfRange(data, offset + start, offset + end)); + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public void destroy() { + Arrays.fill(data, offset, offset + length, '\0'); + destroyed = true; + } +} diff --git a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java index 4a4e43fff..4d09707b9 100644 --- a/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java @@ -1,5 +1,7 @@ package org.cryptomator.ui.controls; +import org.cryptomator.common.Passphrase; + import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.property.StringProperty; @@ -82,7 +84,7 @@ public class NiceSecurePasswordField extends StackPane { return passwordField.textProperty(); } - public CharSequence getCharacters() { + public Passphrase getCharacters() { return passwordField.getCharacters(); } diff --git a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java index 0290f512d..66df79394 100644 --- a/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +++ b/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java @@ -9,6 +9,7 @@ package org.cryptomator.ui.controls; import com.google.common.base.Strings; +import org.cryptomator.common.Passphrase; import javafx.application.Platform; import javafx.beans.NamedArg; @@ -28,7 +29,6 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.input.TransferMode; -import java.nio.CharBuffer; import java.text.Normalizer; import java.text.Normalizer.Form; import java.util.Arrays; @@ -203,8 +203,8 @@ public class SecurePasswordField extends TextField { * @see #wipe() */ @Override - public CharSequence getCharacters() { - return CharBuffer.wrap(content, 0, length); + public Passphrase getCharacters() { + return new Passphrase(content, 0, length); } /** diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 3a5c7548b..a22903973 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -13,6 +13,7 @@ import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.common.Passphrase; import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; @@ -26,10 +27,8 @@ import javafx.stage.Stage; import javafx.stage.Window; import java.io.IOException; import java.net.URI; -import java.nio.CharBuffer; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -49,7 +48,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final AtomicReference filePath; private final KeychainManager keychain; - private char[] passphrase; + private Passphrase passphrase; private boolean savePassphrase; private boolean wrongPassphrase; @@ -63,7 +62,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; this.filePath = filePath; this.keychain = keychain; - this.passphrase = savedPassphrase.orElse(null); + this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null); this.savePassphrase = savedPassphrase.isPresent(); } @@ -78,7 +77,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { if (passphrase == null) { askForPassphrase(); } - var masterkey = masterkeyFileAccess.load(filePath, CharBuffer.wrap(passphrase)); + var masterkey = masterkeyFileAccess.load(filePath, passphrase); //backup if (filePath.startsWith(vault.getPath())) { try { @@ -100,7 +99,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { public boolean recoverFromException(MasterkeyLoadingFailedException exception) { if (exception instanceof InvalidPassphraseException) { this.wrongPassphrase = true; - Arrays.fill(passphrase, '\0'); + passphrase.destroy(); this.passphrase = null; return true; // reattempting key load } else { @@ -113,13 +112,15 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { if (unlockedSuccessfully && savePassphrase) { savePasswordToSystemkeychain(passphrase); } - Arrays.fill(passphrase, '\0'); + if (passphrase != null) { + passphrase.destroy(); + } } - private void savePasswordToSystemkeychain(char[] passphrase) { + private void savePasswordToSystemkeychain(Passphrase passphrase) { if (keychain.isSupported()) { try { - keychain.storePassphrase(vault.getId(), vault.getDisplayName(), CharBuffer.wrap(passphrase)); + keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase); } catch (KeychainAccessException e) { LOG.error("Failed to store passphrase in system keychain.", e); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java index 637ffd5c6..5e072efd0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryComponent.java @@ -3,15 +3,11 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import dagger.BindsInstance; import dagger.Subcomponent; import org.cryptomator.common.Nullable; -import org.cryptomator.ui.common.Animations; +import org.cryptomator.common.Passphrase; import javax.inject.Named; import javafx.scene.Scene; -import javafx.stage.Stage; -import javafx.stage.Window; -import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; @PassphraseEntryScoped @Subcomponent(modules = {PassphraseEntryModule.class}) @@ -27,7 +23,7 @@ public interface PassphraseEntryComponent { interface Builder { @BindsInstance - PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") char[] savedPassword); + PassphraseEntryComponent.Builder savedPassword(@Nullable @Named("savedPassword") Passphrase savedPassword); PassphraseEntryComponent build(); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index d048cb776..35b1b1903 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -4,6 +4,7 @@ import org.cryptomator.common.Nullable; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.common.Passphrase; import org.cryptomator.ui.common.WeakBindings; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; @@ -44,7 +45,7 @@ public class PassphraseEntryController implements FxController { private final Stage window; private final Vault vault; private final CompletableFuture result; - private final char[] savedPassword; + private final Passphrase savedPassword; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; private final StringBinding vaultName; @@ -63,7 +64,7 @@ public class PassphraseEntryController implements FxController { public Animation unlockAnimation; @Inject - public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") char[] savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { this.window = window; this.vault = vault; this.result = result; @@ -137,10 +138,7 @@ public class PassphraseEntryController implements FxController { LOG.trace("UnlockController.unlock()"); unlockInProgress.set(true); CharSequence pwFieldContents = passwordField.getCharacters(); - char[] pw = new char[pwFieldContents.length()]; - for (int i = 0; i < pwFieldContents.length(); i++) { - pw[i] = pwFieldContents.charAt(i); - } + Passphrase pw = Passphrase.copyOf(pwFieldContents); result.complete(new PassphraseEntryResult(pw, savePasswordCheckbox.isSelected())); startUnlockAnimation(); } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java index 2c55c203e..f26184d9d 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java @@ -1,6 +1,8 @@ package org.cryptomator.ui.keyloading.masterkeyfile; +import org.cryptomator.common.Passphrase; + // TODO needs to be public due to Dagger -.- -public record PassphraseEntryResult(char[] passphrase, boolean savePassphrase) { +public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) { } diff --git a/src/test/java/org/cryptomator/common/PassphraseTest.java b/src/test/java/org/cryptomator/common/PassphraseTest.java new file mode 100644 index 000000000..7bd71beb2 --- /dev/null +++ b/src/test/java/org/cryptomator/common/PassphraseTest.java @@ -0,0 +1,130 @@ +package org.cryptomator.common; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class PassphraseTest { + + @ParameterizedTest + @CsvSource(value = { + "-1, 0", + "0, -1", + "0, 10", + "10, 0", + "10, 10" + }) + public void testInvalidConstructorArgs(int offset, int length) { + char[] data = "test".toCharArray(); + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> { + new Passphrase(data, offset, length); + }); + } + + @ParameterizedTest + @CsvSource(value = { + "0, 4", + "0, 0", + "0, 1", + "1, 1", + "2, 2" + }) + public void testValidConstructorArgs(int offset, int length) { + char[] data = "test".toCharArray(); + var pw = new Passphrase(data, offset, length); + Assertions.assertEquals(length, pw.length()); + Assertions.assertEquals("test".substring(offset, offset + length), pw.toString()); + } + + @Test + public void testToString() { + Assertions.assertEquals("test", Passphrase.copyOf("test").toString()); + } + + @Nested + class WithInstances { + + private Passphrase pw1; + private Passphrase pw2; + + @BeforeEach + public void setup() { + char[] foo = "test test".toCharArray(); + pw1 = new Passphrase(foo, 5, 4); + pw2 = Passphrase.copyOf("test"); + } + + @Test + public void testEquals() { + Assertions.assertEquals(pw1, pw2); + Assertions.assertEquals("test", pw1.toString()); + Assertions.assertEquals("test", pw2.toString()); + } + + @Test + public void testHashcode() { + Assertions.assertEquals(pw1.hashCode(), pw2.hashCode()); + } + + @Test + public void testLength() { + Assertions.assertEquals(4, pw1.length()); + Assertions.assertEquals(4, pw2.length()); + } + + @Test + public void testCharAt() { + Assertions.assertEquals('s', pw1.charAt(2)); + } + + @ParameterizedTest + @ValueSource(ints = {-1, 4, 5}) + public void testInvalidCharAt(int idx) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.charAt(idx)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3}) + public void testValidCharAt(int idx) { + Assertions.assertEquals("test".charAt(idx), pw1.charAt(idx)); + } + + @ParameterizedTest + @CsvSource(value = { + "-1, 0", + "0, -1", + "-1, -1", + "0, 5", + "3, 2" + }) + public void testInvalidSubSequence(int start, int end) { + Assertions.assertThrows(IndexOutOfBoundsException.class, () -> pw1.subSequence(start, end)); + } + + @ParameterizedTest + @CsvSource(value = { + "0, 4", + "1, 4", + "0, 2", + "2, 4", + "4, 4", + }) + public void testValidSubSequence(int start, int end) { + Assertions.assertEquals("test".substring(start, end), pw1.subSequence(start, end).toString()); + } + + @Test + public void testDestroy() { + pw2.destroy(); + Assertions.assertFalse(pw1.isDestroyed()); + Assertions.assertTrue(pw2.isDestroyed()); + Assertions.assertNotEquals(pw1, pw2); + } + + } + +} \ No newline at end of file From e95853deacab30afe0420449c05e0f338a561347 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 16:46:57 +0100 Subject: [PATCH 004/141] make test public, fixing InaccessibleObject error in maven build --- .../org/cryptomator/common/PassphraseTest.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/cryptomator/common/PassphraseTest.java b/src/test/java/org/cryptomator/common/PassphraseTest.java index 7bd71beb2..02f640e94 100644 --- a/src/test/java/org/cryptomator/common/PassphraseTest.java +++ b/src/test/java/org/cryptomator/common/PassphraseTest.java @@ -8,7 +8,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; -class PassphraseTest { +public class PassphraseTest { @ParameterizedTest @CsvSource(value = { @@ -40,13 +40,8 @@ class PassphraseTest { Assertions.assertEquals("test".substring(offset, offset + length), pw.toString()); } - @Test - public void testToString() { - Assertions.assertEquals("test", Passphrase.copyOf("test").toString()); - } - @Nested - class WithInstances { + public class InstanceMethods { private Passphrase pw1; private Passphrase pw2; @@ -59,12 +54,16 @@ class PassphraseTest { } @Test - public void testEquals() { - Assertions.assertEquals(pw1, pw2); + public void testToString() { Assertions.assertEquals("test", pw1.toString()); Assertions.assertEquals("test", pw2.toString()); } + @Test + public void testEquals() { + Assertions.assertEquals(pw1, pw2); + } + @Test public void testHashcode() { Assertions.assertEquals(pw1.hashCode(), pw2.hashCode()); From 983a4d0b0faa554664127b41c3b52b67684849dc Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 17:00:49 +0100 Subject: [PATCH 005/141] move masterkey selection to subcomponent and use CompletableFuture instead of UserInteractionLock + AtomicReference --- .../ChooseMasterkeyFileComponent.java | 25 +++++++++++ .../ChooseMasterkeyFileModule.java | 42 ++++++++++++++++++ .../ChooseMasterkeyFileScoped.java | 13 ++++++ .../MasterkeyFileLoadingModule.java | 40 +---------------- .../MasterkeyFileLoadingStrategy.java | 44 +++++++------------ .../SelectMasterkeyFileController.java | 24 +++------- 6 files changed, 103 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java create mode 100644 src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java new file mode 100644 index 000000000..a548cd47d --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileComponent.java @@ -0,0 +1,25 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Subcomponent; + +import javafx.scene.Scene; +import java.nio.file.Path; +import java.util.concurrent.CompletableFuture; + +@ChooseMasterkeyFileScoped +@Subcomponent(modules = {ChooseMasterkeyFileModule.class}) +public interface ChooseMasterkeyFileComponent { + + @ChooseMasterkeyFileScoped + Scene chooseMasterkeyScene(); + + @ChooseMasterkeyFileScoped + CompletableFuture result(); + + @Subcomponent.Builder + interface Builder { + + ChooseMasterkeyFileComponent build(); + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java new file mode 100644 index 000000000..1cc71cbfa --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -0,0 +1,42 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import dagger.Module; +import dagger.Provides; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; + +@Module +abstract class ChooseMasterkeyFileModule { + + @Provides + @ChooseMasterkeyFileScoped + static CompletableFuture provideResult() { + return new CompletableFuture<>(); + } + + @Provides + @ChooseMasterkeyFileScoped + static Scene provideChooseMasterkeyScene(SelectMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + // TODO: simplify FxmlLoaderFactory + try { + var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE.getRessourcePathString()); + var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); + Parent root = loader.load(); + return sceneFactory.apply(root); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load UnlockScene", e); + } + } + + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java new file mode 100644 index 000000000..4bf8c5c24 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.keyloading.masterkeyfile; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface ChooseMasterkeyFileScoped { + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java index 7f55de898..af592cb3b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -8,12 +8,6 @@ import dagger.multibindings.StringKey; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.FxControllerKey; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlLoaderFactory; -import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; @@ -22,27 +16,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; -import javafx.scene.Scene; -import java.nio.file.Path; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class}) +@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class}) public abstract class MasterkeyFileLoadingModule { private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class); - public enum MasterkeyFileProvision { - MASTERKEYFILE_PROVIDED, - CANCELED - } - - @Provides - @KeyLoadingScoped - static UserInteractionLock provideMasterkeyFileProvisionLock() { - return new UserInteractionLock<>(null); - } - @Provides @Named("savedPassword") @KeyLoadingScoped @@ -59,24 +39,6 @@ public abstract class MasterkeyFileLoadingModule { } } - @Provides - @KeyLoadingScoped - static AtomicReference provideUserProvidedMasterkeyPath() { - return new AtomicReference<>(); - } - - @Provides - @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) - @KeyLoadingScoped - static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE); - } - - @Binds - @IntoMap - @FxControllerKey(SelectMasterkeyFileController.class) - abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller); - @Binds @IntoMap @KeyLoadingScoped diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index a22903973..13a7dadda 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -1,7 +1,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import com.google.common.base.Preconditions; -import dagger.Lazy; +import org.cryptomator.common.Passphrase; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.cryptofs.common.BackupHelper; @@ -11,10 +11,6 @@ import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.common.Passphrase; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.unlock.UnlockCancelledException; @@ -22,7 +18,6 @@ import org.cryptomator.ui.unlock.UnlockCancelledException; import javax.inject.Inject; import javax.inject.Named; import javafx.application.Platform; -import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; import java.io.IOException; @@ -32,7 +27,6 @@ import java.nio.file.Path; import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicReference; @KeyLoading public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { @@ -42,10 +36,8 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final Vault vault; private final MasterkeyFileAccess masterkeyFileAccess; private final Stage window; - private final Lazy selectMasterkeyFileScene; private final PassphraseEntryComponent.Builder passphraseEntry; - private final UserInteractionLock masterkeyFileProvisionLock; - private final AtomicReference filePath; + private final ChooseMasterkeyFileComponent.Builder masterkeyChooser; private final KeychainManager keychain; private Passphrase passphrase; @@ -53,14 +45,12 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private boolean wrongPassphrase; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, UserInteractionLock masterkeyFileProvisionLock, AtomicReference filePath, KeychainManager keychain) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyChooser, KeychainManager keychain) { this.vault = vault; this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; - this.selectMasterkeyFileScene = selectMasterkeyFileScene; this.passphraseEntry = passphraseEntry; - this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; - this.filePath = filePath; + this.masterkeyChooser = masterkeyChooser; this.keychain = keychain; this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null); this.savePassphrase = savedPassphrase.isPresent(); @@ -72,7 +62,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { try { Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart()); if (!Files.exists(filePath)) { - filePath = getAlternateMasterkeyFilePath(); + filePath = askUserForMasterkeyFilePath(); } if (passphrase == null) { askForPassphrase(); @@ -127,20 +117,10 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } } - private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException { - if (filePath.get() == null) { - return switch (askUserForMasterkeyFilePath()) { - case MASTERKEYFILE_PROVIDED -> filePath.get(); - case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled."); - }; - } else { - return filePath.get(); - } - } - - private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException { + private Path askUserForMasterkeyFilePath() throws InterruptedException { + var comp = masterkeyChooser.build(); Platform.runLater(() -> { - window.setScene(selectMasterkeyFileScene.get()); + window.setScene(comp.chooseMasterkeyScene()); window.show(); Window owner = window.getOwner(); if (owner != null) { @@ -150,7 +130,13 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { window.centerOnScreen(); } }); - return masterkeyFileProvisionLock.awaitInteraction(); + try { + return comp.result().get(); + } catch (CancellationException e) { + throw new UnlockCancelledException("Choosing masterkey file cancelled."); + } catch (ExecutionException e) { + throw new MasterkeyLoadingFailedException("Failed to select masterkey file.", e); + } } private void askForPassphrase() throws InterruptedException { diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java index 39be2b36e..fc225e0a8 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java @@ -1,10 +1,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.keyloading.KeyLoading; -import org.cryptomator.ui.keyloading.KeyLoadingScoped; -import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,23 +13,21 @@ import javafx.stage.WindowEvent; import java.io.File; import java.nio.file.Path; import java.util.ResourceBundle; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CompletableFuture; -@KeyLoadingScoped +@ChooseMasterkeyFileScoped public class SelectMasterkeyFileController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class); private final Stage window; - private final AtomicReference masterkeyPath; - private final UserInteractionLock masterkeyFileProvisionLock; + private final CompletableFuture result; private final ResourceBundle resourceBundle; @Inject - public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) { + public SelectMasterkeyFileController(@KeyLoading Stage window, CompletableFuture result, ResourceBundle resourceBundle) { this.window = window; - this.masterkeyPath = masterkeyPath; - this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; + this.result = result; this.resourceBundle = resourceBundle; this.window.setOnHiding(this::windowClosed); } @@ -43,11 +38,7 @@ public class SelectMasterkeyFileController implements FxController { } private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, mark this workflow as cancelled: - if (masterkeyFileProvisionLock.awaitingInteraction().get()) { - LOG.debug("Unlock canceled by user."); - masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED); - } + result.cancel(true); } @FXML @@ -59,8 +50,7 @@ public class SelectMasterkeyFileController implements FxController { File masterkeyFile = fileChooser.showOpenDialog(window); if (masterkeyFile != null) { LOG.debug("Chose masterkey file: {}", masterkeyFile); - masterkeyPath.set(masterkeyFile.toPath()); - masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED); + result.complete(masterkeyFile.toPath()); } } From 6ca87d13f57e281ca613f283bcbe75d4b3498ee1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 16 Dec 2021 17:01:56 +0100 Subject: [PATCH 006/141] renamed class --- ...Controller.java => ChooseMasterkeyFileController.java} | 6 +++--- .../masterkeyfile/ChooseMasterkeyFileModule.java | 2 +- .../masterkeyfile/MasterkeyFileLoadingStrategy.java | 8 ++++---- src/main/resources/fxml/unlock_select_masterkeyfile.fxml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/{SelectMasterkeyFileController.java => ChooseMasterkeyFileController.java} (86%) diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java similarity index 86% rename from src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java rename to src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java index fc225e0a8..11cf7bd6b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileController.java @@ -16,16 +16,16 @@ import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; @ChooseMasterkeyFileScoped -public class SelectMasterkeyFileController implements FxController { +public class ChooseMasterkeyFileController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class); + private static final Logger LOG = LoggerFactory.getLogger(ChooseMasterkeyFileController.class); private final Stage window; private final CompletableFuture result; private final ResourceBundle resourceBundle; @Inject - public SelectMasterkeyFileController(@KeyLoading Stage window, CompletableFuture result, ResourceBundle resourceBundle) { + public ChooseMasterkeyFileController(@KeyLoading Stage window, CompletableFuture result, ResourceBundle resourceBundle) { this.window = window; this.result = result; this.resourceBundle = resourceBundle; diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java index 1cc71cbfa..27f1247e4 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -26,7 +26,7 @@ abstract class ChooseMasterkeyFileModule { @Provides @ChooseMasterkeyFileScoped - static Scene provideChooseMasterkeyScene(SelectMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { // TODO: simplify FxmlLoaderFactory try { var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE.getRessourcePathString()); diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 13a7dadda..a1d85cb36 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -37,7 +37,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final MasterkeyFileAccess masterkeyFileAccess; private final Stage window; private final PassphraseEntryComponent.Builder passphraseEntry; - private final ChooseMasterkeyFileComponent.Builder masterkeyChooser; + private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice; private final KeychainManager keychain; private Passphrase passphrase; @@ -45,12 +45,12 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private boolean wrongPassphrase; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyChooser, KeychainManager keychain) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain) { this.vault = vault; this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; this.passphraseEntry = passphraseEntry; - this.masterkeyChooser = masterkeyChooser; + this.masterkeyFileChoice = masterkeyFileChoice; this.keychain = keychain; this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null); this.savePassphrase = savedPassphrase.isPresent(); @@ -118,7 +118,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } private Path askUserForMasterkeyFilePath() throws InterruptedException { - var comp = masterkeyChooser.build(); + var comp = masterkeyFileChoice.build(); Platform.runLater(() -> { window.setScene(comp.chooseMasterkeyScene()); window.show(); diff --git a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml index b6539f88f..43f2f3e46 100644 --- a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml +++ b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml @@ -11,7 +11,7 @@ Date: Thu, 16 Dec 2021 17:09:25 +0100 Subject: [PATCH 007/141] convert Dagger modules to interfaces --- .../masterkeyfile/ChooseMasterkeyFileModule.java | 2 +- .../masterkeyfile/MasterkeyFileLoadingModule.java | 7 ++----- .../ui/keyloading/masterkeyfile/PassphraseEntryModule.java | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java index 27f1247e4..bd9ab42c0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -16,7 +16,7 @@ import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; @Module -abstract class ChooseMasterkeyFileModule { +interface ChooseMasterkeyFileModule { @Provides @ChooseMasterkeyFileScoped diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java index af592cb3b..9375b0cff 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -12,16 +12,13 @@ import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; import org.cryptomator.ui.keyloading.KeyLoading; import org.cryptomator.ui.keyloading.KeyLoadingScoped; import org.cryptomator.ui.keyloading.KeyLoadingStrategy; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Named; import java.util.Optional; @Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class}) -public abstract class MasterkeyFileLoadingModule { - - private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class); +public interface MasterkeyFileLoadingModule { @Provides @Named("savedPassword") @@ -33,7 +30,7 @@ public abstract class MasterkeyFileLoadingModule { try { return Optional.ofNullable(keychain.loadPassphrase(vault.getId())); } catch (KeychainAccessException e) { - LOG.error("Failed to load entry from system keychain.", e); + LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e); return Optional.empty(); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java index aec32e5e6..0d67abd50 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -15,7 +15,7 @@ import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; @Module -abstract class PassphraseEntryModule { +interface PassphraseEntryModule { @Provides @PassphraseEntryScoped From 9856792921b5d553c6db195aeaca3e3199c904be Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 17:21:32 +0100 Subject: [PATCH 008/141] replaced UserInteractionLock with CompletableFuture in LockWorkflow --- .../ui/common/UserInteractionLock.java | 61 ------------------- .../ui/lock/LockForcedController.java | 20 +++--- .../org/cryptomator/ui/lock/LockModule.java | 15 ++--- .../org/cryptomator/ui/lock/LockWorkflow.java | 39 +++++++----- 4 files changed, 36 insertions(+), 99 deletions(-) delete mode 100644 src/main/java/org/cryptomator/ui/common/UserInteractionLock.java diff --git a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java b/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java deleted file mode 100644 index 12c394533..000000000 --- a/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.cryptomator.ui.common; - -import javafx.application.Platform; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class UserInteractionLock> { - - private final Lock lock = new ReentrantLock(); - private final Condition condition = lock.newCondition(); - private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty(); - private final AtomicBoolean interacted = new AtomicBoolean(); - private final AtomicReference state; - - public UserInteractionLock(E initialValue) { - this.state = new AtomicReference<>(initialValue); - } - - public synchronized void reset(E value) { - state.set(value); - interacted.set(false); - } - - public void interacted(E result) { - assert Platform.isFxApplicationThread(); - lock.lock(); - try { - state.set(result); - interacted.set(true); - awaitingInteraction.set(false); - condition.signal(); - } finally { - lock.unlock(); - } - } - - public E awaitInteraction() throws InterruptedException { - assert !Platform.isFxApplicationThread(); - lock.lock(); - try { - Platform.runLater(() -> awaitingInteraction.set(true)); - while (!interacted.get()) { - condition.await(); - } - return state.get(); - } finally { - lock.unlock(); - } - } - - public ReadOnlyBooleanProperty awaitingInteraction() { - return awaitingInteraction; - } - -} diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java index c3a452acc..0b4a23a18 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -2,7 +2,6 @@ package org.cryptomator.ui.lock; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.UserInteractionLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,6 +9,8 @@ import javax.inject.Inject; import javafx.fxml.FXML; import javafx.stage.Stage; import javafx.stage.WindowEvent; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @LockScoped public class LockForcedController implements FxController { @@ -18,40 +19,35 @@ public class LockForcedController implements FxController { private final Stage window; private final Vault vault; - private final UserInteractionLock forceLockDecisionLock; + private final AtomicReference> forceRetryDecision; @Inject - public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock) { + public LockForcedController(@LockWindow Stage window, @LockWindow Vault vault, AtomicReference> forceRetryDecision) { this.window = window; this.vault = vault; - this.forceLockDecisionLock = forceLockDecisionLock; + this.forceRetryDecision = forceRetryDecision; this.window.setOnHiding(this::windowClosed); } @FXML public void cancel() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); window.close(); } @FXML public void retry() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY); + forceRetryDecision.get().complete(false); window.close(); } @FXML public void force() { - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE); + forceRetryDecision.get().complete(true); window.close(); } private void windowClosed(WindowEvent windowEvent) { - // if not already interacted, set the decision to CANCEL - if (forceLockDecisionLock.awaitingInteraction().get()) { - LOG.debug("Lock canceled in force-lock-phase by user."); - forceLockDecisionLock.interacted(LockModule.ForceLockDecision.CANCEL); - } + forceRetryDecision.get().cancel(true); } // ----- Getter & Setter ----- diff --git a/src/main/java/org/cryptomator/ui/lock/LockModule.java b/src/main/java/org/cryptomator/ui/lock/LockModule.java index d1eb5f189..ddee13dff 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockModule.java +++ b/src/main/java/org/cryptomator/ui/lock/LockModule.java @@ -6,13 +6,12 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; -import org.cryptomator.ui.common.UserInteractionLock; import javax.inject.Named; import javax.inject.Provider; @@ -22,20 +21,16 @@ import javafx.stage.Stage; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicReference; @Module abstract class LockModule { - enum ForceLockDecision { - CANCEL, - RETRY, - FORCE; - } - @Provides @LockScoped - static UserInteractionLock provideForceLockDecisionLock() { - return new UserInteractionLock<>(null); + static AtomicReference> provideForceRetryDecisionRef() { + return new AtomicReference<>(); } @Provides diff --git a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java index 00b25c507..1e05ceb73 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java +++ b/src/main/java/org/cryptomator/ui/lock/LockWorkflow.java @@ -8,7 +8,6 @@ import org.cryptomator.common.vaults.Volume; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +17,10 @@ import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.stage.Stage; import javafx.stage.Window; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; /** * The sequence of actions performed and checked during lock of a vault. @@ -34,43 +37,48 @@ public class LockWorkflow extends Task { private final Stage lockWindow; private final Vault vault; - private final UserInteractionLock forceLockDecisionLock; + private final AtomicReference> forceRetryDecision; private final Lazy lockForcedScene; private final Lazy lockFailedScene; private final ErrorComponent.Builder errorComponent; @Inject - public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) { + public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, AtomicReference> forceRetryDecision, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy lockFailedScene, ErrorComponent.Builder errorComponent) { this.lockWindow = lockWindow; this.vault = vault; - this.forceLockDecisionLock = forceLockDecisionLock; + this.forceRetryDecision = forceRetryDecision; this.lockForcedScene = lockForcedScene; this.lockFailedScene = lockFailedScene; this.errorComponent = errorComponent; } @Override - protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException { + protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException, ExecutionException { lock(false); return null; } - private void lock(boolean forced) throws InterruptedException { + private void lock(boolean forced) throws InterruptedException, ExecutionException { try { vault.lock(forced); } catch (Volume.VolumeException | LockNotCompletedException e) { LOG.info("Locking {} failed (forced: {}).", vault.getDisplayName(), forced, e); - var decision = askUserForAction(); - switch (decision) { - case RETRY -> lock(false); - case FORCE -> lock(true); - case CANCEL -> cancel(false); - } + retryOrCancel(); } } - private LockModule.ForceLockDecision askUserForAction() throws InterruptedException { - forceLockDecisionLock.reset(null); + private void retryOrCancel() throws ExecutionException, InterruptedException { + try { + boolean forced = askWhetherToUseTheForce().get(); + lock(forced); + } catch (CancellationException e) { + cancel(false); + } + } + + private CompletableFuture askWhetherToUseTheForce() { + var decision = new CompletableFuture(); + forceRetryDecision.set(decision); // show forcedLock dialogue ... Platform.runLater(() -> { lockWindow.setScene(lockForcedScene.get()); @@ -83,8 +91,7 @@ public class LockWorkflow extends Task { lockWindow.centerOnScreen(); } }); - // ... and wait for answer - return forceLockDecisionLock.awaitInteraction(); + return decision; } @Override From d52e59d7a44090944e182bbd35ad16fcb01a76f4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 19:08:54 +0100 Subject: [PATCH 009/141] dedup by setting title when setting the scene --- .../masterkeyfile/ChooseMasterkeyFileModule.java | 15 +++------------ .../MasterkeyFileLoadingStrategy.java | 7 ++++++- .../masterkeyfile/PassphraseEntryModule.java | 13 ++----------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java index baa302170..34eb78443 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -2,16 +2,13 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import dagger.Module; import dagger.Provides; -import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlLoaderFactory; -import org.cryptomator.ui.keyloading.KeyLoading; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.stage.Stage; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Path; @@ -29,21 +26,15 @@ interface ChooseMasterkeyFileModule { @Provides @ChooseMasterkeyFileScoped - static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle, @KeyLoading Stage window, @KeyLoading Vault v) { + static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { // TODO: simplify FxmlLoaderFactory try { var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE.getRessourcePathString()); var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); Parent root = loader.load(); - var scene = sceneFactory.apply(root); - scene.windowProperty().addListener((prop, oldVal, newVal) -> { - if (window.equals(newVal)) { - window.setTitle(String.format(resourceBundle.getString("unlock.chooseMasterkey.title"), v.getDisplayName())); - } - }); - return scene; + return sceneFactory.apply(root); } catch (IOException e) { - throw new UncheckedIOException("Failed to load UnlockScene", e); + throw new UncheckedIOException("Failed to load ChooseMasterkeyScene", e); } } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index a1d85cb36..b4964f9a0 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -25,6 +25,7 @@ import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; +import java.util.ResourceBundle; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; @@ -39,19 +40,21 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { private final PassphraseEntryComponent.Builder passphraseEntry; private final ChooseMasterkeyFileComponent.Builder masterkeyFileChoice; private final KeychainManager keychain; + private final ResourceBundle resourceBundle; private Passphrase passphrase; private boolean savePassphrase; private boolean wrongPassphrase; @Inject - public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain) { + public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) { this.vault = vault; this.masterkeyFileAccess = masterkeyFileAccess; this.window = window; this.passphraseEntry = passphraseEntry; this.masterkeyFileChoice = masterkeyFileChoice; this.keychain = keychain; + this.resourceBundle = resourceBundle; this.passphrase = savedPassphrase.map(Passphrase::new).orElse(null); this.savePassphrase = savedPassphrase.isPresent(); } @@ -121,6 +124,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { var comp = masterkeyFileChoice.build(); Platform.runLater(() -> { window.setScene(comp.chooseMasterkeyScene()); + window.setTitle(resourceBundle.getString("unlock.chooseMasterkey.title").formatted(vault.getDisplayName())); window.show(); Window owner = window.getOwner(); if (owner != null) { @@ -143,6 +147,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { var comp = passphraseEntry.savedPassword(passphrase).build(); Platform.runLater(() -> { window.setScene(comp.passphraseEntryScene()); + window.setTitle(resourceBundle.getString("unlock.title").formatted(vault.getDisplayName())); window.show(); Window owner = window.getOwner(); if (owner != null) { diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java index aaf65e3f8..0d67abd50 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -2,16 +2,13 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import dagger.Module; import dagger.Provides; -import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlLoaderFactory; -import org.cryptomator.ui.keyloading.KeyLoading; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; -import javafx.stage.Stage; import java.io.IOException; import java.io.UncheckedIOException; import java.util.ResourceBundle; @@ -28,19 +25,13 @@ interface PassphraseEntryModule { @Provides @PassphraseEntryScoped - static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle, @KeyLoading Stage window, @KeyLoading Vault v) { + static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { // TODO: simplify FxmlLoaderFactory try { var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_ENTER_PASSWORD.getRessourcePathString()); var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); Parent root = loader.load(); - var scene = sceneFactory.apply(root); - scene.windowProperty().addListener((prop, oldVal, newVal) -> { - if (window.equals(newVal)) { - window.setTitle(String.format(resourceBundle.getString("unlock.title"), v.getDisplayName())); - } - }); - return scene; + return sceneFactory.apply(root); } catch (IOException e) { throw new UncheckedIOException("Failed to load UnlockScene", e); } From 4d4098e0e0e92f99313ea12dac4a30940e7362e0 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 19:49:33 +0100 Subject: [PATCH 010/141] cleanup --- src/main/java/org/cryptomator/common/Passphrase.java | 12 ++++-------- .../org/cryptomator/ui/common/FxmlLoaderFactory.java | 4 ++++ .../masterkeyfile/ChooseMasterkeyFileModule.java | 11 +---------- .../masterkeyfile/PassphraseEntryModule.java | 11 +---------- .../masterkeyfile/PassphraseEntryResult.java | 2 +- .../cryptomator/ui/lock/LockForcedController.java | 4 ---- 6 files changed, 11 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/cryptomator/common/Passphrase.java b/src/main/java/org/cryptomator/common/Passphrase.java index d57252507..036e87104 100644 --- a/src/main/java/org/cryptomator/common/Passphrase.java +++ b/src/main/java/org/cryptomator/common/Passphrase.java @@ -64,16 +64,12 @@ public class Passphrase implements Destroyable, CharSequence { @Override public int hashCode() { - // TODO: do we really need to a secure hashcode? toString leaks the pw anyway - var md = MessageDigestSupplier.SHA256.get(); - ByteBuffer buf = ByteBuffer.allocate(Character.BYTES * length); + // basically Arrays.hashCode, but only for a certain subarray + int result = 1; for (int i = 0; i < length; i++) { - char c = charAt(i); - buf.putChar(i * Character.BYTES, c); + result = 31 * result + charAt(i); } - buf.flip(); - md.update(buf); - return Arrays.hashCode(md.digest()); + return result; } @Override diff --git a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java index c10054ef4..dcff93aab 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlLoaderFactory.java @@ -22,6 +22,10 @@ public class FxmlLoaderFactory { this.resourceBundle = resourceBundle; } + public static FxmlLoaderFactory forController(T controller, Function sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(Map.of(controller.getClass(), () -> controller), sceneFactory, resourceBundle); + } + /** * @return A new FXMLLoader instance */ diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java index 34eb78443..c7a084584 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -27,16 +27,7 @@ interface ChooseMasterkeyFileModule { @Provides @ChooseMasterkeyFileScoped static Scene provideChooseMasterkeyScene(ChooseMasterkeyFileController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { - // TODO: simplify FxmlLoaderFactory - try { - var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE.getRessourcePathString()); - var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); - Parent root = loader.load(); - return sceneFactory.apply(root); - } catch (IOException e) { - throw new UncheckedIOException("Failed to load ChooseMasterkeyScene", e); - } + return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE); } - } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java index 0d67abd50..ed1feb7ee 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -26,16 +26,7 @@ interface PassphraseEntryModule { @Provides @PassphraseEntryScoped static Scene provideUnlockScene(PassphraseEntryController controller, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { - // TODO: simplify FxmlLoaderFactory - try { - var url = FxmlLoaderFactory.class.getResource(FxmlFile.UNLOCK_ENTER_PASSWORD.getRessourcePathString()); - var loader = new FXMLLoader(url, resourceBundle, null, clazz -> controller); - Parent root = loader.load(); - return sceneFactory.apply(root); - } catch (IOException e) { - throw new UncheckedIOException("Failed to load UnlockScene", e); - } + return FxmlLoaderFactory.forController(controller, sceneFactory, resourceBundle).createScene(FxmlFile.UNLOCK_ENTER_PASSWORD); } - } diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java index f26184d9d..19057acca 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryResult.java @@ -2,7 +2,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile; import org.cryptomator.common.Passphrase; -// TODO needs to be public due to Dagger -.- +// TODO: change to package-private, as soon as this works for Dagger -.- public record PassphraseEntryResult(Passphrase passphrase, boolean savePassphrase) { } diff --git a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java index 0b4a23a18..15cf119be 100644 --- a/src/main/java/org/cryptomator/ui/lock/LockForcedController.java +++ b/src/main/java/org/cryptomator/ui/lock/LockForcedController.java @@ -2,8 +2,6 @@ package org.cryptomator.ui.lock; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.inject.Inject; import javafx.fxml.FXML; @@ -15,8 +13,6 @@ import java.util.concurrent.atomic.AtomicReference; @LockScoped public class LockForcedController implements FxController { - private static final Logger LOG = LoggerFactory.getLogger(LockForcedController.class); - private final Stage window; private final Vault vault; private final AtomicReference> forceRetryDecision; From e1a72c41a5fcbe87c5f7d328ac456e514a59202b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Wed, 19 Jan 2022 20:01:48 +0100 Subject: [PATCH 011/141] remove unused imports --- src/main/java/org/cryptomator/common/Passphrase.java | 3 --- .../keyloading/masterkeyfile/ChooseMasterkeyFileModule.java | 4 ---- .../ui/keyloading/masterkeyfile/PassphraseEntryModule.java | 4 ---- 3 files changed, 11 deletions(-) diff --git a/src/main/java/org/cryptomator/common/Passphrase.java b/src/main/java/org/cryptomator/common/Passphrase.java index 036e87104..cf64c7a10 100644 --- a/src/main/java/org/cryptomator/common/Passphrase.java +++ b/src/main/java/org/cryptomator/common/Passphrase.java @@ -1,9 +1,6 @@ package org.cryptomator.common; -import org.cryptomator.cryptolib.common.MessageDigestSupplier; - import javax.security.auth.Destroyable; -import java.nio.ByteBuffer; import java.util.Arrays; /** diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java index c7a084584..21ae2b26c 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/ChooseMasterkeyFileModule.java @@ -6,11 +6,7 @@ import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlLoaderFactory; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; -import java.io.IOException; -import java.io.UncheckedIOException; import java.nio.file.Path; import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java index ed1feb7ee..2c65d440b 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryModule.java @@ -6,11 +6,7 @@ import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlLoaderFactory; -import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; -import java.io.IOException; -import java.io.UncheckedIOException; import java.util.ResourceBundle; import java.util.concurrent.CompletableFuture; From 4bd4cd671bdff22e410ae2e2a8eda5f154bf7ac8 Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 21 Feb 2022 14:24:04 +0100 Subject: [PATCH 012/141] removed unnecessary entitlement in mac build (cherry picked from commit 3e8690ca119104487b072835218d0a03645b456a) --- dist/mac/Cryptomator.entitlements | 2 -- 1 file changed, 2 deletions(-) diff --git a/dist/mac/Cryptomator.entitlements b/dist/mac/Cryptomator.entitlements index 00f46d649..16890d644 100644 --- a/dist/mac/Cryptomator.entitlements +++ b/dist/mac/Cryptomator.entitlements @@ -2,8 +2,6 @@ - com.apple.security.cs.allow-dyld-environment-variables - com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory From ee68f9d47b41457155a121339714f164aee9d41a Mon Sep 17 00:00:00 2001 From: Tobias Hagemann Date: Mon, 21 Feb 2022 14:45:01 +0100 Subject: [PATCH 013/141] preparing 1.6.6 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 39008b723..14287a907 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator cryptomator - 1.6.5 + 1.6.6 Cryptomator Desktop App From 7172462b4b4c975c606311a99b0f3aa4ee40c999 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 23 Feb 2022 22:27:41 +0100 Subject: [PATCH 014/141] Create installation bundle with winfsp: * via wix burn engine * licenses generated on the fly * customized theme * local only --- dist/win/.gitignore | 6 +- dist/win/build.ps1 | 74 +++++++++++-- dist/win/bundle/bundleWithWinfsp.wxs | 43 ++++++++ dist/win/bundle/customBootstrapperTheme.wxl | 64 +++++++++++ dist/win/bundle/customBootstrapperTheme.xml | 100 ++++++++++++++++++ dist/win/bundle/resources/Cryptomator.ico | Bin 0 -> 162342 bytes dist/win/bundle/resources/licenseTemplate.ftl | 41 +++++++ dist/win/bundle/resources/logo.png | Bin 0 -> 2659 bytes dist/win/bundle/resources/logoSide.png | Bin 0 -> 37864 bytes dist/win/resources/license.rtf | 84 --------------- dist/win/resources/licenseTemplate.ftl | 37 +++++++ 11 files changed, 354 insertions(+), 95 deletions(-) create mode 100644 dist/win/bundle/bundleWithWinfsp.wxs create mode 100644 dist/win/bundle/customBootstrapperTheme.wxl create mode 100644 dist/win/bundle/customBootstrapperTheme.xml create mode 100644 dist/win/bundle/resources/Cryptomator.ico create mode 100644 dist/win/bundle/resources/licenseTemplate.ftl create mode 100644 dist/win/bundle/resources/logo.png create mode 100644 dist/win/bundle/resources/logoSide.png delete mode 100644 dist/win/resources/license.rtf create mode 100644 dist/win/resources/licenseTemplate.ftl diff --git a/dist/win/.gitignore b/dist/win/.gitignore index 2b66ddbed..9cce929df 100644 --- a/dist/win/.gitignore +++ b/dist/win/.gitignore @@ -1,3 +1,7 @@ runtime Cryptomator -installer \ No newline at end of file +installer +*.wixobj +*.pdb +*.msi +license.rtf \ No newline at end of file diff --git a/dist/win/build.ps1 b/dist/win/build.ps1 index 2919e493a..565ffd582 100644 --- a/dist/win/build.ps1 +++ b/dist/win/build.ps1 @@ -1,11 +1,14 @@ +# check parameters +$clean = $args[0] -eq "fresh" + # check preconditions -if ((Get-Command "git" -ErrorAction SilentlyContinue) -eq $null) -{ +if ((Get-Command "git" -ErrorAction SilentlyContinue) -eq $null) +{ Write-Host "Unable to find git.exe in your PATH (try: choco install git)" exit 1 } -if ((Get-Command "mvn" -ErrorAction SilentlyContinue) -eq $null) -{ +if ((Get-Command "mvn" -ErrorAction SilentlyContinue) -eq $null) +{ Write-Host "Unable to find mvn.cmd in your PATH (try: choco install maven)" exit 1 } @@ -21,11 +24,19 @@ Write-Output "`$revisionNo=$revisionNo" Write-Output "`$buildDir=$buildDir" Write-Output "`$Env:JAVA_HOME=$Env:JAVA_HOME" +$vendor = "Skymatic GmbH" +$copyright = "(C) 2016 - 2022 Skymatic GmbH" + # compile &mvn -B -f $buildDir/../../pom.xml clean package -DskipTests -Pwin Copy-Item "$buildDir\..\..\target\cryptomator-*.jar" -Destination "$buildDir\..\..\target\mods" # add runtime +$runtimeImagePath = '.\runtime' +if ($clean -and (Test-Path -Path $runtimeImagePath)) { + Remove-Item -Path $runtimeImagePath -Force -Recurse +} + & "$Env:JAVA_HOME\bin\jlink" ` --verbose ` --output runtime ` @@ -36,6 +47,11 @@ Copy-Item "$buildDir\..\..\target\cryptomator-*.jar" -Destination "$buildDir\..\ --strip-debug ` --compress=1 +$appPath = '.\Cryptomator' +if ($clean -and (Test-Path -Path $appPath)) { + Remove-Item -Path $appPath -Force -Recurse +} + # create app dir & "$Env:JAVA_HOME\bin\jpackage" ` --verbose ` @@ -46,8 +62,8 @@ Copy-Item "$buildDir\..\..\target\cryptomator-*.jar" -Destination "$buildDir\..\ --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator ` --dest . ` --name Cryptomator ` - --vendor "Skymatic GmbH" ` - --copyright "(C) 2016 - 2022 Skymatic GmbH" ` + --vendor $vendor ` + --copyright $copyright ` --java-options "-Xss5m" ` --java-options "-Xmx256m" ` --java-options "-Dcryptomator.appVersion=`"$semVerNo`"" ` @@ -64,11 +80,21 @@ Copy-Item "$buildDir\..\..\target\cryptomator-*.jar" -Destination "$buildDir\..\ --resource-dir resources ` --icon resources/Cryptomator.ico +#Create RTF license for msi +&mvn -B -f $buildDir/../../pom.xml license:add-third-party ` + "-Dlicense.thirdPartyFilename=license.rtf" ` + "-Dlicense.fileTemplate=$buildDir\resources\licenseTemplate.ftl" ` + "-Dlicense.outputDirectory=$buildDir\resources\" + # patch app dir Copy-Item "contrib\*" -Destination "Cryptomator" attrib -r "Cryptomator\Cryptomator.exe" -# create .msi bundle +$aboutUrl="https://cryptomator.org" +$updateUrl="https://cryptomator.org/downloads/" +$helpUrl="https://cryptomator.org/contact/" + +# create .msi $Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources" & "$Env:JAVA_HOME\bin\jpackage" ` --verbose ` @@ -77,14 +103,42 @@ $Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources" --app-image Cryptomator ` --dest installer ` --name Cryptomator ` - --vendor "Skymatic GmbH" ` - --copyright "(C) 2016 - 2022 Skymatic GmbH" ` + --vendor $vendor ` + --copyright $copyright ` --app-version "$semVerNo" ` --win-menu ` --win-dir-chooser ` --win-shortcut-prompt ` - --win-update-url "https:\\cryptomator.org" ` + --win-update-url $updateUrl ` --win-menu-group Cryptomator ` --resource-dir resources ` + --about-url $aboutUrl ` --license-file resources/license.rtf ` --file-associations resources/FAvaultFile.properties + +#Create RTF license for bundle +#TODO: actually we should patch also the third-party-file in the Cryptomator jar +&mvn -B -f $buildDir/../../pom.xml license:add-third-party ` + "-Dlicense.thirdPartyFilename=license.rtf" ` + "-Dlicense.fileTemplate=$buildDir\bundle\resources\licenseTemplate.ftl" ` + "-Dlicense.outputDirectory=$buildDir\bundle\resources\" + +# download Winfsp +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ProgressPreference = 'SilentlyContinue' # disables Invoke-WebRequest's progress bar, which slows down downloads to a few bytes/s +$winfspMsiUrl = "https://github.com/billziss-gh/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi" +Write-Output "Downloading ${winfspMsiUrl}..." +Invoke-WebRequest $winfspMsiUrl -OutFile ".\bundle\resources\winfsp.msi" # redirects are followed by default + +# copy MSI to bundle resources +Copy-Item ".\installer\Cryptomator-*.msi" -Destination ".\bundle\resources\Cryptomator.msi" + +# create bundle including winfsp +& "$env:WIX\bin\candle.exe" .\bundle\bundleWithWinfsp.wxs -ext WixBalExtension -out bundle\ ` + -dBundleVersion="$semVerNo.$revisionNo" ` + -dBundleVendor="$vendor" ` + -dBundleCopyright="$copyright" ` + -dAboutUrl="$aboutUrl" ` + -dHelpUrl="$helpUrl" ` + -dUpdateUrl="$updateUrl" +& "$env:WIX\bin\light.exe" -b . .\bundle\BundlewithWinfsp.wixobj -ext WixBalExtension -out installer\CryptomatorBundle.exe \ No newline at end of file diff --git a/dist/win/bundle/bundleWithWinfsp.wxs b/dist/win/bundle/bundleWithWinfsp.wxs new file mode 100644 index 000000000..91093df7d --- /dev/null +++ b/dist/win/bundle/bundleWithWinfsp.wxs @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/dist/win/bundle/customBootstrapperTheme.wxl b/dist/win/bundle/customBootstrapperTheme.wxl new file mode 100644 index 000000000..cc464ce9c --- /dev/null +++ b/dist/win/bundle/customBootstrapperTheme.wxl @@ -0,0 +1,64 @@ + + + + + + [WixBundleName] Setup + [WixBundleName] + Welcome + This Setup will install [WixBundleName] and additional dependencies on your computer. + Version [WixBundleVersion] + Are you sure you want to cancel? + Previous version + Setup Help + /install | /repair | /uninstall | /layout [directory] - installs, repairs, uninstalls or + creates a complete local copy of the bundle in directory. Install is the default. + +/passive | /quiet - displays minimal UI with no prompts or displays no UI and + no prompts. By default UI and all prompts are displayed. + +/norestart - suppress any attempts to restart. By default UI will prompt before restart. +/log log.txt - logs to a specific file. By default a log file is created in %TEMP%. + &Close + [WixBundleName] <a href="#">license terms</a>. + I &agree to the license terms and conditions + &Options + &Install + &Close + Setup Options + Install location: + &Browse + &OK + &Cancel + Setup Progress + Processing: + Initializing... + &Cancel + Modify Setup + &Repair + &Uninstall + &Close + Repair Successfully Completed + Uninstall Successfully Completed + Installation Successfully Completed + Setup Successful + &Launch + You must restart your computer before you can use the software. + &Restart + &Close + Setup Failed + Setup Failed + Uninstall Failed + Repair Failed + One or more issues caused the setup to fail. Please fix the issues and then retry setup. For more information see the <a href="#">log file</a>. + You must restart your computer to complete the rollback of the software. + &Restart + &Close + Files In Use + The following applications are using files that need to be updated: + Close the &applications and attempt to restart them. + &Do not close applications. A reboot will be required. + &OK + &Cancel + No action was taken as a system reboot is required. + diff --git a/dist/win/bundle/customBootstrapperTheme.xml b/dist/win/bundle/customBootstrapperTheme.xml new file mode 100644 index 000000000..adb90f8dd --- /dev/null +++ b/dist/win/bundle/customBootstrapperTheme.xml @@ -0,0 +1,100 @@ + + + + + + #(loc.Caption) + Segoe UI + Segoe UI + Segoe UI + Segoe UI + Segoe UI + + + #(loc.Title) + + + + #(loc.Title) + #(loc.HelpHeader) + #(loc.HelpText) + + + + #(loc.Title) + + #(loc.InstallHeader) + #(loc.InstallMessage) + + #(loc.InstallAcceptCheckbox) + + #(loc.InstallVersion) + + + + + + #(loc.Title) + #(loc.OptionsHeader) + #(loc.OptionsLocationLabel) + + + + + + + + #(loc.Title) + #(loc.FilesInUseHeader) + #(loc.FilesInUseLabel) + A + + + + + + + + + #(loc.Title) + + #(loc.ProgressHeader) + #(loc.ProgressLabel) + #(loc.OverallProgressPackageText) + + + + + + #(loc.Title) + #(loc.ModifyHeader) + + + + + + #(loc.Title) + + #(loc.SuccessHeader) + #(loc.SuccessInstallHeader) + #(loc.SuccessRepairHeader) + #(loc.SuccessUninstallHeader) + + #(loc.SuccessRestartText) + + + + + #(loc.Title) + + #(loc.FailureHeader) + #(loc.FailureInstallHeader) + #(loc.FailureUninstallHeader) + #(loc.FailureRepairHeader) + #(loc.FailureHyperlinkLogText) + + #(loc.FailureRestartText) + + + + diff --git a/dist/win/bundle/resources/Cryptomator.ico b/dist/win/bundle/resources/Cryptomator.ico new file mode 100644 index 0000000000000000000000000000000000000000..7d1d8be880abb6680387d2a67b814a0bd38368c4 GIT binary patch literal 162342 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 literal 0 HcmV?d00001 diff --git a/dist/win/bundle/resources/licenseTemplate.ftl b/dist/win/bundle/resources/licenseTemplate.ftl new file mode 100644 index 000000000..bd137a1a1 --- /dev/null +++ b/dist/win/bundle/resources/licenseTemplate.ftl @@ -0,0 +1,41 @@ +<#function artifactFormat p> + <#if p.name?index_of('Unnamed') > -1> + <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - {{\\field{\\*\\fldinst{HYPERLINK " + (p.url!"no url defined") + "}}{\\fldrslt{" + (p.url!"no url defined") + "\\ul0\\cf0}}}}\\f0\\fs16 ) "> + <#else> + <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - {{\\field{\\*\\fldinst{HYPERLINK " + (p.url!"no url defined") + "}}{\\fldrslt{" + (p.url!"no url defined") + "\\ul0\\cf0}}}}\\f0\\fs16 ) "> + + +{\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Arial;}} +{\colortbl ;\red0\green0\blue255;} +\viewkind4\uc1 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par +\par +\b\'a9 2016 \endash 2022 Skymatic GmbH\b0\par +\par +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par +\par +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\par +\par +You should have received a copy of the GNU General Public License along with this program. If not, see {{\field{\*\fldinst{HYPERLINK http://www.gnu.org/licenses/ }}{\fldrslt{http://www.gnu.org/licenses/\ul0\cf0}}}}\f0\fs16 .\par +\par + +\b Cryptomator uses ${dependencyMap?size} third-party dependencies under the following licenses:\b0\par +<#list licenseMap as e> +<#assign license = e.getKey()/> +<#assign projects = e.getValue()/> +<#if projects?size > 0> +\tab ${license}:\par +<#list projects as project> +\tab\tab- ${artifactFormat(project)}\par + + + +\par +\b Cryptomator uses other third-party assets under the following licenses:\b0\par +\tab SIL OFL 1.1 License:\par +\tab\tab - Font Awesome 5.12.0 ({{\field{\*\fldinst{HYPERLINK https://fontawesome.com/ }}{\fldrslt{https://fontawesome.com/\ul0\cf0}}}}\f0\fs16 )\b\par +\par +\b Cryptomator dynamically links to third-party libraries under the following license:\b0\par +\tab Uncategorized License:\par +\tab\tab - WinFsp - Windows File System Proxy, Copyright (C) Bill Zissimopoulos ({{\field{\*\fldinst{HYPERLINK https://github.com/billziss-gh/winfsp }}{\fldrslt{https://github.com/billziss-gh/winfsp\ul0\cf0}}}}\f0\fs16 )\b\par +} \ No newline at end of file diff --git a/dist/win/bundle/resources/logo.png b/dist/win/bundle/resources/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fdfdd6235347edf63ab057644211742d19b2dec1 GIT binary patch literal 2659 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 ReEt9c002ovPDHLkV1jVj0D;&SYDK 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 literal 0 HcmV?d00001 diff --git a/dist/win/resources/license.rtf b/dist/win/resources/license.rtf deleted file mode 100644 index 28956ed42..000000000 --- a/dist/win/resources/license.rtf +++ /dev/null @@ -1,84 +0,0 @@ -{\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Arial;}} -{\colortbl ;\red0\green0\blue255;} -{\*\generator Riched20 10.0.17134}\viewkind4\uc1 -\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par -\par -\b\'a9 2016 \endash 2022 Skymatic GmbH\b0\par -\par -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par -\par -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\par -\par -You should have received a copy of the GNU General Public License along with this program. If not, see {{\field{\*\fldinst{HYPERLINK http://www.gnu.org/licenses/ }}{\fldrslt{http://www.gnu.org/licenses/\ul0\cf0}}}}\f0\fs16 .\par -\par -\b Cryptomator uses 40 third-party dependencies under the following licenses:\b0\par -\tab Apache License v2.0:\par -\tab\tab - jffi (com.github.jnr:jffi:1.2.23 - {{\field{\*\fldinst{HYPERLINK http://github.com/jnr/jffi }}{\fldrslt{http://github.com/jnr/jffi\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - jnr-a64asm (com.github.jnr:jnr-a64asm:1.0.0 - {{\field{\*\fldinst{HYPERLINK http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm }}{\fldrslt{http://nexus.sonatype.org/oss-repository-hosting.html/jnr-a64asm\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - jnr-constants (com.github.jnr:jnr-constants:0.9.15 - {{\field{\*\fldinst{HYPERLINK http://github.com/jnr/jnr-constants }}{\fldrslt{http://github.com/jnr/jnr-constants\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - jnr-ffi (com.github.jnr:jnr-ffi:2.1.12 - {{\field{\*\fldinst{HYPERLINK http://github.com/jnr/jnr-ffi }}{\fldrslt{http://github.com/jnr/jnr-ffi\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Gson (com.google.code.gson:gson:2.8.7 - {{\field{\*\fldinst{HYPERLINK https://github.com/google/gson/gson }}{\fldrslt{https://github.com/google/gson/gson\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Dagger (com.google.dagger:dagger:2.38.1 - {{\field{\*\fldinst{HYPERLINK https://github.com/google/dagger }}{\fldrslt{https://github.com/google/dagger\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Guava InternalFutureFailureAccess and InternalFutures (com.google.guava:failureaccess:1.0.1 - {{\field{\*\fldinst{HYPERLINK https://github.com/google/guava/failureaccess }}{\fldrslt{https://github.com/google/guava/failureaccess\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Guava: Google Core Libraries for Java (com.google.guava:guava:30.1.1-jre - {{\field{\*\fldinst{HYPERLINK https://github.com/google/guava/guava }}{\fldrslt{https://github.com/google/guava/guava\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Apache Commons CLI (commons-cli:commons-cli:1.4 - {{\field{\*\fldinst{HYPERLINK http://commons.apache.org/proper/commons-cli/ }}{\fldrslt{http://commons.apache.org/proper/commons-cli/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - javax.inject (javax.inject:javax.inject:1 - {{\field{\*\fldinst{HYPERLINK http://code.google.com/p/atinject/ }}{\fldrslt{http://code.google.com/p/atinject/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Java Native Access (net.java.dev.jna:jna:5.7.0 - {{\field{\*\fldinst{HYPERLINK https://github.com/java-native-access/jna }}{\fldrslt{https://github.com/java-native-access/jna\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Java Native Access Platform (net.java.dev.jna:jna-platform:5.7.0 - {{\field{\*\fldinst{HYPERLINK https://github.com/java-native-access/jna }}{\fldrslt{https://github.com/java-native-access/jna\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Apache Commons Lang (org.apache.commons:commons-lang3:3.12.0 - {{\field{\*\fldinst{HYPERLINK https://commons.apache.org/proper/commons-lang/ }}{\fldrslt{https://commons.apache.org/proper/commons-lang/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.14 - {{\field{\*\fldinst{HYPERLINK http://hc.apache.org/httpcomponents-core-ga }}{\fldrslt{http://hc.apache.org/httpcomponents-core-ga\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jackrabbit WebDAV Library (org.apache.jackrabbit:jackrabbit-webdav:2.21.5 - {{\field{\*\fldinst{HYPERLINK http://jackrabbit.apache.org/jackrabbit-webdav/ }}{\fldrslt{http://jackrabbit.apache.org/jackrabbit-webdav/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Http Utility (org.eclipse.jetty:jetty-http:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-http }}{\fldrslt{https://eclipse.org/jetty/jetty-http\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: IO Utility (org.eclipse.jetty:jetty-io:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-io }}{\fldrslt{https://eclipse.org/jetty/jetty-io\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-security }}{\fldrslt{https://eclipse.org/jetty/jetty-security\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-server }}{\fldrslt{https://eclipse.org/jetty/jetty-server\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-servlet }}{\fldrslt{https://eclipse.org/jetty/jetty-servlet\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-util }}{\fldrslt{https://eclipse.org/jetty/jetty-util\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-servlet-api }}{\fldrslt{https://eclipse.org/jetty/jetty-servlet-api\ul0\cf0}}}}\f0\fs16 )\par -\tab BSD:\par -\tab\tab - asm (org.ow2.asm:asm:7.1 - {{\field{\*\fldinst{HYPERLINK http://asm.ow2.org/ }}{\fldrslt{http://asm.ow2.org/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - asm-analysis (org.ow2.asm:asm-analysis:7.1 - {{\field{\*\fldinst{HYPERLINK http://asm.ow2.org/ }}{\fldrslt{http://asm.ow2.org/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - asm-commons (org.ow2.asm:asm-commons:7.1 - {{\field{\*\fldinst{HYPERLINK http://asm.ow2.org/ }}{\fldrslt{http://asm.ow2.org/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - asm-tree (org.ow2.asm:asm-tree:7.1 - {{\field{\*\fldinst{HYPERLINK http://asm.ow2.org/ }}{\fldrslt{http://asm.ow2.org/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - asm-util (org.ow2.asm:asm-util:7.1 - {{\field{\*\fldinst{HYPERLINK http://asm.ow2.org/ }}{\fldrslt{http://asm.ow2.org/\ul0\cf0}}}}\f0\fs16 )\par -\tab Eclipse Public License - Version 1.0:\par -\tab\tab - Jetty :: Servlet API and Schemas for JPMS and OSGi (org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-servlet-api }}{\fldrslt{https://eclipse.org/jetty/jetty-servlet-api\ul0\cf0}}}}\f0\fs16 )\par -\tab Eclipse Public License - Version 2.0:\par -\tab\tab - Jetty :: Http Utility (org.eclipse.jetty:jetty-http:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-http }}{\fldrslt{https://eclipse.org/jetty/jetty-http\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: IO Utility (org.eclipse.jetty:jetty-io:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-io }}{\fldrslt{https://eclipse.org/jetty/jetty-io\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Security (org.eclipse.jetty:jetty-security:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-security }}{\fldrslt{https://eclipse.org/jetty/jetty-security\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Server Core (org.eclipse.jetty:jetty-server:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-server }}{\fldrslt{https://eclipse.org/jetty/jetty-server\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Servlet Handling (org.eclipse.jetty:jetty-servlet:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-servlet }}{\fldrslt{https://eclipse.org/jetty/jetty-servlet\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Jetty :: Utilities (org.eclipse.jetty:jetty-util:10.0.6 - {{\field{\*\fldinst{HYPERLINK https://eclipse.org/jetty/jetty-util }}{\fldrslt{https://eclipse.org/jetty/jetty-util\ul0\cf0}}}}\f0\fs16 )\par -\tab Eclipse Public License - v 1.0:\par -\tab\tab - Logback Classic Module (ch.qos.logback:logback-classic:1.2.3 - {{\field{\*\fldinst{HYPERLINK http://logback.qos.ch/logback-classic }}{\fldrslt{http://logback.qos.ch/logback-classic\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Logback Core Module (ch.qos.logback:logback-core:1.2.3 - {{\field{\*\fldinst{HYPERLINK http://logback.qos.ch/logback-core }}{\fldrslt{http://logback.qos.ch/logback-core\ul0\cf0}}}}\f0\fs16 )\par -\tab Eclipse Public License - v 2.0:\par -\tab\tab - jnr-posix (com.github.jnr:jnr-posix:3.0.54 - {{\field{\*\fldinst{HYPERLINK http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix }}{\fldrslt{http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix\ul0\cf0}}}}\f0\fs16 )\par -\tab GNU Lesser General Public License:\par -\tab\tab - Logback Classic Module (ch.qos.logback:logback-classic:1.2.3 - {{\field{\*\fldinst{HYPERLINK http://logback.qos.ch/logback-classic }}{\fldrslt{http://logback.qos.ch/logback-classic\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Logback Core Module (ch.qos.logback:logback-core:1.2.3 - {{\field{\*\fldinst{HYPERLINK http://logback.qos.ch/logback-core }}{\fldrslt{http://logback.qos.ch/logback-core\ul0\cf0}}}}\f0\fs16 )\par -\tab GPLv2:\par -\tab\tab - jnr-posix (com.github.jnr:jnr-posix:3.0.54 - {{\field{\*\fldinst{HYPERLINK http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix }}{\fldrslt{http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix\ul0\cf0}}}}\f0\fs16 )\par -\tab GPLv2+CE:\par -\tab\tab - javafx-base (org.openjfx:javafx-base:16 - {{\field{\*\fldinst{HYPERLINK https://openjdk.java.net/projects/openjfx/javafx-base/ }}{\fldrslt{https://openjdk.java.net/projects/openjfx/javafx-base/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - javafx-controls (org.openjfx:javafx-controls:16 - {{\field{\*\fldinst{HYPERLINK https://openjdk.java.net/projects/openjfx/javafx-controls/ }}{\fldrslt{https://openjdk.java.net/projects/openjfx/javafx-controls/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - javafx-fxml (org.openjfx:javafx-fxml:16 - {{\field{\*\fldinst{HYPERLINK https://openjdk.java.net/projects/openjfx/javafx-fxml/ }}{\fldrslt{https://openjdk.java.net/projects/openjfx/javafx-fxml/\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - javafx-graphics (org.openjfx:javafx-graphics:16 - {{\field{\*\fldinst{HYPERLINK https://openjdk.java.net/projects/openjfx/javafx-graphics/ }}{\fldrslt{https://openjdk.java.net/projects/openjfx/javafx-graphics/\ul0\cf0}}}}\f0\fs16 )\par -\tab LGPL 2.1:\par -\tab\tab - jnr-posix (com.github.jnr:jnr-posix:3.0.54 - {{\field{\*\fldinst{HYPERLINK http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix }}{\fldrslt{http://nexus.sonatype.org/oss-repository-hosting.html/jnr-posix\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Java Native Access (net.java.dev.jna:jna:5.7.0 - {{\field{\*\fldinst{HYPERLINK https://github.com/java-native-access/jna }}{\fldrslt{https://github.com/java-native-access/jna\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - Java Native Access Platform (net.java.dev.jna:jna-platform:5.7.0 - {{\field{\*\fldinst{HYPERLINK https://github.com/java-native-access/jna }}{\fldrslt{https://github.com/java-native-access/jna\ul0\cf0}}}}\f0\fs16 )\par -\tab MIT License:\par -\tab\tab - java jwt (com.auth0:java-jwt:3.18.1 - {{\field{\*\fldinst{HYPERLINK https://github.com/auth0/java-jwt }}{\fldrslt{https://github.com/auth0/java-jwt\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - {{\field{\*\fldinst{HYPERLINK http://github.com/jnr/jnr-x86asm }}{\fldrslt{http://github.com/jnr/jnr-x86asm\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - jnr-fuse (com.github.serceman:jnr-fuse:0.5.5 - {{\field{\*\fldinst{HYPERLINK https://github.com/SerCeMan/jnr-fuse }}{\fldrslt{https://github.com/SerCeMan/jnr-fuse\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - zxcvbn4j (com.nulab-inc:zxcvbn:1.5.2 - {{\field{\*\fldinst{HYPERLINK https://github.com/nulab/zxcvbn4j }}{\fldrslt{https://github.com/nulab/zxcvbn4j\ul0\cf0}}}}\f0\fs16 )\par -\tab\tab - SLF4J API Module (org.slf4j:slf4j-api:1.7.31 - {{\field{\*\fldinst{HYPERLINK http://www.slf4j.org }}{\fldrslt{http://www.slf4j.org\ul0\cf0}}}}\f0\fs16 )\par -\tab The BSD 2-Clause License:\par -\tab\tab - EasyBind (com.tobiasdiez:easybind:2.2 - {{\field{\*\fldinst{HYPERLINK https://github.com/tobiasdiez/EasyBind }}{\fldrslt{https://github.com/tobiasdiez/EasyBind\ul0\cf0}}}}\f0\fs16 )\par -\par -\b Cryptomator uses other third-party assets under the following licenses:\b0\par -\tab SIL OFL 1.1 License:\par -\tab\tab - Font Awesome 5.12.0 ({{\field{\*\fldinst{HYPERLINK https://fontawesome.com/ }}{\fldrslt{https://fontawesome.com/\ul0\cf0}}}}\f0\fs16 )\b\par -} diff --git a/dist/win/resources/licenseTemplate.ftl b/dist/win/resources/licenseTemplate.ftl new file mode 100644 index 000000000..d442e6538 --- /dev/null +++ b/dist/win/resources/licenseTemplate.ftl @@ -0,0 +1,37 @@ +<#function artifactFormat p> + <#if p.name?index_of('Unnamed') > -1> + <#return p.artifactId + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - {{\\field{\\*\\fldinst{HYPERLINK " + (p.url!"no url defined") + "}}{\\fldrslt{" + (p.url!"no url defined") + "\\ul0\\cf0}}}}\\f0\\fs16 ) "> + <#else> + <#return p.name + " (" + p.groupId + ":" + p.artifactId + ":" + p.version + " - {{\\field{\\*\\fldinst{HYPERLINK " + (p.url!"no url defined") + "}}{\\fldrslt{" + (p.url!"no url defined") + "\\ul0\\cf0}}}}\\f0\\fs16 ) "> + + +{\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Arial;}} +{\colortbl ;\red0\green0\blue255;} +\viewkind4\uc1 +\pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\b\fs16\lang7 Cryptomator is distributed under the GPLv3 License, found below. Please see the bottom of this document for any other license applicable to code used within Cryptomator.\b0\par +\par +\b\'a9 2016 \endash 2022 Skymatic GmbH\b0\par +\par +This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.\par +\par +This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.\par +\par +You should have received a copy of the GNU General Public License along with this program. If not, see {{\field{\*\fldinst{HYPERLINK http://www.gnu.org/licenses/ }}{\fldrslt{http://www.gnu.org/licenses/\ul0\cf0}}}}\f0\fs16 .\par +\par + +\b Cryptomator uses ${dependencyMap?size} third-party dependencies under the following licenses:\b0\par +<#list licenseMap as e> +<#assign license = e.getKey()/> +<#assign projects = e.getValue()/> +<#if projects?size > 0> +\tab ${license}:\par +<#list projects as project> +\tab\tab- ${artifactFormat(project)}\par + + + +\par +\b Cryptomator uses other third-party assets under the following licenses:\b0\par +\tab SIL OFL 1.1 License:\par +\tab\tab - Font Awesome 5.12.0 ({{\field{\*\fldinst{HYPERLINK https://fontawesome.com/ }}{\fldrslt{https://fontawesome.com/\ul0\cf0}}}}\f0\fs16 )\b\par +} \ No newline at end of file From 8090f954959589d957c94de7f68f16e842f4adc8 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 23 Feb 2022 22:30:22 +0100 Subject: [PATCH 015/141] integrate windows installation bundle creation into gh action "release" --- .github/workflows/release.yml | 82 +++++++++++++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fe6283ba..3ac783f56 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -503,6 +503,7 @@ jobs: with: distribution: 'temurin' java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' - name: Patch Application Directory run: | cp dist/win/contrib/* appdir/Cryptomator @@ -519,6 +520,12 @@ jobs: timestampUrl: 'http://timestamp.digicert.com' folder: appdir/Cryptomator recursive: true + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win//resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/resources" - name: Create MSI run: > ${JAVA_HOME}/bin/jpackage @@ -559,13 +566,77 @@ jobs: path: installer/*.msi if-no-files-found: error +# +# Windows Cryptomator.exe bundle +# + win-exe: + name: Build Cryptomator.exe bundle + runs-on: windows-latest + needs: [win-msi, metadata] + steps: + - uses: actions/checkout@v2 + - name: Download Windows msi + uses: actions/download-artifact@v2 + with: + name: win-msi + path: dist/win/bundle/resources + - name: Strip version info from msi file name + run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win/bundle/resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/bundle/resources" + - name: Download winfsp + run: + curl --output dist/win/bundle/resources/winfsp.msi -L https://github.com/billziss-gh/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi + - name: Compile to wixObj file + run: > + "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs + -ext WixBalExtension + -out dist/win/bundle/ + -dBundleVersion="${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" + -dBundleVendor="Skymatic GmbH" + -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" + -dAboutUrl="https://cryptomator.org" + -dHelpUrl="https://cryptomator.org/contact" + -dUpdateUrl="https://cryptomator.org/downloads/" + - name: Create executable with linker + run: > + "${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj + -ext WixBalExtension + -out installer/Cryptomator.exe + - name: Codesign EXE + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Add possible alpha/beta tags to installer name + run: mv installer/Cryptomator.exe installer/Cryptomator-${{ needs.metadata.outputs.semVerStr }}-x64.exe + - name: Upload win-exe + uses: actions/upload-artifact@v2 + with: + name: win-exe + path: installer/*.exe + if-no-files-found: error + # # Release # release: name: Draft a release on Github runs-on: ubuntu-latest - needs: [metadata,linux-appimage,mac-dmg,win-msi,ppa] + needs: [metadata,linux-appimage,mac-dmg,win-exe,ppa] if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' steps: - uses: actions/checkout@v2 @@ -583,10 +654,14 @@ jobs: uses: actions/download-artifact@v2 with: name: win-msi + - name: Download Windows exe + uses: actions/download-artifact@v2 + with: + name: win-exe - name: Create detached GPG signature for all release files with key 615D449FE6E6A235 run: | echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - for FILE in `find . -name "*.AppImage" -o -name "*.dmg" -o -name "*.msi" -o -name "*.zsync" -o -name "*.tar.gz"`; do + for FILE in `find . -name "*.AppImage" -o -name "*.dmg" -o -name "*.msi" -o -name "*.exe" -o -name "*.zsync" -o -name "*.tar.gz"`; do echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a ${FILE} done env: @@ -594,7 +669,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - name: Compute SHA256 checksums of release artifacts run: | - SHA256_SUMS=`find . -name "*.AppImage" -o -name "*.dmg" -o -name "*.msi" -o -name "*.tar.gz" | xargs sha256sum` + SHA256_SUMS=`find . -name "*.AppImage" -o -name "*.dmg" -o -name "*.msi" -o -name "*.exe" -o -name "*.tar.gz" | xargs sha256sum` echo "SHA256_SUMS<> $GITHUB_ENV echo "${SHA256_SUMS}" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV @@ -612,6 +687,7 @@ jobs: *.asc *.dmg *.msi + *.exe body: |- :construction: Work in Progress ## What's New From 7cfcfda66f75cb73e4b92da53924b629a0e122a5 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 24 Feb 2022 09:46:05 +0100 Subject: [PATCH 016/141] Apply suggestions from code review Co-authored-by: Sebastian Stenzel --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ac783f56..0bcfb8d35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -524,7 +524,7 @@ jobs: run: > mvn -B license:add-third-party "-Dlicense.thirdPartyFilename=license.rtf" - "-Dlicense.fileTemplate=dist/win//resources/licenseTemplate.ftl" + "-Dlicense.fileTemplate=dist/win/resources/licenseTemplate.ftl" "-Dlicense.outputDirectory=dist/win/resources" - name: Create MSI run: > @@ -636,7 +636,7 @@ jobs: release: name: Draft a release on Github runs-on: ubuntu-latest - needs: [metadata,linux-appimage,mac-dmg,win-exe,ppa] + needs: [metadata,linux-appimage,mac-dmg,win-msi,win-exe,ppa] if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' steps: - uses: actions/checkout@v2 From 771e3ab6f3873554301df76d3ef5a1fb2aea955f Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 24 Feb 2022 09:53:26 +0100 Subject: [PATCH 017/141] remove unused ui elements from theme bundle --- dist/win/bundle/customBootstrapperTheme.xml | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/dist/win/bundle/customBootstrapperTheme.xml b/dist/win/bundle/customBootstrapperTheme.xml index adb90f8dd..792f51a35 100644 --- a/dist/win/bundle/customBootstrapperTheme.xml +++ b/dist/win/bundle/customBootstrapperTheme.xml @@ -1,5 +1,6 @@ + @@ -27,21 +28,10 @@ #(loc.InstallMessage) #(loc.InstallAcceptCheckbox) - #(loc.InstallVersion) - - - #(loc.Title) - #(loc.OptionsHeader) - #(loc.OptionsLocationLabel) - - - - - #(loc.Title) From 0556bcd576941ecc53fafc752fb2fbb5b1257451 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 10:26:37 +0100 Subject: [PATCH 018/141] move metadata to "common" dir --- .../apps/org.cryptomator.Cryptomator.svg | 1 - .../{debian => common}/cryptomator-vault.xml | 0 .../org.cryptomator.Cryptomator.appdata.xml | 0 .../org.cryptomator.Cryptomator.desktop | 2 + .../org.cryptomator.Cryptomator.svg | 0 .../org.cryptomator.Cryptomator256.png} | Bin .../org.cryptomator.Cryptomator512.png} | Bin .../org.cryptomator.Cryptomator.appdata.xml | 69 ------------------ .../org.cryptomator.Cryptomator.desktop | 11 --- .../debian/org.cryptomator.Cryptomator.png | Bin 23272 -> 0 bytes 10 files changed, 2 insertions(+), 81 deletions(-) delete mode 100644 dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg rename dist/linux/{debian => common}/cryptomator-vault.xml (100%) rename dist/linux/{appimage/resources/AppDir/usr/share/metainfo => common}/org.cryptomator.Cryptomator.appdata.xml (100%) rename dist/linux/{appimage/resources/AppDir/usr/share/applications => common}/org.cryptomator.Cryptomator.desktop (89%) rename dist/linux/{debian => common}/org.cryptomator.Cryptomator.svg (100%) rename dist/linux/{appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png => common/org.cryptomator.Cryptomator256.png} (100%) rename dist/linux/{appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png => common/org.cryptomator.Cryptomator512.png} (100%) delete mode 100644 dist/linux/debian/org.cryptomator.Cryptomator.appdata.xml delete mode 100644 dist/linux/debian/org.cryptomator.Cryptomator.desktop delete mode 100644 dist/linux/debian/org.cryptomator.Cryptomator.png diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg b/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg deleted file mode 100644 index b2e12a3c3..000000000 --- a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/dist/linux/debian/cryptomator-vault.xml b/dist/linux/common/cryptomator-vault.xml similarity index 100% rename from dist/linux/debian/cryptomator-vault.xml rename to dist/linux/common/cryptomator-vault.xml diff --git a/dist/linux/appimage/resources/AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml b/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml similarity index 100% rename from dist/linux/appimage/resources/AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml rename to dist/linux/common/org.cryptomator.Cryptomator.appdata.xml diff --git a/dist/linux/appimage/resources/AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop b/dist/linux/common/org.cryptomator.Cryptomator.desktop similarity index 89% rename from dist/linux/appimage/resources/AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop rename to dist/linux/common/org.cryptomator.Cryptomator.desktop index 3e1b34830..81ddc3b4d 100644 --- a/dist/linux/appimage/resources/AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop +++ b/dist/linux/common/org.cryptomator.Cryptomator.desktop @@ -1,10 +1,12 @@ [Desktop Entry] Name=Cryptomator +Version=${VERSION_STR} Comment=Cloud Storage Encryption Utility Exec=cryptomator %F Icon=org.cryptomator.Cryptomator Terminal=false Type=Application Categories=Utility;Security;FileTools; +StartupNotify=true StartupWMClass=org.cryptomator.launcher.Cryptomator MimeType=application/vnd.cryptomator.encrypted;application/x-vnd.cryptomator.vault-metadata; diff --git a/dist/linux/debian/org.cryptomator.Cryptomator.svg b/dist/linux/common/org.cryptomator.Cryptomator.svg similarity index 100% rename from dist/linux/debian/org.cryptomator.Cryptomator.svg rename to dist/linux/common/org.cryptomator.Cryptomator.svg diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png b/dist/linux/common/org.cryptomator.Cryptomator256.png similarity index 100% rename from dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png rename to dist/linux/common/org.cryptomator.Cryptomator256.png diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png b/dist/linux/common/org.cryptomator.Cryptomator512.png similarity index 100% rename from dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png rename to dist/linux/common/org.cryptomator.Cryptomator512.png diff --git a/dist/linux/debian/org.cryptomator.Cryptomator.appdata.xml b/dist/linux/debian/org.cryptomator.Cryptomator.appdata.xml deleted file mode 100644 index ad4af6c70..000000000 --- a/dist/linux/debian/org.cryptomator.Cryptomator.appdata.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - org.cryptomator.Cryptomator - FSFAP - GPL-3.0-or-later - Cryptomator - Multi-platform client-side encryption tool optimized for cloud storages - -

- Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud. -

-

- Features: -

    -
  • Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
  • -
  • Open Source means: No backdoors, control is better than trust
  • -
  • Client-side: No accounts, no data shared with any online service
  • -
  • Totally transparent: Just work on the virtual drive as if it were a USB flash drive
  • -
  • AES encryption with 256-bit key length
  • -
  • File names get encrypted
  • -
  • Folder structure gets obfuscated
  • -
  • Use as many vaults in your Dropbox as you want, each having individual passwords
  • -
  • One thousand commits for the security of your data!! :tada:
  • -
-

-

- Privacy: -

    -
  • 256-bit keys (unlimited strength policy bundled with native binaries)
  • -
  • Scrypt key derivation
  • -
  • Cryptographically secure random numbers for salts, IVs and the masterkey of course
  • -
  • Sensitive data is wiped from the heap asap
  • -
  • Lightweight: Complexity kills security
  • -
-

-

- Consistency: -

    -
  • HMAC over file contents to recognize changed ciphertext before decryption
  • -
  • I/O operations are transactional and atomic, if the filesystems support it
  • -
  • Each file contains all information needed for decryption (except for the key of course), no common metadata means no Single Point of Failure
  • -
-

-
- - Office - Security - FileTools - Java - - http://cryptomator.org - https://github.com/cryptomator/cryptomator/issues - https://community.cryptomator.org/c/kb/faq - https://community.cryptomator.org/ - https://cryptomator.org/ - - none - none - none - none - mild - - Cryptomator - - cryptomator - - org.cryptomator.Cryptomator.desktop -
diff --git a/dist/linux/debian/org.cryptomator.Cryptomator.desktop b/dist/linux/debian/org.cryptomator.Cryptomator.desktop deleted file mode 100644 index d8a5925bd..000000000 --- a/dist/linux/debian/org.cryptomator.Cryptomator.desktop +++ /dev/null @@ -1,11 +0,0 @@ -[Desktop Entry] -Name=Cryptomator -Version=${VERSION_STR} -Comment=Cloud Storage Encryption Utility -Exec=/usr/bin/cryptomator %f -Icon=org.cryptomator.Cryptomator -Terminal=false -Type=Application -Categories=Utility;Security;FileTools; -StartupWMClass=org.cryptomator.launcher.Cryptomator -MimeType=application/vnd.cryptomator.encrypted;application/x-vnd.cryptomator.vault-metadata; \ No newline at end of file diff --git a/dist/linux/debian/org.cryptomator.Cryptomator.png b/dist/linux/debian/org.cryptomator.Cryptomator.png deleted file mode 100644 index 9c863511181b16dd12ad9df06d287e811c350e4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23272 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 Date: Thu, 24 Feb 2022 10:29:41 +0100 Subject: [PATCH 019/141] adjust appimage build to use "common" resources --- .github/workflows/release.yml | 7 +++++++ dist/linux/appimage/build.sh | 8 ++++++++ .../resources/AppDir/usr/share/applications/.gitkeep | 0 .../AppDir/usr/share/icons/hicolor/256x256/apps/.gitkeep | 0 .../AppDir/usr/share/icons/hicolor/512x512/apps/.gitkeep | 0 .../AppDir/usr/share/icons/hicolor/scalable/apps/.gitkeep | 0 .../appimage/resources/AppDir/usr/share/metainfo/.gitkeep | 0 dist/linux/common/org.cryptomator.Cryptomator.desktop | 2 +- 8 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/applications/.gitkeep create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/.gitkeep create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/.gitkeep create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/.gitkeep create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/metainfo/.gitkeep diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6fe6283ba..2c8d314ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -282,6 +282,13 @@ jobs: mv appdir/Cryptomator Cryptomator.AppDir cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh + envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop + cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg + cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop + cp dist/linux/common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml + cp dist/linux/common/cryptomator-vault.xml Cryptomator.AppDir/usr/share/mime/packages/cryptomator-vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index af1f2291f..101e78584 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -9,6 +9,7 @@ command -v mvn >/dev/null 2>&1 || { echo >&2 "mvn not found."; exit 1; } command -v curl >/dev/null 2>&1 || { echo >&2 "curl not found."; exit 1; } VERSION=$(mvn -f ../../../pom.xml help:evaluate -Dexpression=project.version -q -DforceStdout) +SEMVER_STR=${VERSION} # compile mvn -B -f ../../../pom.xml clean package -DskipTests -Plinux @@ -54,6 +55,13 @@ mv Cryptomator Cryptomator.AppDir cp -r resources/AppDir/* Cryptomator.AppDir/ chmod +x Cryptomator.AppDir/lib/runtime/bin/java envsubst '${REVISION_NO}' < resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh +envsubst '${SEMVER_STR}' < ../common/org.cryptomator.Cryptomator.desktop > Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop +cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png +cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png +cp ../common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg +cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop +cp ../common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml +cp ../common/cryptomator-vault.xml Cryptomator.AppDir/usr/share/mime/packages/cryptomator-vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon diff --git a/dist/linux/appimage/resources/AppDir/usr/share/applications/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/applications/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/256x256/apps/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/512x512/apps/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/icons/hicolor/scalable/apps/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/appimage/resources/AppDir/usr/share/metainfo/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/metainfo/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/common/org.cryptomator.Cryptomator.desktop b/dist/linux/common/org.cryptomator.Cryptomator.desktop index 81ddc3b4d..8f584ca82 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.desktop +++ b/dist/linux/common/org.cryptomator.Cryptomator.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Name=Cryptomator -Version=${VERSION_STR} +Version=${SEMVER_STR} Comment=Cloud Storage Encryption Utility Exec=cryptomator %F Icon=org.cryptomator.Cryptomator From 90da61e4950f5a98e3d713cc9fd03c8394f17370 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 10:41:40 +0100 Subject: [PATCH 020/141] adjust ppa build to use "common" resources --- .github/workflows/release.yml | 8 ++++---- dist/linux/debian/cryptomator.install | 11 ++++++----- dist/linux/debian/rules | 2 +- dist/linux/debian/source/include-binaries | 4 ++-- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c8d314ac..73746ff17 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -221,15 +221,15 @@ jobs: - name: patch and rename pkgdir run: | cp -r dist/linux/debian/ pkgdir - cp -r dist/linux/resources/ pkgdir + cp -r dist/linux/common/ pkgdir export RFC2822_TIMESTAMP=`date --rfc-2822` - envsubst '${VERSION_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules - envsubst '${VERSION_STR}' < dist/linux/debian/org.cryptomator.Cryptomator.desktop > pkgdir/debian/org.cryptomator.Cryptomator.desktop + envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules + envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > pkgdir/common/org.cryptomator.Cryptomator.desktop envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog find . -name "*.jar" >> pkgdir/debian/source/include-binaries mv pkgdir cryptomator_${{ needs.metadata.outputs.ppaVerStr }} env: - VERSION_STR: ${{ needs.metadata.outputs.semVerStr }} + SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} VERSION_NUM: ${{ needs.metadata.outputs.semVerNum }} REVISION_NUM: ${{ needs.metadata.outputs.revNum }} PPA_VERSION: ${{ needs.metadata.outputs.ppaVerStr }}-0ppa1 diff --git a/dist/linux/debian/cryptomator.install b/dist/linux/debian/cryptomator.install index 0d5e0b31c..38a469930 100644 --- a/dist/linux/debian/cryptomator.install +++ b/dist/linux/debian/cryptomator.install @@ -1,7 +1,8 @@ cryptomator usr/lib debian/cryptomator.sh usr/lib/cryptomator/bin -debian/org.cryptomator.Cryptomator.desktop usr/share/applications -debian/org.cryptomator.Cryptomator.svg usr/share/icons/hicolor/scalable/apps -debian/org.cryptomator.Cryptomator.png usr/share/icons/hicolor/512x512/apps -debian/org.cryptomator.Cryptomator.appdata.xml usr/share/metainfo -debian/cryptomator-vault.xml usr/share/mime/packages \ No newline at end of file +common/org.cryptomator.Cryptomator.desktop usr/share/applications +common/org.cryptomator.Cryptomator.svg usr/share/icons/hicolor/scalable/apps +common/org.cryptomator.Cryptomator256.png usr/share/icons/hicolor/256x256/apps +common/org.cryptomator.Cryptomator512.png usr/share/icons/hicolor/512x512/apps +common/org.cryptomator.Cryptomator.appdata.xml usr/share/metainfo +common/cryptomator-vault.xml usr/share/mime/packages \ No newline at end of file diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index b381a2331..5aa254b0a 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -40,7 +40,7 @@ override_dh_auto_build: --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \ --java-options "-Dcryptomator.showTrayIcon=false" \ --java-options "-Dcryptomator.buildNumber=\"ppa-${REVISION_NUM}\"" \ - --java-options "-Dcryptomator.appVersion=\"${VERSION_STR}\"" \ + --java-options "-Dcryptomator.appVersion=\"${SEMVER_STR}\"" \ --app-version "${VERSION_NUM}.${REVISION_NUM}" \ --resource-dir resources \ --verbose diff --git a/dist/linux/debian/source/include-binaries b/dist/linux/debian/source/include-binaries index adc7cabd8..6425cc04a 100644 --- a/dist/linux/debian/source/include-binaries +++ b/dist/linux/debian/source/include-binaries @@ -1,2 +1,2 @@ -debian/org.cryptomator.Cryptomator.png -resources/cryptomator.png +common/org.cryptomator.Cryptomator256.png +common/org.cryptomator.Cryptomator512.png \ No newline at end of file From 43e936d117a45cc4739ea1a008d06f0b7b7c9a9e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 10:56:23 +0100 Subject: [PATCH 021/141] rename mimetype file --- .github/workflows/release.yml | 2 +- dist/linux/appimage/build.sh | 2 +- ...ptomator-vault.xml => application-vnd.cryptomator.vault.xml} | 2 +- dist/linux/common/org.cryptomator.Cryptomator.desktop | 2 +- dist/linux/debian/cryptomator.install | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename dist/linux/common/{cryptomator-vault.xml => application-vnd.cryptomator.vault.xml} (77%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73746ff17..55e871ad7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -288,7 +288,7 @@ jobs: cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp dist/linux/common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml - cp dist/linux/common/cryptomator-vault.xml Cryptomator.AppDir/usr/share/mime/packages/cryptomator-vault.xml + cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index 101e78584..9a5e55246 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -61,7 +61,7 @@ cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/ico cp ../common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp ../common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml -cp ../common/cryptomator-vault.xml Cryptomator.AppDir/usr/share/mime/packages/cryptomator-vault.xml +cp ../common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon diff --git a/dist/linux/common/cryptomator-vault.xml b/dist/linux/common/application-vnd.cryptomator.vault.xml similarity index 77% rename from dist/linux/common/cryptomator-vault.xml rename to dist/linux/common/application-vnd.cryptomator.vault.xml index eeb4bc537..3b602f230 100644 --- a/dist/linux/common/cryptomator-vault.xml +++ b/dist/linux/common/application-vnd.cryptomator.vault.xml @@ -1,6 +1,6 @@ - + Cryptomator Vault Metadata diff --git a/dist/linux/common/org.cryptomator.Cryptomator.desktop b/dist/linux/common/org.cryptomator.Cryptomator.desktop index 8f584ca82..a5cc454e4 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.desktop +++ b/dist/linux/common/org.cryptomator.Cryptomator.desktop @@ -9,4 +9,4 @@ Type=Application Categories=Utility;Security;FileTools; StartupNotify=true StartupWMClass=org.cryptomator.launcher.Cryptomator -MimeType=application/vnd.cryptomator.encrypted;application/x-vnd.cryptomator.vault-metadata; +MimeType=application/vnd.cryptomator.encrypted;application/vnd.cryptomator.vault; diff --git a/dist/linux/debian/cryptomator.install b/dist/linux/debian/cryptomator.install index 38a469930..17aa7e869 100644 --- a/dist/linux/debian/cryptomator.install +++ b/dist/linux/debian/cryptomator.install @@ -5,4 +5,4 @@ common/org.cryptomator.Cryptomator.svg usr/share/icons/hicolor/scalable/apps common/org.cryptomator.Cryptomator256.png usr/share/icons/hicolor/256x256/apps common/org.cryptomator.Cryptomator512.png usr/share/icons/hicolor/512x512/apps common/org.cryptomator.Cryptomator.appdata.xml usr/share/metainfo -common/cryptomator-vault.xml usr/share/mime/packages \ No newline at end of file +common/application-vnd.cryptomator.vault.xml usr/share/mime/packages \ No newline at end of file From ec1c255bb402cb6bb228cbd57e9a5f5bdc2273a9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 10:56:36 +0100 Subject: [PATCH 022/141] update appstream metadata --- .../org.cryptomator.Cryptomator.appdata.xml | 136 +++++++++--------- 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml b/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml index ad4af6c70..0340bc83e 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml @@ -1,69 +1,75 @@ - org.cryptomator.Cryptomator - FSFAP - GPL-3.0-or-later - Cryptomator - Multi-platform client-side encryption tool optimized for cloud storages - -

- Cryptomator offers multi-platform transparent client-side encryption of your files in the cloud. -

-

- Features: -

    -
  • Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
  • -
  • Open Source means: No backdoors, control is better than trust
  • -
  • Client-side: No accounts, no data shared with any online service
  • -
  • Totally transparent: Just work on the virtual drive as if it were a USB flash drive
  • -
  • AES encryption with 256-bit key length
  • -
  • File names get encrypted
  • -
  • Folder structure gets obfuscated
  • -
  • Use as many vaults in your Dropbox as you want, each having individual passwords
  • -
  • One thousand commits for the security of your data!! :tada:
  • -
-

-

- Privacy: -

    -
  • 256-bit keys (unlimited strength policy bundled with native binaries)
  • -
  • Scrypt key derivation
  • -
  • Cryptographically secure random numbers for salts, IVs and the masterkey of course
  • -
  • Sensitive data is wiped from the heap asap
  • -
  • Lightweight: Complexity kills security
  • -
-

-

- Consistency: -

    -
  • HMAC over file contents to recognize changed ciphertext before decryption
  • -
  • I/O operations are transactional and atomic, if the filesystems support it
  • -
  • Each file contains all information needed for decryption (except for the key of course), no common metadata means no Single Point of Failure
  • -
-

-
- - Office - Security - FileTools - Java - - http://cryptomator.org - https://github.com/cryptomator/cryptomator/issues - https://community.cryptomator.org/c/kb/faq - https://community.cryptomator.org/ - https://cryptomator.org/ - - none - none - none - none - mild - - Cryptomator - - cryptomator - - org.cryptomator.Cryptomator.desktop + org.cryptomator.Cryptomator + FSFAP + GPL-3.0-or-later + Cryptomator + Multi-platform client-side encryption tool optimized for cloud storages + + +

+ Cryptomator provides transparent, client-side encryption for your cloud. Protect your documents from unauthorized + access. Cryptomator is free and open source software, so you can rest assured there are no backdoors. +

+

+ Cryptomator encrypts file contents and names using AES. Your passphrase is protected against bruteforcing attempts + using scrypt. Directory structures get obfuscated. The only thing which cannot be encrypted without breaking your + cloud synchronization is the modification date of your files. +

+

+ Cryptomator is a free and open source software licensed under the GPLv3. This allows anyone to check our code. It + is impossible to introduce backdoors for third parties. Also we cannot hide vulnerabilities. And the best thing + is: There is no need to trust us, as you can control us! +

+

+ Vendor lock-ins are impossible. Even if we decided to stop development: The source code is already cloned by + hundreds of other developers. As you don't need an account, you will never stand in front of locked doors. +

+
+ + + Office + Security + FileTools + + + org.cryptomator.Cryptomator.desktop + + cryptomator + application/vnd.cryptomator.vault + application/vnd.cryptomator.encrypted + + + + + Light theme + https://cryptomator.org/presskit/linux-screenshot-1.png + + + Dark theme + https://cryptomator.org/presskit/linux-screenshot-2.png + + + + https://cryptomator.org/ + https://github.com/cryptomator/cryptomator/issues/ + https://cryptomator.org/donate + https://community.cryptomator.org/c/kb/faq + https://community.cryptomator.org/ + https://translate.cryptomator.org + + Skymatic GmbH + + + none + none + none + none + mild + + + + +
From 69ff0e44f516f6fafe031a330850d0e77b7cfe48 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 12:00:02 +0100 Subject: [PATCH 023/141] fix build --- .../appimage/resources/AppDir/usr/share/mime/packages/.gitkeep | 0 dist/linux/common/org.cryptomator.Cryptomator.svg | 2 +- dist/linux/debian/source/include-binaries | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 dist/linux/appimage/resources/AppDir/usr/share/mime/packages/.gitkeep diff --git a/dist/linux/appimage/resources/AppDir/usr/share/mime/packages/.gitkeep b/dist/linux/appimage/resources/AppDir/usr/share/mime/packages/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/dist/linux/common/org.cryptomator.Cryptomator.svg b/dist/linux/common/org.cryptomator.Cryptomator.svg index 19d80d49c..c76d99fb9 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.svg +++ b/dist/linux/common/org.cryptomator.Cryptomator.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/dist/linux/debian/source/include-binaries b/dist/linux/debian/source/include-binaries index 6425cc04a..8b9254c65 100644 --- a/dist/linux/debian/source/include-binaries +++ b/dist/linux/debian/source/include-binaries @@ -1,2 +1,2 @@ common/org.cryptomator.Cryptomator256.png -common/org.cryptomator.Cryptomator512.png \ No newline at end of file +common/org.cryptomator.Cryptomator512.png From 7d9aab46a868f3652a67b54beaa1d9b1747af3d3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 16:27:28 +0100 Subject: [PATCH 024/141] fix broken path during mimetype installation --- dist/linux/debian/postinst | 2 +- dist/linux/debian/prerm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/linux/debian/postinst b/dist/linux/debian/postinst index 2af574c6b..5668a5e29 100644 --- a/dist/linux/debian/postinst +++ b/dist/linux/debian/postinst @@ -24,7 +24,7 @@ case "$1" in mkdir -p /usr/share/desktop-directories fi xdg-desktop-menu install --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop - xdg-mime install /usr/share/mime/packages/cryptomator-vault.xml + xdg-mime install /usr/share/mime/packages/application-vnd.cryptomator.vault.xml ;; abort-upgrade|abort-remove|abort-deconfigure) diff --git a/dist/linux/debian/prerm b/dist/linux/debian/prerm index cace6816e..41a54cf33 100644 --- a/dist/linux/debian/prerm +++ b/dist/linux/debian/prerm @@ -22,7 +22,7 @@ case "$1" in echo Removing shortcut xdg-desktop-menu uninstall --novendor /usr/share/applications/org.cryptomator.Cryptomator.desktop - xdg-mime uninstall /usr/share/mime/packages/cryptomator-vault.xml + xdg-mime uninstall /usr/share/mime/packages/application-vnd.cryptomator.vault.xml ;; failed-upgrade) From 891b5597de0d37cbeede2c1797cfd5f9c185eebb Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 24 Feb 2022 16:29:21 +0100 Subject: [PATCH 025/141] fix build, add common/ to debian orig.tar.xz --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55e871ad7..f6870c974 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -216,15 +216,16 @@ jobs: with: name: linux-buildkit path: pkgdir - - name: create orig.tar.gz - run: tar -cJf cryptomator_${{ needs.metadata.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . + - name: create orig.tar.gz with common/ libs/ mods/ + run: | + cp -r dist/linux/common/ pkgdir + envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > pkgdir/common/org.cryptomator.Cryptomator.desktop + tar -cJf cryptomator_${{ needs.metadata.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . - name: patch and rename pkgdir run: | cp -r dist/linux/debian/ pkgdir - cp -r dist/linux/common/ pkgdir export RFC2822_TIMESTAMP=`date --rfc-2822` envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules - envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > pkgdir/common/org.cryptomator.Cryptomator.desktop envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog find . -name "*.jar" >> pkgdir/debian/source/include-binaries mv pkgdir cryptomator_${{ needs.metadata.outputs.ppaVerStr }} @@ -286,7 +287,6 @@ jobs: cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg - cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp dist/linux/common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg From 02fe63c7bc604132a62abb32feb8f52bb472b210 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Feb 2022 09:25:15 +0100 Subject: [PATCH 026/141] version of the spec, not version of the software see https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s06.html --- .github/workflows/release.yml | 3 +-- dist/linux/appimage/build.sh | 1 - dist/linux/common/org.cryptomator.Cryptomator.desktop | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6870c974..6f6b6745f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -219,7 +219,6 @@ jobs: - name: create orig.tar.gz with common/ libs/ mods/ run: | cp -r dist/linux/common/ pkgdir - envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > pkgdir/common/org.cryptomator.Cryptomator.desktop tar -cJf cryptomator_${{ needs.metadata.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . - name: patch and rename pkgdir run: | @@ -283,11 +282,11 @@ jobs: mv appdir/Cryptomator Cryptomator.AppDir cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh - envsubst '${SEMVER_STR}' < dist/linux/common/org.cryptomator.Cryptomator.desktop > Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg cp dist/linux/common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml + cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.desktop cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index 9a5e55246..2473e23ae 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -55,7 +55,6 @@ mv Cryptomator Cryptomator.AppDir cp -r resources/AppDir/* Cryptomator.AppDir/ chmod +x Cryptomator.AppDir/lib/runtime/bin/java envsubst '${REVISION_NO}' < resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh -envsubst '${SEMVER_STR}' < ../common/org.cryptomator.Cryptomator.desktop > Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp ../common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg diff --git a/dist/linux/common/org.cryptomator.Cryptomator.desktop b/dist/linux/common/org.cryptomator.Cryptomator.desktop index a5cc454e4..fad034a33 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.desktop +++ b/dist/linux/common/org.cryptomator.Cryptomator.desktop @@ -1,6 +1,6 @@ [Desktop Entry] Name=Cryptomator -Version=${SEMVER_STR} +Version=1.0.0 Comment=Cloud Storage Encryption Utility Exec=cryptomator %F Icon=org.cryptomator.Cryptomator From 10999b27252d8d4f6a73611a456d9c46fc70ffed Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Feb 2022 09:28:58 +0100 Subject: [PATCH 027/141] renamed .appdata.xml to .metainfo.xml as suggested by @x80486 --- .github/workflows/release.yml | 2 +- dist/linux/appimage/build.sh | 2 +- ...tor.appdata.xml => org.cryptomator.Cryptomator.metainfo.xml} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename dist/linux/common/{org.cryptomator.Cryptomator.appdata.xml => org.cryptomator.Cryptomator.metainfo.xml} (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6f6b6745f..d45fde7be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -285,7 +285,7 @@ jobs: cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg - cp dist/linux/common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml + cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.desktop cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg diff --git a/dist/linux/appimage/build.sh b/dist/linux/appimage/build.sh index 2473e23ae..6dd670df2 100755 --- a/dist/linux/appimage/build.sh +++ b/dist/linux/appimage/build.sh @@ -59,7 +59,7 @@ cp ../common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/ico cp ../common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp ../common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg cp ../common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop -cp ../common/org.cryptomator.Cryptomator.appdata.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.appdata.xml +cp ../common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml cp ../common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg diff --git a/dist/linux/common/org.cryptomator.Cryptomator.appdata.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml similarity index 100% rename from dist/linux/common/org.cryptomator.Cryptomator.appdata.xml rename to dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml From 6553e48ea3efa7ae3a4de2437378b22f16401b82 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Feb 2022 09:33:15 +0100 Subject: [PATCH 028/141] use oars-1.1 as suggested by @x80486 --- dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index 0340bc83e..1c3ae9c27 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -61,11 +61,7 @@ Skymatic GmbH - - none - none - none - none + mild From 7afc17e34e077277d756d3ed7ae57219abedcda9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Feb 2022 09:43:10 +0100 Subject: [PATCH 029/141] fix path --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d45fde7be..905c252a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -286,7 +286,7 @@ jobs: cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml - cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.desktop + cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg From 8ee47aad0f122a196a0a9fbf7c34322f09375453 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 25 Feb 2022 09:58:47 +0100 Subject: [PATCH 030/141] remove version entirely (should have been 1.0, but not required) --- dist/linux/common/org.cryptomator.Cryptomator.desktop | 1 - 1 file changed, 1 deletion(-) diff --git a/dist/linux/common/org.cryptomator.Cryptomator.desktop b/dist/linux/common/org.cryptomator.Cryptomator.desktop index fad034a33..1872b9f38 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.desktop +++ b/dist/linux/common/org.cryptomator.Cryptomator.desktop @@ -1,6 +1,5 @@ [Desktop Entry] Name=Cryptomator -Version=1.0.0 Comment=Cloud Storage Encryption Utility Exec=cryptomator %F Icon=org.cryptomator.Cryptomator From 55d1a8e9352b745a8d01c912bd2037253a8b1a86 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Fri, 25 Feb 2022 13:21:45 +0100 Subject: [PATCH 031/141] Allow custom mount point for winfsp --- .../mountpoint/CustomMountPointChooser.java | 58 ++++++++++++++----- .../vaultoptions/MountOptionsController.java | 46 +++++---------- .../resources/fxml/vault_options_mount.fxml | 4 +- 3 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index c55ede640..d075e1292 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -4,6 +4,7 @@ import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VolumeImpl; +import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.Volume; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,9 +13,7 @@ import javax.inject.Inject; import java.io.IOException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; -import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; -import java.nio.file.LinkOption; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; @@ -35,7 +34,6 @@ class CustomMountPointChooser implements MountPointChooser { @Override public boolean isApplicable(Volume caller) { - //Disable if useExperimentalFuse is required (Win + Fuse), but set to false return caller.getImplementationType() != VolumeImpl.FUSE || !SystemUtils.IS_OS_WINDOWS || environment.useExperimentalFuse(); } @@ -48,8 +46,16 @@ class CustomMountPointChooser implements MountPointChooser { @Override public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException { switch (caller.getMountPointRequirement()) { - case PARENT_NO_MOUNT_POINT -> prepareParentNoMountPoint(mountPoint); - case EMPTY_MOUNT_POINT -> prepareEmptyMountPoint(mountPoint); + case PARENT_NO_MOUNT_POINT -> { + prepareParentNoMountPoint(mountPoint); + LOG.debug("Successfully checked custom mount point: {}", mountPoint); + return true; + } + case EMPTY_MOUNT_POINT -> { + prepareEmptyMountPoint(mountPoint); + LOG.debug("Successfully checked custom mount point: {}", mountPoint); + return false; + } case NONE -> { //Requirement "NONE" doesn't make any sense here. //No need to prepare/verify a Mountpoint without requiring one... @@ -60,21 +66,26 @@ class CustomMountPointChooser implements MountPointChooser { throw new InvalidMountPointException(new IllegalStateException("Not implemented")); } } - LOG.debug("Successfully checked custom mount point: {}", mountPoint); - return false; } private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { //This the case on Windows when using FUSE //See https://github.com/billziss-gh/winfsp/issues/320 - Path parent = mountPoint.getParent(); - if (!Files.isDirectory(parent)) { - throw new InvalidMountPointException(new NotDirectoryException(parent.toString())); - } - //We must use #notExists() here because notExists =/= !exists (see docs) - if (!Files.notExists(mountPoint, LinkOption.NOFOLLOW_LINKS)) { - //File exists OR can't be determined - throw new InvalidMountPointException(new FileAlreadyExistsException(mountPoint.toString())); + assert SystemUtils.IS_OS_WINDOWS; + + Path hideaway = getHideaway(mountPoint); + if (Files.exists(hideaway)) { + LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount."); + } else if (!Files.isDirectory(mountPoint)) { + throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); //simulate we need a directory + } else { + //TODO: should we require it to be empty? + try { + Files.move(mountPoint, hideaway); + Files.setAttribute(hideaway, "dos:hidden", true); + } catch (IOException e) { + throw new InvalidMountPointException(e); + } } } @@ -92,4 +103,21 @@ class CustomMountPointChooser implements MountPointChooser { } } + @Override + public void cleanup(Volume caller, Path mountPoint) { + if (VolumeImpl.FUSE == caller.getImplementationType() && MountPointRequirement.PARENT_NO_MOUNT_POINT == caller.getMountPointRequirement()) { + Path hideaway = getHideaway(mountPoint); + try { + Files.move(hideaway, mountPoint); + Files.setAttribute(mountPoint, "dos:hidden", false); + } catch (IOException e) { + LOG.error("Unable to clean up mountpoint {} for Winfsp mounting."); + } + } + } + + private Path getHideaway(Path mountPoint) { + return mountPoint.resolveSibling(mountPoint.getFileName().toString() + "_tmp"); + } + } diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index 8739be791..fc1962b9d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -11,9 +11,6 @@ import org.cryptomator.ui.common.FxController; import javax.inject.Inject; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; @@ -32,18 +29,16 @@ import java.nio.file.Path; import java.util.ResourceBundle; import java.util.Set; -/** - * TODO: if WebDav is selected on a windows system, custom mount directory is _not_ supported. This is currently not indicated/shown/etc in the ui - */ @VaultOptionsScoped public class MountOptionsController implements FxController { private final Stage window; private final Vault vault; - private final BooleanProperty osIsWindows = new SimpleBooleanProperty(SystemUtils.IS_OS_WINDOWS); - private final BooleanBinding webDavAndWindows; + private final boolean webDavAndWindows; + private final boolean fuseAndWindows; private final WindowsDriveLetters windowsDriveLetters; private final ResourceBundle resourceBundle; + public CheckBox readOnlyCheckbox; public CheckBox customMountFlagsCheckbox; public TextField mountFlags; @@ -53,20 +48,14 @@ public class MountOptionsController implements FxController { public RadioButton mountPointCustomDir; public ChoiceBox driveLetterSelection; - //FUSE + Windows -> Disable some (experimental) features for the user because they are unstable - //Use argument Dfuse.experimental="true" to override - private final BooleanBinding restrictToStableFuseOnWindows; - @Inject MountOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, Settings settings, WindowsDriveLetters windowsDriveLetters, ResourceBundle resourceBundle, Environment environment) { this.window = window; this.vault = vault; - this.webDavAndWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.WEBDAV).and(osIsWindows); + this.webDavAndWindows = settings.preferredVolumeImpl().get() == VolumeImpl.WEBDAV && SystemUtils.IS_OS_WINDOWS; + this.fuseAndWindows = settings.preferredVolumeImpl().get() == VolumeImpl.FUSE && SystemUtils.IS_OS_WINDOWS; this.windowsDriveLetters = windowsDriveLetters; this.resourceBundle = resourceBundle; - - BooleanBinding isFuseOnWindows = settings.preferredVolumeImpl().isEqualTo(VolumeImpl.FUSE).and(osIsWindows); - this.restrictToStableFuseOnWindows = isFuseOnWindows.and(new SimpleBooleanProperty(!environment.useExperimentalFuse())); //Is FUSE on Win and is NOT experimental fuse enabled } @FXML @@ -74,10 +63,11 @@ public class MountOptionsController implements FxController { // readonly: readOnlyCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().usesReadOnlyMode()); - if (getRestrictToStableFuseOnWindows()) { + //TODO: support this feature on Windows + if (fuseAndWindows) { readOnlyCheckbox.setSelected(false); // to prevent invalid states + readOnlyCheckbox.setDisable(true); } - readOnlyCheckbox.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().or(restrictToStableFuseOnWindows)); // custom mount flags: mountFlags.disableProperty().bind(customMountFlagsCheckbox.selectedProperty().not()); @@ -95,9 +85,7 @@ public class MountOptionsController implements FxController { driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle)); driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get()); - if (vault.getVaultSettings().useCustomMountPath().get() - && vault.getVaultSettings().getCustomMountPath().isPresent() - && !getRestrictToStableFuseOnWindows() /* to prevent invalid states */) { + if (vault.getVaultSettings().useCustomMountPath().get() && vault.getVaultSettings().getCustomMountPath().isPresent()) { mountPoint.selectToggle(mountPointCustomDir); } else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) { mountPoint.selectToggle(mountPointWinDriveLetter); @@ -188,20 +176,16 @@ public class MountOptionsController implements FxController { // Getter & Setter - public BooleanProperty osIsWindowsProperty() { - return osIsWindows; - } - public boolean getOsIsWindows() { - return osIsWindows.get(); + return SystemUtils.IS_OS_WINDOWS; } - public BooleanBinding webDavAndWindowsProperty() { + public boolean getCustomMountPointSupported() { return webDavAndWindows; } - public boolean isWebDavAndWindows() { - return webDavAndWindows.get(); + public boolean getReadOnlySupported() { + return fuseAndWindows; } public StringProperty customMountPathProperty() { @@ -212,8 +196,4 @@ public class MountOptionsController implements FxController { return vault.getVaultSettings().customMountPath().get(); } - public Boolean getRestrictToStableFuseOnWindows() { - return restrictToStableFuseOnWindows.get(); - } - } diff --git a/src/main/resources/fxml/vault_options_mount.fxml b/src/main/resources/fxml/vault_options_mount.fxml index a96784020..a0d1ce0e1 100644 --- a/src/main/resources/fxml/vault_options_mount.fxml +++ b/src/main/resources/fxml/vault_options_mount.fxml @@ -42,8 +42,8 @@ - - + + - + From e15b68fc9be800d250739520849c6544668f4bb8 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Mon, 28 Feb 2022 18:54:01 +0100 Subject: [PATCH 038/141] Refactor winfsp mount preps and add unit tests --- .../mountpoint/CustomMountPointChooser.java | 45 ++++-- .../CustomMountPointChooserTest.java | 133 ++++++++++++++++++ 2 files changed, 169 insertions(+), 9 deletions(-) create mode 100644 src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index d075e1292..872815a4e 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -13,7 +13,9 @@ import javax.inject.Inject; import java.io.IOException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.Path; import java.nio.file.Paths; @@ -21,6 +23,8 @@ import java.util.Optional; class CustomMountPointChooser implements MountPointChooser { + private static final String HIDEAWAY_PREFIX = ".~$"; + private static final String HIDEAWAY_SUFFIX = ".tmp"; private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class); private final VaultSettings vaultSettings; @@ -68,19 +72,41 @@ class CustomMountPointChooser implements MountPointChooser { } } - private void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { + void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { //This the case on Windows when using FUSE //See https://github.com/billziss-gh/winfsp/issues/320 assert SystemUtils.IS_OS_WINDOWS; Path hideaway = getHideaway(mountPoint); - if (Files.exists(hideaway)) { - LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount."); - } else if (!Files.isDirectory(mountPoint)) { - throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); //simulate we need a directory - } else { - //TODO: should we require it to be empty? + + var mpExists = Files.exists(mountPoint); + var hideExists = Files.exists(hideaway); + + //both resources exist (whatever type) + //TODO: possible improvement by just deleting an _empty_ hideaway + if (mpExists && hideExists) { + throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString())); + } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist + throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString())); + } else if (!mpExists) { //only hideaway exists + + if (!Files.isDirectory(hideaway)) { + throw new InvalidMountPointException(new NotDirectoryException(hideaway.toString())); + } + LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { + Files.setAttribute(hideaway, "dos:hidden", true); + } catch (IOException e) { + throw new InvalidMountPointException(e); + } + } else { + if (!Files.isDirectory(mountPoint)) { + throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); + } + try { + if(Files.list(mountPoint).findFirst().isPresent()) { + throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); + } Files.move(mountPoint, hideaway); Files.setAttribute(hideaway, "dos:hidden", true); } catch (IOException e) { @@ -116,8 +142,9 @@ class CustomMountPointChooser implements MountPointChooser { } } - private Path getHideaway(Path mountPoint) { - return mountPoint.resolveSibling(mountPoint.getFileName().toString() + "_tmp"); + //visible for testing + Path getHideaway(Path mountPoint) { + return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX); } } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java new file mode 100644 index 000000000..f5be1522b --- /dev/null +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -0,0 +1,133 @@ +package org.cryptomator.common.mountpoint; + +import org.cryptomator.common.Environment; +import org.cryptomator.common.settings.VaultSettings; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class CustomMountPointChooserTest { + + //--- Mocks --- + VaultSettings vaultSettings; + Environment environment; + + CustomMountPointChooser customMpc; + + + @BeforeEach + public void init() { + this.vaultSettings = Mockito.mock(VaultSettings.class); + this.environment = Mockito.mock(Environment.class); + this.customMpc = new CustomMountPointChooser(vaultSettings, environment); + } + + @Nested + class WinfspPreperations { + + @Test + @DisplayName("Test MP preparation for winfsp, if only mountpoint is present") + public void testPrepareParentNoMountpointOnlyMountpoint(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + Files.createDirectory(mntPoint); + + //execute + Assertions.assertDoesNotThrow(() -> customMpc.prepareParentNoMountPoint(mntPoint)); + + //evaluate + Assertions.assertTrue(Files.notExists(mntPoint)); + + Path hideaway = customMpc.getHideaway(mntPoint); + Assertions.assertTrue(Files.exists(hideaway)); + Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); + Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test MP preparation for winfsp, if only non-empty mountpoint is present") + public void testPrepareParentNoMountpointOnlyNonEmptyMountpoint(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + Files.createDirectory(mntPoint); + Files.createFile(mntPoint.resolve("foo")); + + //execute + Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint)); + + //evaluate + Assertions.assertTrue(Files.exists(mntPoint.resolve("foo"))); + } + + @Test + @DisplayName("Test MP preparation for Winfsp, if for any reason only hideaway dir is present") + public void testPrepareParentNoMountpointOnlyHideaway(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + var hideaway = customMpc.getHideaway(mntPoint); + Files.createDirectory(hideaway); //we explicitly do not set the file attributes here + + //execute + Assertions.assertDoesNotThrow(() -> customMpc.prepareParentNoMountPoint(mntPoint)); + + //evaluate + Assertions.assertTrue(Files.exists(hideaway)); + Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); + Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test Winfsp MP preparation, if mountpoint and hideaway dirs are present") + public void testPrepareParentNoMountpointMountPointAndHideaway(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + var hideaway = customMpc.getHideaway(mntPoint); + Files.createDirectory(hideaway); //we explicitly do not set the file attributes here + Files.createDirectory(mntPoint); + + //execute + Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint)); + + //evaluate + Assertions.assertTrue(Files.exists(hideaway)); + Assertions.assertTrue(Files.exists(mntPoint)); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } + + @Test + @DisplayName("Test Winfsp MP preparation, if neither mountpoint nor hideaway dir is present") + public void testPrepareParentNoMountpointNothing(@TempDir Path tmpDir) { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + var hideaway = customMpc.getHideaway(mntPoint); + + //execute + Assertions.assertThrows(InvalidMountPointException.class, () -> customMpc.prepareParentNoMountPoint(mntPoint)); + + //evaluate + Assertions.assertTrue(Files.notExists(hideaway)); + Assertions.assertTrue(Files.notExists(mntPoint)); + } + + } + + +} From ed1459a0f0dcae95b3c802231ded285b10eef4c1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 1 Mar 2022 08:49:47 +0100 Subject: [PATCH 039/141] Update pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 3caa9b899..8975b8693 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ 17.0.2 3.12.0 - 3.18.2 + 3.18.3 2.2 31.0-jre 2.40.3 From df7d9ba79e32b693b2c362d6874f734b7e55382a Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Tue, 1 Mar 2022 15:25:00 +0100 Subject: [PATCH 040/141] Update winfsp download links --- .github/workflows/release.yml | 2 +- dist/win/build.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0bcfb8d35..a6502d201 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -595,7 +595,7 @@ jobs: "-Dlicense.outputDirectory=dist/win/bundle/resources" - name: Download winfsp run: - curl --output dist/win/bundle/resources/winfsp.msi -L https://github.com/billziss-gh/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi + curl --output dist/win/bundle/resources/winfsp.msi -L https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi - name: Compile to wixObj file run: > "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs diff --git a/dist/win/build.ps1 b/dist/win/build.ps1 index c3cb4ba84..0b0e953bc 100644 --- a/dist/win/build.ps1 +++ b/dist/win/build.ps1 @@ -125,7 +125,7 @@ $Env:JP_WIXWIZARD_RESOURCES = "$buildDir\resources" # download Winfsp [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' # disables Invoke-WebRequest's progress bar, which slows down downloads to a few bytes/s -$winfspMsiUrl = "https://github.com/billziss-gh/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi" +$winfspMsiUrl = "https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi" Write-Output "Downloading ${winfspMsiUrl}..." Invoke-WebRequest $winfspMsiUrl -OutFile ".\bundle\resources\winfsp.msi" # redirects are followed by default From 4f4c9924930a4c1fab67133e21fb863e34c2ba2d Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 14:40:27 +0100 Subject: [PATCH 041/141] Further improvements: * make PARENT_NO_MOUNTPOINT preps system agnostic * add unit tests for cleanup --- .../mountpoint/CustomMountPointChooser.java | 21 ++++---- .../CustomMountPointChooserTest.java | 53 +++++++++++++++++-- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 872815a4e..82898d59e 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -72,13 +72,10 @@ class CustomMountPointChooser implements MountPointChooser { } } + //This the case on Windows when using FUSE + //See https://github.com/billziss-gh/winfsp/issues/320 void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { - //This the case on Windows when using FUSE - //See https://github.com/billziss-gh/winfsp/issues/320 - assert SystemUtils.IS_OS_WINDOWS; - Path hideaway = getHideaway(mountPoint); - var mpExists = Files.exists(mountPoint); var hideExists = Files.exists(hideaway); @@ -104,11 +101,13 @@ class CustomMountPointChooser implements MountPointChooser { throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); } try { - if(Files.list(mountPoint).findFirst().isPresent()) { + if (Files.list(mountPoint).findFirst().isPresent()) { throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); } Files.move(mountPoint, hideaway); - Files.setAttribute(hideaway, "dos:hidden", true); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(hideaway, "dos:hidden", true); + } } catch (IOException e) { throw new InvalidMountPointException(e); } @@ -131,13 +130,15 @@ class CustomMountPointChooser implements MountPointChooser { @Override public void cleanup(Volume caller, Path mountPoint) { - if (VolumeImpl.FUSE == caller.getImplementationType() && MountPointRequirement.PARENT_NO_MOUNT_POINT == caller.getMountPointRequirement()) { + if (caller.getMountPointRequirement() == MountPointRequirement.PARENT_NO_MOUNT_POINT) { Path hideaway = getHideaway(mountPoint); try { Files.move(hideaway, mountPoint); - Files.setAttribute(mountPoint, "dos:hidden", false); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(mountPoint, "dos:hidden", false); + } } catch (IOException e) { - LOG.error("Unable to clean up mountpoint {} for Winfsp mounting."); + LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e); } } } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index f5be1522b..2126b16b6 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -1,7 +1,10 @@ package org.cryptomator.common.mountpoint; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.VaultSettings; +import org.cryptomator.common.vaults.MountPointRequirement; +import org.cryptomator.common.vaults.Volume; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; @@ -10,6 +13,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.OS; import org.junit.jupiter.api.io.TempDir; +import org.mockito.MockedStatic; import org.mockito.Mockito; import java.io.IOException; @@ -21,12 +25,14 @@ public class CustomMountPointChooserTest { //--- Mocks --- VaultSettings vaultSettings; Environment environment; + Volume volume; CustomMountPointChooser customMpc; @BeforeEach public void init() { + this.volume = Mockito.mock(Volume.class); this.vaultSettings = Mockito.mock(VaultSettings.class); this.environment = Mockito.mock(Environment.class); this.customMpc = new CustomMountPointChooser(vaultSettings, environment); @@ -36,7 +42,7 @@ public class CustomMountPointChooserTest { class WinfspPreperations { @Test - @DisplayName("Test MP preparation for winfsp, if only mountpoint is present") + @DisplayName("PARENT_NO_MOUNTPOINT preparations succeeds, if only mountpoint is present") public void testPrepareParentNoMountpointOnlyMountpoint(@TempDir Path tmpDir) throws IOException { //prepare var mntPoint = tmpDir.resolve("mntPoint"); @@ -58,7 +64,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test MP preparation for winfsp, if only non-empty mountpoint is present") + @DisplayName("PARENT_NO_MOUNTPOINT preparations fail, if only non-empty mountpoint is present") public void testPrepareParentNoMountpointOnlyNonEmptyMountpoint(@TempDir Path tmpDir) throws IOException { //prepare var mntPoint = tmpDir.resolve("mntPoint"); @@ -73,7 +79,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test MP preparation for Winfsp, if for any reason only hideaway dir is present") + @DisplayName("PARENT_NO_MOUNTPOINT preparation succeeds, if for any reason only hideaway dir is present") public void testPrepareParentNoMountpointOnlyHideaway(@TempDir Path tmpDir) throws IOException { //prepare var mntPoint = tmpDir.resolve("mntPoint"); @@ -93,7 +99,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test Winfsp MP preparation, if mountpoint and hideaway dirs are present") + @DisplayName("PARENT_NO_MOUNTPOINT preparation fails, if mountpoint and hideaway dirs are present") public void testPrepareParentNoMountpointMountPointAndHideaway(@TempDir Path tmpDir) throws IOException { //prepare var mntPoint = tmpDir.resolve("mntPoint"); @@ -113,7 +119,7 @@ public class CustomMountPointChooserTest { } @Test - @DisplayName("Test Winfsp MP preparation, if neither mountpoint nor hideaway dir is present") + @DisplayName("PARENT_NO_MOUNTPOINT preparation fails, if neither mountpoint nor hideaway dir is present") public void testPrepareParentNoMountpointNothing(@TempDir Path tmpDir) { //prepare var mntPoint = tmpDir.resolve("mntPoint"); @@ -127,6 +133,43 @@ public class CustomMountPointChooserTest { Assertions.assertTrue(Files.notExists(mntPoint)); } + @Test + @DisplayName("Normal Cleanup for PARENT_NO_MOUNTPOINT") + public void testCleanupSuccess(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + var hideaway = customMpc.getHideaway(mntPoint); + + Files.createDirectory(hideaway); + Mockito.when(volume.getMountPointRequirement()).thenReturn(MountPointRequirement.PARENT_NO_MOUNT_POINT); + + //execute + Assertions.assertDoesNotThrow(() -> customMpc.cleanup(volume, mntPoint)); + + //evaluate + Assertions.assertTrue(Files.exists(mntPoint)); + Assertions.assertTrue(Files.notExists(hideaway)); + + Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); + Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden")); + } + + @Test + @DisplayName("On IOException cleanup for PARENT_NO_MOUNTPOINT exits normally") + public void testCleanupIOFailure(@TempDir Path tmpDir) throws IOException { + //prepare + var mntPoint = tmpDir.resolve("mntPoint"); + var hideaway = customMpc.getHideaway(mntPoint); + + Files.createDirectory(hideaway); + Mockito.when(volume.getMountPointRequirement()).thenReturn(MountPointRequirement.PARENT_NO_MOUNT_POINT); + try (MockedStatic filesMock = Mockito.mockStatic(Files.class)) { + filesMock.when(() -> Files.move(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new IOException("error")); + //execute + Assertions.assertDoesNotThrow(() -> customMpc.cleanup(volume, mntPoint)); + } + } + } From f148973bef18bbdf64f80ff958ad74cec80ace67 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 14:50:26 +0100 Subject: [PATCH 042/141] fix wrong visibillity --- .../common/mountpoint/CustomMountPointChooserTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2126b16b6..2ee969363 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -39,7 +39,7 @@ public class CustomMountPointChooserTest { } @Nested - class WinfspPreperations { + public class WinfspPreperations { @Test @DisplayName("PARENT_NO_MOUNTPOINT preparations succeeds, if only mountpoint is present") From fba0df10f9503f0460aa0311d09de6710f13b9f9 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 15:28:01 +0100 Subject: [PATCH 043/141] Resolve code smells and a bug Co-authored-by: sonarcloud --- .../mountpoint/CustomMountPointChooser.java | 51 ++++++++++--------- .../vaultoptions/MountOptionsController.java | 4 +- .../CustomMountPointChooserTest.java | 18 +++++-- 3 files changed, 42 insertions(+), 31 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 82898d59e..d174e5dc6 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import java.io.IOException; import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; @@ -25,6 +24,7 @@ class CustomMountPointChooser implements MountPointChooser { private static final String HIDEAWAY_PREFIX = ".~$"; private static final String HIDEAWAY_SUFFIX = ".tmp"; + private static final String WIN_HIDDEN = "dos:hidden"; private static final Logger LOG = LoggerFactory.getLogger(CustomMountPointChooser.class); private final VaultSettings vaultSettings; @@ -79,34 +79,27 @@ class CustomMountPointChooser implements MountPointChooser { var mpExists = Files.exists(mountPoint); var hideExists = Files.exists(hideaway); - //both resources exist (whatever type) //TODO: possible improvement by just deleting an _empty_ hideaway - if (mpExists && hideExists) { + if (mpExists && hideExists) { //both resources exist (whatever type) throw new InvalidMountPointException(new FileAlreadyExistsException(hideaway.toString())); } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString())); } else if (!mpExists) { //only hideaway exists - - if (!Files.isDirectory(hideaway)) { - throw new InvalidMountPointException(new NotDirectoryException(hideaway.toString())); - } + isDirectory(hideaway); LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { - Files.setAttribute(hideaway, "dos:hidden", true); + Files.setAttribute(hideaway, WIN_HIDDEN, true); } catch (IOException e) { throw new InvalidMountPointException(e); } - } else { - if (!Files.isDirectory(mountPoint)) { - throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); - } + } else { //only mountpoint exists try { - if (Files.list(mountPoint).findFirst().isPresent()) { - throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); - } + isDirectory(mountPoint); + isEmpty(mountPoint); + Files.move(mountPoint, hideaway); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(hideaway, "dos:hidden", true); + Files.setAttribute(hideaway, WIN_HIDDEN, true); } } catch (IOException e) { throw new InvalidMountPointException(e); @@ -116,13 +109,9 @@ class CustomMountPointChooser implements MountPointChooser { private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException { //This is the case for Windows when using Dokany and for Linux and Mac - if (!Files.isDirectory(mountPoint)) { - throw new InvalidMountPointException(new NotDirectoryException(mountPoint.toString())); - } - try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) { - if (ds.iterator().hasNext()) { - throw new InvalidMountPointException(new DirectoryNotEmptyException(mountPoint.toString())); - } + isDirectory(mountPoint); + try { + isEmpty(mountPoint); } catch (IOException exception) { throw new InvalidMountPointException("IOException while checking folder content", exception); } @@ -135,7 +124,7 @@ class CustomMountPointChooser implements MountPointChooser { try { Files.move(hideaway, mountPoint); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(mountPoint, "dos:hidden", false); + Files.setAttribute(mountPoint, WIN_HIDDEN, false); } } catch (IOException e) { LOG.error("Unable to clean up mountpoint {} for Winfsp mounting.", mountPoint, e); @@ -143,6 +132,20 @@ class CustomMountPointChooser implements MountPointChooser { } } + private void isDirectory(Path toCheck) throws InvalidMountPointException { + if (!Files.isDirectory(toCheck)) { + throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString())); + } + } + + private void isEmpty(Path toCheck) throws InvalidMountPointException, IOException { + try (var dirStream = Files.list(toCheck)) { + if (dirStream.findFirst().isPresent()) { + throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString())); + } + } + } + //visible for testing Path getHideaway(Path mountPoint) { return mountPoint.resolveSibling(HIDEAWAY_PREFIX + mountPoint.getFileName().toString() + HIDEAWAY_SUFFIX); diff --git a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java index bce186511..3be38568d 100644 --- a/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java +++ b/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java @@ -183,7 +183,7 @@ public class MountOptionsController implements FxController { } public boolean isReadOnlySupported() { - return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()) ; + return !(usedVolumeImpl == VolumeImpl.FUSE && isOsWindows()); } public StringProperty customMountPathProperty() { @@ -191,7 +191,7 @@ public class MountOptionsController implements FxController { } public boolean isCustomMountOptionsSupported() { - return !(usedVolumeImpl == VolumeImpl.WEBDAV); + return usedVolumeImpl != VolumeImpl.WEBDAV; } public String getCustomMountPath() { diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2ee969363..2954c9355 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -1,6 +1,5 @@ package org.cryptomator.common.mountpoint; -import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.Environment; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.MountPointRequirement; @@ -41,6 +40,19 @@ public class CustomMountPointChooserTest { @Nested public class WinfspPreperations { + @Test + @DisplayName("Hideaway name for PARENT_NO_MOUNTPOINT is not the same as mountpoint") + public void testGetHideaway() { + //prepare + Path mntPoint = Path.of("/foo/bar"); + //execute + var hideaway = customMpc.getHideaway(mntPoint); + //eval + Assertions.assertNotEquals(hideaway.getFileName(), mntPoint.getFileName()); + Assertions.assertEquals(hideaway.getParent(), mntPoint.getParent()); + Assertions.assertTrue(hideaway.getFileName().toString().contains(mntPoint.getFileName().toString())); + } + @Test @DisplayName("PARENT_NO_MOUNTPOINT preparations succeeds, if only mountpoint is present") public void testPrepareParentNoMountpointOnlyMountpoint(@TempDir Path tmpDir) throws IOException { @@ -56,8 +68,6 @@ public class CustomMountPointChooserTest { Path hideaway = customMpc.getHideaway(mntPoint); Assertions.assertTrue(Files.exists(hideaway)); - Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); - Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); @@ -91,8 +101,6 @@ public class CustomMountPointChooserTest { //evaluate Assertions.assertTrue(Files.exists(hideaway)); - Assertions.assertNotEquals(hideaway.getFileName().toString(), "mntPoint"); - Assertions.assertTrue(hideaway.getFileName().toString().contains("mntPoint")); Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); From 14dc026354730ede3866e2f167166d0e1bd63c09 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 2 Mar 2022 17:24:54 +0100 Subject: [PATCH 044/141] Cleanup Co-authored-by: Sebastian Stenzel --- .../mountpoint/CustomMountPointChooser.java | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index d174e5dc6..9cfc8652f 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -14,6 +14,7 @@ import java.io.IOException; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; import java.nio.file.NotDirectoryException; import java.nio.file.Path; @@ -49,16 +50,16 @@ class CustomMountPointChooser implements MountPointChooser { @Override public boolean prepare(Volume caller, Path mountPoint) throws InvalidMountPointException { - switch (caller.getMountPointRequirement()) { + return switch (caller.getMountPointRequirement()) { case PARENT_NO_MOUNT_POINT -> { prepareParentNoMountPoint(mountPoint); LOG.debug("Successfully checked custom mount point: {}", mountPoint); - return true; + yield true; } case EMPTY_MOUNT_POINT -> { prepareEmptyMountPoint(mountPoint); LOG.debug("Successfully checked custom mount point: {}", mountPoint); - return false; + yield false; } case NONE -> { //Requirement "NONE" doesn't make any sense here. @@ -69,15 +70,15 @@ class CustomMountPointChooser implements MountPointChooser { //Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT" throw new InvalidMountPointException(new IllegalStateException("Not implemented")); } - } + }; } - //This the case on Windows when using FUSE + //This is case on Windows when using FUSE //See https://github.com/billziss-gh/winfsp/issues/320 void prepareParentNoMountPoint(Path mountPoint) throws InvalidMountPointException { Path hideaway = getHideaway(mountPoint); - var mpExists = Files.exists(mountPoint); - var hideExists = Files.exists(hideaway); + var mpExists = Files.exists(mountPoint, LinkOption.NOFOLLOW_LINKS); + var hideExists = Files.exists(hideaway, LinkOption.NOFOLLOW_LINKS); //TODO: possible improvement by just deleting an _empty_ hideaway if (mpExists && hideExists) { //both resources exist (whatever type) @@ -85,21 +86,23 @@ class CustomMountPointChooser implements MountPointChooser { } else if (!mpExists && !hideExists) { //neither mountpoint nor hideaway exist throw new InvalidMountPointException(new NoSuchFileException(mountPoint.toString())); } else if (!mpExists) { //only hideaway exists - isDirectory(hideaway); + checkIsDirectory(hideaway); LOG.info("Mountpoint {} for winfsp mount seems to be not properly cleaned up. Will be fixed on unmount.", mountPoint); try { - Files.setAttribute(hideaway, WIN_HIDDEN, true); + if (SystemUtils.IS_OS_WINDOWS) { + Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS); + } } catch (IOException e) { throw new InvalidMountPointException(e); } } else { //only mountpoint exists try { - isDirectory(mountPoint); - isEmpty(mountPoint); + checkIsDirectory(mountPoint); + checkIsEmpty(mountPoint); Files.move(mountPoint, hideaway); if (SystemUtils.IS_OS_WINDOWS) { - Files.setAttribute(hideaway, WIN_HIDDEN, true); + Files.setAttribute(hideaway, WIN_HIDDEN, true, LinkOption.NOFOLLOW_LINKS); } } catch (IOException e) { throw new InvalidMountPointException(e); @@ -109,9 +112,9 @@ class CustomMountPointChooser implements MountPointChooser { private void prepareEmptyMountPoint(Path mountPoint) throws InvalidMountPointException { //This is the case for Windows when using Dokany and for Linux and Mac - isDirectory(mountPoint); + checkIsDirectory(mountPoint); try { - isEmpty(mountPoint); + checkIsEmpty(mountPoint); } catch (IOException exception) { throw new InvalidMountPointException("IOException while checking folder content", exception); } @@ -132,13 +135,13 @@ class CustomMountPointChooser implements MountPointChooser { } } - private void isDirectory(Path toCheck) throws InvalidMountPointException { - if (!Files.isDirectory(toCheck)) { + private void checkIsDirectory(Path toCheck) throws InvalidMountPointException { + if (!Files.isDirectory(toCheck, LinkOption.NOFOLLOW_LINKS)) { throw new InvalidMountPointException(new NotDirectoryException(toCheck.toString())); } } - private void isEmpty(Path toCheck) throws InvalidMountPointException, IOException { + private void checkIsEmpty(Path toCheck) throws InvalidMountPointException, IOException { try (var dirStream = Files.list(toCheck)) { if (dirStream.findFirst().isPresent()) { throw new InvalidMountPointException(new DirectoryNotEmptyException(toCheck.toString())); From 0c6f4297a27364a910404b2b125b019ca3cbf8fa Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 3 Mar 2022 10:40:40 +0100 Subject: [PATCH 045/141] display version as `x.y.z deb-1234` instead of `x.y.z ppa-1234` --- dist/linux/debian/rules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/debian/rules b/dist/linux/debian/rules index d3119fb5b..e4f824394 100755 --- a/dist/linux/debian/rules +++ b/dist/linux/debian/rules @@ -42,7 +42,7 @@ override_dh_auto_build: --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \ --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \ --java-options "-Dcryptomator.showTrayIcon=false" \ - --java-options "-Dcryptomator.buildNumber=\"ppa-${REVISION_NUM}\"" \ + --java-options "-Dcryptomator.buildNumber=\"deb-${REVISION_NUM}\"" \ --java-options "-Dcryptomator.appVersion=\"${SEMVER_STR}\"" \ --app-version "${VERSION_NUM}.${REVISION_NUM}" \ --resource-dir resources \ From 962b4f28afcbf14a0f0c17caa61f7af646f7ebf9 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 3 Mar 2022 11:59:59 +0100 Subject: [PATCH 046/141] simplify dput using globs --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17934113e..50a7030bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -277,9 +277,7 @@ jobs: name: linux-deb-package path: . - name: dput to beta repo - run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_${PPA_VERSION}_source.changes - env: - PPA_VERSION: ${{ needs.metadata.outputs.ppaVerStr }}-0ppa1 + run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes # # Linux Cryptomator.AppImage From 3f596b385361c62ac1f67379ae0a50eb613d1b6a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 3 Mar 2022 12:45:12 +0100 Subject: [PATCH 047/141] attempt to fix dput --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50a7030bd..f98d9b60c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -256,6 +256,7 @@ jobs: cryptomator_*.dsc cryptomator_*.orig.tar.xz cryptomator_*.debian.tar.xz + cryptomator_*_source.buildinfo cryptomator_*_source.changes cryptomator_*_amd64.deb @@ -271,6 +272,10 @@ jobs: run: | sudo apt-get update sudo apt-get install dput + - name: import public key + run: curl -sSL ${GPG_PUBLIC_KEY_URL} | gpg --import - + env: + GPG_PUBLIC_KEY_URL: https://gist.githubusercontent.com/cryptobot/211111cf092037490275f39d408f461a/raw/E6E6A235.asc - name: download linux-deb-package uses: actions/download-artifact@v2 with: From 9b001b507155fddea7494409064e3998f8f9d77e Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Thu, 3 Mar 2022 17:45:21 +0100 Subject: [PATCH 048/141] Co-authored-by: Sebastian Stenzel --- .../mountpoint/CustomMountPointChooser.java | 8 +------ .../CustomMountPointChooserTest.java | 21 +++++++++++-------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java index 9cfc8652f..80c5b067b 100644 --- a/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java +++ b/src/main/java/org/cryptomator/common/mountpoint/CustomMountPointChooser.java @@ -61,15 +61,9 @@ class CustomMountPointChooser implements MountPointChooser { LOG.debug("Successfully checked custom mount point: {}", mountPoint); yield false; } - case NONE -> { - //Requirement "NONE" doesn't make any sense here. - //No need to prepare/verify a Mountpoint without requiring one... + case NONE, UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT -> { throw new InvalidMountPointException(new IllegalStateException("Illegal MountPointRequirement")); } - default -> { - //Currently the case for "UNUSED_ROOT_DIR, PARENT_OPT_MOUNT_POINT" - throw new InvalidMountPointException(new IllegalStateException("Not implemented")); - } }; } diff --git a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java index 2954c9355..da2e0fde0 100644 --- a/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java +++ b/src/test/java/org/cryptomator/common/mountpoint/CustomMountPointChooserTest.java @@ -5,7 +5,6 @@ import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.Volume; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -69,8 +68,9 @@ public class CustomMountPointChooserTest { Path hideaway = customMpc.getHideaway(mntPoint); Assertions.assertTrue(Files.exists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -102,8 +102,9 @@ public class CustomMountPointChooserTest { //evaluate Assertions.assertTrue(Files.exists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertTrue((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -122,8 +123,9 @@ public class CustomMountPointChooserTest { Assertions.assertTrue(Files.exists(hideaway)); Assertions.assertTrue(Files.exists(mntPoint)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertFalse((Boolean) Files.getAttribute(hideaway, "dos:hidden")); + } } @Test @@ -158,8 +160,9 @@ public class CustomMountPointChooserTest { Assertions.assertTrue(Files.exists(mntPoint)); Assertions.assertTrue(Files.notExists(hideaway)); - Assumptions.assumeTrue(OS.WINDOWS.isCurrentOs()); - Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden")); + if(OS.WINDOWS.isCurrentOs()) { + Assertions.assertFalse((Boolean) Files.getAttribute(mntPoint, "dos:hidden")); + } } @Test From d9af387a69502de65ca60e5508c29bb9e67e37bd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 7 Mar 2022 08:46:46 +0100 Subject: [PATCH 049/141] updated public key url [ci skip] --- .github/workflows/release.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5a241da4..a24342da3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -273,9 +273,7 @@ jobs: sudo apt-get update sudo apt-get install dput - name: import public key - run: curl -sSL ${GPG_PUBLIC_KEY_URL} | gpg --import - - env: - GPG_PUBLIC_KEY_URL: https://gist.githubusercontent.com/cryptobot/211111cf092037490275f39d408f461a/raw/E6E6A235.asc + run: curl -sSL https://github.com/cryptobot.gpg | gpg --import - - name: download linux-deb-package uses: actions/download-artifact@v2 with: From a32f5bb6d90b163a130d025546657d35eca26d47 Mon Sep 17 00:00:00 2001 From: Ralph Plawetzki Date: Mon, 7 Mar 2022 11:25:40 +0100 Subject: [PATCH 050/141] Use updated Cryptomator screenshots (1.6.5) They do appstream-util validate --- dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml index 1c3ae9c27..bdd0e178e 100644 --- a/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml +++ b/dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml @@ -44,11 +44,11 @@ Light theme - https://cryptomator.org/presskit/linux-screenshot-1.png + https://user-images.githubusercontent.com/11858409/156986109-6e58f59c-8b8c-4501-b33b-bb1e33007cea.png Dark theme - https://cryptomator.org/presskit/linux-screenshot-2.png + https://user-images.githubusercontent.com/11858409/156986113-6c5d7801-86e0-4643-bc2f-aff9d95d3ce0.png From 79e6a4cd488c4136229c533dd002c53141442f41 Mon Sep 17 00:00:00 2001 From: Kevin St-Sauveur Date: Mon, 14 Mar 2022 02:29:25 -0400 Subject: [PATCH 051/141] Modify vault title when unlocked --- .../java/org/cryptomator/ui/traymenu/TrayMenuController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java index 4ce3808c4..66be4c652 100644 --- a/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java +++ b/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java @@ -91,6 +91,8 @@ class TrayMenuController { unlockItem.addActionListener(createActionListenerForVault(vault, this::unlockVault)); submenu.add(unlockItem); } else if (vault.isUnlocked()) { + submenu.setLabel("*".concat(submenu.getLabel())); + MenuItem lockItem = new MenuItem(resourceBundle.getString("traymenu.vault.lock")); lockItem.addActionListener(createActionListenerForVault(vault, this::lockVault)); submenu.add(lockItem); From c29cc9ab85f582c458ce981559d3b6f7bfd82803 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 16 Mar 2022 15:08:09 +0100 Subject: [PATCH 052/141] supress false positive in dependency-check plugin --- pom.xml | 5 ++++- suppression.xml | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8975b8693..1cfd73ac7 100644 --- a/pom.xml +++ b/pom.xml @@ -52,6 +52,9 @@ 5.8.1 3.12.4 2.2 + + + 7.0.0 @@ -263,7 +266,7 @@ org.owasp dependency-check-maven - 6.3.1 + ${dependency-check.version} diff --git a/suppression.xml b/suppression.xml index c747f92a7..ccd1a1cdf 100644 --- a/suppression.xml +++ b/suppression.xml @@ -25,4 +25,23 @@ org.eclipse.jetty.toolchain:jetty-servlet-api:4.0.6 .* + + + + ^org\.cryptomator:.*$ + cpe:/a:cryptomator:cryptomator + CVE-2022-25366 + + + + + ^commons\-cli:commons\-cli:.*$ + cpe:/a:apache:james + + cpe:/a:spirit-project:spirit + \ No newline at end of file From f231c25dfc471ebde04d4697a7bbf38fdb3ceafd Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 16 Mar 2022 15:08:46 +0100 Subject: [PATCH 053/141] Update maven plugin dependencies --- pom.xml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 1cfd73ac7..469ab6398 100644 --- a/pom.xml +++ b/pom.xml @@ -55,6 +55,7 @@ 7.0.0 + 0.8.7 @@ -231,7 +232,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.8.1 + 3.10.1 org.apache.maven.plugins @@ -241,7 +242,7 @@ org.apache.maven.plugins maven-dependency-plugin - 3.2.0 + 3.3.0 org.apache.maven.plugins @@ -256,12 +257,12 @@ org.apache.maven.plugins maven-jar-plugin - 3.2.0 + 3.2.2 org.jacoco jacoco-maven-plugin - 0.8.7 + ${jacoco.version} org.owasp From d3e92395316b5f3a4d83ab27ab54e510e7b64684 Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 16 Mar 2022 15:24:55 +0100 Subject: [PATCH 054/141] simplify name of test file for location check on vault creation --- .../ui/addvaultwizard/CreateNewVaultLocationController.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index 35f2be069..1fd463432 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -35,14 +35,13 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; -import java.util.UUID; @AddVaultWizardScoped public class CreateNewVaultLocationController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultLocationController.class); private static final Path DEFAULT_CUSTOM_VAULT_PATH = Paths.get(System.getProperty("user.home")); - private static final String TEMP_FILE_FORMAT = "cryptomator-%s.tmp"; + private static final String TEMP_FILE_FORMAT = ".locationTest.cryptomator.tmp"; private final Stage window; private final Lazy chooseNameScene; @@ -112,7 +111,7 @@ public class CreateNewVaultLocationController implements FxController { } private boolean isActuallyWritable(Path p) { - Path tmpFile = p.resolve(String.format(TEMP_FILE_FORMAT, UUID.randomUUID())); + Path tmpFile = p.resolve(TEMP_FILE_FORMAT); try (var chan = Files.newByteChannel(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE, StandardOpenOption.DELETE_ON_CLOSE)) { return true; } catch (IOException e) { From 6c176d5484d7844cabad147ea991387521b38789 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 10:31:45 +0100 Subject: [PATCH 055/141] separate workflow for building .AppImage --- .github/workflows/appimage.yml | 149 +++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/appimage.yml diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml new file mode 100644 index 000000000..c74650745 --- /dev/null +++ b/.github/workflows/appimage.yml @@ -0,0 +1,149 @@ +name: Build AppImage + +on: + release: + types: [published] + workflow_dispatch: + inputs: + semver: + description: 'SemVer' + required: true + default: '0.99.99-SNAPSHOT' + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Run Maven Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + else + SEM_VER_STR=${{ github.event.inputs.semver }} + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + mvn versions:set -DnewVersion=${SEM_VER_STR} + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Build and Test + run: mvn -B clean package -Pdependency-check,linux + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/linux/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" + --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" + --java-options "-Dcryptomator.showTrayIcon=false" + --java-options "-Dcryptomator.buildNumber=\"appimage-${{ steps.versions.outputs.revNum }}\"" + --resource-dir dist/linux/resources + - name: Patch Cryptomator.AppDir + run: | + mv appdir/Cryptomator Cryptomator.AppDir + cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ + envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh + cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png + cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg + cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml + cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop + cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg + ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon + ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/Cryptomator.desktop + ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun + env: + REVISION_NO: ${{ steps.versions.outputs.revNum }} + SEMVER_STR: ${{ steps.versions.outputs.semVerStr }} + - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 + run: | + JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` + ${JAVA_HOME}/bin/jar -xf lib/app/${JFFI_NATIVE_JAR} /jni/x86_64-Linux/ + mv jni/x86_64-Linux/* lib/app/libjffi.so + working-directory: Cryptomator.AppDir + - name: Download AppImageKit + run: | + curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage + chmod +x appimagetool.AppImage + ./appimagetool.AppImage --appimage-extract + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Build AppImage + run: > + ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ steps.versions.outputs.semVerStr }}-x86_64.AppImage + -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' + --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" + - name: Upload AppImage + uses: actions/upload-artifact@v2 + with: + name: linux-appimage + path: | + cryptomator-*.AppImage + cryptomator-*.AppImage.zsync + if-no-files-found: error + - name: Publish AppImage on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.AppImage + *.zsync + *.asc \ No newline at end of file From ba037007a47b837b099492369c3edd157077ecfa Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 10:34:41 +0100 Subject: [PATCH 056/141] run workflow on push (otherwise it won't show up) --- .github/workflows/appimage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index c74650745..8da9b2da1 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -1,6 +1,7 @@ name: Build AppImage on: + push: # TODO remove before merging into develop release: types: [published] workflow_dispatch: From 0f5a358c42f2065c577f986f72f1049d6d59a7ac Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 10:37:29 +0100 Subject: [PATCH 057/141] read version from pom.xml (except for tagged commits) --- .github/workflows/appimage.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 8da9b2da1..6bde439c8 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -5,18 +5,13 @@ on: release: types: [published] workflow_dispatch: - inputs: - semver: - description: 'SemVer' - required: true - default: '0.99.99-SNAPSHOT' env: JAVA_VERSION: 17 jobs: build: - name: Run Maven Build + name: Build AppImage runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -32,12 +27,12 @@ jobs: run: | if [[ $GITHUB_REF == refs/tags/* ]]; then SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} else - SEM_VER_STR=${{ github.event.inputs.semver }} + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` fi SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` REVCOUNT=`git rev-list --count HEAD` - mvn versions:set -DnewVersion=${SEM_VER_STR} echo "::set-output name=semVerStr::${SEM_VER_STR}" echo "::set-output name=semVerNum::${SEM_VER_NUM}" echo "::set-output name=revNum::${REVCOUNT}" From e078869f33b482462ad237d2c7baac090b0d7536 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 10:51:09 +0100 Subject: [PATCH 058/141] Create .asc signatures --- .github/workflows/appimage.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 6bde439c8..dac5bbf99 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -39,14 +39,14 @@ jobs: - uses: skymatic/semver-validation-action@v1 with: version: ${{ steps.versions.outputs.semVerStr }} - - name: Build and Test - run: mvn -B clean package -Pdependency-check,linux + - name: Run maven + run: mvn -B clean package -Pdependency-check,linux -DskipTests - name: Patch target dir run: | cp LICENSE.txt target cp dist/linux/launcher.sh target cp target/cryptomator-*.jar target/mods - - name: jlink + - name: Run jlink run: > ${JAVA_HOME}/bin/jlink --verbose @@ -57,7 +57,7 @@ jobs: --no-man-pages --strip-debug --compress=1 - - name: jpackage + - name: Run jpackage run: > ${JAVA_HOME}/bin/jpackage --verbose @@ -125,13 +125,18 @@ jobs: ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ steps.versions.outputs.semVerStr }}-x86_64.AppImage -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" + - name: Create detached GPG signatures + run: | + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync - name: Upload AppImage uses: actions/upload-artifact@v2 with: - name: linux-appimage + name: appimage path: | cryptomator-*.AppImage cryptomator-*.AppImage.zsync + cryptomator-*.asc if-no-files-found: error - name: Publish AppImage on GitHub Releases if: startsWith(github.ref, 'refs/tags/') From 47a206cf252282f3814c67d42723e2f6f7217992 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 11:16:46 +0100 Subject: [PATCH 059/141] separate workflow for building .deb --- .github/workflows/appimage.yml | 3 +- .github/workflows/debian.yml | 111 +++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/debian.yml diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index dac5bbf99..3e3e33e34 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -1,7 +1,6 @@ name: Build AppImage on: - push: # TODO remove before merging into develop release: types: [published] workflow_dispatch: @@ -129,7 +128,7 @@ jobs: run: | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync - - name: Upload AppImage + - name: Upload artifacts uses: actions/upload-artifact@v2 with: name: appimage diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml new file mode 100644 index 000000000..3d1cfcdc2 --- /dev/null +++ b/.github/workflows/debian.yml @@ -0,0 +1,111 @@ +name: Build Debian Package + +on: + push: # TODO remove before merging into develop + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Build Debian Package + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install debhelper devscripts dput + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" + - uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,linux -DskipTests + - name: create orig.tar.gz with common/ libs/ mods/ + run: | + mkdir pkgdir + cp -r target/libs pkgdir + cp -r target/mods pkgdir + cp -r dist/linux/common/ pkgdir + cp target/cryptomator-*.jar pkgdir/mods + tar -cJf cryptomator_${{ steps.versions.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . + - name: Patch and rename pkgdir + run: | + cp -r dist/linux/debian/ pkgdir + export RFC2822_TIMESTAMP=`date --rfc-2822` + envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules + envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog + find . -name "*.jar" >> pkgdir/debian/source/include-binaries + mv pkgdir cryptomator_${{ steps.versions.outputs.ppaVerStr }} + env: + SEMVER_STR: ${{ steps.versions.outputs.semVerStr }} + VERSION_NUM: ${{ steps.versions.outputs.semVerNum }} + REVISION_NUM: ${{ steps.versions.outputs.revNum }} + PPA_VERSION: ${{ steps.versions.outputs.ppaVerStr }}-0ppa1 + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: debuild + run: | + debuild -S -sa -d + debuild -b -sa -d + env: + DEBSIGN_PROGRAM: gpg --batch --pinentry-mode loopback + DEBSIGN_KEYID: 615D449FE6E6A235 + working-directory: cryptomator_${{ steps.versions.outputs.ppaVerStr }} + - name: Create detached GPG signatures + run: | + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: linux-deb-package + path: | + cryptomator_*.dsc + cryptomator_*.orig.tar.xz + cryptomator_*.debian.tar.xz + cryptomator_*_source.buildinfo + cryptomator_*_source.changes + cryptomator_*_amd64.deb + cryptomator_*.asc + - name: Run dput to beta repo + # if: startsWith(github.ref, 'refs/tags/') + run: dput --check-only ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes + - name: Publish AppImage on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + cryptomator_*_amd64.deb + cryptomator_*.asc \ No newline at end of file From 184b7799ea464c5b2b19e55669cf35776b7b8e00 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 11:21:21 +0100 Subject: [PATCH 060/141] use README.md for dry-running gpg --- .github/workflows/appimage.yml | 8 +++++--- .github/workflows/debian.yml | 10 ++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 3e3e33e34..ce0739e8b 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -16,7 +16,8 @@ jobs: - uses: actions/checkout@v2 with: fetch-depth: 0 - - uses: actions/setup-java@v2 + - name: Setup Java + uses: actions/setup-java@v2 with: distribution: 'temurin' java-version: ${{ env.JAVA_VERSION }} @@ -35,7 +36,8 @@ jobs: echo "::set-output name=semVerStr::${SEM_VER_STR}" echo "::set-output name=semVerNum::${SEM_VER_NUM}" echo "::set-output name=revNum::${REVCOUNT}" - - uses: skymatic/semver-validation-action@v1 + - name: Validate Version + uses: skymatic/semver-validation-action@v1 with: version: ${{ steps.versions.outputs.semVerStr }} - name: Run maven @@ -115,7 +117,7 @@ jobs: - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 run: | echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md env: GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 3d1cfcdc2..697d93512 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -21,7 +21,8 @@ jobs: run: | sudo apt-get update sudo apt-get install debhelper devscripts dput - - uses: actions/setup-java@v2 + - name: Setup Java + uses: actions/setup-java@v2 with: distribution: 'temurin' java-version: ${{ env.JAVA_VERSION }} @@ -41,12 +42,13 @@ jobs: echo "::set-output name=semVerNum::${SEM_VER_NUM}" echo "::set-output name=revNum::${REVCOUNT}" echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" - - uses: skymatic/semver-validation-action@v1 + - name: Validate Version + uses: skymatic/semver-validation-action@v1 with: version: ${{ steps.versions.outputs.semVerStr }} - name: Run maven run: mvn -B clean package -Pdependency-check,linux -DskipTests - - name: create orig.tar.gz with common/ libs/ mods/ + - name: Create orig.tar.gz with common/ libs/ mods/ run: | mkdir pkgdir cp -r target/libs pkgdir @@ -70,7 +72,7 @@ jobs: - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 run: | echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md env: GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} From f43b033ac1fb3b46ef6dff78b2d63f846fb7dc22 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 12:02:41 +0100 Subject: [PATCH 061/141] remove debug options [ci skip] --- .github/workflows/debian.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 697d93512..86a90723d 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -1,7 +1,6 @@ name: Build Debian Package on: - push: # TODO remove before merging into develop release: types: [published] workflow_dispatch: @@ -100,9 +99,9 @@ jobs: cryptomator_*_amd64.deb cryptomator_*.asc - name: Run dput to beta repo - # if: startsWith(github.ref, 'refs/tags/') - run: dput --check-only ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - - name: Publish AppImage on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes + - name: Publish Debian package on GitHub Releases if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v1 with: From 709d211928cf9452e56931facb4fd28a84324250 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 12:20:43 +0100 Subject: [PATCH 062/141] separate workflow for building .dmg --- .github/workflows/mac-dmg.yml | 236 ++++++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 .github/workflows/mac-dmg.yml diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml new file mode 100644 index 000000000..c43f9aad7 --- /dev/null +++ b/.github/workflows/mac-dmg.yml @@ -0,0 +1,236 @@ +name: Build macOS .dmg + +on: + push: # TODO remove before merging into develop + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + +jobs: + build: + name: Build Cryptomator.app + runs-on: macos-11 + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,linux -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/linux/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --strip-native-commands + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Run jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dapple.awt.enableTemplateImages=true" + --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" + --java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"dmg-${{ steps.versions.outputs.revNum }}\"" + --mac-package-identifier org.cryptomator + --resource-dir dist/mac/resources + - name: Patch Cryptomator.app + run: | + mv appdir/Cryptomator.app Cryptomator.app + mv dist/mac/resources/Cryptomator-Vault.icns Cryptomator.app/Contents/Resources/ + sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist + sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist + env: + VERSION_NO: ${{ steps.versions.outputs.semVerNum }} + REVISION_NO: ${{ steps.versions.outputs.revNum }} + - name: Install codesign certificate + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$CODESIGN_P12_BASE64" | base64 --decode --output $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + security set-keychain-settings -lut 900 $KEYCHAIN_PATH + security unlock-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$CODESIGN_P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + env: + CODESIGN_P12_BASE64: ${{ secrets.MACOS_CODESIGN_P12_BASE64 }} + CODESIGN_P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} + CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }} + - name: Codesign + run: | + find Cryptomator.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do + if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then + JAR_FILENAME=$(basename ${JAR_PATH}) + OUTPUT_PATH=${JAR_PATH%.*} + echo "Codesigning libs in ${JAR_FILENAME}..." + unzip -q ${JAR_PATH} -d ${OUTPUT_PATH} + find ${OUTPUT_PATH} -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + find ${OUTPUT_PATH} -name '*.jnilib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; + rm ${JAR_PATH} + pushd ${OUTPUT_PATH} > /dev/null + zip -qr ../${JAR_FILENAME} * + popd > /dev/null + rm -r ${OUTPUT_PATH} + fi + done + echo "Codesigning Cryptomator.app..." + codesign --force --deep --entitlements dist/mac/Cryptomator.entitlements -o runtime -s ${CODESIGN_IDENTITY} Cryptomator.app + env: + CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} + - name: Prepare .dmg contents + run: | + mkdir dmg + mv Cryptomator.app dmg + cp dist/mac/dmg/resources/macFUSE.webloc dmg + ls -l dmg + - name: Install create-dmg + run: | + brew install create-dmg + create-dmg --help + - name: Create .dmg + run: > + create-dmg + --volname Cryptomator + --volicon "dist/mac/dmg/resources/Cryptomator-Volume.icns" + --background "dist/mac/dmg/resources/Cryptomator-background.tiff" + --window-pos 400 100 + --window-size 640 694 + --icon-size 128 + --icon "Cryptomator.app" 128 245 + --hide-extension "Cryptomator.app" + --icon "macFUSE.webloc" 320 501 + --hide-extension "macFUSE.webloc" + --app-drop-link 512 245 + --eula "dist/mac/dmg/resources/license.rtf" + --icon ".background" 128 758 + --icon ".fseventsd" 320 758 + --icon ".VolumeIcon.icns" 512 758 + Cryptomator-${VERSION_NO}.dmg dmg + env: + VERSION_NO: ${{ steps.versions.outputs.semVerNum }} + - name: Install notarization credentials + if: startsWith(github.ref, 'refs/tags/') + run: | + # create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + security create-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} + security unlock-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} + + # import credentials from secrets + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool store-credentials "${NOTARIZATION_KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_APPLE_ID}" --password "${NOTARIZATION_PW}" --team-id "${NOTARIZATION_TEAM_ID}" --keychain "${KEYCHAIN_PATH}" + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} + NOTARIZATION_PW: ${{ secrets.MACOS_NOTARIZATION_PW }} + NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} + NOTARIZATION_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_NOTARIZATION_TMP_KEYCHAIN_PW }} + - name: Notarize .dmg + if: startsWith(github.ref, 'refs/tags/') + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db + sudo xcode-select -s /Applications/Xcode_13.0.app + xcrun notarytool submit Cryptomator-*.dmg --keychain-profile "${NOTARIZATION_KEYCHAIN_PROFILE}" --keychain "${KEYCHAIN_PATH}" --wait + xcrun stapler staple Cryptomator-*.dmg + env: + NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} + - name: Add possible alpha/beta tags to installer name + run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg + - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Create detached GPG signatures + run: | + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.dmg + - name: Clean up codesign certificate + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db + continue-on-error: true + - name: Clean up notarization credentials + if: ${{ always() }} + run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db + continue-on-error: true + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: dmg + path: Cryptomator-*.dmg + if-no-files-found: error + - name: Publish dmg on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.dmg + *.asc + + From c8651d93752bfb3238df7349049c68ff7ff8f733 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 12:29:45 +0100 Subject: [PATCH 063/141] only use three version numbers for mac apps --- .github/workflows/mac-dmg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index c43f9aad7..a35004430 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -73,7 +73,7 @@ jobs: --name Cryptomator --vendor "Skymatic GmbH" --copyright "(C) 2016 - 2022 Skymatic GmbH" - --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --app-version "${{ steps.versions.outputs.semVerNum }}" --java-options "-Xss5m" --java-options "-Xmx256m" --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" From be448128960adfe811528a05118a71058400be38 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 12:36:13 +0100 Subject: [PATCH 064/141] fix copy-paste errors --- .github/workflows/mac-dmg.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index a35004430..ab643c4f9 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -42,11 +42,11 @@ jobs: with: version: ${{ steps.versions.outputs.semVerStr }} - name: Run maven - run: mvn -B clean package -Pdependency-check,linux -DskipTests + run: mvn -B clean package -Pdependency-check,mac -DskipTests - name: Patch target dir run: | cp LICENSE.txt target - cp dist/linux/launcher.sh target + cp dist/mac/launcher.sh target cp target/cryptomator-*.jar target/mods - name: Run jlink run: > @@ -208,7 +208,7 @@ jobs: GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - name: Create detached GPG signatures run: | - gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.dmg + gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.dmg - name: Clean up codesign certificate if: ${{ always() }} run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db From c7f64f49742a7b539e3c65fe7430ddfd8a161c30 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 12:59:56 +0100 Subject: [PATCH 065/141] remove debug options [ci skip] --- .github/workflows/mac-dmg.yml | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index ab643c4f9..fc4e337fa 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -1,7 +1,6 @@ name: Build macOS .dmg on: - push: # TODO remove before merging into develop release: types: [published] workflow_dispatch: @@ -73,10 +72,10 @@ jobs: --name Cryptomator --vendor "Skymatic GmbH" --copyright "(C) 2016 - 2022 Skymatic GmbH" - --app-version "${{ steps.versions.outputs.semVerNum }}" + --app-version "${{ steps.versions.outputs.semVerNum }}" --java-options "-Xss5m" --java-options "-Xmx256m" - --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" + --java-options "-Dcryptomator.appVersion=\"${{ steps.versions.outputs.semVerStr }}\"" --java-options "-Dfile.encoding=\"utf-8\"" --java-options "-Dapple.awt.enableTemplateImages=true" --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" @@ -84,7 +83,7 @@ jobs: --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" --java-options "-Dcryptomator.showTrayIcon=true" - --java-options "-Dcryptomator.buildNumber=\"dmg-${{ steps.versions.outputs.revNum }}\"" + --java-options "-Dcryptomator.buildNumber=\"dmg-${{ steps.versions.outputs.revNum }}\"" --mac-package-identifier org.cryptomator --resource-dir dist/mac/resources - name: Patch Cryptomator.app @@ -169,7 +168,7 @@ jobs: --icon ".VolumeIcon.icns" 512 758 Cryptomator-${VERSION_NO}.dmg dmg env: - VERSION_NO: ${{ steps.versions.outputs.semVerNum }} + VERSION_NO: ${{ steps.versions.outputs.semVerNum }} - name: Install notarization credentials if: startsWith(github.ref, 'refs/tags/') run: | @@ -198,17 +197,14 @@ jobs: env: NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} - name: Add possible alpha/beta tags to installer name - run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg - - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 + run: mv Cryptomator-*.dmg Cryptomator-${{ steps.versions.outputs.semVerStr }}.dmg + - name: Create detached GPG signature with key 615D449FE6E6A235 run: | echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign README.md + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.dmg env: GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: Create detached GPG signatures - run: | - gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.dmg - name: Clean up codesign certificate if: ${{ always() }} run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db From 6cd349523b374aeec475809eb18a638b8689529e Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 13:00:14 +0100 Subject: [PATCH 066/141] separate workflow for building .exe --- .github/workflows/win-exe.yml | 256 ++++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 .github/workflows/win-exe.yml diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml new file mode 100644 index 000000000..1cb85bfca --- /dev/null +++ b/.github/workflows/win-exe.yml @@ -0,0 +1,256 @@ +name: Build Windows Installer + +on: + push: # TODO remove before merging into develop + release: + types: [published] + workflow_dispatch: + +env: + JAVA_VERSION: 17 + WINFSP_MSI: https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi + +defaults: + run: + shell: bash + +jobs: + build-msi: + name: Build .msi Installer + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - id: versions + name: Apply version information + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + SEM_VER_STR=${GITHUB_REF##*/} + mvn versions:set -DnewVersion=${SEM_VER_STR} + else + SEM_VER_STR=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout` + fi + SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` + REVCOUNT=`git rev-list --count HEAD` + echo "::set-output name=semVerStr::${SEM_VER_STR}" + echo "::set-output name=semVerNum::${SEM_VER_NUM}" + echo "::set-output name=revNum::${REVCOUNT}" + - name: Validate Version + uses: skymatic/semver-validation-action@v1 + with: + version: ${{ steps.versions.outputs.semVerStr }} + - name: Run maven + run: mvn -B clean package -Pdependency-check,win -DskipTests + - name: Patch target dir + run: | + cp LICENSE.txt target + cp dist/linux/launcher.sh target + cp target/cryptomator-*.jar target/mods + - name: Run jlink + run: > + ${JAVA_HOME}/bin/jlink + --verbose + --output runtime + --module-path "${JAVA_HOME}/jmods" + --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr + --strip-native-commands + --no-header-files + --no-man-pages + --strip-debug + --compress=1 + - name: Run jpackage + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type app-image + --runtime-image runtime + --input target/libs + --module-path target/mods + --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator + --dest appdir + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}.${{ steps.versions.outputs.revNum }}" + --java-options "-Xss5m" + --java-options "-Xmx256m" + --java-options "-Dfile.encoding=\"utf-8\"" + --java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\"" + --java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\"" + --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" + --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" + --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" + --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" + --java-options "-Dcryptomator.showTrayIcon=true" + --java-options "-Dcryptomator.buildNumber=\"msi-${{ steps.versions.outputs.revNum }}\"" + --resource-dir dist/win/resources + --icon dist/win/resources/Cryptomator.ico + - name: Patch Application Directory + run: | + cp dist/win/contrib/* appdir/Cryptomator + - name: Fix permissions + run: attrib -r appdir/Cryptomator/Cryptomator.exe + shell: pwsh + - name: Codesign + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator + timestampUrl: 'http://timestamp.digicert.com' + folder: appdir/Cryptomator + recursive: true + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win/resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/resources" + - name: Create MSI + run: > + ${JAVA_HOME}/bin/jpackage + --verbose + --type msi + --win-upgrade-uuid bda45523-42b1-4cae-9354-a45475ed4775 + --app-image appdir/Cryptomator + --dest installer + --name Cryptomator + --vendor "Skymatic GmbH" + --copyright "(C) 2016 - 2022 Skymatic GmbH" + --app-version "${{ steps.versions.outputs.semVerNum }}" + --win-menu + --win-dir-chooser + --win-shortcut-prompt + --win-update-url "https:\\cryptomator.org" + --win-menu-group Cryptomator + --resource-dir dist/win/resources + --license-file dist/win/resources/license.rtf + --file-associations dist/win/resources/FAvaultFile.properties + env: + JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs + - name: Codesign MSI + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Add possible alpha/beta tags to installer name + run: mv installer/Cryptomator-*.msi Cryptomator-${{ steps.versions.outputs.semVerStr }}-x64.msi + - name: Create detached GPG signature with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.msi + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: msi + path: | + Cryptomator-*.msi + Cryptomator-*.asc + if-no-files-found: error + - name: Publish .msi on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.msi + *.asc + outputs: + semVerNum: ${{ steps.versions.outputs.semVerNum }} + semVerStr: ${{ steps.versions.outputs.semVerStr }} + revNum: ${{ steps.versions.outputs.revNum }} + + build-exe: + name: Build .exe installer + runs-on: windows-latest + needs: [build-msi] + steps: + - uses: actions/checkout@v2 + - name: Download .msi + uses: actions/download-artifact@v2 + with: + name: msi + path: dist/win/bundle/resources + - name: Strip version info from msi file name + run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi + - uses: actions/setup-java@v2 + with: + distribution: 'temurin' + java-version: ${{ env.JAVA_VERSION }} + cache: 'maven' + - name: Generate license + run: > + mvn -B license:add-third-party + "-Dlicense.thirdPartyFilename=license.rtf" + "-Dlicense.fileTemplate=dist/win/bundle/resources/licenseTemplate.ftl" + "-Dlicense.outputDirectory=dist/win/bundle/resources" + - name: Download WinFsp + run: + curl --output dist/win/bundle/resources/winfsp.msi -L ${{ env.WINFSP_MSI }} + - name: Compile to wixObj file + run: > + "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs + -ext WixBalExtension + -out dist/win/bundle/ + -dBundleVersion="${{ needs.build-exe.outputs.semVerNum }}.${{ needs.build-exe.outputs.revNum }}" + -dBundleVendor="Skymatic GmbH" + -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" + -dAboutUrl="https://cryptomator.org" + -dHelpUrl="https://cryptomator.org/contact" + -dUpdateUrl="https://cryptomator.org/downloads/" + - name: Create executable with linker + run: > + "${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj + -ext WixBalExtension + -out installer/Cryptomator.exe + - name: Codesign EXE + uses: skymatic/code-sign-action@v1 + with: + certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} + password: ${{ secrets.WIN_CODESIGN_P12_PW }} + certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B + description: Cryptomator Installer + timestampUrl: 'http://timestamp.digicert.com' + folder: installer + - name: Add possible alpha/beta tags to installer name + run: mv installer/Cryptomator.exe Cryptomator-${{ needs.build-exe.outputs.semVerStr }}-x64.exe + - name: Create detached GPG signature with key 615D449FE6E6A235 + run: | + echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import + echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a Cryptomator-*.exe + env: + GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: exe + path: | + Cryptomator-*.exe + Cryptomator-*.asc + if-no-files-found: error + - name: Publish .msi on GitHub Releases + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + fail_on_unmatched_files: true + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + files: | + *.exe + *.asc \ No newline at end of file From 0443bfb0a2099e2feffa94c380090072fbed8c67 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 13:14:18 +0100 Subject: [PATCH 067/141] wrong job name in expression --- .github/workflows/win-exe.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 1cb85bfca..5be12cbe4 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -208,7 +208,7 @@ jobs: "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs -ext WixBalExtension -out dist/win/bundle/ - -dBundleVersion="${{ needs.build-exe.outputs.semVerNum }}.${{ needs.build-exe.outputs.revNum }}" + -dBundleVersion="${{ needs.build-msi.outputs.semVerNum }}.${{ needs.build-msi.outputs.revNum }}" -dBundleVendor="Skymatic GmbH" -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" -dAboutUrl="https://cryptomator.org" @@ -229,7 +229,7 @@ jobs: timestampUrl: 'http://timestamp.digicert.com' folder: installer - name: Add possible alpha/beta tags to installer name - run: mv installer/Cryptomator.exe Cryptomator-${{ needs.build-exe.outputs.semVerStr }}-x64.exe + run: mv installer/Cryptomator.exe Cryptomator-${{ needs.build-msi.outputs.semVerStr }}-x64.exe - name: Create detached GPG signature with key 615D449FE6E6A235 run: | echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import From e0b3525504a6d1de885581d32948f94b33930efe Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:01:06 +0100 Subject: [PATCH 068/141] bumped actions/upload-artifact version --- .github/workflows/appimage.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/win-exe.yml | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index ce0739e8b..ef06ef1e2 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -131,7 +131,7 @@ jobs: gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator-*.AppImage.zsync - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: appimage path: | diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 86a90723d..807e833a1 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -87,7 +87,7 @@ jobs: run: | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a cryptomator_*_amd64.deb - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: linux-deb-package path: | diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index fc4e337fa..0c4043208 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -214,7 +214,7 @@ jobs: run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db continue-on-error: true - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: dmg path: Cryptomator-*.dmg diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 5be12cbe4..853dcf789 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -155,7 +155,7 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: msi path: | @@ -238,7 +238,7 @@ jobs: GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: exe path: | From 5f17a666324e9409fbbcc59533d89abde079cb0b Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:02:00 +0100 Subject: [PATCH 069/141] draft a release for tagged builds if build succeeds --- .github/workflows/build.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 750b64826..40d2b2e1b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,4 +48,16 @@ jobs: run: bash <(curl -Ls https://coverage.codacy.com/get.sh) env: CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} - continue-on-error: true \ No newline at end of file + continue-on-error: true + - name: Draft a release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + draft: true + discussion_category_name: releases + token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} + generate_release_notes: true + body: |- + :construction: Work in Progress + + --- From 88db74d595bd6bdbaff4e1f8b03738307edc6e89 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:03:07 +0100 Subject: [PATCH 070/141] remove old release workflow --- .github/workflows/release.yml | 738 ---------------------------------- 1 file changed, 738 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a24342da3..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,738 +0,0 @@ -name: Installers and Release - -on: - workflow_dispatch: - inputs: - semver: - description: 'SemVer' - required: true - default: '0.99.99-SNAPSHOT' - push: - tags: # see https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet - - '[0-9]+.[0-9]+.[0-9]+' - - '[0-9]+.[0-9]+.[0-9]+-*' - -env: - JAVA_VERSION: 17 - -defaults: - run: - shell: bash - -jobs: - -# -# Buildkit -# - buildkit: - name: Build ${{ matrix.profile }}-buildkit - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - include: - - os: ubuntu-latest - profile: linux - - os: windows-latest - profile: win - - os: macos-latest - profile: mac - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Ensure to use tagged version - run: mvn versions:set -DnewVersion=${GITHUB_REF##*/} # use shell parameter expansion to strip of 'refs/tags' - if: startsWith(github.ref, 'refs/tags/') - - name: Build and Test - run: mvn -B clean package -Pdependency-check,${{ matrix.profile }} - - name: Patch buildkit - run: | - cp LICENSE.txt target - cp dist/${{ matrix.profile }}/launcher* target - cp target/cryptomator-*.jar target/mods - - name: Upload ${{ matrix.profile }}-buildkit - uses: actions/upload-artifact@v2 - with: - name: ${{ matrix.profile }}-buildkit - path: | - target/libs - target/mods - target/LICENSE.txt - target/launcher* - if-no-files-found: error - -# -# Release Metadata -# - metadata: - name: Determine Version Metadata - runs-on: ubuntu-latest - outputs: - semVerNum: ${{ steps.versions.outputs.semVerNum }} - semVerStr: ${{ steps.versions.outputs.semVerStr }} - ppaVerStr: ${{ steps.versions.outputs.ppaVerStr }} - revNum: ${{ steps.versions.outputs.revNum }} - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - id: versions - run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then - SEM_VER_STR=${GITHUB_REF##*/} - else - SEM_VER_STR=${{ github.event.inputs.semver }} - fi - SEM_VER_NUM=`echo ${SEM_VER_STR} | sed -E 's/([0-9]+\.[0-9]+\.[0-9]+).*/\1/'` - REVCOUNT=`git rev-list --count HEAD` - echo "::set-output name=semVerStr::${SEM_VER_STR}" - echo "::set-output name=semVerNum::${SEM_VER_NUM}" - echo "::set-output name=ppaVerStr::${SEM_VER_STR/-/\~}-${REVCOUNT}" - echo "::set-output name=revNum::${REVCOUNT}" - - uses: skymatic/semver-validation-action@v1 - with: - version: ${{ steps.versions.outputs.semVerStr }} - -# -# Application Directory -# - appdir: - name: Create ${{ matrix.profile }}-appdir - needs: [buildkit, metadata] - runs-on: ${{ matrix.os }} - strategy: - fail-fast: true - matrix: - include: - - os: ubuntu-latest - profile: linux - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" - --java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" - --java-options "-Dcryptomator.showTrayIcon=false" - --java-options "-Dcryptomator.buildNumber=\"appimage-${{ needs.metadata.outputs.revNum }}\"" - --resource-dir dist/linux/resources - - os: windows-latest - profile: win - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dcryptomator.logDir=\"~/AppData/Roaming/Cryptomator\"" - --java-options "-Dcryptomator.pluginDir=\"~/AppData/Roaming/Cryptomator/Plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/AppData/Roaming/Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/AppData/Roaming/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.keychainPath=\"~/AppData/Roaming/Cryptomator/keychain.json\"" - --java-options "-Dcryptomator.mountPointsDir=\"~/Cryptomator\"" - --java-options "-Dcryptomator.showTrayIcon=true" - --java-options "-Dcryptomator.buildNumber=\"msi-${{ needs.metadata.outputs.revNum }}\"" - --resource-dir dist/win/resources - --icon dist/win/resources/Cryptomator.ico - - os: macos-latest - profile: mac - jpackageoptions: > - --app-version "${{ needs.metadata.outputs.semVerNum }}" - --java-options "-Dfile.encoding=\"utf-8\"" - --java-options "-Dapple.awt.enableTemplateImages=true" - --java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\"" - --java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\"" - --java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\"" - --java-options "-Dcryptomator.ipcSocketPath=\"~/Library/Application Support/Cryptomator/ipc.socket\"" - --java-options "-Dcryptomator.showTrayIcon=true" - --java-options "-Dcryptomator.buildNumber=\"dmg-${{ needs.metadata.outputs.revNum }}\"" - --mac-package-identifier org.cryptomator - --resource-dir dist/mac/resources - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - - name: Download ${{ matrix.profile }}-buildkit - uses: actions/download-artifact@v2 - with: - name: ${{ matrix.profile }}-buildkit - path: buildkit - - name: Create Runtime Image - run: > - ${JAVA_HOME}/bin/jlink - --verbose - --output runtime - --module-path "${JAVA_HOME}/jmods" - --add-modules java.base,java.desktop,java.logging,java.naming,java.net.http,java.scripting,java.sql,java.xml,jdk.unsupported,jdk.crypto.ec,jdk.accessibility,jdk.management.jfr - --no-header-files - --no-man-pages - --strip-debug - --compress=1 - - name: Create App Directory - run: > - ${JAVA_HOME}/bin/jpackage - --verbose - --type app-image - --runtime-image runtime - --input buildkit/libs - --module-path buildkit/mods - --module org.cryptomator.desktop/org.cryptomator.launcher.Cryptomator - --dest appdir - --name Cryptomator - --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 Skymatic GmbH" - --java-options "-Xss5m" - --java-options "-Xmx256m" - --java-options "-Dcryptomator.appVersion=\"${{ needs.metadata.outputs.semVerStr }}\"" - ${{ matrix.jpackageoptions }} - - name: Create appdir.tar - run: tar -cvf appdir.tar appdir - - name: Upload ${{ matrix.profile }}-appdir - uses: actions/upload-artifact@v2 - with: - name: ${{ matrix.profile }}-appdir - path: appdir.tar - if-no-files-found: error - -# -# Debian Package -# - deb: - name: Create Debian Package - needs: [buildkit, metadata] - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@v2 - - name: install build tools - run: | - sudo apt-get update - sudo apt-get install debhelper devscripts - - name: Download linux-buildkit - uses: actions/download-artifact@v2 - with: - name: linux-buildkit - path: pkgdir - - name: create orig.tar.gz with common/ libs/ mods/ - run: | - cp -r dist/linux/common/ pkgdir - tar -cJf cryptomator_${{ needs.metadata.outputs.ppaVerStr }}.orig.tar.xz -C pkgdir . - - name: patch and rename pkgdir - run: | - cp -r dist/linux/debian/ pkgdir - export RFC2822_TIMESTAMP=`date --rfc-2822` - envsubst '${SEMVER_STR} ${VERSION_NUM} ${REVISION_NUM}' < dist/linux/debian/rules > pkgdir/debian/rules - envsubst '${PPA_VERSION} ${RFC2822_TIMESTAMP}' < dist/linux/debian/changelog > pkgdir/debian/changelog - find . -name "*.jar" >> pkgdir/debian/source/include-binaries - mv pkgdir cryptomator_${{ needs.metadata.outputs.ppaVerStr }} - env: - SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} - VERSION_NUM: ${{ needs.metadata.outputs.semVerNum }} - REVISION_NUM: ${{ needs.metadata.outputs.revNum }} - PPA_VERSION: ${{ needs.metadata.outputs.ppaVerStr }}-0ppa1 - - name: import gpg key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign dist/linux/debian/rules - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: debuild - run: | - debuild -S -sa -d - debuild -b -sa -d - env: - DEBSIGN_PROGRAM: gpg --batch --pinentry-mode loopback - DEBSIGN_KEYID: 615D449FE6E6A235 - working-directory: cryptomator_${{ needs.metadata.outputs.ppaVerStr }} - - name: Upload artifacts - uses: actions/upload-artifact@v2 - with: - name: linux-deb-package - path: | - cryptomator_*.dsc - cryptomator_*.orig.tar.xz - cryptomator_*.debian.tar.xz - cryptomator_*_source.buildinfo - cryptomator_*_source.changes - cryptomator_*_amd64.deb - -# -# Upload Source Package to PPA -# - ppa: - name: Upload Source Package to PPA - needs: [deb] - runs-on: ubuntu-18.04 - steps: - - name: install dput - run: | - sudo apt-get update - sudo apt-get install dput - - name: import public key - run: curl -sSL https://github.com/cryptobot.gpg | gpg --import - - - name: download linux-deb-package - uses: actions/download-artifact@v2 - with: - name: linux-deb-package - path: . - - name: dput to beta repo - run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - -# -# Linux Cryptomator.AppImage -# - linux-appimage: - name: Build Cryptomator.AppImage - runs-on: ubuntu-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download linux-appdir - uses: actions/download-artifact@v2 - with: - name: linux-appdir - - name: Untar appdir.tar - run: | - tar -xvf appdir.tar - - name: Patch Cryptomator.AppDir - run: | - mv appdir/Cryptomator Cryptomator.AppDir - cp -r dist/linux/appimage/resources/AppDir/* Cryptomator.AppDir/ - envsubst '${REVISION_NO} ${SEMVER_STR}' < dist/linux/appimage/resources/AppDir/bin/cryptomator.sh > Cryptomator.AppDir/bin/cryptomator.sh - cp dist/linux/common/org.cryptomator.Cryptomator256.png Cryptomator.AppDir/usr/share/icons/hicolor/256x256/apps/org.cryptomator.Cryptomator.png - cp dist/linux/common/org.cryptomator.Cryptomator512.png Cryptomator.AppDir/usr/share/icons/hicolor/512x512/apps/org.cryptomator.Cryptomator.png - cp dist/linux/common/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg - cp dist/linux/common/org.cryptomator.Cryptomator.metainfo.xml Cryptomator.AppDir/usr/share/metainfo/org.cryptomator.Cryptomator.metainfo.xml - cp dist/linux/common/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/usr/share/applications/org.cryptomator.Cryptomator.desktop - cp dist/linux/common/application-vnd.cryptomator.vault.xml Cryptomator.AppDir/usr/share/mime/packages/application-vnd.cryptomator.vault.xml - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/org.cryptomator.Cryptomator.svg - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/Cryptomator.svg - ln -s usr/share/icons/hicolor/scalable/apps/org.cryptomator.Cryptomator.svg Cryptomator.AppDir/.DirIcon - ln -s usr/share/applications/org.cryptomator.Cryptomator.desktop Cryptomator.AppDir/Cryptomator.desktop - ln -s bin/cryptomator.sh Cryptomator.AppDir/AppRun - env: - REVISION_NO: ${{ needs.metadata.outputs.revNum }} - SEMVER_STR: ${{ needs.metadata.outputs.semVerStr }} - - name: Extract libjffi.so # workaround for https://github.com/cryptomator/cryptomator-linux/issues/27 - run: | - JFFI_NATIVE_JAR=`ls lib/app/ | grep -e 'jffi-[1-9]\.[0-9]\{1,2\}.[0-9]\{1,2\}-native.jar'` - ${JAVA_HOME}/bin/jar -xf lib/app/${JFFI_NATIVE_JAR} /jni/x86_64-Linux/ - mv jni/x86_64-Linux/* lib/app/libjffi.so - working-directory: Cryptomator.AppDir - - name: Download AppImageKit - run: | - curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o appimagetool.AppImage - chmod +x appimagetool.AppImage - ./appimagetool.AppImage --appimage-extract - - name: Prepare GPG-Agent for signing with key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --dry-run --sign Cryptomator.AppDir/AppRun - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: Build AppImage - run: > - ./squashfs-root/AppRun Cryptomator.AppDir cryptomator-${{ needs.metadata.outputs.semVerStr }}-x86_64.AppImage - -u 'gh-releases-zsync|cryptomator|cryptomator|latest|cryptomator-*-x86_64.AppImage.zsync' - --sign --sign-key=615D449FE6E6A235 --sign-args="--batch --pinentry-mode loopback" - - name: Upload AppImage - uses: actions/upload-artifact@v2 - with: - name: linux-appimage - path: | - cryptomator-*.AppImage - cryptomator-*.AppImage.zsync - if-no-files-found: error - -# -# macOS Cryptomator.app -# - mac-app: - name: Build Cryptomator.app - runs-on: macos-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download mac-appdir - uses: actions/download-artifact@v2 - with: - name: mac-appdir - - name: Untar appdir.tar - run: tar -xvf appdir.tar - - name: Patch Cryptomator.app - run: | - mv appdir/Cryptomator.app Cryptomator.app - mv dist/mac/resources/Cryptomator-Vault.icns Cryptomator.app/Contents/Resources/ - sed -i '' "s|###BUNDLE_SHORT_VERSION_STRING###|${VERSION_NO}|g" Cryptomator.app/Contents/Info.plist - sed -i '' "s|###BUNDLE_VERSION###|${REVISION_NO}|g" Cryptomator.app/Contents/Info.plist - env: - VERSION_NO: ${{ needs.metadata.outputs.semVerNum }} - REVISION_NO: ${{ needs.metadata.outputs.revNum }} - - name: Install codesign certificate - env: - CODESIGN_P12_BASE64: ${{ secrets.MACOS_CODESIGN_P12_BASE64 }} - CODESIGN_P12_PW: ${{ secrets.MACOS_CODESIGN_P12_PW }} - CODESIGN_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_CODESIGN_TMP_KEYCHAIN_PW }} - run: | - # create variables - CERTIFICATE_PATH=$RUNNER_TEMP/codesign.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/codesign.keychain-db - - # import certificate and provisioning profile from secrets - echo -n "$CODESIGN_P12_BASE64" | base64 --decode --output $CERTIFICATE_PATH - - # create temporary keychain - security create-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH - security set-keychain-settings -lut 900 $KEYCHAIN_PATH - security unlock-keychain -p "$CODESIGN_TMP_KEYCHAIN_PW" $KEYCHAIN_PATH - - # import certificate to keychain - security import $CERTIFICATE_PATH -P "$CODESIGN_P12_PW" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - name: Codesign - env: - CODESIGN_IDENTITY: ${{ secrets.MACOS_CODESIGN_IDENTITY }} - run: | - find Cryptomator.app/Contents/runtime/Contents/MacOS -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - for JAR_PATH in `find Cryptomator.app -name "*.jar"`; do - if [[ `unzip -l ${JAR_PATH} | grep '.dylib\|.jnilib'` ]]; then - JAR_FILENAME=$(basename ${JAR_PATH}) - OUTPUT_PATH=${JAR_PATH%.*} - echo "Codesigning libs in ${JAR_FILENAME}..." - unzip -q ${JAR_PATH} -d ${OUTPUT_PATH} - find ${OUTPUT_PATH} -name '*.dylib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - find ${OUTPUT_PATH} -name '*.jnilib' -exec codesign --force -s ${CODESIGN_IDENTITY} {} \; - rm ${JAR_PATH} - pushd ${OUTPUT_PATH} > /dev/null - zip -qr ../${JAR_FILENAME} * - popd > /dev/null - rm -r ${OUTPUT_PATH} - fi - done - echo "Codesigning Cryptomator.app..." - codesign --force --deep --entitlements dist/mac/Cryptomator.entitlements -o runtime -s ${CODESIGN_IDENTITY} Cryptomator.app - - name: Clean up codesign certificate - if: ${{ always() }} - run: security delete-keychain $RUNNER_TEMP/codesign.keychain-db - - name: Create app.tar - run: tar -cvf app.tar Cryptomator.app - - name: Upload mac-app - uses: actions/upload-artifact@v2 - with: - name: mac-app - path: app.tar - if-no-files-found: error - -# -# macOS Cryptomator.dmg -# - mac-dmg: - name: Build Cryptomator.dmg - runs-on: macos-11 - needs: [mac-app, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download mac-appdir - uses: actions/download-artifact@v2 - with: - name: mac-app - - name: Untar app.tar - run: tar -xvf app.tar - - name: Prepare .dmg contents - run: | - mkdir dmg - mv Cryptomator.app dmg - cp dist/mac/dmg/resources/macFUSE.webloc dmg - ls -l dmg - - name: Install create-dmg - run: | - brew install create-dmg - create-dmg --help - - name: Create .dmg - run: > - create-dmg - --volname Cryptomator - --volicon "dist/mac/dmg/resources/Cryptomator-Volume.icns" - --background "dist/mac/dmg/resources/Cryptomator-background.tiff" - --window-pos 400 100 - --window-size 640 694 - --icon-size 128 - --icon "Cryptomator.app" 128 245 - --hide-extension "Cryptomator.app" - --icon "macFUSE.webloc" 320 501 - --hide-extension "macFUSE.webloc" - --app-drop-link 512 245 - --eula "dist/mac/dmg/resources/license.rtf" - --icon ".background" 128 758 - --icon ".fseventsd" 320 758 - --icon ".VolumeIcon.icns" 512 758 - Cryptomator-${VERSION_NO}.dmg dmg - env: - VERSION_NO: ${{ needs.metadata.outputs.semVerNum }} - - name: Install notarization credentials - env: - NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} - NOTARIZATION_APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }} - NOTARIZATION_PW: ${{ secrets.MACOS_NOTARIZATION_PW }} - NOTARIZATION_TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }} - NOTARIZATION_TMP_KEYCHAIN_PW: ${{ secrets.MACOS_NOTARIZATION_TMP_KEYCHAIN_PW }} - run: | - # create temporary keychain - KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db - security create-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} - security set-keychain-settings -lut 900 ${KEYCHAIN_PATH} - security unlock-keychain -p "${NOTARIZATION_TMP_KEYCHAIN_PW}" ${KEYCHAIN_PATH} - - # import credentials from secrets - sudo xcode-select -s /Applications/Xcode_13.0.app - xcrun notarytool store-credentials "${NOTARIZATION_KEYCHAIN_PROFILE}" --apple-id "${NOTARIZATION_APPLE_ID}" --password "${NOTARIZATION_PW}" --team-id "${NOTARIZATION_TEAM_ID}" --keychain "${KEYCHAIN_PATH}" - - name: Notarize .dmg - env: - NOTARIZATION_KEYCHAIN_PROFILE: ${{ secrets.MACOS_NOTARIZATION_KEYCHAIN_PROFILE }} - run: | - KEYCHAIN_PATH=$RUNNER_TEMP/notarization.keychain-db - sudo xcode-select -s /Applications/Xcode_13.0.app - xcrun notarytool submit Cryptomator-*.dmg --keychain-profile "${NOTARIZATION_KEYCHAIN_PROFILE}" --keychain "${KEYCHAIN_PATH}" --wait - xcrun stapler staple Cryptomator-*.dmg - - name: Clean up notarization credentials - if: ${{ always() }} - run: security delete-keychain $RUNNER_TEMP/notarization.keychain-db - - name: Add possible alpha/beta tags to installer name - run: mv Cryptomator-*.dmg Cryptomator-${{ needs.metadata.outputs.semVerStr }}.dmg - - name: Upload mac-dmg - uses: actions/upload-artifact@v2 - with: - name: mac-dmg - path: Cryptomator-*.dmg - if-no-files-found: error - -# -# MSI package -# - win-msi: - name: Build Cryptomator.msi - runs-on: windows-latest - needs: [appdir, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download win-appdir - uses: actions/download-artifact@v2 - with: - name: win-appdir - - name: Untar appdir.tar - run: tar -xvf appdir.tar - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Patch Application Directory - run: | - cp dist/win/contrib/* appdir/Cryptomator - - name: Fix permissions - run: attrib -r appdir/Cryptomator/Cryptomator.exe - shell: pwsh - - name: Codesign - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator - timestampUrl: 'http://timestamp.digicert.com' - folder: appdir/Cryptomator - recursive: true - - name: Generate license - run: > - mvn -B license:add-third-party - "-Dlicense.thirdPartyFilename=license.rtf" - "-Dlicense.fileTemplate=dist/win/resources/licenseTemplate.ftl" - "-Dlicense.outputDirectory=dist/win/resources" - - name: Create MSI - run: > - ${JAVA_HOME}/bin/jpackage - --verbose - --type msi - --win-upgrade-uuid bda45523-42b1-4cae-9354-a45475ed4775 - --app-image appdir/Cryptomator - --dest installer - --name Cryptomator - --vendor "Skymatic GmbH" - --copyright "(C) 2016 - 2022 Skymatic GmbH" - --app-version "${{ needs.metadata.outputs.semVerNum }}" - --win-menu - --win-dir-chooser - --win-shortcut-prompt - --win-update-url "https:\\cryptomator.org" - --win-menu-group Cryptomator - --resource-dir dist/win/resources - --license-file dist/win/resources/license.rtf - --file-associations dist/win/resources/FAvaultFile.properties - env: - JP_WIXWIZARD_RESOURCES: ${{ github.workspace }}/dist/win/resources # requires abs path, used in resources/main.wxs - - name: Codesign MSI - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator Installer - timestampUrl: 'http://timestamp.digicert.com' - folder: installer - - name: Add possible alpha/beta tags to installer name - run: mv installer/Cryptomator-*.msi installer/Cryptomator-${{ needs.metadata.outputs.semVerStr }}-x64.msi - - name: Upload win-msi - uses: actions/upload-artifact@v2 - with: - name: win-msi - path: installer/*.msi - if-no-files-found: error - -# -# Windows Cryptomator.exe bundle -# - win-exe: - name: Build Cryptomator.exe bundle - runs-on: windows-latest - needs: [win-msi, metadata] - steps: - - uses: actions/checkout@v2 - - name: Download Windows msi - uses: actions/download-artifact@v2 - with: - name: win-msi - path: dist/win/bundle/resources - - name: Strip version info from msi file name - run: mv dist/win/bundle/resources/Cryptomator*.msi dist/win/bundle/resources/Cryptomator.msi - - uses: actions/setup-java@v2 - with: - distribution: 'temurin' - java-version: ${{ env.JAVA_VERSION }} - cache: 'maven' - - name: Generate license - run: > - mvn -B license:add-third-party - "-Dlicense.thirdPartyFilename=license.rtf" - "-Dlicense.fileTemplate=dist/win/bundle/resources/licenseTemplate.ftl" - "-Dlicense.outputDirectory=dist/win/bundle/resources" - - name: Download winfsp - run: - curl --output dist/win/bundle/resources/winfsp.msi -L https://github.com/winfsp/winfsp/releases/download/v1.10/winfsp-1.10.22006.msi - - name: Compile to wixObj file - run: > - "${WIX}/bin/candle.exe" dist/win/bundle/bundleWithWinfsp.wxs - -ext WixBalExtension - -out dist/win/bundle/ - -dBundleVersion="${{ needs.metadata.outputs.semVerNum }}.${{ needs.metadata.outputs.revNum }}" - -dBundleVendor="Skymatic GmbH" - -dBundleCopyright="(C) 2016 - 2022 Skymatic GmbH" - -dAboutUrl="https://cryptomator.org" - -dHelpUrl="https://cryptomator.org/contact" - -dUpdateUrl="https://cryptomator.org/downloads/" - - name: Create executable with linker - run: > - "${WIX}/bin/light.exe" -b dist/win/ dist/win/bundle/bundleWithWinfsp.wixobj - -ext WixBalExtension - -out installer/Cryptomator.exe - - name: Codesign EXE - uses: skymatic/code-sign-action@v1 - with: - certificate: ${{ secrets.WIN_CODESIGN_P12_BASE64 }} - password: ${{ secrets.WIN_CODESIGN_P12_PW }} - certificatesha1: FF52240075AD7D14AF25629FDF69635357C7D14B - description: Cryptomator Installer - timestampUrl: 'http://timestamp.digicert.com' - folder: installer - - name: Add possible alpha/beta tags to installer name - run: mv installer/Cryptomator.exe installer/Cryptomator-${{ needs.metadata.outputs.semVerStr }}-x64.exe - - name: Upload win-exe - uses: actions/upload-artifact@v2 - with: - name: win-exe - path: installer/*.exe - if-no-files-found: error - -# -# Release -# - release: - name: Draft a release on Github - runs-on: ubuntu-latest - needs: [metadata,linux-appimage,mac-dmg,win-msi,win-exe,ppa] - if: startsWith(github.ref, 'refs/tags/') && github.repository == 'cryptomator/cryptomator' - steps: - - uses: actions/checkout@v2 - - name: Create tarball - run: git archive --prefix="cryptomator-${{ needs.metadata.outputs.semVerStr }}/" -o "cryptomator-${{ needs.metadata.outputs.semVerStr }}.tar.gz" ${{ github.ref }} - - name: Download Debian package - uses: actions/download-artifact@v2 - with: - name: linux-deb-package - - name: Download linux appimage - uses: actions/download-artifact@v2 - with: - name: linux-appimage - - name: Download macOS dmg - uses: actions/download-artifact@v2 - with: - name: mac-dmg - - name: Download Windows msi - uses: actions/download-artifact@v2 - with: - name: win-msi - - name: Download Windows exe - uses: actions/download-artifact@v2 - with: - name: win-exe - - name: Create detached GPG signature for all release files with key 615D449FE6E6A235 - run: | - echo "${GPG_PRIVATE_KEY}" | gpg --batch --quiet --import - for FILE in `find . -name "*.AppImage" -o -name "*.deb" -o -name "*.dmg" -o -name "*.exe" -o -name "*.msi" -o -name "*.zsync" -o -name "*.tar.gz"`; do - echo "${GPG_PASSPHRASE}" | gpg --batch --quiet --passphrase-fd 0 --pinentry-mode loopback -u 615D449FE6E6A235 --detach-sign -a ${FILE} - done - env: - GPG_PRIVATE_KEY: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} - GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }} - - name: Compute SHA256 checksums of release artifacts - run: | - SHA256_SUMS=`find . -name "*.AppImage" -o -name "*.deb" -o -name "*.dmg" -o -name "*.exe" -o -name "*.msi" -o -name "*.tar.gz" | xargs sha256sum` - echo "SHA256_SUMS<> $GITHUB_ENV - echo "${SHA256_SUMS}" >> $GITHUB_ENV - echo "EOF" >> $GITHUB_ENV - continue-on-error: true - - name: Create release draft - uses: softprops/action-gh-release@v1 - with: - draft: true - fail_on_unmatched_files: true - discussion_category_name: releases - token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} - files: | - *.AppImage - *.zsync - *.asc - *.deb - *.dmg - *.msi - *.exe - body: |- - :construction: Work in Progress - ## What's New - ## Bugfixes - ## Misc - - --- - - :scroll: A complete list of closed issues is available [here](LINK). - - --- - - :floppy_disk: SHA-256 checksums of release artifacts: - ``` - ${{ env.SHA256_SUMS }} - ``` From b37886c98cffb60a56327684386e8718ef49e25c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:10:28 +0100 Subject: [PATCH 071/141] remove debug options [ci skip] --- .github/workflows/win-exe.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 853dcf789..8f821f54d 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -1,7 +1,6 @@ name: Build Windows Installer on: - push: # TODO remove before merging into develop release: types: [published] workflow_dispatch: From e2f872ba9b7459363c0dd01064fae254423d5004 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:18:05 +0100 Subject: [PATCH 072/141] control whether to run `dput` when manually dispatching debian workflow --- .github/workflows/debian.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 807e833a1..83084121d 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -4,6 +4,12 @@ on: release: types: [published] workflow_dispatch: + inputs: + dput: + description: 'Upload to PPA' + required: true + default: false + type: boolean env: JAVA_VERSION: 17 @@ -99,7 +105,7 @@ jobs: cryptomator_*_amd64.deb cryptomator_*.asc - name: Run dput to beta repo - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - name: Publish Debian package on GitHub Releases if: startsWith(github.ref, 'refs/tags/') From 746d3974a01c31c282007397c6585e52f239a07d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:54:20 +0100 Subject: [PATCH 073/141] treat workflow input as string see https://github.com/actions/runner/issues/1483 --- .github/workflows/debian.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 83084121d..3f713bce8 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -105,7 +105,7 @@ jobs: cryptomator_*_amd64.deb cryptomator_*.asc - name: Run dput to beta repo - if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput + if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput == 'true' run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - name: Publish Debian package on GitHub Releases if: startsWith(github.ref, 'refs/tags/') From d680d7fe9d68a8bfa1793b3e7bc159d0c7aba6ae Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 14:58:53 +0100 Subject: [PATCH 074/141] renamed step [ci skip] --- .github/workflows/debian.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 3f713bce8..6caee6069 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -104,7 +104,7 @@ jobs: cryptomator_*_source.changes cryptomator_*_amd64.deb cryptomator_*.asc - - name: Run dput to beta repo + - name: Publish on PPA if: startsWith(github.ref, 'refs/tags/') || github.event.inputs.dput == 'true' run: dput ppa:sebastian-stenzel/cryptomator-beta cryptomator_*_source.changes - name: Publish Debian package on GitHub Releases From c22ede3bf4b557efc8d3ed5a267337f921c7ae8c Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 15:52:56 +0100 Subject: [PATCH 075/141] only enforce version if tag contains semver string --- .github/workflows/appimage.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/win-exe.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index ef06ef1e2..694f6ee45 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -25,7 +25,7 @@ jobs: - id: versions name: Apply version information run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then SEM_VER_STR=${GITHUB_REF##*/} mvn versions:set -DnewVersion=${SEM_VER_STR} else diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index 6caee6069..f7893bbaa 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -35,7 +35,7 @@ jobs: - id: versions name: Apply version information run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then SEM_VER_STR=${GITHUB_REF##*/} mvn versions:set -DnewVersion=${SEM_VER_STR} else diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 0c4043208..58b7e9408 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -25,7 +25,7 @@ jobs: - id: versions name: Apply version information run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then SEM_VER_STR=${GITHUB_REF##*/} mvn versions:set -DnewVersion=${SEM_VER_STR} else diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 8f821f54d..9a855b646 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -30,7 +30,7 @@ jobs: - id: versions name: Apply version information run: | - if [[ $GITHUB_REF == refs/tags/* ]]; then + if [[ $GITHUB_REF =~ refs/tags/[0-9]+\.[0-9]+\.[0-9]+.* ]]; then SEM_VER_STR=${GITHUB_REF##*/} mvn versions:set -DnewVersion=${SEM_VER_STR} else From e67772c9a6848e9d597c212d63beade803972cbf Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 16:10:40 +0100 Subject: [PATCH 076/141] attempt to trigger build when drafting a release --- .github/workflows/appimage.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/win-exe.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 694f6ee45..fa4928b75 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -2,7 +2,7 @@ name: Build AppImage on: release: - types: [published] + types: [created] workflow_dispatch: env: diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index f7893bbaa..b6f174e25 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -2,7 +2,7 @@ name: Build Debian Package on: release: - types: [published] + types: [created] workflow_dispatch: inputs: dput: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 58b7e9408..75015a2fc 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -2,7 +2,7 @@ name: Build macOS .dmg on: release: - types: [published] + types: [created] workflow_dispatch: env: diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 9a855b646..387eb0c72 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -2,7 +2,7 @@ name: Build Windows Installer on: release: - types: [published] + types: [created] workflow_dispatch: env: From 0d783183733c1c4771193e2b91997fe39f3a4219 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 16:44:17 +0100 Subject: [PATCH 077/141] Revert "attempt to trigger build when drafting a release" This reverts commit e67772c9a6848e9d597c212d63beade803972cbf. --- .github/workflows/appimage.yml | 2 +- .github/workflows/debian.yml | 2 +- .github/workflows/mac-dmg.yml | 2 +- .github/workflows/win-exe.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index fa4928b75..694f6ee45 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -2,7 +2,7 @@ name: Build AppImage on: release: - types: [created] + types: [published] workflow_dispatch: env: diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml index b6f174e25..f7893bbaa 100644 --- a/.github/workflows/debian.yml +++ b/.github/workflows/debian.yml @@ -2,7 +2,7 @@ name: Build Debian Package on: release: - types: [created] + types: [published] workflow_dispatch: inputs: dput: diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 75015a2fc..58b7e9408 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -2,7 +2,7 @@ name: Build macOS .dmg on: release: - types: [created] + types: [published] workflow_dispatch: env: diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 387eb0c72..9a855b646 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -2,7 +2,7 @@ name: Build Windows Installer on: release: - types: [created] + types: [published] workflow_dispatch: env: From 7b4a3e13138d2239965a8c139d36aef329860835 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 18:06:08 +0100 Subject: [PATCH 078/141] make sure not to upload unrelated artifacts to a release --- .github/workflows/appimage.yml | 6 +++--- .github/workflows/mac-dmg.yml | 4 ++-- .github/workflows/win-exe.yml | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/appimage.yml b/.github/workflows/appimage.yml index 694f6ee45..aab954476 100644 --- a/.github/workflows/appimage.yml +++ b/.github/workflows/appimage.yml @@ -146,6 +146,6 @@ jobs: fail_on_unmatched_files: true token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} files: | - *.AppImage - *.zsync - *.asc \ No newline at end of file + cryptomator-*.AppImage + cryptomator-*.zsync + cryptomator-*.asc \ No newline at end of file diff --git a/.github/workflows/mac-dmg.yml b/.github/workflows/mac-dmg.yml index 58b7e9408..66af92d6d 100644 --- a/.github/workflows/mac-dmg.yml +++ b/.github/workflows/mac-dmg.yml @@ -226,7 +226,7 @@ jobs: fail_on_unmatched_files: true token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} files: | - *.dmg - *.asc + Cryptomator-*.dmg + Cryptomator-*.asc diff --git a/.github/workflows/win-exe.yml b/.github/workflows/win-exe.yml index 9a855b646..f024d7347 100644 --- a/.github/workflows/win-exe.yml +++ b/.github/workflows/win-exe.yml @@ -251,5 +251,5 @@ jobs: fail_on_unmatched_files: true token: ${{ secrets.CRYPTOBOT_RELEASE_TOKEN }} files: | - *.exe - *.asc \ No newline at end of file + Cryptomator-*.exe + Cryptomator-*.asc \ No newline at end of file From 77e2be22dea911e89f8c43033d8e99963067f946 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 18 Mar 2022 18:06:31 +0100 Subject: [PATCH 079/141] updated .gitignore [ci skip] --- dist/mac/dmg/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dist/mac/dmg/.gitignore b/dist/mac/dmg/.gitignore index c186170c9..b8ef35283 100644 --- a/dist/mac/dmg/.gitignore +++ b/dist/mac/dmg/.gitignore @@ -1,4 +1,5 @@ # created during build +Cryptomator.app/ runtime/ dmg/ -*.dmg +*.dmg \ No newline at end of file From 4a3d579b76b5e8f7906d447d40a03473e442b0aa Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Mon, 21 Mar 2022 14:01:35 +0100 Subject: [PATCH 080/141] changed button title as suggested in review --- src/main/resources/fxml/unlock_select_masterkeyfile.fxml | 2 +- src/main/resources/i18n/strings.properties | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml index 43f2f3e46..d37289fca 100644 --- a/src/main/resources/fxml/unlock_select_masterkeyfile.fxml +++ b/src/main/resources/fxml/unlock_select_masterkeyfile.fxml @@ -34,7 +34,7 @@