Merge branch 'develop' into feature/autoLock

This commit is contained in:
Sebastian Stenzel
2021-06-02 14:31:28 +02:00
committed by GitHub
142 changed files with 4252 additions and 998 deletions
@@ -6,10 +6,10 @@ 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.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
@@ -33,13 +33,6 @@ import java.util.ResourceBundle;
@Module
public abstract class AddVaultModule {
@Provides
@AddVaultWizardScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@AddVaultWizardWindow
@AddVaultWizardScoped
@@ -167,8 +160,8 @@ public abstract class AddVaultModule {
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
return new NewPasswordController(resourceBundle, strengthRater);
}
@Binds
@@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController {
Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault);
window.setScene(successScene.get());
} catch (NoSuchFileException e) {
} catch (IOException e) {
LOG.error("Failed to open existing vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
@@ -1,10 +1,10 @@
package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -15,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ResourceBundle;
@@ -43,55 +43,74 @@ public class CreateNewVaultLocationController implements FxController {
private final Stage window;
private final Lazy<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene;
private final ErrorComponent.Builder errorComponent;
private final LocationPresets locationPresets;
private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName;
private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath;
private final BooleanProperty usePresetPath;
private final StringProperty warningText;
private final StringProperty statusText;
private final ObjectProperty<Node> statusGraphic;
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
//FXML
public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton;
public RadioButton megaRadioButton;
public RadioButton pcloudRadioButton;
public RadioButton customRadioButton;
public Label vaultPathStatus;
public FontAwesome5IconView goodLocation;
public FontAwesome5IconView badLocation;
@Inject
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ErrorComponent.Builder errorComponent, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
this.window = window;
this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene;
this.errorComponent = errorComponent;
this.locationPresets = locationPresets;
this.vaultPath = vaultPath;
this.vaultName = vaultName;
this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
this.usePresetPath = new SimpleBooleanProperty();
this.warningText = new SimpleStringProperty();
this.statusText = new SimpleStringProperty();
this.statusGraphic = new SimpleObjectProperty<>();
}
private boolean isValidVaultPath() {
return vaultPath.get() != null && Files.notExists(vaultPath.get());
private boolean validateVaultPathAndSetStatus() {
final Path p = vaultPath.get();
if (p == null) {
statusText.set("Error: Path is NULL.");
statusGraphic.set(badLocation);
return false;
} else if (!Files.exists(p.getParent())) {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
statusGraphic.set(badLocation);
return false;
} else if (!Files.isWritable(p.getParent())) {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
statusGraphic.set(badLocation);
return false;
} else if (!Files.notExists(p)) {
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
statusGraphic.set(badLocation);
return false;
} else {
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
statusGraphic.set(goodLocation);
return true;
}
}
@FXML
public void initialize() {
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
vaultPath.addListener(this::vaultPathDidChange);
}
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
if (!Files.notExists(newValue)) {
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} else {
warningText.set(null);
}
}
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
@@ -103,6 +122,10 @@ public class CreateNewVaultLocationController implements FxController {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
} else if (megaRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
} else if (pcloudRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
} else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get()));
}
@@ -115,21 +138,10 @@ public class CreateNewVaultLocationController implements FxController {
@FXML
public void next() {
try {
// check if we have write access AND the vaultPath doesn't already exist:
assert Files.isDirectory(vaultPath.get().getParent());
Path createdDir = Files.createDirectory(vaultPath.get());
Files.delete(createdDir); // assert: dir exists and is empty
if (validateVaultPathAndSetStatus()) {
window.setScene(choosePasswordScene.get());
} catch (FileAlreadyExistsException e) {
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
} catch (NoSuchFileException e) {
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
} catch (IOException e) {
LOG.error("Failed to create and delete directory at chosen vault path.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} else {
validVaultPath.invalidate();
}
}
@@ -179,19 +191,27 @@ public class CreateNewVaultLocationController implements FxController {
return usePresetPath.get();
}
public StringProperty warningTextProperty() {
return warningText;
public BooleanBinding anyRadioButtonSelectedProperty() {
return predefinedLocationToggler.selectedToggleProperty().isNotNull();
}
public String getWarningText() {
return warningText.get();
public boolean isAnyRadioButtonSelected() {
return anyRadioButtonSelectedProperty().get();
}
public BooleanBinding showWarningProperty() {
return warningText.isNotEmpty();
public StringProperty statusTextProperty() {
return statusText;
}
public boolean isShowWarning() {
return showWarningProperty().get();
public String getStatusText() {
return statusText.get();
}
public ObjectProperty<Node> statusGraphicProperty() {
return statusGraphic;
}
public Node getStatusGraphic() {
return statusGraphic.get();
}
}
@@ -5,11 +5,18 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultCipherCombo;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,7 +24,6 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@@ -31,13 +37,13 @@ import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.security.SecureRandom;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
@@ -48,6 +54,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
private final Stage window;
private final Lazy<Scene> chooseLocationScene;
@@ -62,8 +69,9 @@ public class CreateNewVaultPasswordController implements FxController {
private final StringProperty recoveryKeyProperty;
private final VaultListManager vaultListManager;
private final ResourceBundle resourceBundle;
private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState;
@@ -71,9 +79,10 @@ public class CreateNewVaultPasswordController implements FxController {
public ToggleGroup recoveryKeyChoice;
public Toggle showRecoveryKey;
public Toggle skipRecoveryKey;
public NewPasswordController newPasswordSceneController;
@Inject
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator) {
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window;
this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene;
@@ -87,8 +96,9 @@ public class CreateNewVaultPasswordController implements FxController {
this.recoveryKeyProperty = recoveryKey;
this.vaultListManager = vaultListManager;
this.resourceBundle = resourceBundle;
this.password = password;
this.readmeGenerator = readmeGenerator;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
@@ -96,8 +106,11 @@ public class CreateNewVaultPasswordController implements FxController {
@FXML
public void initialize() {
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
window.setOnHiding(event -> {
newPasswordSceneController.passwordField.wipe();
newPasswordSceneController.reenterField.wipe();
});
}
@FXML
@@ -130,8 +143,8 @@ public class CreateNewVaultPasswordController implements FxController {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
initializeVault(pathToVault);
return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters());
}).onSuccess(recoveryKey -> {
initializationSucceeded(pathToVault);
recoveryKeyProperty.set(recoveryKey);
@@ -148,7 +161,7 @@ public class CreateNewVaultPasswordController implements FxController {
Path pathToVault = vaultPathProperty.get();
processing.set(true);
Tasks.create(() -> {
initializeVault(pathToVault, password.get());
initializeVault(pathToVault);
}).onSuccess(() -> {
initializationSucceeded(pathToVault);
window.setScene(successScene.get());
@@ -160,24 +173,35 @@ public class CreateNewVaultPasswordController implements FxController {
}).runOnce(executor);
}
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withPassphrase(passphrase) //
.withFlags(Collections.emptySet()) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
private void initializeVault(Path path) throws IOException {
// 1. write masterkey:
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
try (Masterkey masterkey = Masterkey.generate(csprng)) {
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters());
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
// 2. initialize vault:
try {
MasterkeyLoader loader = ignored -> masterkey.clone();
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
// 3. write vault-internal readme file:
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
}
} catch (CryptoException e) {
throw new IOException("Failed initialize vault.", e);
}
}
// 4. write vault-external readme file:
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
}
LOG.info("Created vault at {}", path);
}
@@ -185,7 +209,7 @@ public class CreateNewVaultPasswordController implements FxController {
try {
Vault newVault = vaultListManager.add(pathToVault);
vaultProperty.set(newVault);
} catch (NoSuchFileException e) {
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@@ -16,15 +16,21 @@ public class LocationPresets {
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final ReadOnlyObjectProperty<Path> megaLocation;
private final ReadOnlyObjectProperty<Path> pcloudLocation;
private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive;
private final BooleanBinding foundMega;
private final BooleanBinding foundPcloud;
@Inject
public LocationPresets() {
@@ -32,10 +38,14 @@ public class LocationPresets {
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
this.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull();
this.foundMega = megaLocation.isNotNull();
this.foundPcloud = pcloudLocation.isNotNull();
}
private static Path existingWritablePath(String... candidates) {
@@ -122,4 +132,36 @@ public class LocationPresets {
return foundOnedrive.get();
}
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
return megaLocation;
}
public Path getMegaLocation() {
return megaLocation.get();
}
public BooleanBinding foundMegaProperty() {
return foundMega;
}
public boolean isFoundMega() {
return foundMega.get();
}
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
return pcloudLocation;
}
public Path getPcloudLocation() {
return pcloudLocation.get();
}
public BooleanBinding foundPcloudProperty() {
return foundPcloud;
}
public boolean isFoundPcloud() {
return foundPcloud.get();
}
}
@@ -2,29 +2,33 @@ package org.cryptomator.ui.changepassword;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.NewPasswordController;
import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.nio.CharBuffer;
import java.util.Optional;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.security.SecureRandom;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@ChangePasswordScoped
@@ -34,29 +38,36 @@ public class ChangePasswordController implements FxController {
private final Stage window;
private final Vault vault;
private final ObjectProperty<CharSequence> newPassword;
private final ErrorComponent.Builder errorComponent;
private final KeychainManager keychain;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox;
public Button finishButton;
public NewPasswordController newPasswordController;
@Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
this.window = window;
this.vault = vault;
this.newPassword = newPassword;
this.errorComponent = errorComponent;
this.keychain = keychain;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
}
@FXML
public void initialize() {
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
window.setOnHiding(event -> {
oldPasswordField.wipe();
newPasswordController.passwordField.wipe();
newPasswordController.reenterField.wipe();
});
}
@FXML
@@ -67,24 +78,31 @@ public class ChangePasswordController implements FxController {
@FXML
public void finish() {
try {
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
CharSequence oldPassphrase = oldPasswordField.getCharacters();
CharSequence newPassphrase = newPasswordController.passwordField.getCharacters();
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);
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());
window.close();
updatePasswordInSystemkeychain();
} catch (IOException e) {
LOG.error("IO error occured during password change. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
window.close();
} catch (InvalidPassphraseException e) {
Animations.createShakeWindowAnimation(window).play();
oldPasswordField.selectAll();
oldPasswordField.requestFocus();
} catch (IOException | CryptoException e) {
LOG.error("Password change failed. Unable to perform operation.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
}
private void updatePasswordInSystemkeychain() {
if (keychain.isSupported()) {
if (keychain.isSupported() && !keychain.isLocked()) {
try {
keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters());
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
} catch (KeychainAccessException e) {
LOG.error("Failed to update password in system keychain.", e);
@@ -5,10 +5,10 @@ import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
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.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
@@ -16,8 +16,6 @@ import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
@@ -27,13 +25,6 @@ import java.util.ResourceBundle;
@Module
abstract class ChangePasswordModule {
@Provides
@ChangePasswordScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
@Provides
@ChangePasswordWindow
@ChangePasswordScoped
@@ -71,8 +62,8 @@ abstract class ChangePasswordModule {
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
return new NewPasswordController(resourceBundle, strengthRater);
}
}
@@ -11,6 +11,8 @@ public enum FxmlFile {
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
ERROR("/fxml/error.fxml"), //
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
HEALTH_START("/fxml/health_start.fxml"), //
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
LOCK_FORCED("/fxml/lock_forced.fxml"), //
LOCK_FAILED("/fxml/lock_failed.fxml"), //
MAIN_WINDOW("/fxml/main_window.fxml"), //
@@ -26,8 +28,9 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK("/fxml/unlock.fxml"),
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
VAULT_STATISTICS("/fxml/stats.fxml"), //
@@ -8,7 +8,8 @@ import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
@@ -18,8 +19,8 @@ public class NewPasswordController implements FxController {
private final ResourceBundle resourceBundle;
private final PasswordStrengthUtil strengthRater;
private final ObjectProperty<CharSequence> password;
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
public NiceSecurePasswordField passwordField;
public NiceSecurePasswordField reenterField;
@@ -31,10 +32,9 @@ public class NewPasswordController implements FxController {
public FontAwesome5IconView passwordMatchCheckmark;
public FontAwesome5IconView passwordMatchCross;
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
this.resourceBundle = resourceBundle;
this.strengthRater = strengthRater;
this.password = password;
}
@FXML
@@ -44,7 +44,7 @@ public class NewPasswordController implements FxController {
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
@@ -54,6 +54,7 @@ public class NewPasswordController implements FxController {
reenterField.textProperty().addListener(this::passwordsDidChange);
}
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
if (passwordField.getCharacters().length() == 0) {
return null;
@@ -67,17 +68,19 @@ public class NewPasswordController implements FxController {
}
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
password.set(passwordField.getCharacters());
} else {
password.set("");
if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
passwordsMatchAndSufficient.setValue(true);
}
}
private boolean hasSamePasswordInBothFields() {
private boolean passwordFieldsMatch() {
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
}
public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
return passwordsMatchAndSufficient.getReadOnlyProperty();
}
/* Getter/Setter */
public IntegerProperty passwordStrengthProperty() {
@@ -1,5 +1,6 @@
package org.cryptomator.ui.common;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
@@ -175,24 +176,29 @@ public class VaultService {
}
@Override
protected Vault call() throws Volume.VolumeException {
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
vault.lock(forced);
return vault;
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
}
@Override
protected void succeeded() {
vault.setState(VaultState.LOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.UNLOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
}
@Override
protected void cancelled() {
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
}
}
@@ -6,8 +6,10 @@ package org.cryptomator.ui.controls;
public enum FontAwesome5Icon {
ANCHOR("\uF13D"), //
ARROW_UP("\uF062"), //
BAN("\uF05E"), //
BUG("\uF188"), //
CHECK("\uF00C"), //
CLOCK("\uF017"), //
COG("\uF013"), //
COGS("\uF085"), //
COPY("\uF0C5"), //
@@ -12,6 +12,7 @@ public class FormattedLabel extends Label {
private final StringProperty format = new SimpleStringProperty("");
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
private final ObjectProperty<Object> arg2 = new SimpleObjectProperty<>();
// add arg2, arg3, ... on demand
public FormattedLabel() {
@@ -19,11 +20,11 @@ public class FormattedLabel extends Label {
}
protected StringBinding createStringBinding() {
return Bindings.createStringBinding(this::updateText, format, arg1);
return Bindings.createStringBinding(this::updateText, format, arg1, arg2);
}
private String updateText() {
return String.format(format.get(), arg1.get());
return String.format(format.get(), arg1.get(), arg2.get());
}
/* Observables */
@@ -51,4 +52,16 @@ public class FormattedLabel extends Label {
public void setArg1(Object arg1) {
this.arg1.set(arg1);
}
public ObjectProperty<Object> arg2Property() {
return arg2;
}
public Object getArg2() {
return arg2.get();
}
public void setArg2(Object arg2) {
this.arg2.set(arg2);
}
}
@@ -123,7 +123,7 @@ public class SecurePasswordField extends TextField {
}
private void updateCapsLocked() {
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
//TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680)
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
}
@@ -14,11 +14,13 @@ import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent;
@@ -47,8 +49,9 @@ public class FxApplication extends Application {
private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow;
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
private final ErrorComponent.Builder errorWindowBuilder;
private final Optional<TrayIntegrationProvider> trayIntegration;
private final Optional<UiAppearanceProvider> appearanceProvider;
private final VaultService vaultService;
@@ -60,13 +63,14 @@ public class FxApplication extends Application {
private final ScheduledExecutorService scheduledExecutorService;
@Inject
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) {
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder, VaultListManager vaultListManager, ScheduledExecutorService scheduledExecutorService) {
this.settings = settings;
this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow;
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
this.lockWindowBuilderProvider = lockWindowBuilderProvider;
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
this.quitWindow = quitWindow;
this.errorWindowBuilder = errorWindowBuilder;
this.trayIntegration = trayIntegration;
this.appearanceProvider = appearanceProvider;
this.vaultService = vaultService;
@@ -93,7 +97,7 @@ public class FxApplication extends Application {
}
private void hasVisibleStagesChanged(@SuppressWarnings("unused") ObservableValue<? extends Boolean> observableValue, @SuppressWarnings("unused") boolean oldValue, boolean newValue) {
LOG.warn("has visible stages: {}", newValue);
LOG.debug("has visible stages: {}", newValue);
if (newValue) {
trayIntegration.ifPresent(TrayIntegrationProvider::restoredFromTray);
} else {
@@ -120,16 +124,24 @@ public class FxApplication extends Application {
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> {
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
}
});
checkAutolock(vault, owner);
}
public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> {
lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
} else {
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
}
});
}
@@ -15,6 +15,7 @@ import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;
import javafx.util.Duration;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
@@ -39,8 +40,13 @@ public abstract class UpdateCheckerModule {
@Provides
@FxApplicationScoped
static HttpClient provideHttpClient() {
return HttpClient.newHttpClient();
static Optional<HttpClient> provideHttpClient() {
try {
return Optional.of(HttpClient.newHttpClient());
} catch (UncheckedIOException e) {
LOG.error("HttpClient for update check cannot be created.", e);
return Optional.empty();
}
}
@Provides
@@ -66,11 +72,20 @@ public abstract class UpdateCheckerModule {
@Provides
@FxApplicationScoped
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
ScheduledService<String> service = new ScheduledService<>() {
@Override
protected Task<String> createTask() {
return new UpdateCheckerTask(httpClient, checkForUpdatesRequest);
if (httpClient.isPresent()) {
return new UpdateCheckerTask(httpClient.get(), checkForUpdatesRequest);
} else {
return new Task<>() {
@Override
protected String call() {
throw new NullPointerException("No HttpClient present.");
}
};
}
}
};
service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
@@ -0,0 +1,36 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import com.google.common.base.Suppliers;
import dagger.Lazy;
import javax.inject.Inject;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.function.Supplier;
public class BatchService extends Service<Void> {
private final Iterator<HealthCheckTask> remainingTasks;
@Inject
public BatchService(Iterable<HealthCheckTask> tasks) {
this.remainingTasks = tasks.iterator();
}
@Override
protected Task<Void> createTask() {
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
return remainingTasks.next();
}
@Override
protected void succeeded() {
if (remainingTasks.hasNext()) {
this.restart();
}
}
}
@@ -0,0 +1,170 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.EasyObservableList;
import com.tobiasdiez.easybind.Subscription;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.concurrent.Worker;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import java.util.function.Function;
import java.util.stream.Stream;
@HealthCheckScoped
public class CheckDetailController implements FxController {
private final EasyObservableList<DiagnosticResult> results;
private final OptionalBinding<Worker.State> taskState;
private final Binding<String> taskName;
private final Binding<Number> taskDuration;
private final ResultListCellFactory resultListCellFactory;
private final Binding<Boolean> taskRunning;
private final Binding<Boolean> taskScheduled;
private final Binding<Boolean> taskFinished;
private final Binding<Boolean> taskNotStarted;
private final Binding<Boolean> taskSucceeded;
private final Binding<Boolean> taskFailed;
private final Binding<Boolean> taskCancelled;
private final Binding<Number> countOfWarnSeverity;
private final Binding<Number> countOfCritSeverity;
public ListView<DiagnosticResult> resultsListView;
private Subscription resultSubscription;
@Inject
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory) {
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L);
this.resultListCellFactory = resultListCellFactory;
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
selectedTask.addListener(this::selectedTaskChanged);
}
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
if (resultSubscription != null) {
resultSubscription.unsubscribe();
}
if (newValue != null) {
resultSubscription = EasyBind.bindContent(results, newValue.results());
}
}
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
return stream -> stream.filter(item -> severity.equals(item.getServerity())).count();
}
@FXML
public void initialize() {
resultsListView.setItems(results);
resultsListView.setCellFactory(resultListCellFactory);
}
/* Getter/Setter */
public String getTaskName() {
return taskName.getValue();
}
public Binding<String> taskNameProperty() {
return taskName;
}
public Number getTaskDuration() {
return taskDuration.getValue();
}
public Binding<Number> taskDurationProperty() {
return taskDuration;
}
public long getCountOfWarnSeverity() {
return countOfWarnSeverity.getValue().longValue();
}
public Binding<Number> countOfWarnSeverityProperty() {
return countOfWarnSeverity;
}
public long getCountOfCritSeverity() {
return countOfCritSeverity.getValue().longValue();
}
public Binding<Number> countOfCritSeverityProperty() {
return countOfCritSeverity;
}
public boolean isTaskRunning() {
return taskRunning.getValue();
}
public Binding<Boolean> taskRunningProperty() {
return taskRunning;
}
public boolean isTaskFinished() {
return taskFinished.getValue();
}
public Binding<Boolean> taskFinishedProperty() {
return taskFinished;
}
public boolean isTaskScheduled() {
return taskScheduled.getValue();
}
public Binding<Boolean> taskScheduledProperty() {
return taskScheduled;
}
public boolean isTaskNotStarted() {
return taskNotStarted.getValue();
}
public Binding<Boolean> taskNotStartedProperty() {
return taskNotStarted;
}
public boolean isTaskSucceeded() {
return taskSucceeded.getValue();
}
public Binding<Boolean> taskSucceededProperty() {
return taskSucceeded;
}
public boolean isTaskFailed() {
return taskFailed.getValue();
}
public Binding<Boolean> taskFailedProperty() {
return taskFailed;
}
public boolean isTaskCancelled() {
return taskCancelled.getValue();
}
public Binding<Boolean> taskCancelledProperty() {
return taskCancelled;
}
}
@@ -0,0 +1,64 @@
package org.cryptomator.ui.health;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
class CheckListCell extends ListCell<HealthCheckTask> {
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
CheckListCell() {
setPadding(new Insets(6));
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
}
@Override
protected void updateItem(HealthCheckTask item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
textProperty().bind(item.titleProperty());
item.stateProperty().addListener(this::stateChanged);
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty()));
stateIcon.setGlyph(glyphForState(item.getState()));
} else {
textProperty().unbind();
graphicProperty().unbind();
setGraphic(null);
setText(null);
}
}
private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
stateIcon.setGlyph(glyphForState(newState));
stateIcon.setVisible(true);
}
private Node graphicForState(Worker.State state) {
return switch (state) {
case READY -> null;
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
};
}
private FontAwesome5Icon glyphForState(Worker.State state) {
return switch (state) {
case READY -> FontAwesome5Icon.COG; //just a placeholder
case SCHEDULED -> FontAwesome5Icon.CLOCK;
case RUNNING -> FontAwesome5Icon.SPINNER;
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
case CANCELLED -> FontAwesome5Icon.BAN;
case SUCCEEDED -> FontAwesome5Icon.CHECK;
};
}
}
@@ -0,0 +1,180 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import com.tobiasdiez.easybind.EasyBind;
import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Worker;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@HealthCheckScoped
public class CheckListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
private final Stage window;
private final ObservableList<HealthCheckTask> tasks;
private final ReportWriter reportWriter;
private final ExecutorService executorService;
private final ObjectProperty<HealthCheckTask> selectedTask;
private final Lazy<ErrorComponent.Builder> errorComponenBuilder;
private final SimpleObjectProperty<Worker<?>> runningTask;
private final Binding<Boolean> running;
private final Binding<Boolean> finished;
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
private final IntegerProperty numberOfPickedChecks;
private final BooleanBinding anyCheckSelected;
private final BooleanProperty showResultScreen;
/* FXML */
public ListView<HealthCheckTask> checksListView;
@Inject
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponenBuilder) {
this.window = window;
this.tasks = FXCollections.observableArrayList(tasks.get());
this.reportWriter = reportWriteTask;
this.executorService = executorService;
this.selectedTask = selectedTask;
this.errorComponenBuilder = errorComponenBuilder;
this.runningTask = new SimpleObjectProperty<>();
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
this.listPickIndicators = new HashMap<>();
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
this.tasks.forEach(task -> {
var entrySelectedProp = new SimpleBooleanProperty(false);
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
listPickIndicators.put(task, entrySelectedProp);
});
this.anyCheckSelected = selectedTask.isNotNull();
this.showResultScreen = new SimpleBooleanProperty(false);
}
@FXML
public void initialize() {
checksListView.setItems(tasks);
checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter<HealthCheckTask>() {
@Override
public String toString(HealthCheckTask object) {
return object.getTitle();
}
@Override
public HealthCheckTask fromString(String string) {
return null;
}
}));
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
}
@FXML
public void toggleSelectAll(ActionEvent event) {
if (event.getSource() instanceof CheckBox c) {
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
}
}
@FXML
public void runSelectedChecks() {
Preconditions.checkState(runningTask.get() == null);
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
var batchService = new BatchService(batch);
batchService.setExecutor(executorService);
batchService.start();
runningTask.set(batchService);
showResultScreen.set(true);
checksListView.getSelectionModel().select(batch.get(0));
checksListView.setCellFactory(view -> new CheckListCell());
window.sizeToScene();
}
@FXML
public synchronized void cancelCheck() {
Preconditions.checkState(runningTask.get() != null);
runningTask.get().cancel();
}
@FXML
public void exportResults() {
try {
reportWriter.writeReport(tasks);
} catch (IOException e) {
LOG.error("Failed to write health check report.", e);
errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
}
/* Getter/Setter */
public boolean isRunning() {
return running.getValue();
}
public Binding<Boolean> runningProperty() {
return running;
}
public boolean isFinished() {
return finished.getValue();
}
public Binding<Boolean> finishedProperty() {
return finished;
}
public boolean isAnyCheckSelected() {
return anyCheckSelected.get();
}
public BooleanBinding anyCheckSelectedProperty() {
return anyCheckSelected;
}
public boolean getShowResultScreen() {
return showResultScreen.get();
}
public BooleanProperty showResultScreenProperty() {
return showResultScreen;
}
public int getNumberOfPickedChecks() {
return numberOfPickedChecks.get();
}
public IntegerProperty numberOfPickedChecksProperty() {
return numberOfPickedChecks;
}
}
@@ -0,0 +1,39 @@
package org.cryptomator.ui.health;
import dagger.BindsInstance;
import dagger.Lazy;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javafx.scene.Scene;
import javafx.stage.Stage;
@HealthCheckScoped
@Subcomponent(modules = {HealthCheckModule.class})
public interface HealthCheckComponent {
@HealthCheckWindow
Stage window();
@FxmlScene(FxmlFile.HEALTH_START)
Lazy<Scene> scene();
default Stage showHealthCheckWindow() {
Stage stage = window();
stage.setScene(scene().get());
stage.show();
return stage;
}
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@HealthCheckWindow Vault vault);
HealthCheckComponent build();
}
}
@@ -0,0 +1,141 @@
package org.cryptomator.ui.health;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.ui.common.DefaultSceneFactory;
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.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class HealthCheckModule {
@Provides
@HealthCheckScoped
static AtomicReference<Masterkey> provideMasterkeyRef() {
return new AtomicReference<>();
}
@Provides
@HealthCheckScoped
static AtomicReference<VaultConfig> provideVaultConfigRef() {
return new AtomicReference<>();
}
@Provides
@HealthCheckScoped
static Collection<HealthCheck> provideAvailableHealthChecks() {
return HealthCheck.allChecks();
}
@Provides
@HealthCheckScoped
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
return new SimpleObjectProperty<>();
}
/* Only inject with Lazy-Wrapper!*/
@Provides
@HealthCheckScoped
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@HealthCheckWindow
@HealthCheckScoped
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("health.title"));
stage.setResizable(true);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
return stage;
}
@Provides
@HealthCheckScoped
static ChangeListener<Boolean> provideWindowShowingChangeListener(AtomicReference<Masterkey> masterkey) {
return (observable, wasShowing, isShowing) -> {
if (!isShowing) {
Optional.ofNullable(masterkey.getAndSet(null)).ifPresent(Masterkey::destroy);
}
};
}
@Provides
@FxmlScene(FxmlFile.HEALTH_START)
@HealthCheckScoped
static Scene provideHealthStartScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
}
@Provides
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
@HealthCheckScoped
static Scene provideHealthCheckListScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.HEALTH_CHECK_LIST);
}
@Binds
@IntoMap
@FxControllerKey(StartController.class)
abstract FxController bindStartController(StartController controller);
@Binds
@IntoMap
@FxControllerKey(CheckListController.class)
abstract FxController bindCheckController(CheckListController controller);
@Binds
@IntoMap
@FxControllerKey(CheckDetailController.class)
abstract FxController bindCheckDetailController(CheckDetailController controller);
@Binds
@IntoMap
@FxControllerKey(ResultListCellController.class)
abstract FxController bindResultListCellController(ResultListCellController controller);
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.health;
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 HealthCheckScoped {
}
@@ -0,0 +1,108 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptofs.health.api.HealthCheck;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.MissingResourceException;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.concurrent.CancellationException;
class HealthCheckTask extends Task<Void> {
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
private final Path vaultPath;
private final VaultConfig vaultConfig;
private final Masterkey masterkey;
private final SecureRandom csprng;
private final HealthCheck check;
private final ObservableList<DiagnosticResult> results;
private final LongProperty durationInMillis;
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
this.vaultPath = Objects.requireNonNull(vaultPath);
this.vaultConfig = Objects.requireNonNull(vaultConfig);
this.masterkey = Objects.requireNonNull(masterkey);
this.csprng = Objects.requireNonNull(csprng);
this.check = Objects.requireNonNull(check);
this.results = FXCollections.observableArrayList();
try {
updateTitle(resourceBundle.getString("health." + check.identifier()));
} catch (MissingResourceException e) {
LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier());
updateTitle(check.identifier());
}
this.durationInMillis = new SimpleLongProperty(-1);
}
@Override
protected Void call() {
Instant start = Instant.now();
try (var masterkeyClone = masterkey.clone(); //
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
if (isCancelled()) {
throw new CancellationException();
}
// FIXME: slowdown for demonstration purposes only:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
if (isCancelled()) {
return;
} else {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
Platform.runLater(() -> results.add(result));
});
}
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
return null;
}
@Override
protected void scheduled() {
LOG.info("starting {}", check.identifier());
}
@Override
protected void done() {
LOG.info("finished {}", check.identifier());
}
/* Getter */
public ObservableList<DiagnosticResult> results() {
return results;
}
public HealthCheck getCheck() {
return check;
}
public LongProperty durationInMillisProperty() {
return durationInMillis;
}
public long getDurationInMillis() {
return durationInMillis.get();
}
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.health;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
@interface HealthCheckWindow {
}
@@ -0,0 +1,105 @@
package org.cryptomator.ui.health;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.cryptomator.common.Environment;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
import javafx.concurrent.Worker;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Collection;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
@HealthCheckScoped
public class ReportWriter {
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
private static final String REPORT_HEADER = """
**************************************
* Cryptomator Vault Health Report *
**************************************
Analyzed vault: %s (Current name "%s")
Vault storage path: %s
""";
private static final String REPORT_CHECK_HEADER = """
Check %s
------------------------------
""";
private static final String REPORT_CHECK_RESULT = "%8s - %s\n";
private static final DateTimeFormatter TIME_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault());
private final Vault vault;
private final VaultConfig vaultConfig;
private final Application application;
private final Path exportDestination;
@Inject
public ReportWriter(@HealthCheckWindow Vault vault, AtomicReference<VaultConfig> vaultConfigRef, Application application, Environment env) {
this.vault = vault;
this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
this.application = application;
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
}
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
for (var task : tasks) {
if (task.getState() == Worker.State.READY) {
LOG.debug("Skipping not performed check {}.", task.getCheck().identifier());
continue;
}
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier()));
switch (task.getState()) {
case SUCCEEDED -> {
writer.write("STATUS: SUCCESS\nRESULTS:\n");
for (var result : task.results()) {
writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString()));
}
}
case CANCELLED -> writer.write("STATUS: CANCELED\n");
case FAILED -> {
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier());
writer.write(prepareFailureMsg(task));
}
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
}
}
}
reveal();
}
private String prepareFailureMsg(HealthCheckTask task) {
if (task.getException() != null) {
return ExceptionUtils.getStackTrace(task.getException()) //
.lines() //
.map(line -> "\t\t" + line + "\n") //
.collect(Collectors.joining());
} else {
return "Unknown reason of failure.";
}
}
private void reveal() {
application.getHostServices().showDocument(exportDestination.getParent().toUri().toString());
}
}
@@ -0,0 +1,47 @@
package org.cryptomator.ui.health;
import com.google.common.base.Preconditions;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.cryptolib.api.Masterkey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.scene.control.Alert;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
class ResultFixApplier {
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
private final VaultConfig vaultConfig;
@Inject
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
this.vaultPath = vault.getPath();
this.masterkey = masterkeyRef.get();
this.vaultConfig = vaultConfigRef.get();
this.csprng = csprng;
}
public void fix(DiagnosticResult result) {
Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result");
try (var masterkeyClone = masterkey.clone(); //
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
} catch (Exception e) {
LOG.error("Failed to apply fix", e);
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
alert.showAndWait();
//TODO: real error/not supported handling
}
}
}
@@ -0,0 +1,92 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
// unscoped because each cell needs its own controller
public class ResultListCellController implements FxController {
private final ResultFixApplier fixApplier;
private final ObjectProperty<DiagnosticResult> result;
private final Binding<String> description;
public FontAwesome5IconView iconView;
public Button actionButton;
@Inject
public ResultListCellController(ResultFixApplier fixApplier) {
this.result = new SimpleObjectProperty<>(null);
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
this.fixApplier = fixApplier;
result.addListener(this::updateCellContent);
}
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
iconView.getStyleClass().clear();
actionButton.setVisible(false);
//TODO: see comment in case WARN
actionButton.setManaged(false);
switch (newVal.getServerity()) {
case INFO -> {
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
iconView.getStyleClass().add("glyph-icon-muted");
}
case GOOD -> {
iconView.setGlyph(FontAwesome5Icon.CHECK);
iconView.getStyleClass().add("glyph-icon-primary");
}
case WARN -> {
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
iconView.getStyleClass().add("glyph-icon-orange");
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
// before both are not fix, do not show the button
//actionButton.setVisible(true);
}
case CRITICAL -> {
iconView.setGlyph(FontAwesome5Icon.TIMES);
iconView.getStyleClass().add("glyph-icon-red");
}
}
}
@FXML
public void runResultAction() {
final var realResult = result.get();
if (realResult != null) {
fixApplier.fix(realResult);
}
}
/* Getter & Setter */
public DiagnosticResult getResult() {
return result.get();
}
public void setResult(DiagnosticResult result) {
this.result.set(result);
}
public ObjectProperty<DiagnosticResult> resultProperty() {
return result;
}
public String getDescription() {
return description.getValue();
}
public Binding<String> descriptionProperty() {
return description;
}
}
@@ -0,0 +1,60 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.util.Callback;
import java.io.IOException;
import java.io.UncheckedIOException;
@HealthCheckScoped
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
private final FxmlLoaderFactory fxmlLoaders;
@Inject
ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
this.fxmlLoaders = fxmlLoaders;
}
@Override
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
} catch (IOException e) {
throw new UncheckedIOException("Failed to load /fxml/health_result_listcell.fxml.", e);
}
}
private static class Cell extends ListCell<DiagnosticResult> {
private final Parent node;
private final ResultListCellController controller;
public Cell(Parent node, ResultListCellController controller) {
this.node = node;
this.controller = controller;
}
@Override
protected void updateItem(DiagnosticResult item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
controller.setResult(item);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(node);
}
}
}
}
@@ -0,0 +1,127 @@
package org.cryptomator.ui.health;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig;
import org.cryptomator.cryptofs.VaultConfigLoadException;
import org.cryptomator.cryptofs.VaultKeyInvalidException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
public class StartController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
private final Stage window;
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final KeyLoadingStrategy keyLoadingStrategy;
private final ExecutorService executor;
private final AtomicReference<Masterkey> masterkeyRef;
private final AtomicReference<VaultConfig> vaultConfigRef;
private final Lazy<Scene> checkScene;
private final Lazy<ErrorComponent.Builder> errorComponent;
/* FXML */
@Inject
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
this.window = window;
this.keyLoadingStrategy = keyLoadingStrategy;
this.executor = executor;
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
//TODO: this is ugly
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
//or: load vault config in a previous step to see if it is loadable.
VaultConfig.UnverifiedVaultConfig tmp;
try {
tmp = vault.getUnverifiedVaultConfig();
} catch (IOException e) {
e.printStackTrace();
tmp = null;
}
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
}
@FXML
public void close() {
LOG.trace("StartController.close()");
window.close();
}
@FXML
public void next() {
LOG.trace("StartController.next()");
executor.submit(this::loadKey);
}
private void loadKey() {
assert !Platform.isFxApplicationThread();
assert unverifiedVaultConfig.isPresent();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
var unverifiedCfg = unverifiedVaultConfig.get();
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
vaultConfigRef.set(verifiedCfg);
var old = masterkeyRef.getAndSet(masterkey.clone());
if (old != null) {
old.destroy();
}
Platform.runLater(this::loadedKey);
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
// retry
loadKey();
} else {
Platform.runLater(() -> loadingKeyFailed(e));
}
} catch (VaultKeyInvalidException e) {
Platform.runLater(() -> loadingKeyFailed(e));
} catch (VaultConfigLoadException e) {
Platform.runLater(() -> loadingKeyFailed(e));
}
}
private void loadedKey() {
LOG.debug("Loaded valid key");
window.setScene(checkScene.get());
}
private void loadingKeyFailed(Exception e) {
if (e instanceof UnlockCancelledException) {
// ok
} else if (e instanceof VaultKeyInvalidException) {
LOG.error("Invalid key"); //TODO: specific error screen
errorComponent.get().window(window).cause(e).build().showErrorScene();
} else {
LOG.error("Failed to load key.", e);
errorComponent.get().window(window).cause(e).build().showErrorScene();
}
}
public boolean isInvalidConfig() {
return unverifiedVaultConfig.isEmpty();
}
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.keyloading;
import javax.inject.Qualifier;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface KeyLoading {
}
@@ -0,0 +1,31 @@
package org.cryptomator.ui.keyloading;
import dagger.BindsInstance;
import dagger.Subcomponent;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import javafx.stage.Stage;
import java.util.Map;
import java.util.function.Supplier;
@KeyLoadingScoped
@Subcomponent(modules = {KeyLoadingModule.class})
public interface KeyLoadingComponent {
@KeyLoading
KeyLoadingStrategy keyloadingStrategy();
@Subcomponent.Builder
interface Builder {
@BindsInstance
Builder vault(@KeyLoading Vault vault);
@BindsInstance
Builder window(@KeyLoading Stage window);
KeyLoadingComponent build();
}
}
@@ -0,0 +1,42 @@
package org.cryptomator.ui.keyloading;
import dagger.Module;
import dagger.Provides;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
import javax.inject.Provider;
import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@Module(includes = {MasterkeyFileLoadingModule.class})
abstract class KeyLoadingModule {
@Provides
@KeyLoading
@KeyLoadingScoped
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
}
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
try {
String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
} catch (IOException e) {
return KeyLoadingStrategy.failed(e);
}
}
}
@@ -0,0 +1,13 @@
package org.cryptomator.ui.keyloading;
import javax.inject.Scope;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface KeyLoadingScoped {
}
@@ -0,0 +1,63 @@
package org.cryptomator.ui.keyloading;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.api.MasterkeyLoader;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import java.net.URI;
/**
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
*/
@FunctionalInterface
public interface KeyLoadingStrategy extends MasterkeyLoader {
/**
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
* <p>
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
*
* @param keyId An URI uniquely identifying the source and identity of the key
* @return The raw key bytes. Must not be null
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
*/
@Override
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
/**
* Allows the loader to try and recover from an exception thrown during the last attempt.
*
* @param exception An exception thrown by {@link #loadKey(URI)}.
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
*/
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
return false;
}
/**
* Release any ressources or do follow-up tasks after loading a key.
*
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
*/
default void cleanup(boolean unlockedSuccessfully) {
// no-op
}
/**
* A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}.
*
* @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped.
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
*/
static KeyLoadingStrategy failed(Exception exception) {
return keyid -> {
if (exception instanceof MasterkeyLoadingFailedException e) {
throw e;
} else {
throw new MasterkeyLoadingFailedException("Can not load key", exception);
}
};
}
}
@@ -0,0 +1,62 @@
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<char[]> storedPassword;
private final AtomicReference<char[]> enteredPassword;
private final AtomicBoolean shouldSavePassword;
private final KeychainManager keychain;
@Inject
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> 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(), 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, ' ');
}
}
}
@@ -0,0 +1,123 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
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;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
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.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.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<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@KeyLoadingScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@KeyLoadingScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
if (!keychain.isSupported() || keychain.isLocked()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@KeyLoadingScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference<>();
}
@Provides
@KeyLoadingScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference<>(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@KeyLoadingScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> 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
static Scene provideUnlockSelectMasterkeyFileScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE);
}
@Binds
@IntoMap
@FxControllerKey(PassphraseEntryController.class)
abstract FxController bindUnlockController(PassphraseEntryController controller);
@Binds
@IntoMap
@FxControllerKey(SelectMasterkeyFileController.class)
abstract FxController bindUnlockSelectMasterkeyFileController(SelectMasterkeyFileController controller);
@Binds
@IntoMap
@KeyLoadingScoped
@StringKey(MasterkeyFileLoadingStrategy.SCHEME)
abstract KeyLoadingStrategy bindMasterkeyFileLoadingStrategy(MasterkeyFileLoadingStrategy strategy);
}
@@ -0,0 +1,150 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import com.google.common.base.Preconditions;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
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.ui.common.Animations;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.unlock.UnlockCancelledException;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoading
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
public static final String SCHEME = "masterkeyfile";
private final Vault vault;
private final MasterkeyFileAccess masterkeyFileAcccess;
private final Stage window;
private final Lazy<Scene> passphraseEntryScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
private final AtomicReference<char[]> password;
private final AtomicReference<Path> filePath;
private final MasterkeyFileLoadingFinisher finisher;
private boolean wrongPassword;
@Inject
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
this.vault = vault;
this.masterkeyFileAcccess = masterkeyFileAcccess;
this.window = window;
this.passphraseEntryScene = passphraseEntryScene;
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
this.passwordEntryLock = passwordEntryLock;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.password = password;
this.filePath = filePath;
this.finisher = finisher;
}
@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 masterkeyFileAcccess.load(filePath, passphrase);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new UnlockCancelledException("Unlock interrupted", e);
}
}
@Override
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
if (exception instanceof InvalidPassphraseException) {
this.wrongPassword = true;
password.set(null);
return true; // reattempting key load
} else {
return false; // nothing we can do
}
}
@Override
public void cleanup(boolean unlockedSuccessfully) {
finisher.cleanup(unlockedSuccessfully);
}
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 {
Platform.runLater(() -> {
window.setScene(selectMasterkeyFileScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
});
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 {
Platform.runLater(() -> {
window.setScene(passphraseEntryScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (wrongPassword) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
}
@@ -1,4 +1,4 @@
package org.cryptomator.ui.unlock;
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
@@ -7,6 +7,9 @@ import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings;
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;
@@ -38,17 +41,17 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@UnlockScoped
public class UnlockController implements FxController {
@KeyLoadingScoped
public class PassphraseEntryController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
private final Stage window;
private final Vault vault;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
@@ -66,7 +69,7 @@ public class UnlockController implements FxController {
public Animation unlockAnimation;
@Inject
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
this.window = window;
this.vault = vault;
this.password = password;
@@ -138,7 +141,7 @@ public class UnlockController implements FxController {
// if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
passwordEntryLock.interacted(PasswordEntry.CANCELED);
}
}
@@ -154,7 +157,7 @@ public class UnlockController implements FxController {
if (oldPw != null) {
Arrays.fill(oldPw, ' ');
}
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
startUnlockAnimation();
}
@@ -0,0 +1,67 @@
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;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import java.io.File;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
public class SelectMasterkeyFileController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
private final Stage window;
private final AtomicReference<Path> masterkeyPath;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final ResourceBundle resourceBundle;
@Inject
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
this.window = window;
this.masterkeyPath = masterkeyPath;
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
this.resourceBundle = resourceBundle;
this.window.setOnHiding(this::windowClosed);
}
@FXML
public void cancel() {
window.close();
}
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);
}
}
@FXML
public void proceed() {
LOG.trace("proceed()");
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
File masterkeyFile = fileChooser.showOpenDialog(window);
if (masterkeyFile != null) {
LOG.debug("Chose masterkey file: {}", masterkeyFile);
masterkeyPath.set(masterkeyFile.toPath());
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
}
}
}
@@ -10,6 +10,7 @@ import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import javafx.application.Platform;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.concurrent.BlockingQueue;
@@ -78,7 +79,7 @@ class AppLaunchEventHandler {
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
}
LOG.debug("Added vault {}", potentialVaultPath);
} catch (NoSuchFileException e) {
} catch (IOException e) {
LOG.error("Failed to add vault " + potentialVaultPath, e);
}
}
@@ -1,6 +1,7 @@
package org.cryptomator.ui.launcher;
import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume;
@@ -24,11 +25,13 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@Singleton
public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch;
@@ -127,6 +130,8 @@ public class AppLifecycleListener {
vault.lock(true);
} catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
}
}
}
@@ -1,9 +1,11 @@
package org.cryptomator.ui.lock;
import dagger.Lazy;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
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;
@@ -35,21 +37,23 @@ public class LockWorkflow extends Task<Void> {
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene;
private final ErrorComponent.Builder errorComponent;
@Inject
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
this.lockWindow = lockWindow;
this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock;
this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
}
@Override
protected Void call() throws Volume.VolumeException, InterruptedException {
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
try {
vault.lock(false);
} catch (Volume.VolumeException e) {
} catch (Volume.VolumeException | LockNotCompletedException e) {
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
var decision = askUserForAction();
switch (decision) {
@@ -77,29 +81,29 @@ public class LockWorkflow extends Task<Void> {
return forceLockDecisionLock.awaitInteraction();
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override
protected void succeeded() {
LOG.info("Lock of {} succeeded.", vault.getDisplayName());
vault.setState(VaultState.LOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
@Override
protected void failed() {
LOG.warn("Failed to lock {}.", vault.getDisplayName());
vault.setState(VaultState.UNLOCKED);
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
final var throwable = super.getException();
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
if (throwable instanceof Volume.VolumeException) {
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
} else {
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
}
}
@Override
protected void cancelled() {
LOG.debug("Lock of {} canceled.", vault.getDisplayName());
vault.setState(VaultState.UNLOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
}
}
@@ -3,6 +3,8 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger;
@@ -20,6 +22,7 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
@@ -27,6 +30,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MainWindowScoped
public class MainWindowController implements FxController {
@@ -91,23 +95,21 @@ public class MainWindowController implements FxController {
}
private boolean containsVault(Path path) {
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return true;
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
return true;
} else {
try {
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
} catch (IOException e) {
return false;
}
}
private void addVault(Path pathToVault) {
try {
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
vaultListManager.add(pathToVault.getParent());
} else {
vaultListManager.add(pathToVault);
}
} catch (NoSuchFileException e) {
} catch (IOException e) {
LOG.debug("Not a vault: {}", pathToVault);
}
}
@@ -12,6 +12,7 @@ import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.migration.MigrationComponent;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -27,7 +28,7 @@ import javafx.stage.StageStyle;
import java.util.Map;
import java.util.ResourceBundle;
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
@Module(subcomponents = {AddVaultWizardComponent.class, HealthCheckComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
abstract class MainWindowModule {
@Provides
@@ -86,6 +87,11 @@ abstract class MainWindowModule {
@FxControllerKey(VaultListController.class)
abstract FxController bindVaultListController(VaultListController controller);
@Binds
@IntoMap
@FxControllerKey(VaultListContextMenuController.class)
abstract FxController bindVaultListContextMenuController(VaultListContextMenuController controller);
@Binds
@IntoMap
@FxControllerKey(VaultDetailController.class)
@@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController {
@FXML
public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
}
/* Getter/Setter */
@@ -32,7 +32,8 @@ public class VaultDetailController implements FxController {
this.anyVaultSelected = vault.isNotNull();
}
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
// TODO deduplicate w/ VaultListCellController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) {
return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK;
@@ -5,6 +5,7 @@ import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.health.HealthCheckComponent;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
@@ -13,6 +14,7 @@ import javafx.beans.binding.BooleanExpression;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.Optional;
@@ -28,13 +30,13 @@ public class VaultDetailLockedController implements FxController {
private final BooleanExpression passwordSaved;
@Inject
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
this.vault = vault;
this.application = application;
this.vaultOptionsWindow = vaultOptionsWindow;
this.keychain = keychain;
this.mainWindow = mainWindow;
if (keychain.isSupported()) {
if (keychain.isSupported() && !keychain.isLocked()) {
this.passwordSaved = BooleanExpression.booleanExpression(EasyBind.select(vault).selectObject(v -> keychain.getPassphraseStoredProperty(v.getId())));
} else {
this.passwordSaved = new SimpleBooleanProperty(false);
@@ -24,7 +24,8 @@ public class VaultListCellController implements FxController {
.map(this::getGlyphForVaultState);
}
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
// TODO deduplicate w/ VaultDetailController
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) {
return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK;
@@ -0,0 +1,128 @@
package org.cryptomator.ui.mainwindow;
import com.tobiasdiez.easybind.EasyBind;
import com.tobiasdiez.easybind.optional.ObservableOptionalValue;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.fxapp.FxApplication;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject;
import javafx.beans.binding.Binding;
import javafx.beans.property.ObjectProperty;
import javafx.fxml.FXML;
import javafx.stage.Stage;
import java.util.EnumSet;
import java.util.Optional;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@MainWindowScoped
public class VaultListContextMenuController implements FxController {
private final ObservableOptionalValue<Vault> selectedVault;
private final Stage mainWindow;
private final FxApplication application;
private final KeychainManager keychain;
private final RemoveVaultComponent.Builder removeVault;
private final VaultOptionsComponent.Builder vaultOptionsWindow;
private final OptionalBinding<VaultState.Value> selectedVaultState;
private final Binding<Boolean> selectedVaultPassphraseStored;
private final Binding<Boolean> selectedVaultRemovable;
private final Binding<Boolean> selectedVaultUnlockable;
private final Binding<Boolean> selectedVaultLockable;
@Inject
VaultListContextMenuController(ObjectProperty<Vault> selectedVault, @MainWindow Stage mainWindow, FxApplication application, KeychainManager keychain, RemoveVaultComponent.Builder removeVault, VaultOptionsComponent.Builder vaultOptionsWindow) {
this.selectedVault = EasyBind.wrapNullable(selectedVault);
this.mainWindow = mainWindow;
this.application = application;
this.keychain = keychain;
this.removeVault = removeVault;
this.vaultOptionsWindow = vaultOptionsWindow;
this.selectedVaultState = this.selectedVault.mapObservable(Vault::stateProperty);
this.selectedVaultPassphraseStored = this.selectedVault.map(this::isPasswordStored).orElse(false);
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
}
private boolean isPasswordStored(Vault vault) {
return keychain.getPassphraseStoredProperty(vault.getId()).get();
}
@FXML
public void didClickRemoveVault() {
selectedVault.ifValuePresent(v -> {
removeVault.vault(v).build().showRemoveVault();
});
}
@FXML
public void didClickShowVaultOptions() {
selectedVault.ifValuePresent(v -> {
vaultOptionsWindow.vault(v).build().showVaultOptionsWindow(SelectedVaultOptionsTab.ANY);
});
}
@FXML
public void didClickUnlockVault() {
selectedVault.ifValuePresent(v -> {
application.startUnlockWorkflow(v, Optional.of(mainWindow));
});
}
@FXML
public void didClickLockVault() {
selectedVault.ifValuePresent(v -> {
application.startLockWorkflow(v, Optional.of(mainWindow));
});
}
@FXML
public void didClickRevealVault() {
selectedVault.ifValuePresent(v -> {
application.getVaultService().reveal(v);
});
}
// Getter and Setter
public Binding<Boolean> selectedVaultUnlockableProperty() {
return selectedVaultUnlockable;
}
public boolean isSelectedVaultUnlockable() {
return selectedVaultUnlockable.getValue();
}
public Binding<Boolean> selectedVaultLockableProperty() {
return selectedVaultLockable;
}
public boolean isSelectedVaultLockable() {
return selectedVaultLockable.getValue();
}
public Binding<Boolean> selectedVaultRemovableProperty() {
return selectedVaultRemovable;
}
public boolean isSelectedVaultRemovable() {
return selectedVaultRemovable.getValue();
}
public Binding<Boolean> selectedVaultPassphraseStoredProperty() {
return selectedVaultPassphraseStored;
}
public boolean isSelectedVaultPassphraseStored() {
return selectedVaultPassphraseStored.getValue();
}
}
@@ -1,12 +1,11 @@
package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.beans.binding.Bindings;
@@ -17,30 +16,43 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import java.util.EnumSet;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
@MainWindowScoped
public class VaultListController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(VaultListController.class);
private final Stage mainWindow;
private final ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault;
private final VaultListCellFactory cellFactory;
private final AddVaultWizardComponent.Builder addVaultWizard;
private final RemoveVaultComponent.Builder removeVault;
private final BooleanBinding noVaultSelected;
private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
public ListView<Vault> vaultList;
@Inject
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
this.mainWindow = mainWindow;
this.vaults = vaults;
this.selectedVault = selectedVault;
this.cellFactory = cellFactory;
this.addVaultWizard = addVaultWizard;
this.removeVault = removeVault;
this.noVaultSelected = selectedVault.isNull();
this.removeVaultDialogue = removeVaultDialogue;
this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange);
}
@@ -56,6 +68,41 @@ public class VaultListController implements FxController {
}
}
});
vaultList.addEventFilter(MouseEvent.MOUSE_RELEASED, this::deselect);
vaultList.addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, request -> {
if (selectedVault.get() == null) {
request.consume();
}
});
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.getCode() == KeyCode.DELETE) {
pressedShortcutToRemoveVault();
keyEvent.consume();
}
});
if (SystemUtils.IS_OS_MAC) {
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
if (keyEvent.getCode() == KeyCode.BACK_SPACE) {
pressedShortcutToRemoveVault();
keyEvent.consume();
}
});
}
//register vault selection shortcut to the main window
mainWindow.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
if (keyEvent.isShortcutDown() && keyEvent.getCode().isDigitKey()) {
vaultList.getSelectionModel().select(Integer.parseInt(keyEvent.getText()) - 1);
keyEvent.consume();
}
});
}
private void deselect(MouseEvent released) {
if (released.getY() > (vaultList.getItems().size() * vaultList.fixedCellSizeProperty().get())) {
vaultList.getSelectionModel().clearSelection();
released.consume();
}
}
private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
@@ -70,13 +117,10 @@ public class VaultListController implements FxController {
addVaultWizard.build().showAddVaultWizard();
}
@FXML
public void didClickRemoveVault() {
Vault v = selectedVault.get();
if (v != null) {
removeVault.vault(v).build().showRemoveVault();
} else {
LOG.debug("Cannot remove a vault if none is selected.");
private void pressedShortcutToRemoveVault() {
final var vault = selectedVault.get();
if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) {
removeVaultDialogue.vault(vault).build().showRemoveVault();
}
}
@@ -90,11 +134,4 @@ public class VaultListController implements FxController {
return emptyVaultList.get();
}
public BooleanBinding noVaultSelectedProperty() {
return noVaultSelected;
}
public boolean isNoVaultSelected() {
return noVaultSelected.get();
}
}
@@ -26,6 +26,7 @@ import javax.inject.Named;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
@@ -43,6 +44,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MigrationScoped
public class MigrationRunController implements FxController {
@@ -89,7 +91,12 @@ public class MigrationRunController implements FxController {
if (keychain.isSupported()) {
loadStoredPassword();
}
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
.or(passwordField.textProperty().isEmpty()));
window.setOnHiding(event -> passwordField.wipe());
}
@FXML
@@ -101,7 +108,7 @@ public class MigrationRunController implements FxController {
public void migrate() {
LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters();
vault.setState(VaultState.PROCESSING);
vault.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING);
passwordField.setDisable(true);
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
Platform.runLater(() -> {
@@ -110,15 +117,15 @@ public class MigrationRunController implements FxController {
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
Tasks.create(() -> {
Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) {
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
vault.setState(VaultState.NEEDS_MIGRATION);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
} else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
vault.setState(VaultState.LOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
passwordField.wipe();
window.setScene(successScene.get());
}
@@ -127,20 +134,20 @@ public class MigrationRunController implements FxController {
passwordField.setDisable(false);
passwordField.selectAll();
passwordField.requestFocus();
vault.setState(VaultState.NEEDS_MIGRATION);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Underlying file system not supported.", e);
vault.setState(VaultState.NEEDS_MIGRATION);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get());
}).onError(FileNameTooLongException.class, e -> {
LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
vault.setState(VaultState.NEEDS_MIGRATION);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
window.setScene(impossibleScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e);
vault.setState(VaultState.NEEDS_MIGRATION);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
}).andFinally(() -> {
passwordField.setDisable(false);
@@ -157,8 +157,8 @@ public class GeneralPreferencesController implements FxController {
@FXML
public void showDonationTab() {
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
public void showContributeTab() {
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
}
@FXML
@@ -26,7 +26,7 @@ public class PreferencesController implements FxController {
public Tab generalTab;
public Tab volumeTab;
public Tab updatesTab;
public Tab donationKeyTab;
public Tab contributeTab;
public Tab aboutTab;
@Inject
@@ -52,7 +52,7 @@ public class PreferencesController implements FxController {
return switch (selectedTab) {
case UPDATES -> updatesTab;
case VOLUME -> volumeTab;
case DONATION_KEY -> donationKeyTab;
case CONTRIBUTE -> contributeTab;
case GENERAL -> generalTab;
case ABOUT -> aboutTab;
case ANY -> updateAvailable.get() ? updatesTab : generalTab;
@@ -77,8 +77,8 @@ abstract class PreferencesModule {
@Binds
@IntoMap
@FxControllerKey(DonationKeyPreferencesController.class)
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
@FxControllerKey(SupporterCertificateController.class)
abstract FxController bindSupporterCertificatePreferencesController(SupporterCertificateController controller);
@Binds
@IntoMap
@@ -22,9 +22,9 @@ public enum SelectedPreferencesTab {
UPDATES,
/**
* Show donation key tab
* Show contribute tab
*/
DONATION_KEY,
CONTRIBUTE,
/**
* Show about tab
@@ -14,17 +14,17 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter;
@PreferencesScoped
public class DonationKeyPreferencesController implements FxController {
public class SupporterCertificateController implements FxController {
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
private static final String SUPPORTER_URI = "https://store.cryptomator.org/desktop";
private final Application application;
private final LicenseHolder licenseHolder;
private final Settings settings;
public TextArea donationKeyField;
public TextArea supporterCertificateField;
@Inject
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
SupporterCertificateController(Application application, LicenseHolder licenseHolder, Settings settings) {
this.application = application;
this.licenseHolder = licenseHolder;
this.settings = settings;
@@ -32,9 +32,9 @@ public class DonationKeyPreferencesController implements FxController {
@FXML
public void initialize() {
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
donationKeyField.textProperty().addListener(this::registrationKeyChanged);
donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null));
supporterCertificateField.textProperty().addListener(this::registrationKeyChanged);
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
}
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
@@ -53,8 +53,8 @@ public class DonationKeyPreferencesController implements FxController {
}
@FXML
public void getDonationKey() {
application.getHostServices().showDocument(DONATION_URI);
public void getSupporterCertificate() {
application.getHostServices().showDocument(SUPPORTER_URI);
}
public LicenseHolder getLicenseHolder() {
@@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey;
import dagger.Lazy;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent;
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
}
@Override
protected String call() throws IOException {
protected String call() throws IOException, CryptoException {
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
}
@@ -2,28 +2,34 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions;
import com.google.common.hash.Hashing;
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.Masterkey;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.Collection;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@Singleton
public class RecoveryKeyFactory {
private static final byte[] PEPPER = new byte[0];
private final WordEncoder wordEncoder;
private final MasterkeyFileAccess masterkeyFileAccess;
@Inject
public RecoveryKeyFactory(WordEncoder wordEncoder) {
public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) {
this.wordEncoder = wordEncoder;
this.masterkeyFileAccess = masterkeyFileAccess;
}
public Collection<String> getDictionary() {
@@ -36,11 +42,14 @@ public class RecoveryKeyFactory {
* @return The recovery key of the vault at the given path
* @throws IOException If the masterkey file could not be read
* @throws InvalidPassphraseException If the provided password is wrong
* @throws CryptoException In case of other cryptographic errors
* @apiNote This is a long-running operation and should be invoked in a background thread
*/
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
try {
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
byte[] rawKey = new byte[0];
try (var masterkey = masterkeyFileAccess.load(masterkeyPath, password)) {
rawKey = masterkey.getEncoded();
return createRecoveryKey(rawKey);
} finally {
Arrays.fill(rawKey, (byte) 0x00);
@@ -72,8 +81,15 @@ public class RecoveryKeyFactory {
*/
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
final byte[] rawKey = decodeRecoveryKey(recoveryKey);
try {
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
try (var masterkey = new Masterkey(rawKey)) {
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
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);
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
}
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
} finally {
Arrays.fill(rawKey, (byte) 0x00);
}
@@ -6,10 +6,10 @@ 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.NewPasswordController;
import org.cryptomator.ui.common.PasswordStrengthUtil;
@@ -17,8 +17,6 @@ import org.cryptomator.ui.common.StageFactory;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
@@ -56,14 +54,6 @@ abstract class RecoveryKeyModule {
return new SimpleStringProperty();
}
@Provides
@RecoveryKeyScoped
@Named("newPassword")
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
return new SimpleObjectProperty<>("");
}
// ------------------
@Provides
@@ -126,8 +116,8 @@ abstract class RecoveryKeyModule {
@Provides
@IntoMap
@FxControllerKey(NewPasswordController.class)
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
return new NewPasswordController(resourceBundle, strengthRater, password);
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
return new NewPasswordController(resourceBundle, strengthRater);
}
}
@@ -6,14 +6,12 @@ import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.NewPasswordController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
@@ -32,21 +30,19 @@ public class RecoveryKeyResetPasswordController implements FxController {
private final RecoveryKeyFactory recoveryKeyFactory;
private final ExecutorService executor;
private final StringProperty recoveryKey;
private final ObjectProperty<CharSequence> newPassword;
private final Lazy<Scene> recoverScene;
private final BooleanBinding invalidNewPassword;
private final ErrorComponent.Builder errorComponent;
public NewPasswordController newPasswordController;
@Inject
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword") ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
this.window = window;
this.vault = vault;
this.recoveryKeyFactory = recoveryKeyFactory;
this.executor = executor;
this.recoveryKey = recoveryKey;
this.newPassword = newPassword;
this.recoverScene = recoverScene;
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
this.errorComponent = errorComponent;
}
@@ -81,7 +77,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
@Override
protected Void call() throws IOException, IllegalArgumentException {
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get());
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
return null;
}
@@ -89,11 +85,12 @@ public class RecoveryKeyResetPasswordController implements FxController {
/* Getter/Setter */
public BooleanBinding invalidNewPasswordProperty() {
return invalidNewPassword;
public ReadOnlyBooleanProperty validPasswordProperty() {
return newPasswordController.passwordsMatchAndSufficientProperty();
}
public boolean isInvalidNewPassword() {
return newPassword.get() == null || newPassword.get().length() == 0;
public boolean isValidPassword() {
return newPasswordController.passwordsMatchAndSufficientProperty().get();
}
}
@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
var weakStage = new WeakReference<>(stage);
vault.stateProperty().addListener(new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
if (newValue != VaultState.UNLOCKED) {
public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) {
if (newValue != VaultState.Value.UNLOCKED) {
Stage stage = weakStage.get();
if (stage != null) {
stage.hide();
@@ -44,6 +44,9 @@ class TrayMenuController {
public void initTrayMenu() {
vaults.addListener(this::vaultListChanged);
vaults.forEach(v -> {
v.displayNameProperty().addListener(this::vaultListChanged);
});
rebuildMenu();
}
@@ -0,0 +1,14 @@
package org.cryptomator.ui.unlock;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
public UnlockCancelledException(String message) {
super(message);
}
public UnlockCancelledException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -1,15 +1,11 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@@ -17,19 +13,17 @@ import javafx.stage.Stage;
public class UnlockInvalidMountPointController implements FxController {
private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault;
@Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
this.window = window;
this.unlockScene = unlockScene;
this.vault = vault;
}
@FXML
public void back() {
window.setScene(unlockScene.get());
public void close() {
window.close();
}
/* Getter/Setter */
@@ -4,20 +4,16 @@ import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
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 org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import javax.inject.Named;
import javax.inject.Provider;
@@ -27,54 +23,10 @@ import javafx.stage.Stage;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {ForgetPasswordComponent.class})
@Module(subcomponents = {KeyLoadingComponent.class})
abstract class UnlockModule {
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
public enum PasswordEntry {
PASSWORD_ENTERED,
CANCELED
}
@Provides
@UnlockScoped
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow Vault vault) {
if (!keychain.isSupported()) {
return Optional.empty();
} else {
try {
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
} catch (KeychainAccessException e) {
LOG.error("Failed to load entry from system keychain.", e);
return Optional.empty();
}
}
}
@Provides
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicReference(storedPassword.orElse(null));
}
@Provides
@Named("savePassword")
@UnlockScoped
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
return new AtomicBoolean(storedPassword.isPresent());
}
@Provides
@UnlockWindow
@UnlockScoped
@@ -99,10 +51,10 @@ abstract class UnlockModule {
}
@Provides
@FxmlScene(FxmlFile.UNLOCK)
@UnlockWindow
@UnlockScoped
static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return fxmlLoaders.createScene(FxmlFile.UNLOCK);
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
}
@Provides
@@ -121,11 +73,6 @@ abstract class UnlockModule {
// ------------------
@Binds
@IntoMap
@FxControllerKey(UnlockController.class)
abstract FxController bindUnlockController(UnlockController controller);
@Binds
@IntoMap
@FxControllerKey(UnlockSuccessController.class)
@@ -1,40 +1,31 @@
package org.cryptomator.ui.unlock;
import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
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.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NotDirectoryException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* A multi-step task that consists of background activities as well as user interaction.
@@ -49,113 +40,47 @@ public class UnlockWorkflow extends Task<Boolean> {
private final Stage window;
private final Vault vault;
private final VaultService vaultService;
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final KeychainManager keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
private final KeyLoadingStrategy keyLoadingStrategy;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
this.window = window;
this.vault = vault;
this.vaultService = vaultService;
this.password = password;
this.savePassword = savePassword;
this.savedPassword = savedPassword;
this.passwordEntryLock = passwordEntryLock;
this.keychain = keychain;
this.unlockScene = unlockScene;
this.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent;
setOnFailed(event -> {
Throwable throwable = event.getSource().getException();
if (throwable instanceof InvalidMountPointException e) {
handleInvalidMountPoint(e);
} else {
handleGenericError(throwable);
}
});
this.keyLoadingStrategy = keyLoadingStrategy;
}
@Override
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
try {
if (attemptUnlock()) {
handleSuccess();
return true;
attemptUnlock();
return true;
} catch (UnlockCancelledException e) {
cancel(false); // set Tasks state to cancelled
return false;
}
}
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
boolean success = false;
try {
vault.unlock(keyLoadingStrategy);
success = true;
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
attemptUnlock();
} else {
cancel(false); // set Tasks state to cancelled
return false;
throw e;
}
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
}
}
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
while (proceed) {
try {
vault.unlock(CharBuffer.wrap(password.get()));
return true;
} catch (InvalidPassphraseException e) {
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
}
}
return false;
}
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
Platform.runLater(() -> {
window.setScene(unlockScene.get());
window.show();
Window owner = window.getOwner();
if (owner != null) {
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
} else {
window.centerOnScreen();
}
if (animateShake) {
Animations.createShakeWindowAnimation(window).play();
}
});
return passwordEntryLock.awaitInteraction();
}
private void handleSuccess() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
if (savePassword.get()) {
savePasswordToSystemkeychain();
}
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
}
private void savePasswordToSystemkeychain() {
if (keychain.isSupported()) {
try {
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
} catch (KeychainAccessException e) {
LOG.error("Failed to store passphrase in system keychain.", e);
}
keyLoadingStrategy.cleanup(success);
}
}
@@ -173,15 +98,12 @@ public class UnlockWorkflow extends Task<Boolean> {
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
}
showInvalidMountPointScene();
return;
} else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
showInvalidMountPointScene();
return;
} else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
showInvalidMountPointScene();
return;
} else {
handleGenericError(impExc);
}
@@ -196,33 +118,44 @@ public class UnlockWorkflow extends Task<Boolean> {
private void handleGenericError(Throwable e) {
LOG.error("Unlock failed for technical reasons.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
}
private void wipePassword(char[] pw) {
if (pw != null) {
Arrays.fill(pw, ' ');
}
}
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
errorComponent.cause(e).window(window).build().showErrorScene();
}
@Override
protected void succeeded() {
vault.setState(VaultState.UNLOCKED);
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
case ASK -> Platform.runLater(() -> {
window.setScene(successScene.get());
window.show();
});
case REVEAL -> {
Platform.runLater(window::close);
vaultService.reveal(vault);
}
case IGNORE -> Platform.runLater(window::close);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
}
@Override
protected void failed() {
vault.setState(VaultState.LOCKED);
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
Throwable throwable = super.getException();
if (throwable instanceof InvalidMountPointException e) {
handleInvalidMountPoint(e);
} else {
handleGenericError(throwable);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
@Override
protected void cancelled() {
vault.setState(VaultState.LOCKED);
LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
}
}
@@ -3,6 +3,7 @@ package org.cryptomator.ui.vaultoptions;
import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.health.HealthCheckComponent;
import javax.inject.Inject;
import javafx.beans.Observable;
@@ -22,6 +23,7 @@ public class GeneralVaultOptionsController implements FxController {
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
private final ResourceBundle resourceBundle;
public TextField vaultName;
@@ -29,9 +31,10 @@ public class GeneralVaultOptionsController implements FxController {
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
@Inject
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
this.window = window;
this.vault = vault;
this.healthCheckWindow = healthCheckWindow;
this.resourceBundle = resourceBundle;
}
@@ -61,6 +64,12 @@ public class GeneralVaultOptionsController implements FxController {
}
}
@FXML
public void showHealthCheck() {
healthCheckWindow.vault(vault).build().showHealthCheckWindow();
}
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
private final ResourceBundle resourceBundle;
@@ -36,7 +36,7 @@ public class MasterkeyOptionsController implements FxController {
this.changePasswordWindow = changePasswordWindow;
this.recoveryKeyWindow = recoveryKeyWindow;
this.keychain = keychain;
if (keychain.isSupported()) {
if (keychain.isSupported() && !keychain.isLocked()) {
this.passwordSaved = Bindings.createBooleanBinding(this::isPasswordSaved, keychain.getPassphraseStoredProperty(vault.getId()));
} else {
this.passwordSaved = new SimpleBooleanProperty(false);
@@ -74,7 +74,7 @@ public class MasterkeyOptionsController implements FxController {
}
public boolean isPasswordSaved() {
if (keychain.isSupported() && vault != null) {
if (keychain.isSupported() && !keychain.isLocked() && vault != null) {
return keychain.getPassphraseStoredProperty(vault.getId()).get();
} else return false;
}
@@ -20,6 +20,8 @@
alignment="CENTER_LEFT">
<fx:define>
<ToggleGroup fx:id="predefinedLocationToggler"/>
<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
</fx:define>
<padding>
<Insets topRightBottomLeft="24"/>
@@ -33,6 +35,8 @@
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/>
<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/>
<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/>
<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.locationPresets.foundMega}" managed="${controller.locationPresets.foundMega}"/>
<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.locationPresets.foundPcloud}" managed="${controller.locationPresets.foundPcloud}"/>
<HBox spacing="12" alignment="CENTER_LEFT">
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
@@ -47,12 +51,8 @@
<VBox spacing="6">
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" disable="true" HBox.hgrow="ALWAYS"/>
<Label text="${controller.warningText}" wrapText="true" visible="${controller.showWarning}">
<graphic>
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
</graphic>
</Label>
<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
<Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
</VBox>
<Region VBox.vgrow="ALWAYS"/>
@@ -23,7 +23,7 @@
<Insets topRightBottomLeft="24"/>
</padding>
<children>
<fx:include source="/fxml/new_password.fxml"/>
<fx:include fx:id="newPasswordScene" source="/fxml/new_password.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
@@ -25,7 +25,7 @@
<Region prefHeight="12" VBox.vgrow="NEVER"/>
<fx:include source="/fxml/new_password.fxml"/>
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
<CheckBox fx:id="finalConfirmationCheckbox" text="%changepassword.finalConfirmation" wrapText="true"/>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.CheckDetailController"
prefWidth="500"
spacing="6">
<FormattedLabel fx:id="checkTitle" styleClass="label-large" format="%health.check.detail.header" arg1="${controller.taskName}"/>
<Label text="%health.check.detail.taskNotStarted" visible="${controller.taskNotStarted}" managed="${controller.taskNotStarted}"/>
<Label text="%health.check.detail.taskRunning" visible="${controller.taskRunning}" managed="${controller.taskRunning}"/>
<Label text="%health.check.detail.taskScheduled" visible="${controller.taskScheduled}" managed="${controller.taskScheduled}"/>
<Label text="%health.check.detail.taskCancelled" visible="${controller.taskCancelled}" managed="${controller.taskCancelled}"/>
<Label text="%health.check.detail.taskFailed" visible="${controller.taskFailed}" managed="${controller.taskFailed}"/>
<FormattedLabel styleClass="label" format="%health.check.detail.taskSucceeded" arg1="${controller.taskDuration}" visible="${controller.taskSucceeded}" managed="${controller.taskSucceeded}"/>
<FormattedLabel styleClass="label" format="%health.check.detail.problemCount" arg1="${controller.countOfWarnSeverity}" arg2="${controller.countOfCritSeverity}" visible="${!controller.taskNotStarted}"
managed="${!controller.taskNotStarted}" />
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS"/>
</VBox>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import java.lang.Integer?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.CheckListController"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<fx:define>
<Integer fx:id="ZERO" fx:value="0"/>
</fx:define>
<children>
<HBox spacing="12" VBox.vgrow="ALWAYS">
<VBox minWidth="80" maxWidth="200" spacing="6" HBox.hgrow="ALWAYS" >
<Label fx:id="listHeading" text="%health.checkList.header"/>
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
</VBox>
<StackPane visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
<VBox minWidth="300" alignment="CENTER" visible="${!controller.anyCheckSelected}" managed="${!controller.anyCheckSelected}" >
<Label text="%health.check.detail.noSelectedCheck" wrapText="true" alignment="CENTER" />
</VBox>
<fx:include source="/fxml/health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" />
</StackPane>
</HBox>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" />
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${!controller.finished}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" onAction="#exportResults"/>
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.numberOfPickedChecks == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
</buttons>
</ButtonBar>
</children>
</VBox>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.ResultListCellController"
prefHeight="25"
prefWidth="200"
spacing="6"
alignment="CENTER_LEFT">
<!-- Remark: Check the containing list view for a fixed cell size before editing height properties -->
<padding>
<Insets topRightBottomLeft="6"/>
</padding>
<children>
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16"/>
<Label text="${controller.description}"/>
<Region HBox.hgrow="ALWAYS"/>
<!-- TODO: setting the minWidth of the button is just a workaround.
What we actually want to do is to prevent shrinking the button more than the text
-> own subclass of HBox is needed -->
<Button fx:id="actionButton" text="%health.check.fixBtn" onAction="#runResultAction" alignment="CENTER" visible="false" minWidth="-Infinity"/>
</children>
</HBox>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.StartController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<Label text="%health.start.introduction" wrapText="true"/>
<!-- TODO: combine the two below labels to one and bind the properties accordingly or, preferably think about a new flow -->
<Label text="%health.start.configInvalid" visible="${controller.invalidConfig}" managed="${controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red" />
</graphic>
</Label>
<Label text="%health.start.configValid" visible="${!controller.invalidConfig}" managed="${!controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary" />
</graphic>
</Label>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${controller.invalidConfig}" defaultButton="true" onAction="#next"/>
</buttons>
</ButtonBar>
</children>
</VBox>
@@ -38,12 +38,12 @@
<fx:include source="/fxml/preferences_updates.fxml"/>
</content>
</Tab>
<Tab fx:id="donationKeyTab" id="DONATION_KEY" text="%preferences.donationKey">
<Tab fx:id="contributeTab" id="CONTRIBUTE" text="%preferences.contribute">
<graphic>
<FontAwesome5IconView glyph="HEART"/>
</graphic>
<content>
<fx:include source="/fxml/preferences_donationkey.fxml"/>
<fx:include source="/fxml/preferences_contribute.fxml"/>
</content>
</Tab>
<Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about">
@@ -12,7 +12,7 @@
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController"
fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController"
spacing="18">
<padding>
<Insets topRightBottomLeft="12"/>
@@ -24,7 +24,7 @@
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/>
</StackPane>
<FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
<FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
</HBox>
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}">
@@ -33,8 +33,8 @@
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
</StackPane>
<VBox HBox.hgrow="ALWAYS" spacing="6">
<Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/>
<Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT">
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/>
<Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="LINK"/>
</graphic>
@@ -43,6 +43,6 @@
</HBox>
</StackPane>
<TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
<TextArea fx:id="supporterCertificateField" promptText="%preferences.contribute.promptText" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
</children>
</VBox>
@@ -23,7 +23,7 @@
<HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%preferences.general.theme"/>
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showDonationTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
</HBox>
<HBox spacing="6" alignment="CENTER_LEFT">
@@ -17,7 +17,7 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<fx:include source="/fxml/new_password.fxml"/>
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
<Region VBox.vgrow="ALWAYS"/>
@@ -25,7 +25,7 @@
<ButtonBar buttonMinWidth="120" buttonOrder="B+I">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${controller.invalidNewPassword}"/>
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${!controller.validPassword}"/>
</buttons>
</ButtonBar>
</VBox>
@@ -14,7 +14,7 @@
<?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.unlock.UnlockController"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.PassphraseEntryController"
minWidth="400"
maxWidth="400"
minHeight="145"
@@ -37,10 +37,10 @@
<Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="B+U">
<ButtonBar buttonMinWidth="120" buttonOrder="U+C">
<buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Region ButtonBar.buttonData="OTHER"/>
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons>
</ButtonBar>
</VBox>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.SelectMasterkeyFileController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="24"/>
</StackPane>
<VBox spacing="6">
<Label text="%unlock.chooseMasterkey.prompt" wrapText="true" HBox.hgrow="ALWAYS"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>
@@ -2,10 +2,8 @@
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
@@ -15,13 +13,9 @@
fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
minWidth="206">
<StackPane VBox.vgrow="ALWAYS">
<ListView fx:id="vaultList" editable="true">
<ListView fx:id="vaultList" editable="true" fixedCellSize="60">
<contextMenu>
<ContextMenu>
<items>
<MenuItem text="%main.vaultlist.contextMenu.remove" onAction="#didClickRemoveVault" disable="${controller.noVaultSelected}"/>
</items>
</ContextMenu>
<fx:include source="vault_list_contextmenu.fxml"/>
</contextMenu>
</ListView>
<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
@@ -13,6 +13,7 @@
prefWidth="200"
spacing="12"
alignment="CENTER_LEFT">
<!-- Remark Check the containing list view for a fixed cell size before editing height properties -->
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<ContextMenu xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.mainwindow.VaultListContextMenuController">
<items>
<MenuItem fx:id="revealEntry" text="%main.vaultlist.contextMenu.reveal" onAction="#didClickRevealVault" visible="${controller.selectedVaultLockable}"/>
<MenuItem fx:id="lockEntry" text="%main.vaultlist.contextMenu.lock" onAction="#didClickLockVault" visible="${controller.selectedVaultLockable}"/>
<MenuItem fx:id="unlockEntry" text="%main.vaultlist.contextMenu.unlock" onAction="#didClickUnlockVault" visible="${controller.selectedVaultUnlockable &amp;&amp; !controller.selectedVaultPassphraseStored}"/>
<MenuItem fx:id="unlockNowEntry" text="%main.vaultlist.contextMenu.unlockNow" onAction="#didClickUnlockVault" visible="${controller.selectedVaultUnlockable &amp;&amp; controller.selectedVaultPassphraseStored}"/>
<MenuItem fx:id="optionsEntry" text="%main.vaultlist.contextMenu.vaultoptions" onAction="#didClickShowVaultOptions" disable="${!controller.selectedVaultUnlockable}"/>
<MenuItem fx:id="removeEntry" text="%main.vaultlist.contextMenu.remove" onAction="#didClickRemoveVault" disable="${!controller.selectedVaultRemovable}"/>
</items>
</ContextMenu>
@@ -7,6 +7,7 @@
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"
@@ -26,5 +27,6 @@
<Label text="%vaultOptions.general.actionAfterUnlock"/>
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
</HBox>
<Button text="%vaultOptions.general.healthBtn" onAction="#showHealthCheck"/>
</children>
</VBox>
@@ -45,8 +45,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Custom Location
addvaultwizard.new.directoryPickerButton=Choose…
addvaultwizard.new.directoryPickerTitle=Select Directory
addvaultwizard.new.fileAlreadyExists=Vault can not be created at this path because some object already exists.
addvaultwizard.new.locationDoesNotExist=Vault can not be created at this path because at least one path component does not exist.
addvaultwizard.new.fileAlreadyExists=A file or directory with the vault name already exists
addvaultwizard.new.locationDoesNotExist=A directory in the specified path does not exist or cannot be accessed
addvaultwizard.new.locationIsNotWritable=No write access at the specified path
addvaultwizard.new.locationIsOk=Suitable location for your vault
addvaultwizard.new.invalidName=Invalid vault name. Please consider a regular directory name.
### Password
addvaultwizard.new.createVaultBtn=Create Vault
@@ -98,10 +100,13 @@ unlock.title=Unlock Vault
unlock.passwordPrompt=Enter password for "%s":
unlock.savePassword=Remember Password
unlock.unlockBtn=Unlock
##
unlock.chooseMasterkey.prompt=Could not find the masterkey file for this vault at its expected location. Please choose the key file manually.
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible via its virtual drive.
unlock.success.rememberChoice=Remember choice, don't show this again
unlock.success.revealBtn=Reveal Vault
unlock.success.revealBtn=Reveal Drive
## Failure
unlock.error.heading=Unable to unlock vault
### Invalid Mount Point
@@ -141,6 +146,29 @@ migration.impossible.heading=Unable to migrate vault
migration.impossible.reason=The vault cannot be automatically migrated because its storage location or access point is not compatible.
migration.impossible.moreInfo=The vault can still be opened with an older version. For instructions on how to manually migrate a vault, visit
# Health Check
health.title=Vault Health Check
health.start.introduction=The Vault Health Check is a collection of checks to detect and possilby fix problems in the internal structure of your vault. Please note, that not all problems are fixable. You need the vault password to perform the checks.
health.start.configValid=Reading and parsing vault configuration file was successful. Proceed to select checks.
health.start.configInvalid=Error while reading and parsing the vault configuration file.
health.checkList.header=Available Health Checks
health.checkList.selectAllBox=Select All
health.check.runBatchBtn=Run selected Checks
## Detail view
health.check.detail.noSelectedCheck=For results select a finished health check in the left list.
health.check.detail.header=Results of %s
health.check.detail.taskNotStarted=The check was not selected to run.
health.check.detail.taskScheduled=The check is scheduled.
health.check.detail.taskRunning=The check is currently running…
health.check.detail.taskSucceeded=The check finished successfully after %d milliseconds.
health.check.detail.taskFailed=The check exited due to an error.
health.check.detail.taskCancelled=The check was cancelled.
health.check.detail.problemCount=Found %d problems and %d unfixable errors.
health.check.exportBtn=Export Report
health.check.fixBtn=Fix
## Checks
health.org.cryptomator.cryptofs.health.dirid.DirIdCheck=Directory Check
# Preferences
preferences.title=Preferences
## General
@@ -175,11 +203,14 @@ preferences.updates.currentVersion=Current Version: %s
preferences.updates.autoUpdateCheck=Check for updates automatically
preferences.updates.checkNowBtn=Check Now
preferences.updates.updateAvailable=Update to version %s available.
## Donation Key
preferences.donationKey=Donation
preferences.donationKey.registeredFor=Registered for %s
preferences.donationKey.noDonationKey=No valid donation key found. It's like a license key but for awesome people using free software. ;-)
preferences.donationKey.getDonationKey=Get a donation key
## Contribution
preferences.contribute=Support Us
preferences.contribute.registeredFor=Supporter certificate registered for %s
preferences.contribute.noCertificate=Support Cryptomator and receive a supporter certificate. It's like a license key but for awesome people using free software. ;-)
preferences.contribute.getCertificate=Don't have one already? Learn how you can obtain it.
preferences.contribute.promptText=Paste supporter certificate code here
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=About
@@ -224,7 +255,12 @@ main.dropZone.dropVault=Add this vault
main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault
main.vaultlist.contextMenu.remove=Remove Vault
main.vaultlist.contextMenu.remove=Remove…
main.vaultlist.contextMenu.lock=Lock
main.vaultlist.contextMenu.unlock=Unlock…
main.vaultlist.contextMenu.unlockNow=Unlock Now
main.vaultlist.contextMenu.vaultoptions=Show Vault Options
main.vaultlist.contextMenu.reveal=Reveal Drive
main.vaultlist.addVaultBtn=Add Vault
## Vault Detail
### Welcome
@@ -274,6 +310,7 @@ vaultOptions.general.actionAfterUnlock=After successful unlock
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
vaultOptions.general.actionAfterUnlock.ask=Ask
vaultOptions.general.healthBtn=Start Health Check
## Mount
vaultOptions.mount=Mounting
vaultOptions.mount.readonly=Read-Only
@@ -17,7 +17,7 @@ generic.error.title=حدث خطأ غير متوقع
generic.error.instruction=ما كان ينبغي أن يحدث هذا. يرجى الإبلاغ عن نص الخطأ أدناه وإدراج وصف للخطوات التي أدت إلى هذا الخطأ.
# Defaults
defaults.vault.vaultName=خزنة
defaults.vault.vaultName=مخزن
# Tray Menu
traymenu.showMainWindow=اظهار
@@ -38,17 +38,19 @@ addvaultwizard.welcome.existingButton=افتح مخزن موجود
addvaultwizard.new.nameInstruction=اختر اسم للمخزن
addvaultwizard.new.namePrompt=اسم المخزن
### Location
addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة من قبو الخاص بك؟
addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة للمخزن الخاص بك؟
addvaultwizard.new.locationLabel=موقع التخزين
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=موقع مخصص
addvaultwizard.new.directoryPickerButton=اختر…
addvaultwizard.new.directoryPickerTitle=اختر القاموس
addvaultwizard.new.fileAlreadyExists=لا يمكن إنشاء مخزن على هذا المسار لأن بعض الملفات موجودة مسبقا.
addvaultwizard.new.locationDoesNotExist=لا يمكن إنشاء المخزن على هذا المسار لأن جزء من هذا المسار غير موجود.
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى النظر في اسم الدليل المعتاد.
addvaultwizard.new.fileAlreadyExists=ملف أو مجلد بنفس اسم المخزن موجود مسبقاً
addvaultwizard.new.locationDoesNotExist=المجلد في المسار المحدد غير موجود أو لا يمكن الوصول إليه
addvaultwizard.new.locationIsNotWritable=لا يوجد صلاحيات للكتابة على المسار المحدد
addvaultwizard.new.locationIsOk=الموقع المناسب للمخزن الخاص بك
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى إعادة تسمية المجلد بشكل عادي.
### Password
addvaultwizard.new.createVaultBtn=انشئ حافظة
addvaultwizard.new.createVaultBtn=إنشاء المخزن
addvaultwizard.new.generateRecoveryKeyChoice=لن تتمكن من الوصول إلى بياناتك بدون كلمة المرور الخاصة بك. هل تريد مفتاح استرداد في حالة فقدان كلمة المرور الخاصة بك؟
addvaultwizard.new.generateRecoveryKeyChoice.yes=نعم من فضلك، أن تكون آمناً أفضل من الندم
addvaultwizard.new.generateRecoveryKeyChoice.no=لا شكراً، لن أفقد كلمة المرور الخاصة بي
@@ -95,11 +97,14 @@ forgetPassword.confirmBtn=نسيت كلمة المرور
# Unlock
unlock.title=افتح الحافظة
unlock.passwordPrompt=‮أدخل كلمة السر لـ "%s":
unlock.savePassword=تذكر كلمة المرور
unlock.unlockBtn=افتح
##
unlock.chooseMasterkey.filePickerTitle=اختر ملف الـ Masterkey
## Success
unlock.success.message=تم فتح المخزن "%s" بنجاح! يمكنك الوصول إليه الآن.
unlock.success.message=تم فتح "%s" بنجاح! يمكنك الآن الوصول لمخزنك عن طريق القرص الافتراضي الخاص به.
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
unlock.success.revealBtn=افتح الحافظة
unlock.success.revealBtn=اظهار القرص
## Failure
unlock.error.heading=غير قادر على فتح الخزنة
### Invalid Mount Point
@@ -108,9 +113,12 @@ unlock.error.invalidMountPoint.existing=نقطة/مجلد التحميل موج
# Lock
## Force
lock.forced.heading=فشل عملية القفل
lock.forced.message=تم حظر قفل "%s" بواسطة العمليات المعلقة أو الملفات المفتوحة. يمكنك فرض قفل هذا المخزن، ولكن مقاطعة عمليات الادخال والاخراج I/O قد تؤدي لفقدان البيانات غير المحفوظة.
lock.forced.confirmBtn=فرض القفل
## Failure
lock.fail.heading=فشلت عملية اقفال الخزنة.
lock.fail.message=فشل عملية قفل %s". تأكد من حفظ العمل غير المحفوظ في مكان آخر وأن العمليات الهامة للقراءة/الكتابة قد انتهت. من أجل إغلاق المخزن، اقتل تطبيق Cryptomator.
# Migration
migration.title=ترقية الحافظة
@@ -145,6 +153,8 @@ preferences.general.theme.automatic=تلقائي
preferences.general.theme.light=فاتح (أبيض)
preferences.general.theme.dark=مظلم (أسود)
preferences.general.unlockThemes=تفعيل الوضع المظلم
preferences.general.showMinimizeButton=إظهار زر التصغير
preferences.general.showTrayIcon=إظهار أيقونة بجنب الساعة (يتطلب إعادة تشغيل)
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
preferences.general.debugLogging=تمكين سجلات التصحيح
preferences.general.debugDirectory=عرض ملفات السجل
@@ -168,15 +178,19 @@ preferences.updates.currentVersion=الإصدار الحالي: %s
preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوماتيكيا
preferences.updates.checkNowBtn=تحقق الان
preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح.
## Donation Key
preferences.donationKey=التبرعات
preferences.donationKey.registeredFor=مسجل لـ %s
preferences.donationKey.noDonationKey=لم يتم العثور على مفتاح تبرع صحيح. إنه مثل مفتاح الترخيص إلا أنه للأشخاص الرائعين الذين يستخدمون البرمجيات المجانية؛-)
preferences.donationKey.getDonationKey=الحصول على مفتاح تبرع
## Contribution
preferences.contribute=ادعمنا
preferences.contribute.registeredFor=شهادة الداعم مسجلة لـ %s
preferences.contribute.noCertificate=ادعم Cryptomator واحصل على شهادة الداعم. إنها مثل مفتاح الترخيص لكن للأشخاص الرائعين الذين يستخدمون البرامج المجانية ؛-)
preferences.contribute.getCertificate=ليس لديك واحدة بعد؟ تعلم كيف يمكنك الحصول عليها.
preferences.contribute.promptText=قم بلصق رمز شهادة الداعم هنا
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=حول البرنامج
# Vault Statistics
stats.title=إحصائيات عن %s
stats.cacheHitRate=معدل استخدام الكاش
## Read
stats.read.throughput.idle=قراءة: خامل
@@ -195,7 +209,12 @@ main.dropZone.dropVault=أضف هذا المخزن
main.dropZone.unknownDragboardContent=إذا كنت ترغب في إضافة مخزن، قم بسحبه إلى هذه النافذة
## Vault List
main.vaultlist.emptyList.onboardingInstruction=انقر هنا لإضافة خزنة
main.vaultlist.contextMenu.remove=حذف الخزنة
main.vaultlist.contextMenu.remove=حذف…
main.vaultlist.contextMenu.lock=قفل
main.vaultlist.contextMenu.unlock=فتح…
main.vaultlist.contextMenu.unlockNow=افتح الان
main.vaultlist.contextMenu.vaultoptions=إظهار خيارات المخزن
main.vaultlist.contextMenu.reveal=اظهار القرص
main.vaultlist.addVaultBtn=أضِف مخزنًا
## Vault Detail
### Welcome
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Prilagođena lokacija
addvaultwizard.new.directoryPickerButton=Odaberi…
addvaultwizard.new.directoryPickerTitle=Izaberi folder
addvaultwizard.new.fileAlreadyExists=Sef ne može biti kreiran na odabranoj lokaciji jer neki objekti tu već postoje.
addvaultwizard.new.locationDoesNotExist=Sef ne može biti kreiran na odabranoj lokaciji jer najmanje jedna komponenta lokacije ne postoji.
addvaultwizard.new.invalidName=Naziv sefa neispravan. Molimo razmislite o drugom nazivu.
### Password
addvaultwizard.new.createVaultBtn=Kreiraj novi sef
@@ -95,11 +93,14 @@ forgetPassword.confirmBtn=Zaboravili ste šifru
# Unlock
unlock.title=Otključaj sef
unlock.passwordPrompt=Unesite lozinku za "%s":
unlock.savePassword=Zapamti šifru
unlock.unlockBtn=Otključaj
##
unlock.chooseMasterkey.filePickerTitle=Odaberite Masterkey Datoteku
## Success
unlock.success.message=Uspješno ste otključali "%s"! Vaš sef je sada dostupan.
unlock.success.message=Uspješno ste otključali "%s"! Vaš sef je sada dostupan putem svog virtualnog diska.
unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo
unlock.success.revealBtn=Otkrij sef
unlock.success.revealBtn=Otkrij pogon
## Failure
unlock.error.heading=Sef nije moguće otključati
### Invalid Mount Point
@@ -173,11 +174,9 @@ preferences.updates.currentVersion=Trenutna verzija: %s
preferences.updates.autoUpdateCheck=Automatski provjeri ima li ažuriranja
preferences.updates.checkNowBtn=Provjeri sada
preferences.updates.updateAvailable=Dostupno ažuriranje na verziju %s.
## Donation Key
preferences.donationKey=Donacija
preferences.donationKey.registeredFor=Registrovan za %s
preferences.donationKey.noDonationKey=Nije pronađen važeći ključ za donaciju. To je poput licence, ali za sjajne ljude koji koriste besplatni softver. ;-)
preferences.donationKey.getDonationKey=Nabavite ključ za donaciju
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=O programu
@@ -222,7 +221,12 @@ main.dropZone.dropVault=Dodajte ovaj sef
main.dropZone.unknownDragboardContent=Ako želite dodati sef, povucite ga u ovaj prozor
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Kliknite ovdje da dodate sef
main.vaultlist.contextMenu.remove=Ukloni Sef
main.vaultlist.contextMenu.remove=Ukloni…
main.vaultlist.contextMenu.lock=Zaključaj
main.vaultlist.contextMenu.unlock=Otključaj…
main.vaultlist.contextMenu.unlockNow=Otključaj sada
main.vaultlist.contextMenu.vaultoptions=Pokaži opcije sefa
main.vaultlist.contextMenu.reveal=Otkrij pogon
main.vaultlist.addVaultBtn=Dodaj sef
## Vault Detail
### Welcome
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Ubicació personalitzada
addvaultwizard.new.directoryPickerButton=Trieu…
addvaultwizard.new.directoryPickerTitle=Seleccioneu el directori
addvaultwizard.new.fileAlreadyExists=No es pot crear una caixa forta en aquest camí perquè ja hi ha algun objecte.
addvaultwizard.new.locationDoesNotExist=No ha estat possible crear la caixa forta en aquest camí perquè, si més no, un component no existeix.
addvaultwizard.new.invalidName=El nom de la caixa forta no és vàlid. Si us plau, escribiu un mom de directori amb caràcters estàndard.
### Password
addvaultwizard.new.createVaultBtn=Crea la caixa forta
@@ -97,10 +95,12 @@ unlock.title=Desbloquejar la caixa forta
unlock.passwordPrompt=Introduïu la contrasenya de "%s":
unlock.savePassword=Recorda la contrasenya
unlock.unlockBtn=Desbloqueja
##
unlock.chooseMasterkey.filePickerTitle=Seleccioneu el fitxer Masterkey
## Success
unlock.success.message="%s" s'ha desbloquejat correctament! Ja es pot accedir a la caixa forta.
unlock.success.message=S'ha desblocat %s correctament! Podeu accedir a la vostra caixa forta a través de la unitat virtual.
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
unlock.success.revealBtn=Mostra la caixa forta
unlock.success.revealBtn=Mostra la unitat
## Failure
unlock.error.heading=No ha estat possible desblocar la caixa forta
### Invalid Mount Point
@@ -174,11 +174,14 @@ preferences.updates.currentVersion=Versió actual: %s
preferences.updates.autoUpdateCheck=Comprova automàticament si hi ha actualitzacions
preferences.updates.checkNowBtn=Comprova-ho ara
preferences.updates.updateAvailable=L'actualització a la versió %s es troba disponible.
## Donation Key
preferences.donationKey=Donacions
preferences.donationKey.registeredFor=Registrat per %s
preferences.donationKey.noDonationKey=No s'ha trobar una clau de donació vàlida. És sembla a una clau de llicència però per gent meravellosa qui utilitza programari lliure. ;-)
preferences.donationKey.getDonationKey=Obtingau una clau de donació
## Contribution
preferences.contribute=Doneu-nos suport
preferences.contribute.registeredFor=Certificat de col·laborador registrat per a %s
preferences.contribute.noCertificate=Doneu suport a Cryptomator i rebeu un certificat de col·laborador. És com una clau de llicència però per a gent meravellosa que fa servir programari lliure. ;-)
preferences.contribute.getCertificate=Encara no en teniu cap? Sapigueu com aconseguir-ne un.
preferences.contribute.promptText=Enganxeu aquí el certificat de col·laborador
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Quant a
@@ -223,7 +226,12 @@ main.dropZone.dropVault=Afegeix aquesta caixa forta
main.dropZone.unknownDragboardContent=Si voleu afegir una caixa forta, arrossegueu-la a aquesta finestra
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Feu clic aquí per afegir una caixa forta
main.vaultlist.contextMenu.remove=Elimina la caixa forta
main.vaultlist.contextMenu.remove=Elimina…
main.vaultlist.contextMenu.lock=Bloqueja
main.vaultlist.contextMenu.unlock=Desbloca…
main.vaultlist.contextMenu.unlockNow=Desbloqueja ara
main.vaultlist.contextMenu.vaultoptions=Opcions de la caixa forta
main.vaultlist.contextMenu.reveal=Mostra la unitat
main.vaultlist.addVaultBtn=Afegir una caixa forta
## Vault Detail
### Welcome
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=...
addvaultwizard.new.directoryPickerLabel=Vlastní umístění
addvaultwizard.new.directoryPickerButton=Vybrat...
addvaultwizard.new.directoryPickerTitle=Vyberte adresář
addvaultwizard.new.fileAlreadyExists=Trezor nemůže být vytvořen na tomto umístění, protože stejný objekt již existuje.
addvaultwizard.new.locationDoesNotExist=Trezor nemůže být vytvořen v tomto umístění, protože přinejmenším jedna položka z daného umístě neexistuje.
addvaultwizard.new.fileAlreadyExists=Soubor nebo složka s tímto názvem trezoru již existuje
addvaultwizard.new.locationDoesNotExist=Složka v zadané cestě neexistuje nebo není přístupná
addvaultwizard.new.locationIsNotWritable=Není možné zapisovat na zadané cestě
addvaultwizard.new.locationIsOk=Vhodné umístění pro váš trezor
addvaultwizard.new.invalidName=Neplatné jméno trezoru. Prosím změňte jméno adresáře.
### Password
addvaultwizard.new.createVaultBtn=Vytvořit trezor
@@ -97,10 +99,12 @@ unlock.title=Odemknout trezor
unlock.passwordPrompt=Zadejte heslo pro "%s":
unlock.savePassword=Zapamatovat heslo
unlock.unlockBtn=Odemknout
##
unlock.chooseMasterkey.filePickerTitle=Vyberte Masterkey soubor
## Success
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný.
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný jako virtuální jednotka.
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
unlock.success.revealBtn=Zobrazit trezor
unlock.success.revealBtn=Zobrazit jednotku
## Failure
unlock.error.heading=Nelze odemknout trezor
### Invalid Mount Point
@@ -109,7 +113,12 @@ unlock.error.invalidMountPoint.existing=Připojovací bod %s již existuje nebo
# Lock
## Force
lock.forced.heading=Běžné uzamčení selhalo
lock.forced.message=Uzamčení "%s" bylo zablokováno nevyřízenými operacemi nebo otevřenými soubory. Můžete vynutit uzamčení tohoto trezoru, ale přerušení I/O může mít za následek ztrátu neuložených dat.
lock.forced.confirmBtn=Přesto uzamknout
## Failure
lock.fail.heading=Uzamčení trezoru selhalo.
lock.fail.message=Trezor "%s" nelze uzamknout. Ujistěte se, že je neuložená práce uložena jinde a že jsou dokončeny důležité operace čtení/zápis. Za účelem uzavření trezoru ukončte proces Cryptomatoru.
# Migration
migration.title=Upgrade trezoru
@@ -169,11 +178,14 @@ preferences.updates.currentVersion=Aktuální verze: %s
preferences.updates.autoUpdateCheck=Automaticky kontrolovat aktualizace
preferences.updates.checkNowBtn=Zkontrolovat nyní
preferences.updates.updateAvailable=Dostupná nová verze %s.
## Donation Key
preferences.donationKey=Příspěvek
preferences.donationKey.registeredFor=Registrováno na: %s
preferences.donationKey.noDonationKey=Nebyl nalezen žádný platný darovací klíč. Je to jako licenční klíč, ale pro úžasné lidi využívající software zadarmo. ;-)
preferences.donationKey.getDonationKey=Získat darovací klíč
## Contribution
preferences.contribute=Podpořte nás
preferences.contribute.registeredFor=Certifikát podporovatele byl registrován pro %s
preferences.contribute.noCertificate=Podpořte Cryptomator a získejte certifikát podporovatele. Je to jako licenční klíč, ale pro skvělé lidi, kteří používají svobodný software. ;-)
preferences.contribute.getCertificate=Ještě žádný nemáte? Naučte se, jak ho můžete získat.
preferences.contribute.promptText=Sem vložte kód certifikátu podporovatele
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=O aplikaci
@@ -218,7 +230,12 @@ main.dropZone.dropVault=Přidat tento trezor
main.dropZone.unknownDragboardContent=Pokud chcete přidat trezor, přetáhněte jej do tohoto okna
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klikněte zde pro přidání nového trezoru
main.vaultlist.contextMenu.remove=Odstranit trezor
main.vaultlist.contextMenu.remove=Odstranit…
main.vaultlist.contextMenu.lock=Zamknout
main.vaultlist.contextMenu.unlock=Odemknout…
main.vaultlist.contextMenu.unlockNow=Odemknout nyní
main.vaultlist.contextMenu.vaultoptions=Zobrazit možnosti trezoru
main.vaultlist.contextMenu.reveal=Zobrazit jednotku
main.vaultlist.addVaultBtn=Přidat trezor
## Vault Detail
### Welcome
@@ -14,7 +14,7 @@ generic.button.next=Weiter
generic.button.print=Drucken
## Error
generic.error.title=Ein unerwarteter Fehler ist aufgetreten
generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde die folgende Fehlermeldung und beschreibe kurz, durch welche Schritte er aufgetreten ist.
generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde den folgenden Fehlertext und beschreibe kurz, durch welche Schritte der Fehler aufgetreten ist.
# Defaults
defaults.vault.vaultName=Tresor
@@ -38,14 +38,16 @@ addvaultwizard.welcome.existingButton=Existierenden Tresor öffnen
addvaultwizard.new.nameInstruction=Wähle einen Namen für den Tresor
addvaultwizard.new.namePrompt=Tresorname
### Location
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Daten deines Tresors ablegen?
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Dateien deines Tresors ablegen?
addvaultwizard.new.locationLabel=Speicherort
addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Eigener Ort
addvaultwizard.new.directoryPickerButton=Durchsuchen …
addvaultwizard.new.directoryPickerTitle=Verzeichnis auswählen
addvaultwizard.new.fileAlreadyExists=Der Tresor konnte nicht erstellt werden, da am Speicherort so eine Datei bereits existiert.
addvaultwizard.new.locationDoesNotExist=Der Tresor kann nicht erstellt werden, da mindestens ein Speicherort nicht existiert.
addvaultwizard.new.fileAlreadyExists=Eine Datei oder ein Ordner mit diesem Namen ist bereits vorhanden
addvaultwizard.new.locationDoesNotExist=Der Ordner im angegebenen Pfad existiert nicht oder kann nicht geöffnet werden
addvaultwizard.new.locationIsNotWritable=Kein Schreibzugriff auf den angegebenen Pfad
addvaultwizard.new.locationIsOk=Geeigneter Ort für deinen Tresor
addvaultwizard.new.invalidName=Ungültiger Tresorname. Bitte wähle einen regulären Namen, mit dem auch Verzeichnisse erstellt werden können.
### Password
addvaultwizard.new.createVaultBtn=Tresor erstellen
@@ -79,7 +81,7 @@ addvaultwizard.success.unlockNow=Jetzt entsperren
# Remove Vault
removeVault.title=Tresor entfernen
removeVault.information=Dein Tresor wird nicht gelöscht, sondern lediglich aus Cryptomators Tresorliste entfernt. Du kannst ihn daher später jederzeit wieder hinzufügen.
removeVault.information=Dieser Tresor wird nur aus der Tresorliste entfernt. Du kannst den Tresor später jederzeit wieder hinzufügen. Es werden keine verschlüsselten Daten gelöscht.
removeVault.confirmBtn=Tresor entfernen
# Change Password
@@ -97,10 +99,12 @@ unlock.title=Tresor entsperren
unlock.passwordPrompt=Gib das Passwort für „%s“ ein:
unlock.savePassword=Passwort merken
unlock.unlockBtn=Entsperren
##
unlock.chooseMasterkey.filePickerTitle=Masterkey-Datei auswählen
## Success
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du auf deinen Tresor zugreifen.
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du über das virtuelle Laufwerk auf deinen Tresor zugreifen.
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
unlock.success.revealBtn=Tresor anzeigen
unlock.success.revealBtn=Laufwerk anzeigen
## Failure
unlock.error.heading=Tresor konnte nicht entsperrt werden
### Invalid Mount Point
@@ -172,13 +176,16 @@ preferences.volume.webdav.scheme=WebDAV-Schema
preferences.updates=Updates
preferences.updates.currentVersion=Aktuelle Version: %s
preferences.updates.autoUpdateCheck=Automatisch nach Updates suchen
preferences.updates.checkNowBtn=Jetzt suchen
preferences.updates.checkNowBtn=Jetzt prüfen
preferences.updates.updateAvailable=Update auf Version %s verfügbar.
## Donation Key
preferences.donationKey=Spende
preferences.donationKey.registeredFor=Registriert für %s
preferences.donationKey.noDonationKey=Kein gültiger Spendenschlüssel gefunden. Er ist wie ein Lizenzschlüssel, aber für tolle Leute, die freie Software verwenden. ;-)
preferences.donationKey.getDonationKey=Hol dir einen Spendenschlüssel
## Contribution
preferences.contribute=Unterstütze uns
preferences.contribute.registeredFor=Supporter-Zertifikat registriert für %s
preferences.contribute.noCertificate=Unterstütze Cryptomator und erhalte ein Supporter-Zertifikat. Es ist wie ein Lizenzschlüssel, aber für großartige Menschen, die freie Software verwenden. ;-)
preferences.contribute.getCertificate=Du hast noch keines? Erfahre, wie du es erhalten kannst.
preferences.contribute.promptText=Code des Supporter-Zertifikats hier einfügen
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Über
@@ -223,7 +230,12 @@ main.dropZone.dropVault=Diesen Tresor hinzufügen
main.dropZone.unknownDragboardContent=Wenn Sie einen Tresor hinzufügen möchten, ziehen Sie ihn in dieses Fenster
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klicke hier, um einen Tresor hinzuzufügen
main.vaultlist.contextMenu.remove=Tresor entfernen…
main.vaultlist.contextMenu.remove=Entfernen
main.vaultlist.contextMenu.lock=Sperren
main.vaultlist.contextMenu.unlock=Entsperren …
main.vaultlist.contextMenu.unlockNow=Jetzt entsperren
main.vaultlist.contextMenu.vaultoptions=Tresoroptionen anzeigen
main.vaultlist.contextMenu.reveal=Laufwerk anzeigen
main.vaultlist.addVaultBtn=Tresor hinzufügen
## Vault Detail
### Welcome
@@ -246,7 +258,7 @@ main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Tresorstatistik
### Missing
main.vaultDetail.missing.info=Mit diesem Pfad konnte Cryptomator keinen Tresor finden.
main.vaultDetail.missing.info=Cryptomator konnte keinen Tresor mit diesem Pfad finden.
main.vaultDetail.missing.recheck=Erneut prüfen
main.vaultDetail.missing.remove=Aus Tresorliste entfernen…
main.vaultDetail.missing.changeLocation=Speicherort des Tresors ändern…
@@ -261,8 +273,8 @@ wrongFileAlert.header.lead=Für diesen Zweck stellt Cryptomator ein Laufwerk in
wrongFileAlert.instruction.0=Folge diesen Schritten, um Dateien zu verschlüsseln:
wrongFileAlert.instruction.1=1. Entsperre deinen Tresor.
wrongFileAlert.instruction.2=2. Klicke auf „Anzeigen“, um das Laufwerk in deinem Dateimanager zu öffnen.
wrongFileAlert.instruction.3=3. Füge deine Dateien diesem Laufwerk hinzu.
wrongFileAlert.link=Für weitere Unterstützung besuche
wrongFileAlert.instruction.3=3. Füge deine Dateien zu diesem Laufwerk hinzu.
wrongFileAlert.link=Besuche für weitere Hilfe
# Vault Options
## General
@@ -276,26 +288,26 @@ vaultOptions.general.actionAfterUnlock.ask=Nachfragen
## Mount
vaultOptions.mount=Laufwerk
vaultOptions.mount.readonly=Schreibgeschützt
vaultOptions.mount.customMountFlags=Benutzerdefinierte Mount-Flags
vaultOptions.mount.customMountFlags=Benutzerdefinierte Einhänge-Optionen
vaultOptions.mount.winDriveLetterOccupied=belegt
vaultOptions.mount.mountPoint=Einhängepunkt
vaultOptions.mount.mountPoint.auto=Automatisch einen passenden Ort auswählen
vaultOptions.mount.mountPoint.driveLetter=Zugewiesenen Laufwerksbuchstaben verwenden
vaultOptions.mount.mountPoint.custom=Pfad selbst wählen
vaultOptions.mount.mountPoint.auto=Wähle automatisch einen geeigneten Ort aus
vaultOptions.mount.mountPoint.driveLetter=Laufwerksbuchstaben zuweisen
vaultOptions.mount.mountPoint.custom=Eigener Pfad
vaultOptions.mount.mountPoint.directoryPickerButton=Durchsuchen …
vaultOptions.mount.mountPoint.directoryPickerTitle=Wähle ein leeres Verzeichnis
## Master Key
vaultOptions.masterkey=Passwort
vaultOptions.masterkey.changePasswordBtn=Passwort ändern
vaultOptions.masterkey.forgetSavedPasswordBtn=Gespeichertes Passwort vergessen
vaultOptions.masterkey.recoveryKeyExpanation=Bei Verlust deines Passworts ist ein Wiederherstellungsschlüssel deine einzige Möglichkeit, den Zugriff auf einen Tresor wiederherzustellen.
vaultOptions.masterkey.recoveryKeyExpanation=Dein Wiederherstellungsschlüssel gehört nur dir! Dieser wird benötigt, um deinen Tresor wiederherzustellen, für den Fall das du dein Passwort verloren hast.
vaultOptions.masterkey.showRecoveryKeyBtn=Wiederherstellungsschlüssel anzeigen
vaultOptions.masterkey.recoverPasswordBtn=Passwort wiederherstellen
# Recovery Key
recoveryKey.title=Wiederherstellungsschlüssel
recoveryKey.enterPassword.prompt=Gib dein Passwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen:
recoveryKey.display.message=Mit folgendem Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
recoveryKey.display.message=Mit dem folgenden Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
recoveryKey.display.StorageHints=Bewahre ihn möglichst sicher auf, z. B.\n • in einem Passwortmanager\n • auf einem USB-Speicherstick\n • auf Papier ausgedruckt
recoveryKey.recover.prompt=Gib deinen Wiederherstellungsschlüssel für „%s“ ein:
recoveryKey.recover.validKey=Dies ist ein gültiger Wiederherstellungsschlüssel
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Προσαρμοσμένη τοποθεσία
addvaultwizard.new.directoryPickerButton=Επιλογή…
addvaultwizard.new.directoryPickerTitle=Επιλογή φακέλου
addvaultwizard.new.fileAlreadyExists=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς κάποια αντικείμενα υπάρχουν ήδη.
addvaultwizard.new.locationDoesNotExist=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς τουλάχιστον ένα στοιχείο του μονοπατιού δεν υπάρχει.
addvaultwizard.new.fileAlreadyExists=Ένα αρχείο ή φάκελος με το όνομα του vault υπάρχει ήδη
addvaultwizard.new.locationDoesNotExist=Στην καθορισμένη διαδρομή δεν υπάρχει ή δεν μπορεί να προσπελαστεί ένας φάκελος
addvaultwizard.new.locationIsNotWritable=Δεν υπάρχει πρόσβαση εγγραφής στην καθορισμένη διαδρομή
addvaultwizard.new.locationIsOk=Κατάλληλη τοποθεσία για το vault σας
addvaultwizard.new.invalidName=Λάθος όνομα vault. Παρακαλώ χρησιμοποιείστε ένα κανονικό όνομα φακέλου.
### Password
addvaultwizard.new.createVaultBtn=Δημιουργία Vault
@@ -97,10 +99,12 @@ unlock.title=Ξεκλείδωμα Vault
unlock.passwordPrompt=Εισάγετε τον κωδικό για "%s":
unlock.savePassword=Απομνημόνευση κωδικού πρόσβασης
unlock.unlockBtn=Ξεκλείδωμα
##
unlock.chooseMasterkey.filePickerTitle=Επιλέξτε το αρχείο Masterkey
## Success
unlock.success.message="%s" ξεκλειδώθηκε επιτυχώς! Το vault σας είναι διαθέσιμο.
unlock.success.message=Ξεκλειδώθηκε "%s" επιτυχώς! Το vault σας είναι διαθέσιμο μέσω του εικονικού δίσκου του.
unlock.success.rememberChoice=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά
unlock.success.revealBtn=Αποκάλυψη Vault
unlock.success.revealBtn=Αποκάλυψη εικονικού δίσκου
## Failure
unlock.error.heading=Αδυναμία ξεκλειδώματος vault
### Invalid Mount Point
@@ -174,11 +178,14 @@ preferences.updates.currentVersion=Τρέχουσα έκδοση: %s
preferences.updates.autoUpdateCheck=Αυτόματος έλεγχος για ενημερώσεις
preferences.updates.checkNowBtn=Έλεγχος τώρα
preferences.updates.updateAvailable=Η ενημέρωση για την έκδοση %s είναι διαθέσιμη.
## Donation Key
preferences.donationKey=Δωρεά
preferences.donationKey.registeredFor=Καταχωρημένο σε %s
preferences.donationKey.noDonationKey=Δεν βρέθηκε ενεργό κλειδί δωρεάς. Είναι σαν κλειδί αγοράς αλλά για απίθανους ανθρώπους που χρησιμοποιούν δωρεάν λογισμικό. ;-)
preferences.donationKey.getDonationKey=Πάρε ένα κλειδί δωρεάς
## Contribution
preferences.contribute=Υποστηρίξτε μας
preferences.contribute.registeredFor=Το πιστοποιητικό υποστήριξης καταχωρήθηκε για %s
preferences.contribute.noCertificate=Υποστηρίξτε το Cryptomator και λάβετε ένα πιστοποιητικό υποστηρικτή. Είναι σαν κλειδί άδειας χρήσης, αλλά για φοβερά άτομα που χρησιμοποιούν ελεύθερο λογισμικό. ;-)
preferences.contribute.getCertificate=Δεν έχετε ένα ήδη; Μάθετε πώς μπορείτε να το αποκτήσετε.
preferences.contribute.promptText=Επικολλήστε τον κωδικό πιστοποιητικού υποστηρικτή εδώ
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Σχετικά με
@@ -223,7 +230,12 @@ main.dropZone.dropVault=Προσθήκη vault
main.dropZone.unknownDragboardContent=Αν θέλετε να προσθέσετε ένα vault, σύρετε το σε αυτό το παράθυρο
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Κάντε κλικ εδώ για να προσθέσετε ένα vault
main.vaultlist.contextMenu.remove=Διαγραφή Vault
main.vaultlist.contextMenu.remove=Αφαίρεση
main.vaultlist.contextMenu.lock=Κλείδωμα
main.vaultlist.contextMenu.unlock=Ξεκλείδωμα…
main.vaultlist.contextMenu.unlockNow=Ξεκλείδωμα τώρα
main.vaultlist.contextMenu.vaultoptions=Εμφάνιση επιλογών Vault
main.vaultlist.contextMenu.reveal=Αποκάλυψη εικονικού δίσκου
main.vaultlist.addVaultBtn=Προσθήκη Vault
## Vault Detail
### Welcome
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Ubicación personalizada
addvaultwizard.new.directoryPickerButton=Elegir…
addvaultwizard.new.directoryPickerTitle=Seleccionar directorio
addvaultwizard.new.fileAlreadyExists=La bóveda no puede crearse en esta ruta porque ya existe un objeto.
addvaultwizard.new.locationDoesNotExist=La bóveda no puede ser creada en esta ruta porque al menos un componente no existe.
addvaultwizard.new.fileAlreadyExists=Ya existe un archivo o directorio con el nombre de la bóveda
addvaultwizard.new.locationDoesNotExist=No existe un directorio en la ruta especificada o no se puede acceder
addvaultwizard.new.locationIsNotWritable=Sin acceso de escritura en la ruta especificada
addvaultwizard.new.locationIsOk=Ubicación adecuada para la bóveda
addvaultwizard.new.invalidName=Nombre de bóveda inválido. Considere un nombre de directorio regular.
### Password
addvaultwizard.new.createVaultBtn=Crear bóveda
@@ -97,10 +99,12 @@ unlock.title=Desbloquear bóveda
unlock.passwordPrompt=Ingresar contraseña para "%s":
unlock.savePassword=Recordar contraseña
unlock.unlockBtn=Desbloquear
##
unlock.chooseMasterkey.filePickerTitle=Seleccionar archivo Masterkey
## Success
unlock.success.message=¡"%s" se desbloqueó con éxito! La bóveda ya es accesible.
unlock.success.message=¡Desbloqueo de "%s" exitoso! Su bóveda ahora es accesible a través de su unidad virtual.
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
unlock.success.revealBtn=Revelar bóveda
unlock.success.revealBtn=Revelar unidad
## Failure
unlock.error.heading=No se puede desbloquear la bóveda
### Invalid Mount Point
@@ -174,11 +178,9 @@ preferences.updates.currentVersion=Versión actual: %s
preferences.updates.autoUpdateCheck=Buscar actualizaciones automáticamente
preferences.updates.checkNowBtn=Buscar ahora
preferences.updates.updateAvailable=Actualización a la versión %s disponible.
## Donation Key
preferences.donationKey=Donación
preferences.donationKey.registeredFor=Registrado para %s
preferences.donationKey.noDonationKey=No se encontró una clave de donación válida. Es como una clave de licencia pero para personas geniales que usan software libre.
preferences.donationKey.getDonationKey=Obtener clave de donación
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Acerca de
@@ -223,7 +225,12 @@ main.dropZone.dropVault=Añadir esta bóveda
main.dropZone.unknownDragboardContent=Si desea añadir una bóveda, arrástrela a esta ventana
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Hacer clic aquí para añadir una bóveda
main.vaultlist.contextMenu.remove=Eliminar bóveda
main.vaultlist.contextMenu.remove=Eliminar…
main.vaultlist.contextMenu.lock=Bloquear
main.vaultlist.contextMenu.unlock=Desbloquear…
main.vaultlist.contextMenu.unlockNow=Desbloquear ahora
main.vaultlist.contextMenu.vaultoptions=Mostrar opciones de la bóveda
main.vaultlist.contextMenu.reveal=Revelar unidad
main.vaultlist.addVaultBtn=Añadir bóveda
## Vault Detail
### Welcome
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Emplacement personnalisé
addvaultwizard.new.directoryPickerButton=Choisir...
addvaultwizard.new.directoryPickerTitle=Choisissez le répertoire
addvaultwizard.new.fileAlreadyExists=Le coffre ne peut pas être créé sur ce chemin car un objet existe déjà.
addvaultwizard.new.locationDoesNotExist=Le coffre ne peut pas être créé à cet endroit car au moins un répertoire du chemin n'existe pas.
addvaultwizard.new.fileAlreadyExists=Un fichier ou un répertoire avec ce nom existe déjà
addvaultwizard.new.locationDoesNotExist=Un répertoire dans le chemin spécifié n'existe pas ou ne peut pas être accédé
addvaultwizard.new.locationIsNotWritable=Pas d'accès en écriture au chemin spécifié
addvaultwizard.new.locationIsOk=Lieu approprié pour votre coffre
addvaultwizard.new.invalidName=Nom de coffre invalide. Préférez un nom de répertoire habituel.
### Password
addvaultwizard.new.createVaultBtn=Créer un coffre
@@ -97,10 +99,12 @@ unlock.title=Déverrouiller le coffre
unlock.passwordPrompt=Entrez le mot de passe pour “%s” :
unlock.savePassword=Mémoriser le mot de passe
unlock.unlockBtn=Déverrouiller
##
unlock.chooseMasterkey.filePickerTitle=Sélectionner le fichier clef
## Success
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible.
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible par son lecteur virtuel.
unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander
unlock.success.revealBtn=Révéler le coffre
unlock.success.revealBtn=Révéler le lecteur
## Failure
unlock.error.heading=Impossible de déverrouiller le coffre
### Invalid Mount Point
@@ -174,11 +178,14 @@ preferences.updates.currentVersion=Version actuelle : “%s”
preferences.updates.autoUpdateCheck=Vérifier automatiquement lexistence de mise à jour
preferences.updates.checkNowBtn=Vérifier maintenant
preferences.updates.updateAvailable=Mise à jour “%s” disponible.
## Donation Key
preferences.donationKey=Faire un Don
preferences.donationKey.registeredFor=Licence enregistrée à %s
preferences.donationKey.noDonationKey=Aucune clé de don valide trouvée. C'est comme une clé de licence mais pour des personnes géniales utilisant un logiciel libre ;-)
preferences.donationKey.getDonationKey=Obtenir une clé de don
## Contribution
preferences.contribute=Nous soutenir
preferences.contribute.registeredFor=Certificat de soutien enregistré pour %s
preferences.contribute.noCertificate=Soutenez Cryptomator et recevez un certificat de soutien. C'est comme une clé de licence, mais pour des gens géniaux qui utilisent des logiciels libres. ;-)
preferences.contribute.getCertificate=Vous n'en avez pas encore? Découvrez comment vous pouvez l'obtenir.
preferences.contribute.promptText=Collez le code du certificat du supporter ici
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=A propos
@@ -223,7 +230,12 @@ main.dropZone.dropVault=Ajouter ce coffre
main.dropZone.unknownDragboardContent=Si vous voulez ajouter un coffre, faites-le glisser dans cette fenêtre
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Cliquez ici pour ajouter un coffre
main.vaultlist.contextMenu.remove=Supprimer le coffre
main.vaultlist.contextMenu.remove=Retirer
main.vaultlist.contextMenu.lock=Verrouiller
main.vaultlist.contextMenu.unlock=Déverrouiller…
main.vaultlist.contextMenu.unlockNow=Déverrouiller maintenant
main.vaultlist.contextMenu.vaultoptions=Afficher les options du coffre
main.vaultlist.contextMenu.reveal=Révéler le lecteur
main.vaultlist.addVaultBtn=Ajouter un coffre
## Vault Detail
### Welcome
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=अपने पसंद की जगह डालें
addvaultwizard.new.directoryPickerButton=चुनें…
addvaultwizard.new.directoryPickerTitle=निर्देशिका चुनें
addvaultwizard.new.fileAlreadyExists=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कुछ वस्तु पहले से मौजूद है।
addvaultwizard.new.locationDoesNotExist=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कम से कम एक पथ घटक मौजूद नहीं है।
addvaultwizard.new.invalidName=अमान्य वॉल्ट नाम। कृपया एक नियमित निर्देशिका नाम पर विचार करें।
### Password
addvaultwizard.new.createVaultBtn=वॉल्ट बनाएं
@@ -72,6 +70,7 @@ changepassword.title=पासवर्ड बदलें
# Unlock
unlock.unlockBtn=अनलॉक करें
##
## Success
## Failure
### Invalid Mount Point
@@ -94,7 +93,9 @@ preferences.title=प्राथमिकताएं
preferences.general=सामान्य
## Volume
## Updates
## Donation Key
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
# Vault Statistics
@@ -106,6 +107,7 @@ main.closeBtn.tooltip=बंद करें
main.preferencesBtn.tooltip=प्राथमिकताएं
## Drag 'n' Drop
## Vault List
main.vaultlist.contextMenu.lock=लॉक करें
main.vaultlist.addVaultBtn=वाउल्ट डालें
## Vault Detail
### Welcome
@@ -25,6 +25,7 @@
# Forget Password
# Unlock
##
## Success
## Failure
### Invalid Mount Point
@@ -44,7 +45,9 @@
## General
## Volume
## Updates
## Donation Key
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
# Vault Statistics
@@ -43,8 +43,6 @@ addvaultwizard.new.locationLabel=Tárolási hely
addvaultwizard.new.directoryPickerLabel=Egyedi hely
addvaultwizard.new.directoryPickerButton=Választás…
addvaultwizard.new.directoryPickerTitle=Könyvtár kiválasztása
addvaultwizard.new.fileAlreadyExists=Nem lehet a széfet létrehozni ezen a helyen, mert egy fájl már létezik itt.
addvaultwizard.new.locationDoesNotExist=Nem lehet a széfet létrehozni ezen a helyen, mert az útvonal legalább egy darabja nem létezik.
addvaultwizard.new.invalidName=Érvénytelen széf elnevezés. Kérjük vegye figyelembe a szabályos könyvtárelnevezésre vonatkozó szabályokat.
### Password
addvaultwizard.new.createVaultBtn=Új széf létrehozása
@@ -95,8 +93,9 @@ forgetPassword.confirmBtn=Jelszó elfelejtése
unlock.title=Széf feloldása
unlock.passwordPrompt=Írja be a jelszavát a következő széfhez "%s":
unlock.unlockBtn=Feloldás
##
unlock.chooseMasterkey.filePickerTitle=Mesterkulcs fájl kiválasztása
## Success
unlock.success.message="%s" sikreresen feloldásra került! Mostmár hozzáférhet a széféhez.
unlock.success.rememberChoice=Jegyezze meg a választást és ne mutassa többet
unlock.success.revealBtn=Széf megjelenítése
## Failure
@@ -159,11 +158,9 @@ preferences.updates.currentVersion=Jelenlegi verzió: %s
preferences.updates.autoUpdateCheck=Frissítések autómatikus keresése
preferences.updates.checkNowBtn=Ellenőrzés most
preferences.updates.updateAvailable=Frissítés a %s verzióra elérhető.
## Donation Key
preferences.donationKey=Adomány
preferences.donationKey.registeredFor=Regisztrálva %s számára
preferences.donationKey.noDonationKey=Nem található érvényes adománykulcs. Mint egy licenckulcs, csak olyan nagyszerű emberek számára, akik ingyenes szofvereket használnak. ;-)
preferences.donationKey.getDonationKey=Adománykulcs beszerzése
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Rólunk
@@ -207,7 +204,10 @@ main.dropZone.dropVault=Adja hozzá ezt a széfet
main.dropZone.unknownDragboardContent=Ha egy széfet szeretne hozzáadni, akkor húzza át erre az ablakra.
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Kattintson ide egy széf hozzáadásához
main.vaultlist.contextMenu.remove=Széf eltávolítása…
main.vaultlist.contextMenu.lock=Zárolás
main.vaultlist.contextMenu.unlock=Feloldás…
main.vaultlist.contextMenu.unlockNow=Azonnali feloldás
main.vaultlist.contextMenu.reveal=Széf megjelenítése
main.vaultlist.addVaultBtn=Széf hozzáadása
## Vault Detail
### Welcome
@@ -44,8 +44,9 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Sesuaikan Lokasi
addvaultwizard.new.directoryPickerButton=Pilih…
addvaultwizard.new.directoryPickerTitle=Pilih Folder
addvaultwizard.new.fileAlreadyExists=Brankas tidak dibuat disini karena beberapa item telah ada disini.
addvaultwizard.new.locationDoesNotExist=Brankas tidak bisa dibuat disini karena setidaknya satu jalur komponen tidak ada.
addvaultwizard.new.fileAlreadyExists=Sudah ada file atau direktori dengan nama yang sama
addvaultwizard.new.locationDoesNotExist=Direktori pada path yang dipilih tidak ada atau tidak dapat diakses
addvaultwizard.new.locationIsNotWritable=Anda tidak memiliki hak akses untuk menulis pada path yang dipilih
addvaultwizard.new.invalidName=Nama brankas salah. Harap pilih nama folder yang umum.
### Password
addvaultwizard.new.createVaultBtn=Buat Brankas
@@ -57,6 +58,8 @@ addvault.new.readme.storageLocation.fileName=PENTING.rtf
addvault.new.readme.storageLocation.1=⚠️ BERKAS BRANKAS ⚠️
addvault.new.readme.storageLocation.2=Ini adalah lokasi penyimpanan brankas kamu.
addvault.new.readme.storageLocation.3=JANGAN
addvault.new.readme.storageLocation.4=• mengubah file dalam direktori ini, atau
addvault.new.readme.storageLocation.5=• menempelkan file untuk dienkripsi ke dalam direktori ini.
addvault.new.readme.storageLocation.6=Jika kamu ingin mengenkripsi berkas dan melihat isi brankas, lakukan hal berikut:
addvault.new.readme.storageLocation.7=1. Tambahkan brankas ini ke Cryptomator.
addvault.new.readme.storageLocation.8=2. Buka gembok brankas di Cryptomator.
@@ -65,18 +68,34 @@ addvault.new.readme.storageLocation.10=Jika kamu butuh bantuan, kunjungi dokumen
addvault.new.readme.accessLocation.fileName=SELAMAT_DATANG.rtf
addvault.new.readme.accessLocation.1=🔐️ ISI TERENKRIPSI 🔐️
addvault.new.readme.accessLocation.2=Ini adalah lokasi akses brankas kamu.
addvault.new.readme.accessLocation.3=File yang ditambahkan ke volume ini akan dienkripsi oleh Cryptomator. Anda dapat mempergunakan isi vault seperti dalam folder lain. Saat ini Anda sedang mengakses tampilan versi dekripsi, file Anda selalu terenkripsi di dalam cakram keras Anda.
addvault.new.readme.accessLocation.4=Anda dapat menghapus file ini.
## Existing
addvaultwizard.existing.instruction=Pilih file "masterkey.cryptomator" dalam vault Anda.
addvaultwizard.existing.chooseBtn=Pilih…
addvaultwizard.existing.filePickerTitle=Pilih File Kunci Induk
## Success
addvaultwizard.success.nextStepsInstructions=Vault "%s" telah dibuat.\nAnda harus membuka kunci vault ini untuk mengakses atau menambahkan konten. Anda juga dapat membuka kunci vault ini kapan saja di kemudian hari.
addvaultwizard.success.unlockNow=Buka Kunci Sekarang
# Remove Vault
removeVault.title=Hapus Vault
removeVault.information=Cryptomator hanya akan melupakan vault ini. Anda dapat menambahkan vault ini lagi nantinya. File yang telah dienkripsi tidak akan dihapus dari cakram keras Anda.
removeVault.confirmBtn=Hapus Vault
# Change Password
changepassword.title=Ubah Kata Sandi
changepassword.enterOldPassword=Masukkan kata sandi untuk "%s" saat ini
changepassword.finalConfirmation=Saya mengerti bahwa saya tidak akan dapat mengakses data saya apabila saya lupa kata sandi saya
# Forget Password
forgetPassword.title=Lupa Kata Sandi
forgetPassword.confirmBtn=Lupa Kata Sandi
# Unlock
unlock.unlockBtn=Buka Gembok
##
unlock.chooseMasterkey.filePickerTitle=Pilih File Kunci Induk
## Success
## Failure
### Invalid Mount Point
@@ -89,6 +108,7 @@ unlock.unlockBtn=Buka Gembok
## Start
## Run
## Sucess
migration.success.unlockNow=Buka Kunci Sekarang
## Missing file system capabilities
## Impossible
@@ -97,7 +117,9 @@ preferences.title=Preferensi
## General
## Volume
## Updates
## Donation Key
## Contribution
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
# Vault Statistics
@@ -109,10 +131,13 @@ main.closeBtn.tooltip=Tutup
main.preferencesBtn.tooltip=Preferensi
## Drag 'n' Drop
## Vault List
main.vaultlist.contextMenu.lock=Gembok
main.vaultlist.contextMenu.unlockNow=Buka Kunci Sekarang
main.vaultlist.addVaultBtn=Tambah Brankas
## Vault Detail
### Welcome
### Locked
main.vaultDetail.unlockNowBtn=Buka Kunci Sekarang
### Unlocked
main.vaultDetail.lockBtn=Gembok
### Missing
@@ -126,6 +151,7 @@ vaultOptions.general.vaultName=Nama Brankas
## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=Pilih…
## Master Key
vaultOptions.masterkey.changePasswordBtn=Ubah Kata Sandi
# Recovery Key
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Posizione personalizzata
addvaultwizard.new.directoryPickerButton=Scegli…
addvaultwizard.new.directoryPickerTitle=Seleziona cartella
addvaultwizard.new.fileAlreadyExists=La cassaforte non può essere creata in questo percorso perché un oggetto esiste già.
addvaultwizard.new.locationDoesNotExist=La cassaforte non può essere creata in questo percorso perché almeno un componente di percorso non esiste.
addvaultwizard.new.fileAlreadyExists=Un file o una cartella con il nome del vault esiste già
addvaultwizard.new.locationDoesNotExist=Una cartella nel percorso specificato non esiste o non è accessibile
addvaultwizard.new.locationIsNotWritable=Non c'è accesso in scrittura nel percorso specificato
addvaultwizard.new.locationIsOk=Posizione adatta per il tuo vault
addvaultwizard.new.invalidName=Nome della cassaforte non valido. Si prega di considerare un nome di directory regolare.
### Password
addvaultwizard.new.createVaultBtn=Crea Cassaforte
@@ -97,10 +99,12 @@ unlock.title=Sblocca Cassaforte
unlock.passwordPrompt=Inserisci la password per "%s":
unlock.savePassword=Ricorda la Password
unlock.unlockBtn=Sblocca
##
unlock.chooseMasterkey.filePickerTitle=Seleziona file Masterkey
## Success
unlock.success.message=Sbloccato "%s" con successo! La tua cassaforte è ora accessibile.
unlock.success.message="%s" sbloccato con successo! La tua cassaforte è ora accessibile tramite il suo drive virtuale.
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
unlock.success.revealBtn=Rivela Cassaforte
unlock.success.revealBtn=Rivela Drive
## Failure
unlock.error.heading=Impossibile sbloccare la cassaforte
### Invalid Mount Point
@@ -150,6 +154,7 @@ preferences.general.theme.light=Chiaro
preferences.general.theme.dark=Scuro
preferences.general.unlockThemes=Sblocca modalità scura
preferences.general.showMinimizeButton=Mostra pulsante riduci a icona
preferences.general.showTrayIcon=Mostra icona nel tray (richiede riavvio)
preferences.general.startHidden=Nascondi la finestra all'avvio di Cryptomator
preferences.general.debugLogging=Abilita i registri di debug
preferences.general.debugDirectory=Mostra file log
@@ -173,11 +178,14 @@ preferences.updates.currentVersion=Versione attuale %s
preferences.updates.autoUpdateCheck=Controlla automaticamente la presenza di aggiornamenti
preferences.updates.checkNowBtn=Controlla adesso
preferences.updates.updateAvailable=Disponibile aggiornamento alla versione %s.
## Donation Key
preferences.donationKey=Donazione
preferences.donationKey.registeredFor=Registrato per %s
preferences.donationKey.noDonationKey=Nessuna chiave di donazione valida. È come una chiave di licenza ma per persone fantastiche che usano software gratuito. ;-)
preferences.donationKey.getDonationKey=Ottieni una chiave di donazione
## Contribution
preferences.contribute=Supportaci
preferences.contribute.registeredFor=Certificato del supporto registrato per %s
preferences.contribute.noCertificate=Supporta Cryptomator e ricevi un certificato di supporter. È come una chiave di licenza, ma per persone fantastiche che utilizzano software libero. ;-)
preferences.contribute.getCertificate=Non ne hai già uno? Impara come puoi ottenerlo.
preferences.contribute.promptText=Incolla qui il codice del certificato di supporter
#<-- Add entries for donations and code/translation/documentation contribution -->
## About
preferences.about=Informazioni
@@ -221,7 +229,12 @@ main.dropZone.dropVault=Aggiungi questa cassaforte
main.dropZone.unknownDragboardContent=Se vuoi aggiungere una cassaforte, trascinala in questa finestra
## Vault List
main.vaultlist.emptyList.onboardingInstruction=Clicca qui per aggiungere una cassaforte
main.vaultlist.contextMenu.remove=Rimuovi Cassaforte
main.vaultlist.contextMenu.remove=Rimuovi…
main.vaultlist.contextMenu.lock=Blocca
main.vaultlist.contextMenu.unlock=Sblocca…
main.vaultlist.contextMenu.unlockNow=Sblocca Ora
main.vaultlist.contextMenu.vaultoptions=Mostra Opzioni Cassaforte
main.vaultlist.contextMenu.reveal=Rivela Drive
main.vaultlist.addVaultBtn=Aggiungi Cassaforte
## Vault Detail
### Welcome

Some files were not shown because too many files have changed in this diff Show More