mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-22 02:26:55 -04:00
Merge branch 'develop' into feature/displayName
# Conflicts: # pom.xml
This commit is contained in:
@@ -46,6 +46,7 @@ module org.cryptomator.desktop {
|
||||
opens org.cryptomator.ui.fxapp to javafx.fxml;
|
||||
opens org.cryptomator.ui.health to javafx.fxml;
|
||||
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
|
||||
opens org.cryptomator.ui.lock to javafx.fxml;
|
||||
opens org.cryptomator.ui.mainwindow to javafx.fxml;
|
||||
opens org.cryptomator.ui.migration to javafx.fxml;
|
||||
opens org.cryptomator.ui.preferences to javafx.fxml;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
@@ -80,7 +81,7 @@ public class ErrorCode {
|
||||
if (causalChain.size() > 1) {
|
||||
var rootCause = causalChain.get(causalChain.size() - 1);
|
||||
var parentOfRootCause = causalChain.get(causalChain.size() - 2);
|
||||
var rootSpecificFrames = nonOverlappingFrames(parentOfRootCause.getStackTrace(), rootCause.getStackTrace());
|
||||
var rootSpecificFrames = countTopmostFrames(rootCause.getStackTrace(), parentOfRootCause.getStackTrace());
|
||||
return new ErrorCode(throwable, rootCause, rootSpecificFrames);
|
||||
} else {
|
||||
return new ErrorCode(throwable, throwable, ALL_FRAMES);
|
||||
@@ -107,11 +108,31 @@ public class ErrorCode {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int nonOverlappingFrames(StackTraceElement[] frames, StackTraceElement[] enclosingFrames) {
|
||||
// Compute the number of elements in `frames` not contained in `enclosingFrames` by iterating backwards
|
||||
// Result should usually be equal to the difference in size of both traces
|
||||
var i = reverseStream(enclosingFrames).iterator();
|
||||
return (int) reverseStream(frames).dropWhile(f -> i.hasNext() && i.next().equals(f)).count();
|
||||
/**
|
||||
* Counts the number of <em>additional</em> frames contained in <code>allFrames</code> but not in <code>bottomFrames</code>.
|
||||
* <p>
|
||||
* If <code>allFrames</code> does not end with <code>bottomFrames</code>, it is considered distinct and all its frames are counted.
|
||||
*
|
||||
* @param allFrames Some stack frames
|
||||
* @param bottomFrames Other stack frames, potentially forming the bottom of the stack of <code>allFrames</code>
|
||||
* @return The number of additional frames in <code>allFrames</code>. In most cases this should be equal to the difference in size.
|
||||
*/
|
||||
// visible for testing
|
||||
static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) {
|
||||
if (allFrames.length < bottomFrames.length) {
|
||||
// if frames had been stacked on top of bottomFrames, allFrames would be larger
|
||||
return allFrames.length;
|
||||
} else {
|
||||
return allFrames.length - commonSuffixLength(allFrames, bottomFrames);
|
||||
}
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
static <T> int commonSuffixLength(T[] set, T[] subset) {
|
||||
Preconditions.checkArgument(set.length >= subset.length);
|
||||
// iterate items backwards as long as they are identical
|
||||
var iterator = reverseStream(subset).iterator();
|
||||
return (int) reverseStream(set).takeWhile(item -> iterator.hasNext() && iterator.next().equals(item)).count();
|
||||
}
|
||||
|
||||
private static <T> Stream<T> reverseStream(T[] array) {
|
||||
|
||||
@@ -44,7 +44,9 @@ public interface IpcCommunicator extends Closeable {
|
||||
}
|
||||
// Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
|
||||
try {
|
||||
return Server.create(socketPaths.iterator().next());
|
||||
final var socketPath = socketPaths.iterator().next();
|
||||
Files.deleteIfExists(socketPath); // ensure path does not exist before creating it
|
||||
return Server.create(socketPath);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to create IPC server", e);
|
||||
return new LoopbackCommunicator();
|
||||
|
||||
@@ -56,11 +56,10 @@ public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
this.screenshot = new Image(in);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac.png").toString());
|
||||
} else {
|
||||
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-win.png").toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
public void chooseFileAndNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Vault", "*.cryptomator"));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.changepassword;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptofs.common.BackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
@@ -83,7 +83,7 @@ public class ChangePasswordController implements FxController {
|
||||
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + BackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.ErrorCode;
|
||||
import org.cryptomator.common.Nullable;
|
||||
|
||||
@@ -25,7 +26,11 @@ public class ErrorController implements FxController {
|
||||
private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
|
||||
private static final String SEARCH_ERRORCODE_DELIM = " OR ";
|
||||
private static final String REPORT_BODY_TEMPLATE = """
|
||||
OS: %s / %s
|
||||
App: %s / %s
|
||||
|
||||
<!-- ✏️ Please describe what happened as accurately as possible. -->
|
||||
|
||||
<!-- 📋 Please also copy and paste the detail text from the error window. -->
|
||||
""";
|
||||
|
||||
@@ -34,16 +39,18 @@ public class ErrorController implements FxController {
|
||||
private final ErrorCode errorCode;
|
||||
private final Scene previousScene;
|
||||
private final Stage window;
|
||||
private final Environment environment;
|
||||
|
||||
private BooleanProperty copiedDetails = new SimpleBooleanProperty();
|
||||
|
||||
@Inject
|
||||
ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window) {
|
||||
ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window, Environment environment) {
|
||||
this.application = application;
|
||||
this.stackTrace = stackTrace;
|
||||
this.errorCode = errorCode;
|
||||
this.previousScene = previousScene;
|
||||
this.window = window;
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -67,7 +74,12 @@ public class ErrorController implements FxController {
|
||||
@FXML
|
||||
public void reportError() {
|
||||
var title = URLEncoder.encode(getErrorCode(), StandardCharsets.UTF_8);
|
||||
var body = URLEncoder.encode(REPORT_BODY_TEMPLATE, StandardCharsets.UTF_8);
|
||||
var enhancedTemplate = String.format(REPORT_BODY_TEMPLATE, //
|
||||
System.getProperty("os.name"), //
|
||||
System.getProperty("os.version"), //
|
||||
environment.getAppVersion().orElse("undefined"), //
|
||||
environment.getBuildNumber().orElse("undefined"));
|
||||
var body = URLEncoder.encode(enhancedTemplate, StandardCharsets.UTF_8);
|
||||
application.getHostServices().showDocument(REPORT_URL_FORMAT.formatted(title, body));
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,11 @@ public class UserInteractionLock<E extends Enum> {
|
||||
private volatile E state;
|
||||
|
||||
public UserInteractionLock(E initialValue) {
|
||||
state = initialValue;
|
||||
this.state = initialValue;
|
||||
}
|
||||
|
||||
public synchronized void reset(E value) {
|
||||
this.state = value;
|
||||
}
|
||||
|
||||
public void interacted(E result) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
import com.google.common.base.Preconditions;
|
||||
import dagger.Lazy;
|
||||
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;
|
||||
@@ -20,6 +21,7 @@ import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
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;
|
||||
@@ -61,14 +63,24 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
@Override
|
||||
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
|
||||
|
||||
try {
|
||||
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
|
||||
if (!Files.exists(filePath)) {
|
||||
filePath = getAlternateMasterkeyFilePath();
|
||||
}
|
||||
CharSequence passphrase = getPassphrase();
|
||||
return masterkeyFileAccess.load(filePath, passphrase);
|
||||
var masterkey = masterkeyFileAccess.load(filePath, passphrase);
|
||||
//backup
|
||||
if (filePath.startsWith(vault.getPath())) {
|
||||
try {
|
||||
BackupHelper.attemptBackup(filePath);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to create backup for masterkey file.");
|
||||
}
|
||||
} else {
|
||||
LOG.info("Masterkey file not stored inside vault. Not creating a backup.");
|
||||
}
|
||||
return masterkey;
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new UnlockCancelledException("Unlock interrupted", e);
|
||||
|
||||
@@ -35,7 +35,13 @@ public class LockForcedController implements FxController {
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void confirmForcedLock() {
|
||||
public void retry() {
|
||||
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.RETRY);
|
||||
window.close();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void force() {
|
||||
forceLockDecisionLock.interacted(LockModule.ForceLockDecision.FORCE);
|
||||
window.close();
|
||||
}
|
||||
@@ -54,4 +60,8 @@ public class LockForcedController implements FxController {
|
||||
return vault.getDisplayName();
|
||||
}
|
||||
|
||||
public boolean isForceSupported() {
|
||||
return vault.supportsForcedUnmount();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ abstract class LockModule {
|
||||
|
||||
enum ForceLockDecision {
|
||||
CANCEL,
|
||||
RETRY,
|
||||
FORCE;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,20 +51,26 @@ public class LockWorkflow extends Task<Void> {
|
||||
|
||||
@Override
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
|
||||
try {
|
||||
vault.lock(false);
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
|
||||
var decision = askUserForAction();
|
||||
switch (decision) {
|
||||
case FORCE -> vault.lock(true);
|
||||
case CANCEL -> cancel(false);
|
||||
}
|
||||
}
|
||||
lock(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
private void lock(boolean forced) throws InterruptedException {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private LockModule.ForceLockDecision askUserForAction() throws InterruptedException {
|
||||
forceLockDecisionLock.reset(null);
|
||||
// show forcedLock dialogue ...
|
||||
Platform.runLater(() -> {
|
||||
lockWindow.setScene(lockForcedScene.get());
|
||||
|
||||
@@ -34,10 +34,10 @@ public class SupporterCertificateController implements FxController {
|
||||
public void initialize() {
|
||||
supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null));
|
||||
supporterCertificateField.textProperty().addListener(this::registrationKeyChanged);
|
||||
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::removeWhitespaces));
|
||||
}
|
||||
|
||||
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
|
||||
private TextFormatter.Change removeWhitespaces(TextFormatter.Change change) {
|
||||
if (change.isContentChange()) {
|
||||
var strippedText = CharMatcher.whitespace().removeFrom(change.getText());
|
||||
change.setText(strippedText);
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.hash.Hashing;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptofs.common.BackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
@@ -86,7 +86,7 @@ public class RecoveryKeyFactory {
|
||||
if (Files.exists(masterkeyPath)) {
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
// TODO: deduplicate with ChangePasswordController:
|
||||
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + BackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
|
||||
|
||||
@@ -2,9 +2,6 @@ package org.cryptomator.ui.traymenu;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.integrations.uiappearance.Theme;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -12,38 +9,25 @@ import javax.inject.Inject;
|
||||
import java.awt.AWTException;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.util.Optional;
|
||||
|
||||
@TrayMenuScoped
|
||||
public class TrayIconController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TrayIconController.class);
|
||||
|
||||
private final TrayImageFactory imageFactory;
|
||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||
private final TrayMenuController trayMenuController;
|
||||
private final TrayIcon trayIcon;
|
||||
private volatile boolean initialized;
|
||||
|
||||
@Inject
|
||||
TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController, Optional<UiAppearanceProvider> appearanceProvider) {
|
||||
TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController) {
|
||||
this.trayMenuController = trayMenuController;
|
||||
this.imageFactory = imageFactory;
|
||||
this.appearanceProvider = appearanceProvider;
|
||||
this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu());
|
||||
}
|
||||
|
||||
public synchronized void initializeTrayIcon() throws IllegalStateException {
|
||||
Preconditions.checkState(!initialized);
|
||||
|
||||
appearanceProvider.ifPresent(appearanceProvider -> {
|
||||
try {
|
||||
appearanceProvider.addListener(this::systemInterfaceThemeChanged);
|
||||
} catch (UiAppearanceException e) {
|
||||
LOG.error("Failed to enable automatic tray icon theme switching.");
|
||||
}
|
||||
});
|
||||
|
||||
trayIcon.setImageAutoSize(true);
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
trayIcon.addActionListener(trayMenuController::showMainWindow);
|
||||
@@ -61,10 +45,6 @@ public class TrayIconController {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private void systemInterfaceThemeChanged(Theme theme) {
|
||||
trayIcon.setImage(imageFactory.loadImage()); // TODO refactor "theme" is re-queried in loadImage()
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,7 @@ class TrayImageFactory {
|
||||
}
|
||||
|
||||
private String getMacResourceName() {
|
||||
var theme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT);
|
||||
return switch (theme) {
|
||||
case DARK -> "/img/tray_icon_mac_white.png";
|
||||
case LIGHT -> "/img/tray_icon_mac_black.png";
|
||||
};
|
||||
return "/img/tray_icon_mac.png";
|
||||
}
|
||||
|
||||
private String getWinOrLinuxResourceName() {
|
||||
|
||||
@@ -47,7 +47,7 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
public void initialize() {
|
||||
vaultName.textProperty().set(vault.getVaultSettings().displayName().get());
|
||||
vaultName.focusedProperty().addListener(this::trimVaultNameOnFocusLoss);
|
||||
vaultName.setTextFormatter(new TextFormatter<>(this::removeWhitespaces));
|
||||
vaultName.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
|
||||
actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
|
||||
actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
|
||||
@@ -63,7 +63,7 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private TextFormatter.Change removeWhitespaces(TextFormatter.Change change) {
|
||||
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
|
||||
if (change.isContentChange() && change.getControlNewText().length() > VAULTNAME_TRUNCATE_THRESHOLD) {
|
||||
return null; // reject any change that would lead to a text exceeding threshold
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user