Compare commits

..
Author SHA1 Message Date
Sebastian Stenzel 845189c638 experimental smooth I/O charts 2021-03-30 11:58:31 +02:00
115 changed files with 987 additions and 2812 deletions
-10
View File
@@ -92,16 +92,6 @@ jobs:
* [ ] add Linux appimage, zsync file and signature file * [ ] add Linux appimage, zsync file and signature file
* [ ] add Windows installer and signature file * [ ] add Windows installer and signature file
* [ ] add MacOs disk image and signature file * [ ] add MacOs disk image and signature file
## What's new
## Bugfixes
## Misc
---
:scroll: A complete list of closed issues is available [here](LINK)
draft: true draft: true
prerelease: false prerelease: false
- name: Upload buildkit-linux.zip to GitHub Releases - name: Upload buildkit-linux.zip to GitHub Releases
-1
View File
@@ -21,6 +21,5 @@ pom.xml.versionsBackup
.idea/compiler.xml .idea/compiler.xml
.idea/encodings.xml .idea/encodings.xml
.idea/jarRepositories.xml .idea/jarRepositories.xml
.idea/uiDesigner.xml
.idea/**/libraries/ .idea/**/libraries/
*.iml *.iml
+1 -1
View File
@@ -48,7 +48,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
## Features ## Features
- Works with Dropbox, Google Drive, OneDrive, MEGA, pCloud, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory - Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
- Open Source means: No backdoors, control is better than trust - Open Source means: No backdoors, control is better than trust
- Client-side: No accounts, no data shared with any online service - Client-side: No accounts, no data shared with any online service
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive - Totally transparent: Just work on the virtual drive as if it were a USB flash drive
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.6.0</version> <version>1.6.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>buildkit</artifactId> <artifactId>buildkit</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.6.0</version> <version>1.6.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>commons</artifactId> <artifactId>commons</artifactId>
<name>Cryptomator Commons</name> <name>Cryptomator Commons</name>
@@ -15,7 +15,6 @@ import org.cryptomator.common.settings.SettingsProvider;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultComponent; import org.cryptomator.common.vaults.VaultComponent;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.frontend.webdav.WebDavServer; import org.cryptomator.frontend.webdav.WebDavServer;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -26,8 +25,6 @@ import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Comparator; import java.util.Comparator;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
@@ -58,22 +55,6 @@ public abstract class CommonsModule {
"""; """;
} }
@Provides
@Singleton
static SecureRandom provideCSPRNG() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
}
}
@Provides
@Singleton
static MasterkeyFileAccess provideMasterkeyFileAccess(SecureRandom csprng) {
return new MasterkeyFileAccess(Constants.PEPPER, csprng);
}
@Provides @Provides
@Singleton @Singleton
@Named("SemVer") @Named("SemVer")
@@ -3,8 +3,5 @@ package org.cryptomator.common;
public interface Constants { public interface Constants {
String MASTERKEY_FILENAME = "masterkey.cryptomator"; String MASTERKEY_FILENAME = "masterkey.cryptomator";
String MASTERKEY_BACKUP_SUFFIX = ".bkup";
String VAULTCONFIG_FILENAME = "vault.cryptomator";
byte[] PEPPER = new byte[0];
} }
@@ -24,6 +24,8 @@ import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
/** /**
* The settings specific to a single vault. * The settings specific to a single vault.
@@ -35,7 +37,7 @@ public class VaultSettings {
public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false; public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false;
public static final boolean DEFAULT_USES_READONLY_MODE = false; public static final boolean DEFAULT_USES_READONLY_MODE = false;
public static final String DEFAULT_MOUNT_FLAGS = ""; public static final String DEFAULT_MOUNT_FLAGS = "";
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1; public static final int DEFAULT_FILENAME_LENGTH_LIMIT = -1;
public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK; public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
private static final Random RNG = new Random(); private static final Random RNG = new Random();
@@ -50,7 +52,7 @@ public class VaultSettings {
private final StringProperty customMountPath = new SimpleStringProperty(); private final StringProperty customMountPath = new SimpleStringProperty();
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE); private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS);
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH); private final IntegerProperty filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK); private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
private final StringBinding mountName; private final StringBinding mountName;
@@ -61,7 +63,7 @@ public class VaultSettings {
} }
Observable[] observables() { Observable[] observables() {
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock}; return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
} }
public static VaultSettings withRandomId() { public static VaultSettings withRandomId() {
@@ -150,8 +152,8 @@ public class VaultSettings {
return mountFlags; return mountFlags;
} }
public IntegerProperty maxCleartextFilenameLength() { public IntegerProperty filenameLengthLimit() {
return maxCleartextFilenameLength; return filenameLengthLimit;
} }
public ObjectProperty<WhenUnlocked> actionAfterUnlock() { public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
@@ -29,7 +29,7 @@ class VaultSettingsJsonAdapter {
out.name("customMountPath").value(value.customMountPath().get()); out.name("customMountPath").value(value.customMountPath().get());
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get()); out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
out.name("mountFlags").value(value.mountFlags().get()); out.name("mountFlags").value(value.mountFlags().get());
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get()); out.name("filenameLengthLimit").value(value.filenameLengthLimit().get());
out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name()); out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
out.endObject(); out.endObject();
} }
@@ -46,7 +46,7 @@ class VaultSettingsJsonAdapter {
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH; boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS; String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH; int filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK; WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
in.beginObject(); in.beginObject();
@@ -64,7 +64,7 @@ class VaultSettingsJsonAdapter {
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString(); case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean(); case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
case "mountFlags" -> mountFlags = in.nextString(); case "mountFlags" -> mountFlags = in.nextString();
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt(); case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString()); case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
default -> { default -> {
LOG.warn("Unsupported vault setting found in JSON: " + name); LOG.warn("Unsupported vault setting found in JSON: " + name);
@@ -88,7 +88,7 @@ class VaultSettingsJsonAdapter {
vaultSettings.customMountPath().set(customMountPath); vaultSettings.customMountPath().set(customMountPath);
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode); vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
vaultSettings.mountFlags().set(mountFlags); vaultSettings.mountFlags().set(mountFlags);
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength); vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
vaultSettings.actionAfterUnlock().set(actionAfterUnlock); vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
return vaultSettings; return vaultSettings;
} }
@@ -5,15 +5,15 @@ import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
import org.cryptomator.frontend.dokany.Mount; import org.cryptomator.frontend.dokany.Mount;
import org.cryptomator.frontend.dokany.MountFactory; import org.cryptomator.frontend.dokany.MountFactory;
import org.cryptomator.frontend.dokany.MountFailedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import java.util.function.Consumer; import java.util.concurrent.ExecutorService;
public class DokanyVolume extends AbstractVolume { public class DokanyVolume extends AbstractVolume {
@@ -22,13 +22,15 @@ public class DokanyVolume extends AbstractVolume {
private static final String FS_TYPE_NAME = "CryptomatorFS"; private static final String FS_TYPE_NAME = "CryptomatorFS";
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final MountFactory mountFactory;
private Mount mount; private Mount mount;
@Inject @Inject
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) { public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
super(choosers); super(choosers);
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.mountFactory = new MountFactory(executorService);
} }
@Override @Override
@@ -37,11 +39,11 @@ public class DokanyVolume extends AbstractVolume {
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException { public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint(); this.mountPoint = determineMountPoint();
try { try {
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction); this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
} catch (DokanyMountFailedException e) { } catch (MountFailedException e) {
if (vaultSettings.getCustomMountPath().isPresent()) { if (vaultSettings.getCustomMountPath().isPresent()) {
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint); LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
} }
@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.mountpoint.MountPointChooser; import org.cryptomator.common.mountpoint.MountPointChooser;
import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.common.settings.VolumeImpl;
import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables; import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
import org.cryptomator.frontend.fuse.mount.FuseMountException;
import org.cryptomator.frontend.fuse.mount.FuseMountFactory; import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException; import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
import org.cryptomator.frontend.fuse.mount.Mount; import org.cryptomator.frontend.fuse.mount.Mount;
@@ -20,7 +20,6 @@ import javax.inject.Named;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class FuseVolume extends AbstractVolume { public class FuseVolume extends AbstractVolume {
@@ -36,21 +35,20 @@ public class FuseVolume extends AbstractVolume {
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException { public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
this.mountPoint = determineMountPoint(); this.mountPoint = determineMountPoint();
mount(fs.getPath("/"), mountFlags, onExitAction);
mount(fs.getPath("/"), mountFlags);
} }
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException { private void mount(Path root, String mountFlags) throws VolumeException {
try { try {
Mounter mounter = FuseMountFactory.getMounter(); Mounter mounter = FuseMountFactory.getMounter();
EnvironmentVariables envVars = EnvironmentVariables.create() // EnvironmentVariables envVars = EnvironmentVariables.create() //
.withFlags(splitFlags(mountFlags)) // .withFlags(splitFlags(mountFlags)).withMountPoint(mountPoint) //
.withMountPoint(mountPoint) //
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
.build(); .build();
this.mount = mounter.mount(root, envVars, onExitAction); this.mount = mounter.mount(root, envVars);
} catch ( FuseMountException | FuseNotSupportedException e) { } catch (CommandFailedException | FuseNotSupportedException e) {
throw new VolumeException("Unable to mount Filesystem", e); throw new VolumeException("Unable to mount Filesystem", e);
} }
} }
@@ -91,7 +89,8 @@ public class FuseVolume extends AbstractVolume {
public synchronized void unmountForced() throws VolumeException { public synchronized void unmountForced() throws VolumeException {
try { try {
mount.unmountForced(); mount.unmountForced();
} catch (FuseMountException e) { mount.close();
} catch (CommandFailedException e) {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanupMountPoint(); cleanupMountPoint();
@@ -101,7 +100,8 @@ public class FuseVolume extends AbstractVolume {
public synchronized void unmount() throws VolumeException { public synchronized void unmount() throws VolumeException {
try { try {
mount.unmount(); mount.unmount();
} catch (FuseMountException e) { mount.close();
} catch (CommandFailedException e) {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanupMountPoint(); cleanupMountPoint();
@@ -1,12 +0,0 @@
package org.cryptomator.common.vaults;
public class LockNotCompletedException extends Exception {
public LockNotCompletedException(String reason) {
super(reason);
}
public LockNotCompletedException(Throwable cause) {
super(cause);
}
}
@@ -17,12 +17,10 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.VaultConfig; import org.cryptomator.cryptofs.common.Constants;
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker; import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.MasterkeyLoader; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -37,29 +35,28 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.EnumSet; import java.util.EnumSet;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@PerVault @PerVault
public class Vault { public class Vault {
private static final Logger LOG = LoggerFactory.getLogger(Vault.class); private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME); private static final Path HOME_DIR = Paths.get(SystemUtils.USER_HOME);
private static final int UNLIMITED_FILENAME_LENGTH = Integer.MAX_VALUE;
private final VaultSettings vaultSettings; private final VaultSettings vaultSettings;
private final Provider<Volume> volumeProvider; private final Provider<Volume> volumeProvider;
private final StringBinding defaultMountFlags; private final StringBinding defaultMountFlags;
private final AtomicReference<CryptoFileSystem> cryptoFileSystem; private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
private final VaultState state; private final ObjectProperty<VaultState> state;
private final ObjectProperty<Exception> lastKnownException; private final ObjectProperty<Exception> lastKnownException;
private final VaultStats stats; private final VaultStats stats;
private final StringBinding displayName; private final StringBinding displayName;
@@ -77,7 +74,7 @@ public class Vault {
private volatile Volume volume; private volatile Volume volume;
@Inject @Inject
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) { Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
this.vaultSettings = vaultSettings; this.vaultSettings = vaultSettings;
this.volumeProvider = volumeProvider; this.volumeProvider = volumeProvider;
this.defaultMountFlags = defaultMountFlags; this.defaultMountFlags = defaultMountFlags;
@@ -102,31 +99,24 @@ public class Vault {
// Commands // Commands
// ********************************************************************************/ // ********************************************************************************/
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException { private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class); Set<FileSystemFlags> flags = EnumSet.noneOf(FileSystemFlags.class);
if (vaultSettings.usesReadOnlyMode().get()) { if (vaultSettings.usesReadOnlyMode().get()) {
flags.add(FileSystemFlags.READONLY); flags.add(FileSystemFlags.READONLY);
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
LOG.debug("Determining cleartext filename length limitations...");
var checker = new FileSystemCapabilityChecker();
int shorteningThreshold = getUnverifiedVaultConfig().orElseThrow().allegedShorteningThreshold();
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
if (ciphertextLimit < shorteningThreshold) {
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
} else {
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
}
} }
if (!flags.contains(FileSystemFlags.READONLY) && vaultSettings.filenameLengthLimit().get() == -1) {
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) { LOG.debug("Determining file name length limitations...");
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get()); int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
vaultSettings.filenameLengthLimit().set(limit);
LOG.info("Storing file name length limit of {}", limit);
} }
assert vaultSettings.filenameLengthLimit().get() > 0;
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
.withKeyLoader(keyLoader) // .withPassphrase(passphrase) //
.withFlags(flags) // .withFlags(flags) //
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) // .withMasterkeyFilename(MASTERKEY_FILENAME) //
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
.build(); .build();
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
} }
@@ -143,51 +133,29 @@ public class Vault {
} }
} }
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException { public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
if (cryptoFileSystem.get() != null) { if (cryptoFileSystem.get() == null) {
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
cryptoFileSystem.set(fs);
try {
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags());
} catch (Exception e) {
destroyCryptoFileSystem();
throw e;
}
} else {
throw new IllegalStateException("Already unlocked."); throw new IllegalStateException("Already unlocked.");
} }
CryptoFileSystem fs = createCryptoFileSystem(keyLoader);
boolean success = false;
try {
cryptoFileSystem.set(fs);
volume = volumeProvider.get();
volume.mount(fs, getEffectiveMountFlags(), this::lockOnVolumeExit);
success = true;
} finally {
if (!success) {
destroyCryptoFileSystem();
}
}
} }
private void lockOnVolumeExit(Throwable t) { public synchronized void lock(boolean forced) throws VolumeException {
LOG.info("Unmounted vault '{}'", getDisplayName());
destroyCryptoFileSystem();
state.set(VaultState.Value.LOCKED);
if (t != null) {
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
}
}
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
//initiate unmount
if (forced && volume.supportsForcedUnmount()) { if (forced && volume.supportsForcedUnmount()) {
volume.unmountForced(); volume.unmountForced();
} else { } else {
volume.unmount(); volume.unmount();
} }
destroyCryptoFileSystem();
//wait for lockOnVolumeExit to be executed
try {
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
if (!locked) {
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockNotCompletedException(e);
}
} }
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException { public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
@@ -198,12 +166,16 @@ public class Vault {
// Observable Properties // Observable Properties
// ******************************************************************************* // *******************************************************************************
public VaultState stateProperty() { public ObjectProperty<VaultState> stateProperty() {
return state; return state;
} }
public VaultState.Value getState() { public VaultState getState() {
return state.getValue(); return state.get();
}
public void setState(VaultState value) {
state.setValue(value);
} }
public ObjectProperty<Exception> lastKnownExceptionProperty() { public ObjectProperty<Exception> lastKnownExceptionProperty() {
@@ -223,7 +195,7 @@ public class Vault {
} }
public boolean isLocked() { public boolean isLocked() {
return state.get() == VaultState.Value.LOCKED; return state.get() == VaultState.LOCKED;
} }
public BooleanBinding processingProperty() { public BooleanBinding processingProperty() {
@@ -231,7 +203,7 @@ public class Vault {
} }
public boolean isProcessing() { public boolean isProcessing() {
return state.get() == VaultState.Value.PROCESSING; return state.get() == VaultState.PROCESSING;
} }
public BooleanBinding unlockedProperty() { public BooleanBinding unlockedProperty() {
@@ -239,7 +211,7 @@ public class Vault {
} }
public boolean isUnlocked() { public boolean isUnlocked() {
return state.get() == VaultState.Value.UNLOCKED; return state.get() == VaultState.UNLOCKED;
} }
public BooleanBinding missingProperty() { public BooleanBinding missingProperty() {
@@ -247,7 +219,7 @@ public class Vault {
} }
public boolean isMissing() { public boolean isMissing() {
return state.get() == VaultState.Value.MISSING; return state.get() == VaultState.MISSING;
} }
public BooleanBinding needsMigrationProperty() { public BooleanBinding needsMigrationProperty() {
@@ -255,7 +227,7 @@ public class Vault {
} }
public boolean isNeedsMigration() { public boolean isNeedsMigration() {
return state.get() == VaultState.Value.NEEDS_MIGRATION; return state.get() == VaultState.NEEDS_MIGRATION;
} }
public BooleanBinding unknownErrorProperty() { public BooleanBinding unknownErrorProperty() {
@@ -263,7 +235,7 @@ public class Vault {
} }
public boolean isUnknownError() { public boolean isUnknownError() {
return state.get() == VaultState.Value.ERROR; return state.get() == VaultState.ERROR;
} }
public StringBinding displayNameProperty() { public StringBinding displayNameProperty() {
@@ -279,7 +251,7 @@ public class Vault {
} }
public String getAccessPoint() { public String getAccessPoint() {
if (state.getValue() == VaultState.Value.UNLOCKED) { if (state.get() == VaultState.UNLOCKED) {
assert volume != null; assert volume != null;
return volume.getMountPoint().orElse(Path.of("")).toString(); return volume.getMountPoint().orElse(Path.of("")).toString();
} else { } else {
@@ -327,16 +299,6 @@ public class Vault {
return stats; return stats;
} }
public Optional<UnverifiedVaultConfig> getUnverifiedVaultConfig() {
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
try {
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
return Optional.of(VaultConfig.decode(token));
} catch (IOException e) {
return Optional.empty();
}
}
public Observable[] observables() { public Observable[] observables() {
return new Observable[]{state}; return new Observable[]{state};
} }
@@ -26,7 +26,7 @@ public interface VaultComponent {
Builder vaultSettings(VaultSettings vaultSettings); Builder vaultSettings(VaultSettings vaultSettings);
@BindsInstance @BindsInstance
Builder initialVaultState(VaultState.Value vaultState); Builder initialVaultState(VaultState vaultState);
@BindsInstance @BindsInstance
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause); Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
@@ -11,7 +11,6 @@ package org.cryptomator.common.vaults;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VaultSettings;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptofs.DirStructure;
import org.cryptomator.cryptofs.migration.Migrators; import org.cryptomator.cryptofs.migration.Migrators;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -21,16 +20,14 @@ import javax.inject.Singleton;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Collection; import java.util.Collection;
import java.util.Optional; import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
@Singleton @Singleton
public class VaultListManager { public class VaultListManager {
@@ -55,18 +52,19 @@ public class VaultListManager {
return vaultList; return vaultList;
} }
public Vault add(Path pathToVault) throws IOException { public Vault add(Path pathToVault) throws NoSuchFileException {
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath(); Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) { if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory"); throw new NoSuchFileException(normalizedPathToVault.toString(), null, "Not a vault directory");
} }
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
return get(normalizedPathToVault) // if (alreadyExistingVault.isPresent()) {
.orElseGet(() -> { return alreadyExistingVault.get();
Vault newVault = create(newVaultSettings(normalizedPathToVault)); } else {
vaultList.add(newVault); Vault newVault = create(newVaultSettings(normalizedPathToVault));
return newVault; vaultList.add(newVault);
}); return newVault;
}
} }
private VaultSettings newVaultSettings(Path path) { private VaultSettings newVaultSettings(Path path) {
@@ -96,45 +94,43 @@ public class VaultListManager {
private Vault create(VaultSettings vaultSettings) { private Vault create(VaultSettings vaultSettings) {
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings); VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
try { try {
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get()); VaultState vaultState = determineVaultState(vaultSettings.path().get());
compBuilder.initialVaultState(vaultState); compBuilder.initialVaultState(vaultState);
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e); LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
compBuilder.initialVaultState(ERROR); compBuilder.initialVaultState(VaultState.ERROR);
compBuilder.initialErrorCause(e); compBuilder.initialErrorCause(e);
} }
return compBuilder.build().vault(); return compBuilder.build().vault();
} }
public static VaultState.Value redetermineVaultState(Vault vault) { public static VaultState redetermineVaultState(Vault vault) {
VaultState state = vault.stateProperty(); VaultState previousState = vault.getState();
VaultState.Value previousState = state.getValue();
return switch (previousState) { return switch (previousState) {
case LOCKED, NEEDS_MIGRATION, MISSING -> { case LOCKED, NEEDS_MIGRATION, MISSING -> {
try { try {
var determinedState = determineVaultState(vault.getPath()); VaultState determinedState = determineVaultState(vault.getPath());
state.set(determinedState); vault.setState(determinedState);
yield determinedState; yield determinedState;
} catch (IOException e) { } catch (IOException e) {
LOG.warn("Failed to determine vault state for " + vault.getPath(), e); LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
state.set(ERROR); vault.setState(VaultState.ERROR);
vault.setLastKnownException(e); vault.setLastKnownException(e);
yield ERROR; yield VaultState.ERROR;
} }
} }
case ERROR, UNLOCKED, PROCESSING -> previousState; case ERROR, UNLOCKED, PROCESSING -> previousState;
}; };
} }
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException { private static VaultState determineVaultState(Path pathToVault) throws IOException {
if (!Files.exists(pathToVault)) { if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.Value.MISSING; return VaultState.MISSING;
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
return VaultState.NEEDS_MIGRATION;
} else {
return VaultState.LOCKED;
} }
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
case VAULT -> VaultState.Value.LOCKED;
case UNRELATED -> VaultState.Value.MISSING;
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
};
} }
} }
@@ -40,6 +40,12 @@ public class VaultModule {
return new AtomicReference<>(); return new AtomicReference<>();
} }
@Provides
@PerVault
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
return new SimpleObjectProperty<>(initialState);
}
@Provides @Provides
@Named("lastKnownException") @Named("lastKnownException")
@PerVault @PerVault
@@ -47,6 +53,7 @@ public class VaultModule {
return new SimpleObjectProperty<>(initialErrorCause); return new SimpleObjectProperty<>(initialErrorCause);
} }
@Provides @Provides
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) { public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get(); VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
@@ -1,141 +1,34 @@
package org.cryptomator.common.vaults; package org.cryptomator.common.vaults;
import com.google.common.base.Preconditions; public enum VaultState {
import org.slf4j.Logger; /**
import org.slf4j.LoggerFactory; * No vault found at the provided path
*/
import javax.inject.Inject; MISSING,
import javafx.application.Platform;
import javafx.beans.value.ObservableObjectValue;
import javafx.beans.value.ObservableValueBase;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@PerVault
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
public enum Value {
/**
* No vault found at the provided path
*/
MISSING,
/**
* Vault requires migration to a newer vault format
*/
NEEDS_MIGRATION,
/**
* Vault ready to be unlocked
*/
LOCKED,
/**
* Vault in transition between two other states
*/
PROCESSING,
/**
* Vault is unlocked
*/
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
}
private final AtomicReference<Value> value;
private final Lock lock = new ReentrantLock();
private final Condition valueChanged = lock.newCondition();
@Inject
public VaultState(VaultState.Value initialValue) {
this.value = new AtomicReference<>(initialValue);
}
@Override
public Value get() {
return getValue();
}
@Override
public Value getValue() {
return value.get();
}
/** /**
* Transitions from <code>fromState</code> to <code>toState</code>. * Vault requires migration to a newer vault format
*
* @param fromState Previous state
* @param toState New state
* @return <code>true</code> if successful
*/ */
public boolean transition(Value fromState, Value toState) { NEEDS_MIGRATION,
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
boolean success = value.compareAndSet(fromState, toState);
if (success) {
fireValueChangedEvent();
} else {
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
}
return success;
}
public void set(Value newState) {
var oldState = value.getAndSet(newState);
if (oldState != newState) {
fireValueChangedEvent();
}
}
/** /**
* Waits for the specified time, until the desired state is reached. * Vault ready to be unlocked
*
* @param desiredState what state to wait for
* @param time the maximum time to wait
* @param unit the time unit of the {@code time} argument
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
* @throws InterruptedException if the current thread is interrupted
*/ */
public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException { LOCKED,
lock.lock();
try {
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
while (value.get() != desiredState) {
if (remaining <= 0L) {
return false;
}
remaining = valueChanged.awaitNanos(remaining);
}
return true;
} finally {
lock.unlock();
}
}
private void signal() { /**
lock.lock(); * Vault in transition between two other states
try { */
valueChanged.signalAll(); PROCESSING,
} finally {
lock.unlock(); /**
} * Vault is unlocked
} */
UNLOCKED,
/**
* Unknown state due to preceeding unrecoverable exceptions.
*/
ERROR;
@Override
protected void fireValueChangedEvent() {
signal();
if (Platform.isFxApplicationThread()) {
super.fireValueChangedEvent();
} else {
Platform.runLater(super::fireValueChangedEvent);
}
}
} }
@@ -26,7 +26,7 @@ public class VaultStats {
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class); private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
private final AtomicReference<CryptoFileSystem> fs; private final AtomicReference<CryptoFileSystem> fs;
private final VaultState state; private final ObjectProperty<VaultState> state;
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService; private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
private final LongProperty bytesPerSecondRead = new SimpleLongProperty(); private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty(); private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
@@ -41,7 +41,7 @@ public class VaultStats {
private final LongProperty filesWritten = new SimpleLongProperty(); private final LongProperty filesWritten = new SimpleLongProperty();
@Inject @Inject
VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) { VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
this.fs = fs; this.fs = fs;
this.state = state; this.state = state;
this.updateService = new UpdateStatsService(); this.updateService = new UpdateStatsService();
@@ -52,13 +52,13 @@ public class VaultStats {
} }
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) { private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
if (VaultState.Value.UNLOCKED == state.get()) { if (VaultState.UNLOCKED.equals(state.get())) {
assert fs.get() != null; assert fs.get() != null;
LOG.debug("start recording stats"); LOG.debug("start recording stats");
Platform.runLater(() -> updateService.restart()); updateService.restart();
} else { } else {
LOG.debug("stop recording stats"); LOG.debug("stop recording stats");
Platform.runLater(() -> updateService.cancel()); updateService.cancel();
} }
} }
@@ -7,8 +7,6 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletionStage;
import java.util.function.Consumer;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
@@ -34,7 +32,7 @@ public interface Volume {
* @param fs * @param fs
* @throws IOException * @throws IOException
*/ */
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException; void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
/** /**
* Reveals the mounted volume. * Reveals the mounted volume.
@@ -17,7 +17,6 @@ import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier; import java.util.function.Supplier;
public class WebDavVolume implements Volume { public class WebDavVolume implements Volume {
@@ -32,7 +31,6 @@ public class WebDavVolume implements Volume {
private WebDavServer server; private WebDavServer server;
private WebDavServletController servlet; private WebDavServletController servlet;
private Mounter.Mount mount; private Mounter.Mount mount;
private Consumer<Throwable> onExitAction;
@Inject @Inject
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) { public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
@@ -43,13 +41,12 @@ public class WebDavVolume implements Volume {
} }
@Override @Override
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException { public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
startServlet(fs); startServlet(fs);
mountServlet(); mountServlet();
this.onExitAction = onExitAction;
} }
private void startServlet(CryptoFileSystem fs) { private void startServlet(CryptoFileSystem fs){
if (server == null) { if (server == null) {
server = serverProvider.get(); server = serverProvider.get();
} }
@@ -69,7 +66,7 @@ public class WebDavVolume implements Volume {
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free. //on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
Supplier<String> driveLetterSupplier; Supplier<String> driveLetterSupplier;
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) { if(System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null); driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
} else { } else {
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get(); driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
@@ -104,7 +101,6 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
@@ -115,7 +111,6 @@ public class WebDavVolume implements Volume {
throw new VolumeException(e); throw new VolumeException(e);
} }
cleanup(); cleanup();
onExitAction.accept(null);
} }
@Override @Override
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.6.0</version> <version>1.6.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>launcher</artifactId> <artifactId>launcher</artifactId>
<name>Cryptomator Launcher</name> <name>Cryptomator Launcher</name>
+17 -17
View File
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.6.0</version> <version>1.6.0-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Cryptomator</name> <name>Cryptomator</name>
@@ -25,29 +25,29 @@
<project.jdk.version>16</project.jdk.version> <project.jdk.version>16</project.jdk.version>
<!-- cryptomator dependencies --> <!-- cryptomator dependencies -->
<cryptomator.cryptofs.version>2.0.0-rc2</cryptomator.cryptofs.version> <cryptomator.cryptofs.version>1.9.14</cryptomator.cryptofs.version>
<cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version> <cryptomator.integrations.version>1.0.0-beta2</cryptomator.integrations.version>
<cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version> <cryptomator.integrations.win.version>1.0.0-beta2</cryptomator.integrations.win.version>
<cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version> <cryptomator.integrations.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
<cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version> <cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
<cryptomator.fuse.version>1.3.1</cryptomator.fuse.version> <cryptomator.fuse.version>1.2.9</cryptomator.fuse.version>
<cryptomator.dokany.version>1.3.1</cryptomator.dokany.version> <cryptomator.dokany.version>1.2.4</cryptomator.dokany.version>
<cryptomator.webdav.version>1.2.0</cryptomator.webdav.version> <cryptomator.webdav.version>1.1.4</cryptomator.webdav.version>
<!-- 3rd party dependencies --> <!-- 3rd party dependencies -->
<javafx.version>16</javafx.version> <javafx.version>15</javafx.version>
<commons-lang3.version>3.11</commons-lang3.version> <commons-lang3.version>3.11</commons-lang3.version>
<jwt.version>3.15.0</jwt.version> <jwt.version>3.12.0</jwt.version>
<easybind.version>2.1.0</easybind.version> <easybind.version>2.1.0</easybind.version>
<guava.version>30.1.1-jre</guava.version> <guava.version>30.0-jre</guava.version>
<dagger.version>2.35.1</dagger.version> <dagger.version>2.32</dagger.version>
<gson.version>2.8.6</gson.version> <gson.version>2.8.6</gson.version>
<slf4j.version>1.7.30</slf4j.version> <slf4j.version>1.7.30</slf4j.version>
<logback.version>1.2.3</logback.version> <logback.version>1.2.3</logback.version>
<!-- test dependencies --> <!-- test dependencies -->
<junit.jupiter.version>5.7.1</junit.jupiter.version> <junit.jupiter.version>5.7.0</junit.jupiter.version>
<mockito.version>3.9.0</mockito.version> <mockito.version>3.6.0</mockito.version>
<hamcrest.version>2.2</hamcrest.version> <hamcrest.version>2.2</hamcrest.version>
</properties> </properties>
@@ -76,12 +76,6 @@
<artifactId>cryptofs</artifactId> <artifactId>cryptofs</artifactId>
<version>${cryptomator.cryptofs.version}</version> <version>${cryptomator.cryptofs.version}</version>
</dependency> </dependency>
<!--TODO: only temporary workaround until 1.6.0-beta -->
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>cryptolib</artifactId>
<version>2.0.0-rc1</version>
</dependency>
<dependency> <dependency>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>fuse-nio-adapter</artifactId> <artifactId>fuse-nio-adapter</artifactId>
@@ -224,6 +218,12 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<!-- TODO: temporary fix for XXE attack, can be removed once java-jwt is updated -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.5.1</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
+5
View File
@@ -1,6 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin --> <!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd"> <suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
<suppress>
<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
<cve>CVE-2020-25649</cve>
</suppress>
<suppress> <suppress>
<notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes> <notes><![CDATA[ Suppress known vulnerabilities in FUSE libraries for fuse-nio-adapter. For more info, see suppression.xml of https://github.com/cryptomator/fuse-nio-adapter ]]></notes>
<gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav> <gav regex="true">^org\.cryptomator:fuse-nio-adapter:.*$</gav>
+1 -1
View File
@@ -4,7 +4,7 @@
<parent> <parent>
<groupId>org.cryptomator</groupId> <groupId>org.cryptomator</groupId>
<artifactId>main</artifactId> <artifactId>main</artifactId>
<version>1.6.0</version> <version>1.6.0-SNAPSHOT</version>
</parent> </parent>
<artifactId>ui</artifactId> <artifactId>ui</artifactId>
<name>Cryptomator GUI</name> <name>Cryptomator GUI</name>
@@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController {
Vault newVault = vaultListManager.add(vaultPath.get()); Vault newVault = vaultListManager.add(vaultPath.get());
vault.set(newVault); vault.set(newVault);
window.setScene(successScene.get()); window.setScene(successScene.get());
} catch (IOException e) { } catch (NoSuchFileException e) {
LOG.error("Failed to open existing vault.", e); LOG.error("Failed to open existing vault.", e);
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
} }
@@ -1,10 +1,10 @@
package org.cryptomator.ui.addvaultwizard; package org.cryptomator.ui.addvaultwizard;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -15,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton; import javafx.scene.control.RadioButton;
import javafx.scene.control.Toggle; import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup; import javafx.scene.control.ToggleGroup;
import javafx.stage.DirectoryChooser; import javafx.stage.DirectoryChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.util.ResourceBundle; import java.util.ResourceBundle;
@@ -43,74 +43,55 @@ public class CreateNewVaultLocationController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> chooseNameScene; private final Lazy<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene; private final Lazy<Scene> choosePasswordScene;
private final ErrorComponent.Builder errorComponent;
private final LocationPresets locationPresets; private final LocationPresets locationPresets;
private final ObjectProperty<Path> vaultPath; private final ObjectProperty<Path> vaultPath;
private final StringProperty vaultName; private final StringProperty vaultName;
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final BooleanBinding validVaultPath; private final BooleanBinding validVaultPath;
private final BooleanProperty usePresetPath; private final BooleanProperty usePresetPath;
private final StringProperty statusText; private final StringProperty warningText;
private final ObjectProperty<Node> statusGraphic;
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
//FXML
public ToggleGroup predefinedLocationToggler; public ToggleGroup predefinedLocationToggler;
public RadioButton iclouddriveRadioButton; public RadioButton iclouddriveRadioButton;
public RadioButton dropboxRadioButton; public RadioButton dropboxRadioButton;
public RadioButton gdriveRadioButton; public RadioButton gdriveRadioButton;
public RadioButton onedriveRadioButton; public RadioButton onedriveRadioButton;
public RadioButton megaRadioButton;
public RadioButton pcloudRadioButton;
public RadioButton customRadioButton; public RadioButton customRadioButton;
public Label vaultPathStatus;
public FontAwesome5IconView goodLocation;
public FontAwesome5IconView badLocation;
@Inject @Inject
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) { 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) {
this.window = window; this.window = window;
this.chooseNameScene = chooseNameScene; this.chooseNameScene = chooseNameScene;
this.choosePasswordScene = choosePasswordScene; this.choosePasswordScene = choosePasswordScene;
this.errorComponent = errorComponent;
this.locationPresets = locationPresets; this.locationPresets = locationPresets;
this.vaultPath = vaultPath; this.vaultPath = vaultPath;
this.vaultName = vaultName; this.vaultName = vaultName;
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath); this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
this.usePresetPath = new SimpleBooleanProperty(); this.usePresetPath = new SimpleBooleanProperty();
this.statusText = new SimpleStringProperty(); this.warningText = new SimpleStringProperty();
this.statusGraphic = new SimpleObjectProperty<>();
} }
private boolean validateVaultPathAndSetStatus() { private boolean isValidVaultPath() {
final Path p = vaultPath.get(); return vaultPath.get() != null && Files.notExists(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 @FXML
public void initialize() { public void initialize() {
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); 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) { private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
@@ -122,10 +103,6 @@ public class CreateNewVaultLocationController implements FxController {
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
} else if (onedriveRadioButton.equals(newValue)) { } else if (onedriveRadioButton.equals(newValue)) {
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get())); 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)) { } else if (customRadioButton.equals(newValue)) {
vaultPath.set(customVaultPath.resolve(vaultName.get())); vaultPath.set(customVaultPath.resolve(vaultName.get()));
} }
@@ -138,10 +115,21 @@ public class CreateNewVaultLocationController implements FxController {
@FXML @FXML
public void next() { public void next() {
if (validateVaultPathAndSetStatus()) { 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
window.setScene(choosePasswordScene.get()); window.setScene(choosePasswordScene.get());
} else { } catch (FileAlreadyExistsException e) {
validVaultPath.invalidate(); 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();
} }
} }
@@ -191,27 +179,19 @@ public class CreateNewVaultLocationController implements FxController {
return usePresetPath.get(); return usePresetPath.get();
} }
public BooleanBinding anyRadioButtonSelectedProperty() { public StringProperty warningTextProperty() {
return predefinedLocationToggler.selectedToggleProperty().isNotNull(); return warningText;
} }
public boolean isAnyRadioButtonSelected() { public String getWarningText() {
return anyRadioButtonSelectedProperty().get(); return warningText.get();
} }
public StringProperty statusTextProperty() { public BooleanBinding showWarningProperty() {
return statusText; return warningText.isNotEmpty();
} }
public String getStatusText() { public boolean isShowWarning() {
return statusText.get(); return showWarningProperty().get();
}
public ObjectProperty<Node> statusGraphicProperty() {
return statusGraphic;
}
public Node getStatusGraphic() {
return statusGraphic.get();
} }
} }
@@ -5,17 +5,11 @@ import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.cryptofs.CryptoFileSystemProperties; import org.cryptomator.cryptofs.CryptoFileSystemProperties;
import org.cryptomator.cryptofs.CryptoFileSystemProvider; 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.ErrorComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.common.Tasks;
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -37,14 +31,13 @@ import javafx.scene.control.ToggleGroup;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.channels.WritableByteChannel; import java.nio.channels.WritableByteChannel;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.security.SecureRandom; import java.util.Collections;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@@ -55,7 +48,6 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
public class CreateNewVaultPasswordController implements FxController { public class CreateNewVaultPasswordController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class); 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 Stage window;
private final Lazy<Scene> chooseLocationScene; private final Lazy<Scene> chooseLocationScene;
@@ -72,8 +64,6 @@ public class CreateNewVaultPasswordController implements FxController {
private final ResourceBundle resourceBundle; private final ResourceBundle resourceBundle;
private final ObjectProperty<CharSequence> password; private final ObjectProperty<CharSequence> password;
private final ReadmeGenerator readmeGenerator; private final ReadmeGenerator readmeGenerator;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
private final BooleanProperty processing; private final BooleanProperty processing;
private final BooleanProperty readyToCreateVault; private final BooleanProperty readyToCreateVault;
private final ObjectBinding<ContentDisplay> createVaultButtonState; private final ObjectBinding<ContentDisplay> createVaultButtonState;
@@ -83,7 +73,7 @@ public class CreateNewVaultPasswordController implements FxController {
public Toggle skipRecoveryKey; public Toggle skipRecoveryKey;
@Inject @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, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { 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) {
this.window = window; this.window = window;
this.chooseLocationScene = chooseLocationScene; this.chooseLocationScene = chooseLocationScene;
this.recoveryKeyScene = recoveryKeyScene; this.recoveryKeyScene = recoveryKeyScene;
@@ -99,8 +89,6 @@ public class CreateNewVaultPasswordController implements FxController {
this.resourceBundle = resourceBundle; this.resourceBundle = resourceBundle;
this.password = password; this.password = password;
this.readmeGenerator = readmeGenerator; this.readmeGenerator = readmeGenerator;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
this.processing = new SimpleBooleanProperty(); this.processing = new SimpleBooleanProperty();
this.readyToCreateVault = new SimpleBooleanProperty(); this.readyToCreateVault = new SimpleBooleanProperty();
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing); this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
@@ -173,34 +161,23 @@ public class CreateNewVaultPasswordController implements FxController {
} }
private void initializeVault(Path path, CharSequence passphrase) throws IOException { private void initializeVault(Path path, CharSequence passphrase) throws IOException {
// 1. write masterkey: CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME); CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
try (Masterkey masterkey = Masterkey.generate(csprng)) { .withPassphrase(passphrase) //
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase); .withFlags(Collections.emptySet()) //
.withMasterkeyFilename(MASTERKEY_FILENAME) //
.build();
// 2. initialize vault: String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
try { try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
MasterkeyLoader loader = ignored -> masterkey.clone(); WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build(); ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
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"); String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf())); ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
} }
LOG.info("Created vault at {}", path); LOG.info("Created vault at {}", path);
} }
@@ -208,7 +185,7 @@ public class CreateNewVaultPasswordController implements FxController {
try { try {
Vault newVault = vaultListManager.add(pathToVault); Vault newVault = vaultListManager.add(pathToVault);
vaultProperty.set(newVault); vaultProperty.set(newVault);
} catch (IOException e) { } catch (NoSuchFileException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
} }
@@ -16,21 +16,15 @@ public class LocationPresets {
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"}; private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"}; private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"}; 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> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation; private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation; private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation; private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final ReadOnlyObjectProperty<Path> megaLocation;
private final ReadOnlyObjectProperty<Path> pcloudLocation;
private final BooleanBinding foundIclouddrive; private final BooleanBinding foundIclouddrive;
private final BooleanBinding foundDropbox; private final BooleanBinding foundDropbox;
private final BooleanBinding foundGdrive; private final BooleanBinding foundGdrive;
private final BooleanBinding foundOnedrive; private final BooleanBinding foundOnedrive;
private final BooleanBinding foundMega;
private final BooleanBinding foundPcloud;
@Inject @Inject
public LocationPresets() { public LocationPresets() {
@@ -38,14 +32,10 @@ public class LocationPresets {
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS)); this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS)); this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_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.foundIclouddrive = iclouddriveLocation.isNotNull();
this.foundDropbox = dropboxLocation.isNotNull(); this.foundDropbox = dropboxLocation.isNotNull();
this.foundGdrive = gdriveLocation.isNotNull(); this.foundGdrive = gdriveLocation.isNotNull();
this.foundOnedrive = onedriveLocation.isNotNull(); this.foundOnedrive = onedriveLocation.isNotNull();
this.foundMega = megaLocation.isNotNull();
this.foundPcloud = pcloudLocation.isNotNull();
} }
private static Path existingWritablePath(String... candidates) { private static Path existingWritablePath(String... candidates) {
@@ -132,36 +122,4 @@ public class LocationPresets {
return foundOnedrive.get(); 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,10 +2,8 @@ package org.cryptomator.ui.changepassword;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
@@ -25,13 +23,8 @@ import javafx.scene.control.CheckBox;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer; import java.nio.CharBuffer;
import java.nio.file.Files; import java.util.Optional;
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; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@ChangePasswordScoped @ChangePasswordScoped
@@ -44,22 +37,18 @@ public class ChangePasswordController implements FxController {
private final ObjectProperty<CharSequence> newPassword; private final ObjectProperty<CharSequence> newPassword;
private final ErrorComponent.Builder errorComponent; private final ErrorComponent.Builder errorComponent;
private final KeychainManager keychain; private final KeychainManager keychain;
private final SecureRandom csprng;
private final MasterkeyFileAccess masterkeyFileAccess;
public NiceSecurePasswordField oldPasswordField; public NiceSecurePasswordField oldPasswordField;
public CheckBox finalConfirmationCheckbox; public CheckBox finalConfirmationCheckbox;
public Button finishButton; public Button finishButton;
@Inject @Inject
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.newPassword = newPassword; this.newPassword = newPassword;
this.errorComponent = errorComponent; this.errorComponent = errorComponent;
this.keychain = keychain; this.keychain = keychain;
this.csprng = csprng;
this.masterkeyFileAccess = masterkeyFileAccess;
} }
@FXML @FXML
@@ -78,26 +67,17 @@ public class ChangePasswordController implements FxController {
@FXML @FXML
public void finish() { public void finish() {
try { try {
//String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC); CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
//String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC);
CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized?
CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized?
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()); LOG.info("Successfully changed password for {}", vault.getDisplayName());
window.close(); window.close();
updatePasswordInSystemkeychain(); 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();
} catch (InvalidPassphraseException e) { } catch (InvalidPassphraseException e) {
Animations.createShakeWindowAnimation(window).play(); Animations.createShakeWindowAnimation(window).play();
oldPasswordField.selectAll(); oldPasswordField.selectAll();
oldPasswordField.requestFocus(); 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();
} }
} }
@@ -26,9 +26,8 @@ public enum FxmlFile {
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), // RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), // RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
REMOVE_VAULT("/fxml/remove_vault.fxml"), // REMOVE_VAULT("/fxml/remove_vault.fxml"), //
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"), UNLOCK("/fxml/unlock.fxml"),
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.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"), // UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
VAULT_OPTIONS("/fxml/vault_options.fxml"), // VAULT_OPTIONS("/fxml/vault_options.fxml"), //
VAULT_STATISTICS("/fxml/stats.fxml"), // VAULT_STATISTICS("/fxml/stats.fxml"), //
@@ -1,6 +1,5 @@
package org.cryptomator.ui.common; package org.cryptomator.ui.common;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
@@ -176,29 +175,24 @@ public class VaultService {
} }
@Override @Override
protected Vault call() throws Volume.VolumeException, LockNotCompletedException { protected Vault call() throws Volume.VolumeException {
vault.lock(forced); vault.lock(forced);
return vault; return vault;
} }
@Override @Override
protected void scheduled() { protected void scheduled() {
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING); vault.setState(VaultState.PROCESSING);
} }
@Override @Override
protected void succeeded() { protected void succeeded() {
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED); vault.setState(VaultState.LOCKED);
} }
@Override @Override
protected void failed() { protected void failed() {
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED); vault.setState(VaultState.UNLOCKED);
}
@Override
protected void cancelled() {
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
} }
} }
@@ -5,13 +5,11 @@ import org.cryptomator.common.LicenseHolder;
import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.Settings;
import org.cryptomator.common.settings.UiTheme; import org.cryptomator.common.settings.UiTheme;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.integrations.tray.TrayIntegrationProvider; import org.cryptomator.integrations.tray.TrayIntegrationProvider;
import org.cryptomator.integrations.uiappearance.Theme; import org.cryptomator.integrations.uiappearance.Theme;
import org.cryptomator.integrations.uiappearance.UiAppearanceException; import org.cryptomator.integrations.uiappearance.UiAppearanceException;
import org.cryptomator.integrations.uiappearance.UiAppearanceListener; import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider; import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.lock.LockComponent; import org.cryptomator.ui.lock.LockComponent;
import org.cryptomator.ui.mainwindow.MainWindowComponent; import org.cryptomator.ui.mainwindow.MainWindowComponent;
@@ -46,9 +44,8 @@ public class FxApplication extends Application {
private final Lazy<MainWindowComponent> mainWindow; private final Lazy<MainWindowComponent> mainWindow;
private final Lazy<PreferencesComponent> preferencesWindow; private final Lazy<PreferencesComponent> preferencesWindow;
private final Lazy<QuitComponent> quitWindow; private final Lazy<QuitComponent> quitWindow;
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider; private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider; private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
private final ErrorComponent.Builder errorWindowBuilder;
private final Optional<TrayIntegrationProvider> trayIntegration; private final Optional<TrayIntegrationProvider> trayIntegration;
private final Optional<UiAppearanceProvider> appearanceProvider; private final Optional<UiAppearanceProvider> appearanceProvider;
private final VaultService vaultService; private final VaultService vaultService;
@@ -58,14 +55,13 @@ public class FxApplication extends Application {
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged; private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
@Inject @Inject
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) { 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) {
this.settings = settings; this.settings = settings;
this.mainWindow = mainWindow; this.mainWindow = mainWindow;
this.preferencesWindow = preferencesWindow; this.preferencesWindow = preferencesWindow;
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider; this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider; this.lockWindowBuilderProvider = lockWindowBuilderProvider;
this.quitWindow = quitWindow; this.quitWindow = quitWindow;
this.errorWindowBuilder = errorWindowBuilder;
this.trayIntegration = trayIntegration; this.trayIntegration = trayIntegration;
this.appearanceProvider = appearanceProvider; this.appearanceProvider = appearanceProvider;
this.vaultService = vaultService; this.vaultService = vaultService;
@@ -117,23 +113,15 @@ public class FxApplication extends Application {
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) { public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> { Platform.runLater(() -> {
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) { unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow(); LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
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.")));
}
}); });
} }
public void startLockWorkflow(Vault vault, Optional<Stage> owner) { public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
Platform.runLater(() -> { Platform.runLater(() -> {
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) { lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow(); LOG.debug("Start lock workflow for {}", vault.getDisplayName());
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,7 +15,6 @@ import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.util.Duration; import javafx.util.Duration;
import java.io.UncheckedIOException;
import java.net.URI; import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@@ -40,13 +39,8 @@ public abstract class UpdateCheckerModule {
@Provides @Provides
@FxApplicationScoped @FxApplicationScoped
static Optional<HttpClient> provideHttpClient() { static HttpClient provideHttpClient() {
try { return HttpClient.newHttpClient();
return Optional.of(HttpClient.newHttpClient());
} catch (UncheckedIOException e) {
LOG.error("HttpClient for update check cannot be created.", e);
return Optional.empty();
}
} }
@Provides @Provides
@@ -72,20 +66,11 @@ public abstract class UpdateCheckerModule {
@Provides @Provides
@FxApplicationScoped @FxApplicationScoped
static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, Optional<HttpClient> httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) { static ScheduledService<String> provideCheckForUpdatesService(ExecutorService executor, HttpClient httpClient, HttpRequest checkForUpdatesRequest, @Named("checkForUpdatesInterval") ObjectBinding<Duration> period) {
ScheduledService<String> service = new ScheduledService<>() { ScheduledService<String> service = new ScheduledService<>() {
@Override @Override
protected Task<String> createTask() { protected Task<String> createTask() {
if (httpClient.isPresent()) { return new UpdateCheckerTask(httpClient, checkForUpdatesRequest);
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())); service.setOnFailed(event -> LOG.error("Failed to execute update service", service.getException()));
@@ -1,14 +0,0 @@
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 {
}
@@ -1,31 +0,0 @@
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();
}
}
@@ -1,48 +0,0 @@
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.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 Optional<URI> provideKeyId(@KeyLoading Vault vault) {
return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId);
}
@Provides
@KeyLoading
@KeyLoadingScoped
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional<URI> keyId, Map<String, Provider<KeyLoadingStrategy>> strategies) {
if (keyId.isEmpty()) {
return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided"));
} else {
String scheme = keyId.get().getScheme();
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
return strategies.getOrDefault(scheme, () -> fallback).get();
}
}
}
@@ -1,13 +0,0 @@
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 {
}
@@ -1,63 +0,0 @@
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);
}
};
}
}
@@ -1,62 +0,0 @@
package org.cryptomator.ui.keyloading.masterkeyfile;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.keyloading.KeyLoading;
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.inject.Named;
import java.nio.CharBuffer;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped
class MasterkeyFileLoadingFinisher {
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
private final Vault vault;
private final Optional<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, ' ');
}
}
}
@@ -1,123 +0,0 @@
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()) {
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);
}
@@ -1,150 +0,0 @@
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 == 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,67 +0,0 @@
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,7 +10,6 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import javafx.application.Platform; import javafx.application.Platform;
import java.io.IOException;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
@@ -79,7 +78,7 @@ class AppLaunchEventHandler {
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v)); fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
} }
LOG.debug("Added vault {}", potentialVaultPath); LOG.debug("Added vault {}", potentialVaultPath);
} catch (IOException e) { } catch (NoSuchFileException e) {
LOG.error("Failed to add vault " + potentialVaultPath, e); LOG.error("Failed to add vault " + potentialVaultPath, e);
} }
} }
@@ -1,7 +1,6 @@
package org.cryptomator.ui.launcher; package org.cryptomator.ui.launcher;
import org.cryptomator.common.ShutdownHook; import org.cryptomator.common.ShutdownHook;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
@@ -25,13 +24,11 @@ import java.util.Set;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import static org.cryptomator.common.vaults.VaultState.Value.*;
@Singleton @Singleton
public class AppLifecycleListener { public class AppLifecycleListener {
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class); private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR); public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
private final FxApplicationStarter fxApplicationStarter; private final FxApplicationStarter fxApplicationStarter;
private final CountDownLatch shutdownLatch; private final CountDownLatch shutdownLatch;
@@ -130,8 +127,6 @@ public class AppLifecycleListener {
vault.lock(true); vault.lock(true);
} catch (Volume.VolumeException e) { } catch (Volume.VolumeException e) {
LOG.error("Failed to unmount vault " + vault.getPath(), e); LOG.error("Failed to unmount vault " + vault.getPath(), e);
} catch (LockNotCompletedException e) {
LOG.error("Failed to lock vault " + vault.getPath(), e);
} }
} }
} }
@@ -1,11 +1,9 @@
package org.cryptomator.ui.lock; package org.cryptomator.ui.lock;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.vaults.LockNotCompletedException;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume; import org.cryptomator.common.vaults.Volume;
import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.common.UserInteractionLock;
@@ -37,23 +35,21 @@ public class LockWorkflow extends Task<Void> {
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock; private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
private final Lazy<Scene> lockForcedScene; private final Lazy<Scene> lockForcedScene;
private final Lazy<Scene> lockFailedScene; private final Lazy<Scene> lockFailedScene;
private final ErrorComponent.Builder errorComponent;
@Inject @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, ErrorComponent.Builder errorComponent) { 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) {
this.lockWindow = lockWindow; this.lockWindow = lockWindow;
this.vault = vault; this.vault = vault;
this.forceLockDecisionLock = forceLockDecisionLock; this.forceLockDecisionLock = forceLockDecisionLock;
this.lockForcedScene = lockForcedScene; this.lockForcedScene = lockForcedScene;
this.lockFailedScene = lockFailedScene; this.lockFailedScene = lockFailedScene;
this.errorComponent = errorComponent;
} }
@Override @Override
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException { protected Void call() throws Volume.VolumeException, InterruptedException {
try { try {
vault.lock(false); vault.lock(false);
} catch (Volume.VolumeException | LockNotCompletedException e) { } catch (Volume.VolumeException e) {
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e); LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
var decision = askUserForAction(); var decision = askUserForAction();
switch (decision) { switch (decision) {
@@ -81,29 +77,29 @@ public class LockWorkflow extends Task<Void> {
return forceLockDecisionLock.awaitInteraction(); return forceLockDecisionLock.awaitInteraction();
} }
@Override
protected void scheduled() {
vault.setState(VaultState.PROCESSING);
}
@Override @Override
protected void succeeded() { protected void succeeded() {
LOG.info("Lock of {} succeeded.", vault.getDisplayName()); LOG.info("Lock of {} succeeded.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED); vault.setState(VaultState.LOCKED);
} }
@Override @Override
protected void failed() { protected void failed() {
final var throwable = super.getException(); LOG.warn("Failed to lock {}.", vault.getDisplayName());
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable); vault.setState(VaultState.UNLOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED); lockWindow.setScene(lockFailedScene.get());
if (throwable instanceof Volume.VolumeException) { lockWindow.show();
lockWindow.setScene(lockFailedScene.get());
lockWindow.show();
} else {
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
}
} }
@Override @Override
protected void cancelled() { protected void cancelled() {
LOG.debug("Lock of {} canceled.", vault.getDisplayName()); LOG.debug("Lock of {} canceled.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED); vault.setState(VaultState.UNLOCKED);
} }
} }
@@ -3,8 +3,6 @@ package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; 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.common.FxController;
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent; import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -22,7 +20,6 @@ import javafx.scene.input.TransferMode;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.NoSuchFileException; import java.nio.file.NoSuchFileException;
import java.nio.file.Path; import java.nio.file.Path;
@@ -30,7 +27,6 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MainWindowScoped @MainWindowScoped
public class MainWindowController implements FxController { public class MainWindowController implements FxController {
@@ -95,21 +91,23 @@ public class MainWindowController implements FxController {
} }
private boolean containsVault(Path path) { private boolean containsVault(Path path) {
try { if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED; return true;
} catch (IOException e) { } else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
return true;
} else {
return false; return false;
} }
} }
private void addVault(Path pathToVault) { private void addVault(Path pathToVault) {
try { try {
if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) { if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
vaultListManager.add(pathToVault.getParent()); vaultListManager.add(pathToVault.getParent());
} else { } else {
vaultListManager.add(pathToVault); vaultListManager.add(pathToVault);
} }
} catch (IOException e) { } catch (NoSuchFileException e) {
LOG.debug("Not a vault: {}", pathToVault); LOG.debug("Not a vault: {}", pathToVault);
} }
} }
@@ -86,11 +86,6 @@ abstract class MainWindowModule {
@FxControllerKey(VaultListController.class) @FxControllerKey(VaultListController.class)
abstract FxController bindVaultListController(VaultListController controller); abstract FxController bindVaultListController(VaultListController controller);
@Binds
@IntoMap
@FxControllerKey(VaultListContextMenuController.class)
abstract FxController bindVaultListContextMenuController(VaultListContextMenuController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(VaultDetailController.class) @FxControllerKey(VaultDetailController.class)
@@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController {
@FXML @FXML
public void showDonationKeyPreferences() { public void showDonationKeyPreferences() {
application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE); application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -32,8 +32,7 @@ public class VaultDetailController implements FxController {
this.anyVaultSelected = vault.isNotNull(); this.anyVaultSelected = vault.isNotNull();
} }
// TODO deduplicate w/ VaultListCellController private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) { if (state != null) {
return switch (state) { return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK; case LOCKED -> FontAwesome5Icon.LOCK;
@@ -24,8 +24,7 @@ public class VaultListCellController implements FxController {
.map(this::getGlyphForVaultState); .map(this::getGlyphForVaultState);
} }
// TODO deduplicate w/ VaultDetailController private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
if (state != null) { if (state != null) {
return switch (state) { return switch (state) {
case LOCKED -> FontAwesome5Icon.LOCK; case LOCKED -> FontAwesome5Icon.LOCK;
@@ -1,128 +0,0 @@
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,11 +1,12 @@
package org.cryptomator.ui.mainwindow; package org.cryptomator.ui.mainwindow;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultListManager; import org.cryptomator.common.vaults.VaultListManager;
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent; import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.removevault.RemoveVaultComponent; import org.cryptomator.ui.removevault.RemoveVaultComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@@ -16,43 +17,30 @@ import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ListView; 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 @MainWindowScoped
public class VaultListController implements FxController { 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 ObservableList<Vault> vaults;
private final ObjectProperty<Vault> selectedVault; private final ObjectProperty<Vault> selectedVault;
private final VaultListCellFactory cellFactory; private final VaultListCellFactory cellFactory;
private final AddVaultWizardComponent.Builder addVaultWizard; private final AddVaultWizardComponent.Builder addVaultWizard;
private final RemoveVaultComponent.Builder removeVault;
private final BooleanBinding noVaultSelected;
private final BooleanBinding emptyVaultList; private final BooleanBinding emptyVaultList;
private final RemoveVaultComponent.Builder removeVaultDialogue;
public ListView<Vault> vaultList; public ListView<Vault> vaultList;
@Inject @Inject
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) { VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVault) {
this.mainWindow = mainWindow;
this.vaults = vaults; this.vaults = vaults;
this.selectedVault = selectedVault; this.selectedVault = selectedVault;
this.cellFactory = cellFactory; this.cellFactory = cellFactory;
this.addVaultWizard = addVaultWizard; this.addVaultWizard = addVaultWizard;
this.removeVaultDialogue = removeVaultDialogue; this.removeVault = removeVault;
this.noVaultSelected = selectedVault.isNull();
this.emptyVaultList = Bindings.isEmpty(vaults); this.emptyVaultList = Bindings.isEmpty(vaults);
selectedVault.addListener(this::selectedVaultDidChange); selectedVault.addListener(this::selectedVaultDidChange);
} }
@@ -68,41 +56,6 @@ 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) { private void selectedVaultDidChange(@SuppressWarnings("unused") ObservableValue<? extends Vault> observableValue, @SuppressWarnings("unused") Vault oldValue, Vault newValue) {
@@ -117,10 +70,13 @@ public class VaultListController implements FxController {
addVaultWizard.build().showAddVaultWizard(); addVaultWizard.build().showAddVaultWizard();
} }
private void pressedShortcutToRemoveVault() { @FXML
final var vault = selectedVault.get(); public void didClickRemoveVault() {
if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) { Vault v = selectedVault.get();
removeVaultDialogue.vault(vault).build().showRemoveVault(); if (v != null) {
removeVault.vault(v).build().showRemoveVault();
} else {
LOG.debug("Cannot remove a vault if none is selected.");
} }
} }
@@ -134,4 +90,11 @@ public class VaultListController implements FxController {
return emptyVaultList.get(); return emptyVaultList.get();
} }
public BooleanBinding noVaultSelectedProperty() {
return noVaultSelected;
}
public boolean isNoVaultSelected() {
return noVaultSelected.get();
}
} }
@@ -26,7 +26,6 @@ import javax.inject.Named;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding; import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.ObjectExpression;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty; import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
@@ -44,7 +43,6 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
@MigrationScoped @MigrationScoped
public class MigrationRunController implements FxController { public class MigrationRunController implements FxController {
@@ -91,12 +89,7 @@ public class MigrationRunController implements FxController {
if (keychain.isSupported()) { if (keychain.isSupported()) {
loadStoredPassword(); 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 @FXML
@@ -108,7 +101,7 @@ public class MigrationRunController implements FxController {
public void migrate() { public void migrate() {
LOG.info("Migrating vault {}", vault.getPath()); LOG.info("Migrating vault {}", vault.getPath());
CharSequence password = passwordField.getCharacters(); CharSequence password = passwordField.getCharacters();
vault.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING); vault.setState(VaultState.PROCESSING);
passwordField.setDisable(true); passwordField.setDisable(true);
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> { ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
Platform.runLater(() -> { Platform.runLater(() -> {
@@ -117,15 +110,15 @@ public class MigrationRunController implements FxController {
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS); }, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
Tasks.create(() -> { Tasks.create(() -> {
Migrators migrators = Migrators.get(); Migrators migrators = Migrators.get();
migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput); migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME); return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
}).onSuccess(needsAnotherMigration -> { }).onSuccess(needsAnotherMigration -> {
if (needsAnotherMigration) { if (needsAnotherMigration) {
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName()); LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
} else { } else {
LOG.info("Migration of '{}' succeeded.", vault.getDisplayName()); LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED); vault.setState(VaultState.LOCKED);
passwordField.wipe(); passwordField.wipe();
window.setScene(successScene.get()); window.setScene(successScene.get());
} }
@@ -134,20 +127,20 @@ public class MigrationRunController implements FxController {
passwordField.setDisable(false); passwordField.setDisable(false);
passwordField.selectAll(); passwordField.selectAll();
passwordField.requestFocus(); passwordField.requestFocus();
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> { }).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
LOG.error("Underlying file system not supported.", e); LOG.error("Underlying file system not supported.", e);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
missingCapability.set(e.getMissingCapability()); missingCapability.set(e.getMissingCapability());
window.setScene(capabilityErrorScene.get()); window.setScene(capabilityErrorScene.get());
}).onError(FileNameTooLongException.class, e -> { }).onError(FileNameTooLongException.class, e -> {
LOG.error("Migration failed because the underlying file system does not support long filenames.", e); LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
window.setScene(impossibleScene.get()); window.setScene(impossibleScene.get());
}).onError(Exception.class, e -> { // including RuntimeExceptions }).onError(Exception.class, e -> { // including RuntimeExceptions
LOG.error("Migration failed for technical reasons.", e); LOG.error("Migration failed for technical reasons.", e);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION); vault.setState(VaultState.NEEDS_MIGRATION);
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene(); errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
}).andFinally(() -> { }).andFinally(() -> {
passwordField.setDisable(false); passwordField.setDisable(false);
@@ -14,17 +14,17 @@ import javafx.scene.control.TextArea;
import javafx.scene.control.TextFormatter; import javafx.scene.control.TextFormatter;
@PreferencesScoped @PreferencesScoped
public class SupporterCertificateController implements FxController { public class DonationKeyPreferencesController implements FxController {
private static final String SUPPORTER_URI = "https://store.cryptomator.org/desktop"; private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
private final Application application; private final Application application;
private final LicenseHolder licenseHolder; private final LicenseHolder licenseHolder;
private final Settings settings; private final Settings settings;
public TextArea supporterCertificateField; public TextArea donationKeyField;
@Inject @Inject
SupporterCertificateController(Application application, LicenseHolder licenseHolder, Settings settings) { DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
this.application = application; this.application = application;
this.licenseHolder = licenseHolder; this.licenseHolder = licenseHolder;
this.settings = settings; this.settings = settings;
@@ -32,9 +32,9 @@ public class SupporterCertificateController implements FxController {
@FXML @FXML
public void initialize() { public void initialize() {
supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null)); donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
supporterCertificateField.textProperty().addListener(this::registrationKeyChanged); donationKeyField.textProperty().addListener(this::registrationKeyChanged);
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength)); donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
} }
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) { private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
@@ -53,8 +53,8 @@ public class SupporterCertificateController implements FxController {
} }
@FXML @FXML
public void getSupporterCertificate() { public void getDonationKey() {
application.getHostServices().showDocument(SUPPORTER_URI); application.getHostServices().showDocument(DONATION_URI);
} }
public LicenseHolder getLicenseHolder() { public LicenseHolder getLicenseHolder() {
@@ -157,8 +157,8 @@ public class GeneralPreferencesController implements FxController {
@FXML @FXML
public void showContributeTab() { public void showDonationTab() {
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE); selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
} }
@FXML @FXML
@@ -26,7 +26,7 @@ public class PreferencesController implements FxController {
public Tab generalTab; public Tab generalTab;
public Tab volumeTab; public Tab volumeTab;
public Tab updatesTab; public Tab updatesTab;
public Tab contributeTab; public Tab donationKeyTab;
public Tab aboutTab; public Tab aboutTab;
@Inject @Inject
@@ -52,7 +52,7 @@ public class PreferencesController implements FxController {
return switch (selectedTab) { return switch (selectedTab) {
case UPDATES -> updatesTab; case UPDATES -> updatesTab;
case VOLUME -> volumeTab; case VOLUME -> volumeTab;
case CONTRIBUTE -> contributeTab; case DONATION_KEY -> donationKeyTab;
case GENERAL -> generalTab; case GENERAL -> generalTab;
case ABOUT -> aboutTab; case ABOUT -> aboutTab;
case ANY -> updateAvailable.get() ? updatesTab : generalTab; case ANY -> updateAvailable.get() ? updatesTab : generalTab;
@@ -77,8 +77,8 @@ abstract class PreferencesModule {
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(SupporterCertificateController.class) @FxControllerKey(DonationKeyPreferencesController.class)
abstract FxController bindSupporterCertificatePreferencesController(SupporterCertificateController controller); abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
@Binds @Binds
@IntoMap @IntoMap
@@ -22,9 +22,9 @@ public enum SelectedPreferencesTab {
UPDATES, UPDATES,
/** /**
* Show contribute tab * Show donation key tab
*/ */
CONTRIBUTE, DONATION_KEY,
/** /**
* Show about tab * Show about tab
@@ -2,7 +2,6 @@ package org.cryptomator.ui.recoverykey;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
@@ -81,7 +80,7 @@ public class RecoveryKeyCreationController implements FxController {
} }
@Override @Override
protected String call() throws IOException, CryptoException { protected String call() throws IOException {
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
} }
@@ -2,34 +2,28 @@ package org.cryptomator.ui.recoverykey;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; import org.cryptomator.cryptofs.CryptoFileSystemProvider;
import org.cryptomator.cryptolib.api.CryptoException;
import org.cryptomator.cryptolib.api.InvalidPassphraseException; 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.Inject;
import javax.inject.Singleton; import javax.inject.Singleton;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@Singleton @Singleton
public class RecoveryKeyFactory { public class RecoveryKeyFactory {
private static final byte[] PEPPER = new byte[0];
private final WordEncoder wordEncoder; private final WordEncoder wordEncoder;
private final MasterkeyFileAccess masterkeyFileAccess;
@Inject @Inject
public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) { public RecoveryKeyFactory(WordEncoder wordEncoder) {
this.wordEncoder = wordEncoder; this.wordEncoder = wordEncoder;
this.masterkeyFileAccess = masterkeyFileAccess;
} }
public Collection<String> getDictionary() { public Collection<String> getDictionary() {
@@ -42,14 +36,11 @@ public class RecoveryKeyFactory {
* @return The recovery key of the vault at the given path * @return The recovery key of the vault at the given path
* @throws IOException If the masterkey file could not be read * @throws IOException If the masterkey file could not be read
* @throws InvalidPassphraseException If the provided password is wrong * @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 * @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, CryptoException { public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME); byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
byte[] rawKey = new byte[0]; try {
try (var masterkey = masterkeyFileAccess.load(masterkeyPath, password)) {
rawKey = masterkey.getEncoded();
return createRecoveryKey(rawKey); return createRecoveryKey(rawKey);
} finally { } finally {
Arrays.fill(rawKey, (byte) 0x00); Arrays.fill(rawKey, (byte) 0x00);
@@ -81,15 +72,8 @@ public class RecoveryKeyFactory {
*/ */
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException { public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
final byte[] rawKey = decodeRecoveryKey(recoveryKey); final byte[] rawKey = decodeRecoveryKey(recoveryKey);
try (var masterkey = new Masterkey(rawKey)) { try {
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME); CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
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 { } finally {
Arrays.fill(rawKey, (byte) 0x00); Arrays.fill(rawKey, (byte) 0x00);
} }
@@ -0,0 +1,78 @@
package org.cryptomator.ui.stats;
import javafx.beans.NamedArg;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.Axis;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
// inspired by http://fxexperience.com/2012/01/curve-fitting-and-styling-areachart/
public class SmoothAreaChart extends AreaChart<Number, Number> {
public SmoothAreaChart(@NamedArg("xAxis") Axis<Number> xAxis, @NamedArg("yAxis") Axis<Number> yAxis) {
super(xAxis, yAxis);
}
@Override
protected void layoutPlotChildren() {
super.layoutPlotChildren();
var iter = getDisplayedSeriesIterator();
while (iter.hasNext()) {
var series = iter.next();
final Path seriesLine = (Path)((Group)series.getNode()).getChildren().get(1);
final Path fillPath = (Path)((Group)series.getNode()).getChildren().get(0);
var dx = getXAxis().getWidth() / series.getData().size();
smooth(seriesLine.getElements(), fillPath.getElements(), dx);
}
}
private static void smooth(ObservableList<PathElement> strokeElements, ObservableList<PathElement> fillElements, double dx) {
// as we do not have direct access to the data, first recreate the list of all the data points we have
final Point2D[] dataPoints = new Point2D[strokeElements.size()];
for (int i = 0; i < strokeElements.size(); i++) {
final PathElement element = strokeElements.get(i);
if (element instanceof MoveTo) {
final MoveTo move = (MoveTo)element;
dataPoints[i] = new Point2D(move.getX(), move.getY());
} else if (element instanceof LineTo) {
final LineTo line = (LineTo)element;
final double x = line.getX(), y = line.getY();
dataPoints[i] = new Point2D(x, y);
}
}
// next we need to know the zero Y value
final double zeroY = ((MoveTo) fillElements.get(0)).getY();
final double dx2 = dx / 2.0;
// now clear and rebuild elements
strokeElements.clear();
fillElements.clear();
// start both paths
strokeElements.add(new MoveTo(dataPoints[0].getX(),dataPoints[0].getY()));
fillElements.add(new MoveTo(dataPoints[0].getX(),zeroY));
fillElements.add(new LineTo(dataPoints[0].getX(),dataPoints[0].getY()));
// add curves
for (int i = 1; i < dataPoints.length; i++) {
final int ci = i-1;
strokeElements.add(new CubicCurveTo(
dataPoints[ci].getX() + dx2, dataPoints[ci].getY(),
dataPoints[i].getX() - dx2, dataPoints[i].getY(),
dataPoints[i].getX(), dataPoints[i].getY()));
fillElements.add(new CubicCurveTo(
dataPoints[ci].getX() + dx2, dataPoints[ci].getY(),
dataPoints[i].getX() - dx2, dataPoints[i].getY(),
dataPoints[i].getX(),dataPoints[i].getY()));
}
// end the paths
fillElements.add(new LineTo(dataPoints[dataPoints.length-1].getX(),zeroY));
fillElements.add(new ClosePath());
}
}
@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
var weakStage = new WeakReference<>(stage); var weakStage = new WeakReference<>(stage);
vault.stateProperty().addListener(new ChangeListener<>() { vault.stateProperty().addListener(new ChangeListener<>() {
@Override @Override
public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) { public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
if (newValue != VaultState.Value.UNLOCKED) { if (newValue != VaultState.UNLOCKED) {
Stage stage = weakStage.get(); Stage stage = weakStage.get();
if (stage != null) { if (stage != null) {
stage.hide(); stage.hide();
@@ -44,9 +44,6 @@ class TrayMenuController {
public void initTrayMenu() { public void initTrayMenu() {
vaults.addListener(this::vaultListChanged); vaults.addListener(this::vaultListChanged);
vaults.forEach(v -> {
v.displayNameProperty().addListener(this::vaultListChanged);
});
rebuildMenu(); rebuildMenu();
} }
@@ -1,14 +0,0 @@
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,4 +1,4 @@
package org.cryptomator.ui.keyloading.masterkeyfile; package org.cryptomator.ui.unlock;
import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
@@ -7,9 +7,6 @@ import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.WeakBindings; import org.cryptomator.ui.common.WeakBindings;
import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.cryptomator.ui.controls.NiceSecurePasswordField;
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -41,17 +38,17 @@ import java.util.Optional;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@KeyLoadingScoped @UnlockScoped
public class PassphraseEntryController implements FxController { public class UnlockController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class); private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final AtomicReference<char[]> password; private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword; private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword; private final Optional<char[]> savedPassword;
private final UserInteractionLock<PasswordEntry> passwordEntryLock; private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
private final ForgetPasswordComponent.Builder forgetPassword; private final ForgetPasswordComponent.Builder forgetPassword;
private final KeychainManager keychain; private final KeychainManager keychain;
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay; private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
@@ -69,7 +66,7 @@ public class PassphraseEntryController implements FxController {
public Animation unlockAnimation; public Animation unlockAnimation;
@Inject @Inject
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) { 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) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.password = password; this.password = password;
@@ -141,7 +138,7 @@ public class PassphraseEntryController implements FxController {
// if not already interacted, mark this workflow as cancelled: // if not already interacted, mark this workflow as cancelled:
if (passwordEntryLock.awaitingInteraction().get()) { if (passwordEntryLock.awaitingInteraction().get()) {
LOG.debug("Unlock canceled by user."); LOG.debug("Unlock canceled by user.");
passwordEntryLock.interacted(PasswordEntry.CANCELED); passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
} }
} }
@@ -157,7 +154,7 @@ public class PassphraseEntryController implements FxController {
if (oldPw != null) { if (oldPw != null) {
Arrays.fill(oldPw, ' '); Arrays.fill(oldPw, ' ');
} }
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED); passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
startUnlockAnimation(); startUnlockAnimation();
} }
@@ -1,11 +1,15 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.unlock;
import dagger.Lazy;
import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
import javax.inject.Inject; import javax.inject.Inject;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
//At the current point in time only the CustomMountPointChooser may cause this window to be shown. //At the current point in time only the CustomMountPointChooser may cause this window to be shown.
@@ -13,17 +17,19 @@ import javafx.stage.Stage;
public class UnlockInvalidMountPointController implements FxController { public class UnlockInvalidMountPointController implements FxController {
private final Stage window; private final Stage window;
private final Lazy<Scene> unlockScene;
private final Vault vault; private final Vault vault;
@Inject @Inject
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) { UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
this.window = window; this.window = window;
this.unlockScene = unlockScene;
this.vault = vault; this.vault = vault;
} }
@FXML @FXML
public void close() { public void back() {
window.close(); window.setScene(unlockScene.get());
} }
/* Getter/Setter */ /* Getter/Setter */
@@ -4,16 +4,20 @@ import dagger.Binds;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import dagger.multibindings.IntoMap; import dagger.multibindings.IntoMap;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.DefaultSceneFactory; import org.cryptomator.ui.common.DefaultSceneFactory;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.StageFactory; import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Named; import javax.inject.Named;
import javax.inject.Provider; import javax.inject.Provider;
@@ -23,10 +27,54 @@ import javafx.stage.Stage;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@Module(subcomponents = {KeyLoadingComponent.class}) @Module(subcomponents = {ForgetPasswordComponent.class})
abstract class UnlockModule { 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 @Provides
@UnlockWindow @UnlockWindow
@UnlockScoped @UnlockScoped
@@ -51,10 +99,10 @@ abstract class UnlockModule {
} }
@Provides @Provides
@UnlockWindow @FxmlScene(FxmlFile.UNLOCK)
@UnlockScoped @UnlockScoped
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) { static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); return fxmlLoaders.createScene(FxmlFile.UNLOCK);
} }
@Provides @Provides
@@ -73,6 +121,11 @@ abstract class UnlockModule {
// ------------------ // ------------------
@Binds
@IntoMap
@FxControllerKey(UnlockController.class)
abstract FxController bindUnlockController(UnlockController controller);
@Binds @Binds
@IntoMap @IntoMap
@FxControllerKey(UnlockSuccessController.class) @FxControllerKey(UnlockSuccessController.class)
@@ -1,31 +1,40 @@
package org.cryptomator.ui.unlock; package org.cryptomator.ui.unlock;
import dagger.Lazy; import dagger.Lazy;
import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.mountpoint.InvalidMountPointException;
import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.MountPointRequirement;
import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.Vault;
import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.VaultState;
import org.cryptomator.common.vaults.Volume.VolumeException; import org.cryptomator.common.vaults.Volume.VolumeException;
import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException;
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.ui.common.Animations;
import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.ErrorComponent;
import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.FxmlScene;
import org.cryptomator.ui.common.UserInteractionLock;
import org.cryptomator.ui.common.VaultService; import org.cryptomator.ui.common.VaultService;
import org.cryptomator.ui.keyloading.KeyLoadingComponent; import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.Window;
import java.io.IOException; import java.io.IOException;
import java.nio.CharBuffer;
import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileAlreadyExistsException;
import java.nio.file.NotDirectoryException; 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. * A multi-step task that consists of background activities as well as user interaction.
@@ -40,47 +49,113 @@ public class UnlockWorkflow extends Task<Boolean> {
private final Stage window; private final Stage window;
private final Vault vault; private final Vault vault;
private final VaultService vaultService; 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> successScene;
private final Lazy<Scene> invalidMountPointScene; private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent; private final ErrorComponent.Builder errorComponent;
private final KeyLoadingStrategy keyLoadingStrategy;
@Inject @Inject
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) { 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) {
this.window = window; this.window = window;
this.vault = vault; this.vault = vault;
this.vaultService = vaultService; 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.successScene = successScene;
this.invalidMountPointScene = invalidMountPointScene; this.invalidMountPointScene = invalidMountPointScene;
this.errorComponent = errorComponent; this.errorComponent = errorComponent;
this.keyLoadingStrategy = keyLoadingStrategy;
setOnFailed(event -> {
Throwable throwable = event.getSource().getException();
if (throwable instanceof InvalidMountPointException e) {
handleInvalidMountPoint(e);
} else {
handleGenericError(throwable);
}
});
} }
@Override @Override
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException { protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
try { try {
attemptUnlock(); if (attemptUnlock()) {
return true; handleSuccess();
} catch (UnlockCancelledException e) { return true;
cancel(false); // set Tasks state to cancelled } else {
return false; cancel(false); // set Tasks state to cancelled
return false;
}
} finally {
wipePassword(password.get());
wipePassword(savedPassword.orElse(null));
} }
} }
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException { private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
boolean success = false; boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
try { while (proceed) {
vault.unlock(keyLoadingStrategy); try {
success = true; vault.unlock(CharBuffer.wrap(password.get()));
} catch (MasterkeyLoadingFailedException e) { return true;
if (keyLoadingStrategy.recoverFromException(e)) { } catch (InvalidPassphraseException e) {
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName()); proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
attemptUnlock(); }
}
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 { } else {
throw e; 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);
} }
} finally {
keyLoadingStrategy.cleanup(success);
} }
} }
@@ -98,12 +173,15 @@ public class UnlockWorkflow extends Task<Boolean> {
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
} }
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else if (cause instanceof FileAlreadyExistsException) { } else if (cause instanceof FileAlreadyExistsException) {
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else if (cause instanceof DirectoryNotEmptyException) { } else if (cause instanceof DirectoryNotEmptyException) {
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage()); LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
showInvalidMountPointScene(); showInvalidMountPointScene();
return;
} else { } else {
handleGenericError(impExc); handleGenericError(impExc);
} }
@@ -118,44 +196,33 @@ public class UnlockWorkflow extends Task<Boolean> {
private void handleGenericError(Throwable e) { private void handleGenericError(Throwable e) {
LOG.error("Unlock failed for technical reasons.", e); LOG.error("Unlock failed for technical reasons.", e);
errorComponent.cause(e).window(window).build().showErrorScene(); 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);
} }
@Override @Override
protected void succeeded() { protected void succeeded() {
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName()); vault.setState(VaultState.UNLOCKED);
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 @Override
protected void failed() { protected void failed() {
LOG.info("Unlock of '{}' failed.", vault.getDisplayName()); vault.setState(VaultState.LOCKED);
Throwable throwable = super.getException();
if (throwable instanceof InvalidMountPointException e) {
handleInvalidMountPoint(e);
} else {
handleGenericError(throwable);
}
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
@Override @Override
protected void cancelled() { protected void cancelled() {
LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName()); vault.setState(VaultState.LOCKED);
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
} }
} }
@@ -20,8 +20,6 @@
alignment="CENTER_LEFT"> alignment="CENTER_LEFT">
<fx:define> <fx:define>
<ToggleGroup fx:id="predefinedLocationToggler"/> <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> </fx:define>
<padding> <padding>
<Insets topRightBottomLeft="24"/> <Insets topRightBottomLeft="24"/>
@@ -35,8 +33,6 @@
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/> <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="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="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"> <HBox spacing="12" alignment="CENTER_LEFT">
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/> <RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}"> <Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
@@ -51,8 +47,12 @@
<VBox spacing="6"> <VBox spacing="6">
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/> <Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/> <TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" disable="true" 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}" /> <Label text="${controller.warningText}" wrapText="true" visible="${controller.showWarning}">
<graphic>
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
</graphic>
</Label>
</VBox> </VBox>
<Region VBox.vgrow="ALWAYS"/> <Region VBox.vgrow="ALWAYS"/>
@@ -38,12 +38,12 @@
<fx:include source="/fxml/preferences_updates.fxml"/> <fx:include source="/fxml/preferences_updates.fxml"/>
</content> </content>
</Tab> </Tab>
<Tab fx:id="contributeTab" id="CONTRIBUTE" text="%preferences.contribute"> <Tab fx:id="donationKeyTab" id="DONATION_KEY" text="%preferences.donationKey">
<graphic> <graphic>
<FontAwesome5IconView glyph="HEART"/> <FontAwesome5IconView glyph="HEART"/>
</graphic> </graphic>
<content> <content>
<fx:include source="/fxml/preferences_contribute.fxml"/> <fx:include source="/fxml/preferences_donationkey.fxml"/>
</content> </content>
</Tab> </Tab>
<Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about"> <Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about">
@@ -12,7 +12,7 @@
<?import javafx.scene.shape.Circle?> <?import javafx.scene.shape.Circle?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController" fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController"
spacing="18"> spacing="18">
<padding> <padding>
<Insets topRightBottomLeft="12"/> <Insets topRightBottomLeft="12"/>
@@ -24,7 +24,7 @@
<Circle styleClass="glyph-icon-primary" radius="24"/> <Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/> <FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/>
</StackPane> </StackPane>
<FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/> <FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
</HBox> </HBox>
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}"> <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"/> <FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
</StackPane> </StackPane>
<VBox HBox.hgrow="ALWAYS" spacing="6"> <VBox HBox.hgrow="ALWAYS" spacing="6">
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/> <Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/>
<Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate" contentDisplay="LEFT"> <Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT">
<graphic> <graphic>
<FontAwesome5IconView glyph="LINK"/> <FontAwesome5IconView glyph="LINK"/>
</graphic> </graphic>
@@ -43,6 +43,6 @@
</HBox> </HBox>
</StackPane> </StackPane>
<TextArea fx:id="supporterCertificateField" promptText="%preferences.contribute.promptText" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/> <TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
</children> </children>
</VBox> </VBox>
@@ -23,7 +23,7 @@
<HBox spacing="6" alignment="CENTER_LEFT"> <HBox spacing="6" alignment="CENTER_LEFT">
<Label text="%preferences.general.theme"/> <Label text="%preferences.general.theme"/>
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/> <ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/> <Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showDonationTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
</HBox> </HBox>
<HBox spacing="6" alignment="CENTER_LEFT"> <HBox spacing="6" alignment="CENTER_LEFT">
+3 -2
View File
@@ -13,6 +13,7 @@
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Arc?> <?import javafx.scene.shape.Arc?>
<?import org.cryptomator.ui.stats.SmoothAreaChart?>
<HBox xmlns="http://javafx.com/javafx" <HBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.cryptomator.ui.stats.VaultStatisticsController" fx:controller="org.cryptomator.ui.stats.VaultStatisticsController"
@@ -38,7 +39,7 @@
<!-- Read --> <!-- Read -->
<VBox prefWidth="300" prefHeight="300" spacing="6" alignment="CENTER"> <VBox prefWidth="300" prefHeight="300" spacing="6" alignment="CENTER">
<ThrougputLabel styleClass="label-large" idleFormat="%stats.read.throughput.idle" kibsFormat="%stats.read.throughput.kibs" mibsFormat="%stats.read.throughput.mibs" bytesPerSecond="${controller.bpsRead}"/> <ThrougputLabel styleClass="label-large" idleFormat="%stats.read.throughput.idle" kibsFormat="%stats.read.throughput.kibs" mibsFormat="%stats.read.throughput.mibs" bytesPerSecond="${controller.bpsRead}"/>
<AreaChart fx:id="readChart" styleClass="io-stats" createSymbols="false" animated="false"> <SmoothAreaChart fx:id="readChart" styleClass="io-stats" createSymbols="false" animated="false">
<xAxis> <xAxis>
<NumberAxis fx:id="readChartXAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="false" side="BOTTOM"/> <NumberAxis fx:id="readChartXAxis" styleClass="io-stats" autoRanging="false" forceZeroInRange="false" side="BOTTOM"/>
</xAxis> </xAxis>
@@ -48,7 +49,7 @@
<cursor> <cursor>
<Cursor fx:constant="DEFAULT"/> <Cursor fx:constant="DEFAULT"/>
</cursor> </cursor>
</AreaChart> </SmoothAreaChart>
<DataLabel byteFormat="%stats.read.total.data.none" kibFormat="%stats.read.total.data.kib" mibFormat="%stats.read.total.data.mib" gibFormat="%stats.read.total.data.gib" dataInBytes="${controller.totalBytesRead}"/> <DataLabel byteFormat="%stats.read.total.data.none" kibFormat="%stats.read.total.data.kib" mibFormat="%stats.read.total.data.mib" gibFormat="%stats.read.total.data.gib" dataInBytes="${controller.totalBytesRead}"/>
<DataLabel byteFormat="%stats.decr.total.data.none" kibFormat="%stats.decr.total.data.kib" mibFormat="%stats.decr.total.data.mib" gibFormat="%stats.decr.total.data.gib" dataInBytes="${controller.totalBytesDecrypted}"/> <DataLabel byteFormat="%stats.decr.total.data.none" kibFormat="%stats.decr.total.data.kib" mibFormat="%stats.decr.total.data.mib" gibFormat="%stats.decr.total.data.gib" dataInBytes="${controller.totalBytesDecrypted}"/>
<FormattedLabel format="%stats.read.accessCount" arg1="${controller.filesRead}"/> <FormattedLabel format="%stats.read.accessCount" arg1="${controller.filesRead}"/>
@@ -14,7 +14,7 @@
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox xmlns:fx="http://javafx.com/fxml" <VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.PassphraseEntryController" fx:controller="org.cryptomator.ui.unlock.UnlockController"
minWidth="400" minWidth="400"
maxWidth="400" maxWidth="400"
minHeight="145" minHeight="145"
@@ -37,10 +37,10 @@
<Region VBox.vgrow="ALWAYS"/> <Region VBox.vgrow="ALWAYS"/>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS"> <VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="U+C"> <ButtonBar buttonMinWidth="120" buttonOrder="B+U">
<buttons> <buttons>
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
<Region ButtonBar.buttonData="OTHER"/> <Region ButtonBar.buttonData="OTHER"/>
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
</buttons> </buttons>
</ButtonBar> </ButtonBar>
</VBox> </VBox>
@@ -1,42 +0,0 @@
<?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,8 +2,10 @@
<?import org.cryptomator.ui.controls.FontAwesome5IconView?> <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.scene.control.Button?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.Label?> <?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?> <?import javafx.scene.control.ListView?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.Region?> <?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?> <?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
@@ -13,9 +15,13 @@
fx:controller="org.cryptomator.ui.mainwindow.VaultListController" fx:controller="org.cryptomator.ui.mainwindow.VaultListController"
minWidth="206"> minWidth="206">
<StackPane VBox.vgrow="ALWAYS"> <StackPane VBox.vgrow="ALWAYS">
<ListView fx:id="vaultList" editable="true" fixedCellSize="60"> <ListView fx:id="vaultList" editable="true">
<contextMenu> <contextMenu>
<fx:include source="vault_list_contextmenu.fxml"/> <ContextMenu>
<items>
<MenuItem text="%main.vaultlist.contextMenu.remove" onAction="#didClickRemoveVault" disable="${controller.noVaultSelected}"/>
</items>
</ContextMenu>
</contextMenu> </contextMenu>
</ListView> </ListView>
<VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER"> <VBox visible="${controller.emptyVaultList}" spacing="6" alignment="CENTER">
@@ -13,7 +13,6 @@
prefWidth="200" prefWidth="200"
spacing="12" spacing="12"
alignment="CENTER_LEFT"> alignment="CENTER_LEFT">
<!-- Remark Check the containing list view for a fixed cell size before editing height properties -->
<padding> <padding>
<Insets topRightBottomLeft="12"/> <Insets topRightBottomLeft="12"/>
</padding> </padding>
@@ -1,16 +0,0 @@
<?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>
@@ -45,10 +45,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Custom Location addvaultwizard.new.directoryPickerLabel=Custom Location
addvaultwizard.new.directoryPickerButton=Choose… addvaultwizard.new.directoryPickerButton=Choose…
addvaultwizard.new.directoryPickerTitle=Select Directory addvaultwizard.new.directoryPickerTitle=Select Directory
addvaultwizard.new.fileAlreadyExists=A file or directory with the vault name already exists addvaultwizard.new.fileAlreadyExists=Vault can not be created at this path because some object already exists.
addvaultwizard.new.locationDoesNotExist=A directory in the specified path does not exist or cannot be accessed addvaultwizard.new.locationDoesNotExist=Vault can not be created at this path because at least one path component does not exist.
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. addvaultwizard.new.invalidName=Invalid vault name. Please consider a regular directory name.
### Password ### Password
addvaultwizard.new.createVaultBtn=Create Vault addvaultwizard.new.createVaultBtn=Create Vault
@@ -100,13 +98,10 @@ unlock.title=Unlock Vault
unlock.passwordPrompt=Enter password for "%s": unlock.passwordPrompt=Enter password for "%s":
unlock.savePassword=Remember Password unlock.savePassword=Remember Password
unlock.unlockBtn=Unlock 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 ## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible via its virtual drive. unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.rememberChoice=Remember choice, don't show this again unlock.success.rememberChoice=Remember choice, don't show this again
unlock.success.revealBtn=Reveal Drive unlock.success.revealBtn=Reveal Vault
## Failure ## Failure
unlock.error.heading=Unable to unlock vault unlock.error.heading=Unable to unlock vault
### Invalid Mount Point ### Invalid Mount Point
@@ -180,14 +175,11 @@ preferences.updates.currentVersion=Current Version: %s
preferences.updates.autoUpdateCheck=Check for updates automatically preferences.updates.autoUpdateCheck=Check for updates automatically
preferences.updates.checkNowBtn=Check Now preferences.updates.checkNowBtn=Check Now
preferences.updates.updateAvailable=Update to version %s available. preferences.updates.updateAvailable=Update to version %s available.
## Contribution ## Donation Key
preferences.contribute=Support Us preferences.donationKey=Donation
preferences.contribute.registeredFor=Supporter certificate registered for %s preferences.donationKey.registeredFor=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.donationKey.noDonationKey=No valid donation key found. 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.donationKey.getDonationKey=Get a donation key
preferences.contribute.promptText=Paste supporter certificate code here
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=About preferences.about=About
@@ -232,12 +224,7 @@ main.dropZone.dropVault=Add this vault
main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window main.dropZone.unknownDragboardContent=If you want to add a vault, drag it to this window
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault main.vaultlist.emptyList.onboardingInstruction=Click here to add a vault
main.vaultlist.contextMenu.remove=Remove… main.vaultlist.contextMenu.remove=Remove Vault
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 main.vaultlist.addVaultBtn=Add Vault
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -17,7 +17,7 @@ generic.error.title=حدث خطأ غير متوقع
generic.error.instruction=ما كان ينبغي أن يحدث هذا. يرجى الإبلاغ عن نص الخطأ أدناه وإدراج وصف للخطوات التي أدت إلى هذا الخطأ. generic.error.instruction=ما كان ينبغي أن يحدث هذا. يرجى الإبلاغ عن نص الخطأ أدناه وإدراج وصف للخطوات التي أدت إلى هذا الخطأ.
# Defaults # Defaults
defaults.vault.vaultName=مخزن defaults.vault.vaultName=خزنة
# Tray Menu # Tray Menu
traymenu.showMainWindow=اظهار traymenu.showMainWindow=اظهار
@@ -38,19 +38,17 @@ addvaultwizard.welcome.existingButton=افتح مخزن موجود
addvaultwizard.new.nameInstruction=اختر اسم للمخزن addvaultwizard.new.nameInstruction=اختر اسم للمخزن
addvaultwizard.new.namePrompt=اسم المخزن addvaultwizard.new.namePrompt=اسم المخزن
### Location ### Location
addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة للمخزن الخاص بك؟ addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة من قبو الخاص بك؟
addvaultwizard.new.locationLabel=موقع التخزين addvaultwizard.new.locationLabel=موقع التخزين
addvaultwizard.new.locationPrompt= addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=موقع مخصص addvaultwizard.new.directoryPickerLabel=موقع مخصص
addvaultwizard.new.directoryPickerButton=اختر… addvaultwizard.new.directoryPickerButton=اختر…
addvaultwizard.new.directoryPickerTitle=اختر القاموس addvaultwizard.new.directoryPickerTitle=اختر القاموس
addvaultwizard.new.fileAlreadyExists=ملف أو مجلد بنفس اسم المخزن موجود مسبقاً addvaultwizard.new.fileAlreadyExists=لا يمكن إنشاء مخزن على هذا المسار لأن بعض الملفات موجودة مسبقا.
addvaultwizard.new.locationDoesNotExist=المجلد في المسار المحدد غير موجود أو لا يمكن الوصول إليه addvaultwizard.new.locationDoesNotExist=لا يمكن إنشاء المخزن على هذا المسار لأن جزء من هذا المسار غير موجود.
addvaultwizard.new.locationIsNotWritable=لا يوجد صلاحيات للكتابة على المسار المحدد addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى النظر في اسم الدليل المعتاد.
addvaultwizard.new.locationIsOk=الموقع المناسب للمخزن الخاص بك
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى إعادة تسمية المجلد بشكل عادي.
### Password ### Password
addvaultwizard.new.createVaultBtn=إنشاء المخزن addvaultwizard.new.createVaultBtn=انشئ حافظة
addvaultwizard.new.generateRecoveryKeyChoice=لن تتمكن من الوصول إلى بياناتك بدون كلمة المرور الخاصة بك. هل تريد مفتاح استرداد في حالة فقدان كلمة المرور الخاصة بك؟ addvaultwizard.new.generateRecoveryKeyChoice=لن تتمكن من الوصول إلى بياناتك بدون كلمة المرور الخاصة بك. هل تريد مفتاح استرداد في حالة فقدان كلمة المرور الخاصة بك؟
addvaultwizard.new.generateRecoveryKeyChoice.yes=نعم من فضلك، أن تكون آمناً أفضل من الندم addvaultwizard.new.generateRecoveryKeyChoice.yes=نعم من فضلك، أن تكون آمناً أفضل من الندم
addvaultwizard.new.generateRecoveryKeyChoice.no=لا شكراً، لن أفقد كلمة المرور الخاصة بي addvaultwizard.new.generateRecoveryKeyChoice.no=لا شكراً، لن أفقد كلمة المرور الخاصة بي
@@ -97,14 +95,11 @@ forgetPassword.confirmBtn=نسيت كلمة المرور
# Unlock # Unlock
unlock.title=افتح الحافظة unlock.title=افتح الحافظة
unlock.passwordPrompt=‮أدخل كلمة السر لـ "%s": unlock.passwordPrompt=‮أدخل كلمة السر لـ "%s":
unlock.savePassword=تذكر كلمة المرور
unlock.unlockBtn=افتح unlock.unlockBtn=افتح
##
unlock.chooseMasterkey.filePickerTitle=اختر ملف الـ Masterkey
## Success ## Success
unlock.success.message=تم فتح "%s" بنجاح! يمكنك الآن الوصول لمخزنك عن طريق القرص الافتراضي الخاص به. unlock.success.message=تم فتح المخزن "%s" بنجاح! يمكنك الوصول إليه الآن.
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
unlock.success.revealBtn=اظهار القرص unlock.success.revealBtn=افتح الحافظة
## Failure ## Failure
unlock.error.heading=غير قادر على فتح الخزنة unlock.error.heading=غير قادر على فتح الخزنة
### Invalid Mount Point ### Invalid Mount Point
@@ -113,12 +108,9 @@ unlock.error.invalidMountPoint.existing=نقطة/مجلد التحميل موج
# Lock # Lock
## Force ## Force
lock.forced.heading=فشل عملية القفل
lock.forced.message=تم حظر قفل "%s" بواسطة العمليات المعلقة أو الملفات المفتوحة. يمكنك فرض قفل هذا المخزن، ولكن مقاطعة عمليات الادخال والاخراج I/O قد تؤدي لفقدان البيانات غير المحفوظة.
lock.forced.confirmBtn=فرض القفل lock.forced.confirmBtn=فرض القفل
## Failure ## Failure
lock.fail.heading=فشلت عملية اقفال الخزنة. lock.fail.heading=فشلت عملية اقفال الخزنة.
lock.fail.message=فشل عملية قفل %s". تأكد من حفظ العمل غير المحفوظ في مكان آخر وأن العمليات الهامة للقراءة/الكتابة قد انتهت. من أجل إغلاق المخزن، اقتل تطبيق Cryptomator.
# Migration # Migration
migration.title=ترقية الحافظة migration.title=ترقية الحافظة
@@ -153,8 +145,6 @@ preferences.general.theme.automatic=تلقائي
preferences.general.theme.light=فاتح (أبيض) preferences.general.theme.light=فاتح (أبيض)
preferences.general.theme.dark=مظلم (أسود) preferences.general.theme.dark=مظلم (أسود)
preferences.general.unlockThemes=تفعيل الوضع المظلم preferences.general.unlockThemes=تفعيل الوضع المظلم
preferences.general.showMinimizeButton=إظهار زر التصغير
preferences.general.showTrayIcon=إظهار أيقونة بجنب الساعة (يتطلب إعادة تشغيل)
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
preferences.general.debugLogging=تمكين سجلات التصحيح preferences.general.debugLogging=تمكين سجلات التصحيح
preferences.general.debugDirectory=عرض ملفات السجل preferences.general.debugDirectory=عرض ملفات السجل
@@ -178,19 +168,15 @@ preferences.updates.currentVersion=الإصدار الحالي: %s
preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوماتيكيا preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوماتيكيا
preferences.updates.checkNowBtn=تحقق الان preferences.updates.checkNowBtn=تحقق الان
preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح. preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح.
## Contribution ## Donation Key
preferences.contribute=ادعمنا preferences.donationKey=التبرعات
preferences.contribute.registeredFor=شهادة الداعم مسجلة لـ %s preferences.donationKey.registeredFor=مسجل لـ %s
preferences.contribute.noCertificate=ادعم Cryptomator واحصل على شهادة الداعم. إنها مثل مفتاح الترخيص لكن للأشخاص الرائعين الذين يستخدمون البرامج المجانية ؛-) preferences.donationKey.noDonationKey=لم يتم العثور على مفتاح تبرع صحيح. إنه مثل مفتاح الترخيص إلا أنه للأشخاص الرائعين الذين يستخدمون البرمجيات المجانية؛-)
preferences.contribute.getCertificate=ليس لديك واحدة بعد؟ تعلم كيف يمكنك الحصول عليها. preferences.donationKey.getDonationKey=الحصول على مفتاح تبرع
preferences.contribute.promptText=قم بلصق رمز شهادة الداعم هنا
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=حول البرنامج preferences.about=حول البرنامج
# Vault Statistics # Vault Statistics
stats.title=إحصائيات عن %s
stats.cacheHitRate=معدل استخدام الكاش stats.cacheHitRate=معدل استخدام الكاش
## Read ## Read
stats.read.throughput.idle=قراءة: خامل stats.read.throughput.idle=قراءة: خامل
@@ -209,12 +195,7 @@ main.dropZone.dropVault=أضف هذا المخزن
main.dropZone.unknownDragboardContent=إذا كنت ترغب في إضافة مخزن، قم بسحبه إلى هذه النافذة main.dropZone.unknownDragboardContent=إذا كنت ترغب في إضافة مخزن، قم بسحبه إلى هذه النافذة
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=انقر هنا لإضافة خزنة 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=أضِف مخزنًا main.vaultlist.addVaultBtn=أضِف مخزنًا
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Prilagođena lokacija addvaultwizard.new.directoryPickerLabel=Prilagođena lokacija
addvaultwizard.new.directoryPickerButton=Odaberi… addvaultwizard.new.directoryPickerButton=Odaberi…
addvaultwizard.new.directoryPickerTitle=Izaberi folder 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. addvaultwizard.new.invalidName=Naziv sefa neispravan. Molimo razmislite o drugom nazivu.
### Password ### Password
addvaultwizard.new.createVaultBtn=Kreiraj novi sef addvaultwizard.new.createVaultBtn=Kreiraj novi sef
@@ -93,14 +95,11 @@ forgetPassword.confirmBtn=Zaboravili ste šifru
# Unlock # Unlock
unlock.title=Otključaj sef unlock.title=Otključaj sef
unlock.passwordPrompt=Unesite lozinku za "%s": unlock.passwordPrompt=Unesite lozinku za "%s":
unlock.savePassword=Zapamti šifru
unlock.unlockBtn=Otključaj unlock.unlockBtn=Otključaj
##
unlock.chooseMasterkey.filePickerTitle=Odaberite Masterkey Datoteku
## Success ## Success
unlock.success.message=Uspješno ste otključali "%s"! Vaš sef je sada dostupan putem svog virtualnog diska. unlock.success.message=Uspješno ste otključali "%s"! Vaš sef je sada dostupan.
unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo
unlock.success.revealBtn=Otkrij pogon unlock.success.revealBtn=Otkrij sef
## Failure ## Failure
unlock.error.heading=Sef nije moguće otključati unlock.error.heading=Sef nije moguće otključati
### Invalid Mount Point ### Invalid Mount Point
@@ -174,9 +173,11 @@ preferences.updates.currentVersion=Trenutna verzija: %s
preferences.updates.autoUpdateCheck=Automatski provjeri ima li ažuriranja preferences.updates.autoUpdateCheck=Automatski provjeri ima li ažuriranja
preferences.updates.checkNowBtn=Provjeri sada preferences.updates.checkNowBtn=Provjeri sada
preferences.updates.updateAvailable=Dostupno ažuriranje na verziju %s. preferences.updates.updateAvailable=Dostupno ažuriranje na verziju %s.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> 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
## About ## About
preferences.about=O programu preferences.about=O programu
@@ -221,12 +222,7 @@ main.dropZone.dropVault=Dodajte ovaj sef
main.dropZone.unknownDragboardContent=Ako želite dodati sef, povucite ga u ovaj prozor main.dropZone.unknownDragboardContent=Ako želite dodati sef, povucite ga u ovaj prozor
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Kliknite ovdje da dodate sef main.vaultlist.emptyList.onboardingInstruction=Kliknite ovdje da dodate sef
main.vaultlist.contextMenu.remove=Ukloni… main.vaultlist.contextMenu.remove=Ukloni Sef
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 main.vaultlist.addVaultBtn=Dodaj sef
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Ubicació personalitzada addvaultwizard.new.directoryPickerLabel=Ubicació personalitzada
addvaultwizard.new.directoryPickerButton=Trieu… addvaultwizard.new.directoryPickerButton=Trieu…
addvaultwizard.new.directoryPickerTitle=Seleccioneu el directori 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. 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 ### Password
addvaultwizard.new.createVaultBtn=Crea la caixa forta addvaultwizard.new.createVaultBtn=Crea la caixa forta
@@ -95,12 +97,10 @@ unlock.title=Desbloquejar la caixa forta
unlock.passwordPrompt=Introduïu la contrasenya de "%s": unlock.passwordPrompt=Introduïu la contrasenya de "%s":
unlock.savePassword=Recorda la contrasenya unlock.savePassword=Recorda la contrasenya
unlock.unlockBtn=Desbloqueja unlock.unlockBtn=Desbloqueja
##
unlock.chooseMasterkey.filePickerTitle=Seleccioneu el fitxer Masterkey
## Success ## Success
unlock.success.message=S'ha desblocat %s correctament! Podeu accedir a la vostra caixa forta a través de la unitat virtual. unlock.success.message="%s" s'ha desbloquejat correctament! Ja es pot accedir a la caixa forta.
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar. unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
unlock.success.revealBtn=Mostra la unitat unlock.success.revealBtn=Mostra la caixa forta
## Failure ## Failure
unlock.error.heading=No ha estat possible desblocar la caixa forta unlock.error.heading=No ha estat possible desblocar la caixa forta
### Invalid Mount Point ### Invalid Mount Point
@@ -174,14 +174,11 @@ preferences.updates.currentVersion=Versió actual: %s
preferences.updates.autoUpdateCheck=Comprova automàticament si hi ha actualitzacions preferences.updates.autoUpdateCheck=Comprova automàticament si hi ha actualitzacions
preferences.updates.checkNowBtn=Comprova-ho ara preferences.updates.checkNowBtn=Comprova-ho ara
preferences.updates.updateAvailable=L'actualització a la versió %s es troba disponible. preferences.updates.updateAvailable=L'actualització a la versió %s es troba disponible.
## Contribution ## Donation Key
preferences.contribute=Doneu-nos suport preferences.donationKey=Donacions
preferences.contribute.registeredFor=Certificat de col·laborador registrat per a %s preferences.donationKey.registeredFor=Registrat per %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.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.contribute.getCertificate=Encara no en teniu cap? Sapigueu com aconseguir-ne un. preferences.donationKey.getDonationKey=Obtingau una clau de donació
preferences.contribute.promptText=Enganxeu aquí el certificat de col·laborador
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=Quant a preferences.about=Quant a
@@ -226,12 +223,7 @@ main.dropZone.dropVault=Afegeix aquesta caixa forta
main.dropZone.unknownDragboardContent=Si voleu afegir una caixa forta, arrossegueu-la a aquesta finestra main.dropZone.unknownDragboardContent=Si voleu afegir una caixa forta, arrossegueu-la a aquesta finestra
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Feu clic aquí per afegir una caixa forta main.vaultlist.emptyList.onboardingInstruction=Feu clic aquí per afegir una caixa forta
main.vaultlist.contextMenu.remove=Elimina… main.vaultlist.contextMenu.remove=Elimina la caixa forta
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 main.vaultlist.addVaultBtn=Afegir una caixa forta
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=...
addvaultwizard.new.directoryPickerLabel=Vlastní umístění addvaultwizard.new.directoryPickerLabel=Vlastní umístění
addvaultwizard.new.directoryPickerButton=Vybrat... addvaultwizard.new.directoryPickerButton=Vybrat...
addvaultwizard.new.directoryPickerTitle=Vyberte adresář addvaultwizard.new.directoryPickerTitle=Vyberte adresář
addvaultwizard.new.fileAlreadyExists=Soubor nebo složka s tímto názvem trezoru již existuje addvaultwizard.new.fileAlreadyExists=Trezor nemůže být vytvořen na tomto umístění, protože stejný objekt již existuje.
addvaultwizard.new.locationDoesNotExist=Složka v zadané cestě neexistuje nebo není přístupná 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.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. addvaultwizard.new.invalidName=Neplatné jméno trezoru. Prosím změňte jméno adresáře.
### Password ### Password
addvaultwizard.new.createVaultBtn=Vytvořit trezor addvaultwizard.new.createVaultBtn=Vytvořit trezor
@@ -99,12 +97,10 @@ unlock.title=Odemknout trezor
unlock.passwordPrompt=Zadejte heslo pro "%s": unlock.passwordPrompt=Zadejte heslo pro "%s":
unlock.savePassword=Zapamatovat heslo unlock.savePassword=Zapamatovat heslo
unlock.unlockBtn=Odemknout unlock.unlockBtn=Odemknout
##
unlock.chooseMasterkey.filePickerTitle=Vyberte Masterkey soubor
## Success ## Success
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný jako virtuální jednotka. unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný.
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
unlock.success.revealBtn=Zobrazit jednotku unlock.success.revealBtn=Zobrazit trezor
## Failure ## Failure
unlock.error.heading=Nelze odemknout trezor unlock.error.heading=Nelze odemknout trezor
### Invalid Mount Point ### Invalid Mount Point
@@ -113,12 +109,7 @@ unlock.error.invalidMountPoint.existing=Připojovací bod %s již existuje nebo
# Lock # Lock
## Force ## 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 ## 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
migration.title=Upgrade trezoru migration.title=Upgrade trezoru
@@ -178,14 +169,11 @@ preferences.updates.currentVersion=Aktuální verze: %s
preferences.updates.autoUpdateCheck=Automaticky kontrolovat aktualizace preferences.updates.autoUpdateCheck=Automaticky kontrolovat aktualizace
preferences.updates.checkNowBtn=Zkontrolovat nyní preferences.updates.checkNowBtn=Zkontrolovat nyní
preferences.updates.updateAvailable=Dostupná nová verze %s. preferences.updates.updateAvailable=Dostupná nová verze %s.
## Contribution ## Donation Key
preferences.contribute=Podpořte nás preferences.donationKey=Příspěvek
preferences.contribute.registeredFor=Certifikát podporovatele byl registrován pro %s preferences.donationKey.registeredFor=Registrováno na: %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.donationKey.noDonationKey=Nebyl nalezen žádný platný darovací klíč. Je to jako licenční klíč, ale pro úžasné lidi využívající software zadarmo. ;-)
preferences.contribute.getCertificate=Ještě žádný nemáte? Naučte se, jak ho můžete získat. preferences.donationKey.getDonationKey=Získat darovací klíč
preferences.contribute.promptText=Sem vložte kód certifikátu podporovatele
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=O aplikaci preferences.about=O aplikaci
@@ -230,12 +218,7 @@ main.dropZone.dropVault=Přidat tento trezor
main.dropZone.unknownDragboardContent=Pokud chcete přidat trezor, přetáhněte jej do tohoto okna main.dropZone.unknownDragboardContent=Pokud chcete přidat trezor, přetáhněte jej do tohoto okna
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klikněte zde pro přidání nového trezoru main.vaultlist.emptyList.onboardingInstruction=Klikněte zde pro přidání nového trezoru
main.vaultlist.contextMenu.remove=Odstranit… main.vaultlist.contextMenu.remove=Odstranit trezor
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 main.vaultlist.addVaultBtn=Přidat trezor
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -14,7 +14,7 @@ generic.button.next=Weiter
generic.button.print=Drucken generic.button.print=Drucken
## Error ## Error
generic.error.title=Ein unerwarteter Fehler ist aufgetreten generic.error.title=Ein unerwarteter Fehler ist aufgetreten
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. generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde die folgende Fehlermeldung und beschreibe kurz, durch welche Schritte er aufgetreten ist.
# Defaults # Defaults
defaults.vault.vaultName=Tresor defaults.vault.vaultName=Tresor
@@ -38,16 +38,14 @@ addvaultwizard.welcome.existingButton=Existierenden Tresor öffnen
addvaultwizard.new.nameInstruction=Wähle einen Namen für den Tresor addvaultwizard.new.nameInstruction=Wähle einen Namen für den Tresor
addvaultwizard.new.namePrompt=Tresorname addvaultwizard.new.namePrompt=Tresorname
### Location ### Location
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Dateien deines Tresors ablegen? addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Daten deines Tresors ablegen?
addvaultwizard.new.locationLabel=Speicherort addvaultwizard.new.locationLabel=Speicherort
addvaultwizard.new.locationPrompt= addvaultwizard.new.locationPrompt=
addvaultwizard.new.directoryPickerLabel=Eigener Ort addvaultwizard.new.directoryPickerLabel=Eigener Ort
addvaultwizard.new.directoryPickerButton=Durchsuchen … addvaultwizard.new.directoryPickerButton=Durchsuchen …
addvaultwizard.new.directoryPickerTitle=Verzeichnis auswählen addvaultwizard.new.directoryPickerTitle=Verzeichnis auswählen
addvaultwizard.new.fileAlreadyExists=Eine Datei oder ein Ordner mit diesem Namen ist bereits vorhanden addvaultwizard.new.fileAlreadyExists=Der Tresor konnte nicht erstellt werden, da am Speicherort so eine Datei bereits existiert.
addvaultwizard.new.locationDoesNotExist=Der Ordner im angegebenen Pfad existiert nicht oder kann nicht geöffnet werden addvaultwizard.new.locationDoesNotExist=Der Tresor kann nicht erstellt werden, da mindestens ein Speicherort nicht existiert.
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. addvaultwizard.new.invalidName=Ungültiger Tresorname. Bitte wähle einen regulären Namen, mit dem auch Verzeichnisse erstellt werden können.
### Password ### Password
addvaultwizard.new.createVaultBtn=Tresor erstellen addvaultwizard.new.createVaultBtn=Tresor erstellen
@@ -81,7 +79,7 @@ addvaultwizard.success.unlockNow=Jetzt entsperren
# Remove Vault # Remove Vault
removeVault.title=Tresor entfernen removeVault.title=Tresor entfernen
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.information=Dein Tresor wird nicht gelöscht, sondern lediglich aus Cryptomators Tresorliste entfernt. Du kannst ihn daher später jederzeit wieder hinzufügen.
removeVault.confirmBtn=Tresor entfernen removeVault.confirmBtn=Tresor entfernen
# Change Password # Change Password
@@ -99,12 +97,10 @@ unlock.title=Tresor entsperren
unlock.passwordPrompt=Gib das Passwort für „%s“ ein: unlock.passwordPrompt=Gib das Passwort für „%s“ ein:
unlock.savePassword=Passwort merken unlock.savePassword=Passwort merken
unlock.unlockBtn=Entsperren unlock.unlockBtn=Entsperren
##
unlock.chooseMasterkey.filePickerTitle=Masterkey-Datei auswählen
## Success ## Success
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du über das virtuelle Laufwerk auf deinen Tresor zugreifen. unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du auf deinen Tresor zugreifen.
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
unlock.success.revealBtn=Laufwerk anzeigen unlock.success.revealBtn=Tresor anzeigen
## Failure ## Failure
unlock.error.heading=Tresor konnte nicht entsperrt werden unlock.error.heading=Tresor konnte nicht entsperrt werden
### Invalid Mount Point ### Invalid Mount Point
@@ -176,16 +172,13 @@ preferences.volume.webdav.scheme=WebDAV-Schema
preferences.updates=Updates preferences.updates=Updates
preferences.updates.currentVersion=Aktuelle Version: %s preferences.updates.currentVersion=Aktuelle Version: %s
preferences.updates.autoUpdateCheck=Automatisch nach Updates suchen preferences.updates.autoUpdateCheck=Automatisch nach Updates suchen
preferences.updates.checkNowBtn=Jetzt prüfen preferences.updates.checkNowBtn=Jetzt suchen
preferences.updates.updateAvailable=Update auf Version %s verfügbar. preferences.updates.updateAvailable=Update auf Version %s verfügbar.
## Contribution ## Donation Key
preferences.contribute=Unterstütze uns preferences.donationKey=Spende
preferences.contribute.registeredFor=Supporter-Zertifikat registriert für %s preferences.donationKey.registeredFor=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.donationKey.noDonationKey=Kein gültiger Spendenschlüssel gefunden. Er ist wie ein Lizenzschlüssel, aber für tolle Leute, die freie Software verwenden. ;-)
preferences.contribute.getCertificate=Du hast noch keines? Erfahre, wie du es erhalten kannst. preferences.donationKey.getDonationKey=Hol dir einen Spendenschlüssel
preferences.contribute.promptText=Code des Supporter-Zertifikats hier einfügen
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=Über preferences.about=Über
@@ -230,12 +223,7 @@ main.dropZone.dropVault=Diesen Tresor hinzufügen
main.dropZone.unknownDragboardContent=Wenn Sie einen Tresor hinzufügen möchten, ziehen Sie ihn in dieses Fenster main.dropZone.unknownDragboardContent=Wenn Sie einen Tresor hinzufügen möchten, ziehen Sie ihn in dieses Fenster
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klicke hier, um einen Tresor hinzuzufügen main.vaultlist.emptyList.onboardingInstruction=Klicke hier, um einen Tresor hinzuzufügen
main.vaultlist.contextMenu.remove=Entfernen main.vaultlist.contextMenu.remove=Tresor 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 main.vaultlist.addVaultBtn=Tresor hinzufügen
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -258,7 +246,7 @@ main.vaultDetail.throughput.kbps=%.1f kiB/s
main.vaultDetail.throughput.mbps=%.1f MiB/s main.vaultDetail.throughput.mbps=%.1f MiB/s
main.vaultDetail.stats=Tresorstatistik main.vaultDetail.stats=Tresorstatistik
### Missing ### Missing
main.vaultDetail.missing.info=Cryptomator konnte keinen Tresor mit diesem Pfad finden. main.vaultDetail.missing.info=Mit diesem Pfad konnte Cryptomator keinen Tresor finden.
main.vaultDetail.missing.recheck=Erneut prüfen main.vaultDetail.missing.recheck=Erneut prüfen
main.vaultDetail.missing.remove=Aus Tresorliste entfernen… main.vaultDetail.missing.remove=Aus Tresorliste entfernen…
main.vaultDetail.missing.changeLocation=Speicherort des Tresors ändern… main.vaultDetail.missing.changeLocation=Speicherort des Tresors ändern…
@@ -273,8 +261,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.0=Folge diesen Schritten, um Dateien zu verschlüsseln:
wrongFileAlert.instruction.1=1. Entsperre deinen Tresor. wrongFileAlert.instruction.1=1. Entsperre deinen Tresor.
wrongFileAlert.instruction.2=2. Klicke auf „Anzeigen“, um das Laufwerk in deinem Dateimanager zu öffnen. wrongFileAlert.instruction.2=2. Klicke auf „Anzeigen“, um das Laufwerk in deinem Dateimanager zu öffnen.
wrongFileAlert.instruction.3=3. Füge deine Dateien zu diesem Laufwerk hinzu. wrongFileAlert.instruction.3=3. Füge deine Dateien diesem Laufwerk hinzu.
wrongFileAlert.link=Besuche für weitere Hilfe wrongFileAlert.link=Für weitere Unterstützung besuche
# Vault Options # Vault Options
## General ## General
@@ -288,26 +276,26 @@ vaultOptions.general.actionAfterUnlock.ask=Nachfragen
## Mount ## Mount
vaultOptions.mount=Laufwerk vaultOptions.mount=Laufwerk
vaultOptions.mount.readonly=Schreibgeschützt vaultOptions.mount.readonly=Schreibgeschützt
vaultOptions.mount.customMountFlags=Benutzerdefinierte Einhänge-Optionen vaultOptions.mount.customMountFlags=Benutzerdefinierte Mount-Flags
vaultOptions.mount.winDriveLetterOccupied=belegt vaultOptions.mount.winDriveLetterOccupied=belegt
vaultOptions.mount.mountPoint=Einhängepunkt vaultOptions.mount.mountPoint=Einhängepunkt
vaultOptions.mount.mountPoint.auto=Wähle automatisch einen geeigneten Ort aus vaultOptions.mount.mountPoint.auto=Automatisch einen passenden Ort auswählen
vaultOptions.mount.mountPoint.driveLetter=Laufwerksbuchstaben zuweisen vaultOptions.mount.mountPoint.driveLetter=Zugewiesenen Laufwerksbuchstaben verwenden
vaultOptions.mount.mountPoint.custom=Eigener Pfad vaultOptions.mount.mountPoint.custom=Pfad selbst wählen
vaultOptions.mount.mountPoint.directoryPickerButton=Durchsuchen … vaultOptions.mount.mountPoint.directoryPickerButton=Durchsuchen …
vaultOptions.mount.mountPoint.directoryPickerTitle=Wähle ein leeres Verzeichnis vaultOptions.mount.mountPoint.directoryPickerTitle=Wähle ein leeres Verzeichnis
## Master Key ## Master Key
vaultOptions.masterkey=Passwort vaultOptions.masterkey=Passwort
vaultOptions.masterkey.changePasswordBtn=Passwort ändern vaultOptions.masterkey.changePasswordBtn=Passwort ändern
vaultOptions.masterkey.forgetSavedPasswordBtn=Gespeichertes Passwort vergessen vaultOptions.masterkey.forgetSavedPasswordBtn=Gespeichertes Passwort vergessen
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.recoveryKeyExpanation=Bei Verlust deines Passworts ist ein Wiederherstellungsschlüssel deine einzige Möglichkeit, den Zugriff auf einen Tresor wiederherzustellen.
vaultOptions.masterkey.showRecoveryKeyBtn=Wiederherstellungsschlüssel anzeigen vaultOptions.masterkey.showRecoveryKeyBtn=Wiederherstellungsschlüssel anzeigen
vaultOptions.masterkey.recoverPasswordBtn=Passwort wiederherstellen vaultOptions.masterkey.recoverPasswordBtn=Passwort wiederherstellen
# Recovery Key # Recovery Key
recoveryKey.title=Wiederherstellungsschlüssel recoveryKey.title=Wiederherstellungsschlüssel
recoveryKey.enterPassword.prompt=Gib dein Passwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen: recoveryKey.enterPassword.prompt=Gib dein Passwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen:
recoveryKey.display.message=Mit dem folgenden Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen: recoveryKey.display.message=Mit folgendem 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.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.prompt=Gib deinen Wiederherstellungsschlüssel für „%s“ ein:
recoveryKey.recover.validKey=Dies ist ein gültiger Wiederherstellungsschlüssel recoveryKey.recover.validKey=Dies ist ein gültiger Wiederherstellungsschlüssel
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Προσαρμοσμένη τοποθεσία addvaultwizard.new.directoryPickerLabel=Προσαρμοσμένη τοποθεσία
addvaultwizard.new.directoryPickerButton=Επιλογή… addvaultwizard.new.directoryPickerButton=Επιλογή…
addvaultwizard.new.directoryPickerTitle=Επιλογή φακέλου addvaultwizard.new.directoryPickerTitle=Επιλογή φακέλου
addvaultwizard.new.fileAlreadyExists=Ένα αρχείο ή φάκελος με το όνομα του vault υπάρχει ήδη addvaultwizard.new.fileAlreadyExists=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς κάποια αντικείμενα υπάρχουν ήδη.
addvaultwizard.new.locationDoesNotExist=Στην καθορισμένη διαδρομή δεν υπάρχει ή δεν μπορεί να προσπελαστεί ένας φάκελος addvaultwizard.new.locationDoesNotExist=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς τουλάχιστον ένα στοιχείο του μονοπατιού δεν υπάρχει.
addvaultwizard.new.locationIsNotWritable=Δεν υπάρχει πρόσβαση εγγραφής στην καθορισμένη διαδρομή
addvaultwizard.new.locationIsOk=Κατάλληλη τοποθεσία για το vault σας
addvaultwizard.new.invalidName=Λάθος όνομα vault. Παρακαλώ χρησιμοποιείστε ένα κανονικό όνομα φακέλου. addvaultwizard.new.invalidName=Λάθος όνομα vault. Παρακαλώ χρησιμοποιείστε ένα κανονικό όνομα φακέλου.
### Password ### Password
addvaultwizard.new.createVaultBtn=Δημιουργία Vault addvaultwizard.new.createVaultBtn=Δημιουργία Vault
@@ -99,12 +97,10 @@ unlock.title=Ξεκλείδωμα Vault
unlock.passwordPrompt=Εισάγετε τον κωδικό για "%s": unlock.passwordPrompt=Εισάγετε τον κωδικό για "%s":
unlock.savePassword=Απομνημόνευση κωδικού πρόσβασης unlock.savePassword=Απομνημόνευση κωδικού πρόσβασης
unlock.unlockBtn=Ξεκλείδωμα unlock.unlockBtn=Ξεκλείδωμα
##
unlock.chooseMasterkey.filePickerTitle=Επιλέξτε το αρχείο Masterkey
## Success ## Success
unlock.success.message=Ξεκλειδώθηκε "%s" επιτυχώς! Το vault σας είναι διαθέσιμο μέσω του εικονικού δίσκου του. unlock.success.message="%s" ξεκλειδώθηκε επιτυχώς! Το vault σας είναι διαθέσιμο.
unlock.success.rememberChoice=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά unlock.success.rememberChoice=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά
unlock.success.revealBtn=Αποκάλυψη εικονικού δίσκου unlock.success.revealBtn=Αποκάλυψη Vault
## Failure ## Failure
unlock.error.heading=Αδυναμία ξεκλειδώματος vault unlock.error.heading=Αδυναμία ξεκλειδώματος vault
### Invalid Mount Point ### Invalid Mount Point
@@ -178,14 +174,11 @@ preferences.updates.currentVersion=Τρέχουσα έκδοση: %s
preferences.updates.autoUpdateCheck=Αυτόματος έλεγχος για ενημερώσεις preferences.updates.autoUpdateCheck=Αυτόματος έλεγχος για ενημερώσεις
preferences.updates.checkNowBtn=Έλεγχος τώρα preferences.updates.checkNowBtn=Έλεγχος τώρα
preferences.updates.updateAvailable=Η ενημέρωση για την έκδοση %s είναι διαθέσιμη. preferences.updates.updateAvailable=Η ενημέρωση για την έκδοση %s είναι διαθέσιμη.
## Contribution ## Donation Key
preferences.contribute=Υποστηρίξτε μας preferences.donationKey=Δωρεά
preferences.contribute.registeredFor=Το πιστοποιητικό υποστήριξης καταχωρήθηκε για %s preferences.donationKey.registeredFor=Καταχωρημένο σε %s
preferences.contribute.noCertificate=Υποστηρίξτε το Cryptomator και λάβετε ένα πιστοποιητικό υποστηρικτή. Είναι σαν κλειδί άδειας χρήσης, αλλά για φοβερά άτομα που χρησιμοποιούν ελεύθερο λογισμικό. ;-) preferences.donationKey.noDonationKey=Δεν βρέθηκε ενεργό κλειδί δωρεάς. Είναι σαν κλειδί αγοράς αλλά για απίθανους ανθρώπους που χρησιμοποιούν δωρεάν λογισμικό. ;-)
preferences.contribute.getCertificate=Δεν έχετε ένα ήδη; Μάθετε πώς μπορείτε να το αποκτήσετε. preferences.donationKey.getDonationKey=Πάρε ένα κλειδί δωρεάς
preferences.contribute.promptText=Επικολλήστε τον κωδικό πιστοποιητικού υποστηρικτή εδώ
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=Σχετικά με preferences.about=Σχετικά με
@@ -230,12 +223,7 @@ main.dropZone.dropVault=Προσθήκη vault
main.dropZone.unknownDragboardContent=Αν θέλετε να προσθέσετε ένα vault, σύρετε το σε αυτό το παράθυρο main.dropZone.unknownDragboardContent=Αν θέλετε να προσθέσετε ένα vault, σύρετε το σε αυτό το παράθυρο
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Κάντε κλικ εδώ για να προσθέσετε ένα vault main.vaultlist.emptyList.onboardingInstruction=Κάντε κλικ εδώ για να προσθέσετε ένα vault
main.vaultlist.contextMenu.remove=Αφαίρεση main.vaultlist.contextMenu.remove=Διαγραφή Vault
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 main.vaultlist.addVaultBtn=Προσθήκη Vault
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Ubicación personalizada addvaultwizard.new.directoryPickerLabel=Ubicación personalizada
addvaultwizard.new.directoryPickerButton=Elegir… addvaultwizard.new.directoryPickerButton=Elegir…
addvaultwizard.new.directoryPickerTitle=Seleccionar directorio addvaultwizard.new.directoryPickerTitle=Seleccionar directorio
addvaultwizard.new.fileAlreadyExists=Ya existe un archivo o directorio con el nombre de la bóveda addvaultwizard.new.fileAlreadyExists=La bóveda no puede crearse en esta ruta porque ya existe un objeto.
addvaultwizard.new.locationDoesNotExist=No existe un directorio en la ruta especificada o no se puede acceder addvaultwizard.new.locationDoesNotExist=La bóveda no puede ser creada en esta ruta porque al menos un componente no existe.
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. addvaultwizard.new.invalidName=Nombre de bóveda inválido. Considere un nombre de directorio regular.
### Password ### Password
addvaultwizard.new.createVaultBtn=Crear bóveda addvaultwizard.new.createVaultBtn=Crear bóveda
@@ -99,12 +97,10 @@ unlock.title=Desbloquear bóveda
unlock.passwordPrompt=Ingresar contraseña para "%s": unlock.passwordPrompt=Ingresar contraseña para "%s":
unlock.savePassword=Recordar contraseña unlock.savePassword=Recordar contraseña
unlock.unlockBtn=Desbloquear unlock.unlockBtn=Desbloquear
##
unlock.chooseMasterkey.filePickerTitle=Seleccionar archivo Masterkey
## Success ## Success
unlock.success.message=¡Desbloqueo de "%s" exitoso! Su bóveda ahora es accesible a través de su unidad virtual. unlock.success.message=¡"%s" se desbloqueó con éxito! La bóveda ya es accesible.
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
unlock.success.revealBtn=Revelar unidad unlock.success.revealBtn=Revelar bóveda
## Failure ## Failure
unlock.error.heading=No se puede desbloquear la bóveda unlock.error.heading=No se puede desbloquear la bóveda
### Invalid Mount Point ### Invalid Mount Point
@@ -178,9 +174,11 @@ preferences.updates.currentVersion=Versión actual: %s
preferences.updates.autoUpdateCheck=Buscar actualizaciones automáticamente preferences.updates.autoUpdateCheck=Buscar actualizaciones automáticamente
preferences.updates.checkNowBtn=Buscar ahora preferences.updates.checkNowBtn=Buscar ahora
preferences.updates.updateAvailable=Actualización a la versión %s disponible. preferences.updates.updateAvailable=Actualización a la versión %s disponible.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> 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
## About ## About
preferences.about=Acerca de preferences.about=Acerca de
@@ -225,12 +223,7 @@ main.dropZone.dropVault=Añadir esta bóveda
main.dropZone.unknownDragboardContent=Si desea añadir una bóveda, arrástrela a esta ventana main.dropZone.unknownDragboardContent=Si desea añadir una bóveda, arrástrela a esta ventana
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Hacer clic aquí para añadir una bóveda main.vaultlist.emptyList.onboardingInstruction=Hacer clic aquí para añadir una bóveda
main.vaultlist.contextMenu.remove=Eliminar… main.vaultlist.contextMenu.remove=Eliminar bóveda
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 main.vaultlist.addVaultBtn=Añadir bóveda
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Emplacement personnalisé addvaultwizard.new.directoryPickerLabel=Emplacement personnalisé
addvaultwizard.new.directoryPickerButton=Choisir... addvaultwizard.new.directoryPickerButton=Choisir...
addvaultwizard.new.directoryPickerTitle=Choisissez le répertoire addvaultwizard.new.directoryPickerTitle=Choisissez le répertoire
addvaultwizard.new.fileAlreadyExists=Un fichier ou un répertoire avec ce nom existe déjà addvaultwizard.new.fileAlreadyExists=Le coffre ne peut pas être créé sur ce chemin car un objet 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.locationDoesNotExist=Le coffre ne peut pas être créé à cet endroit car au moins un répertoire du chemin n'existe pas.
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. addvaultwizard.new.invalidName=Nom de coffre invalide. Préférez un nom de répertoire habituel.
### Password ### Password
addvaultwizard.new.createVaultBtn=Créer un coffre addvaultwizard.new.createVaultBtn=Créer un coffre
@@ -99,12 +97,10 @@ unlock.title=Déverrouiller le coffre
unlock.passwordPrompt=Entrez le mot de passe pour “%s” : unlock.passwordPrompt=Entrez le mot de passe pour “%s” :
unlock.savePassword=Mémoriser le mot de passe unlock.savePassword=Mémoriser le mot de passe
unlock.unlockBtn=Déverrouiller unlock.unlockBtn=Déverrouiller
##
unlock.chooseMasterkey.filePickerTitle=Sélectionner le fichier clef
## Success ## Success
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible par son lecteur virtuel. unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible.
unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander
unlock.success.revealBtn=Révéler le lecteur unlock.success.revealBtn=Révéler le coffre
## Failure ## Failure
unlock.error.heading=Impossible de déverrouiller le coffre unlock.error.heading=Impossible de déverrouiller le coffre
### Invalid Mount Point ### Invalid Mount Point
@@ -178,14 +174,11 @@ preferences.updates.currentVersion=Version actuelle : “%s”
preferences.updates.autoUpdateCheck=Vérifier automatiquement lexistence de mise à jour preferences.updates.autoUpdateCheck=Vérifier automatiquement lexistence de mise à jour
preferences.updates.checkNowBtn=Vérifier maintenant preferences.updates.checkNowBtn=Vérifier maintenant
preferences.updates.updateAvailable=Mise à jour “%s” disponible. preferences.updates.updateAvailable=Mise à jour “%s” disponible.
## Contribution ## Donation Key
preferences.contribute=Nous soutenir preferences.donationKey=Faire un Don
preferences.contribute.registeredFor=Certificat de soutien enregistré pour %s preferences.donationKey.registeredFor=Licence enregistrée à %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.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.contribute.getCertificate=Vous n'en avez pas encore? Découvrez comment vous pouvez l'obtenir. preferences.donationKey.getDonationKey=Obtenir une clé de don
preferences.contribute.promptText=Collez le code du certificat du supporter ici
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=A propos preferences.about=A propos
@@ -230,12 +223,7 @@ main.dropZone.dropVault=Ajouter ce coffre
main.dropZone.unknownDragboardContent=Si vous voulez ajouter un coffre, faites-le glisser dans cette fenêtre main.dropZone.unknownDragboardContent=Si vous voulez ajouter un coffre, faites-le glisser dans cette fenêtre
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Cliquez ici pour ajouter un coffre main.vaultlist.emptyList.onboardingInstruction=Cliquez ici pour ajouter un coffre
main.vaultlist.contextMenu.remove=Retirer main.vaultlist.contextMenu.remove=Supprimer le coffre
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 main.vaultlist.addVaultBtn=Ajouter un coffre
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=अपने पसंद की जगह डालें addvaultwizard.new.directoryPickerLabel=अपने पसंद की जगह डालें
addvaultwizard.new.directoryPickerButton=चुनें… addvaultwizard.new.directoryPickerButton=चुनें…
addvaultwizard.new.directoryPickerTitle=निर्देशिका चुनें addvaultwizard.new.directoryPickerTitle=निर्देशिका चुनें
addvaultwizard.new.fileAlreadyExists=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कुछ वस्तु पहले से मौजूद है।
addvaultwizard.new.locationDoesNotExist=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कम से कम एक पथ घटक मौजूद नहीं है।
addvaultwizard.new.invalidName=अमान्य वॉल्ट नाम। कृपया एक नियमित निर्देशिका नाम पर विचार करें। addvaultwizard.new.invalidName=अमान्य वॉल्ट नाम। कृपया एक नियमित निर्देशिका नाम पर विचार करें।
### Password ### Password
addvaultwizard.new.createVaultBtn=वॉल्ट बनाएं addvaultwizard.new.createVaultBtn=वॉल्ट बनाएं
@@ -70,7 +72,6 @@ changepassword.title=पासवर्ड बदलें
# Unlock # Unlock
unlock.unlockBtn=अनलॉक करें unlock.unlockBtn=अनलॉक करें
##
## Success ## Success
## Failure ## Failure
### Invalid Mount Point ### Invalid Mount Point
@@ -93,9 +94,7 @@ preferences.title=प्राथमिकताएं
preferences.general=सामान्य preferences.general=सामान्य
## Volume ## Volume
## Updates ## Updates
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
# Vault Statistics # Vault Statistics
@@ -107,7 +106,6 @@ main.closeBtn.tooltip=बंद करें
main.preferencesBtn.tooltip=प्राथमिकताएं main.preferencesBtn.tooltip=प्राथमिकताएं
## Drag 'n' Drop ## Drag 'n' Drop
## Vault List ## Vault List
main.vaultlist.contextMenu.lock=लॉक करें
main.vaultlist.addVaultBtn=वाउल्ट डालें main.vaultlist.addVaultBtn=वाउल्ट डालें
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -25,7 +25,6 @@
# Forget Password # Forget Password
# Unlock # Unlock
##
## Success ## Success
## Failure ## Failure
### Invalid Mount Point ### Invalid Mount Point
@@ -45,9 +44,7 @@
## General ## General
## Volume ## Volume
## Updates ## Updates
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
# Vault Statistics # Vault Statistics
@@ -43,6 +43,8 @@ addvaultwizard.new.locationLabel=Tárolási hely
addvaultwizard.new.directoryPickerLabel=Egyedi hely addvaultwizard.new.directoryPickerLabel=Egyedi hely
addvaultwizard.new.directoryPickerButton=Választás… addvaultwizard.new.directoryPickerButton=Választás…
addvaultwizard.new.directoryPickerTitle=Könyvtár kiválasztása 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. 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 ### Password
addvaultwizard.new.createVaultBtn=Új széf létrehozása addvaultwizard.new.createVaultBtn=Új széf létrehozása
@@ -93,9 +95,8 @@ forgetPassword.confirmBtn=Jelszó elfelejtése
unlock.title=Széf feloldása unlock.title=Széf feloldása
unlock.passwordPrompt=Írja be a jelszavát a következő széfhez "%s": unlock.passwordPrompt=Írja be a jelszavát a következő széfhez "%s":
unlock.unlockBtn=Feloldás unlock.unlockBtn=Feloldás
##
unlock.chooseMasterkey.filePickerTitle=Mesterkulcs fájl kiválasztása
## Success ## 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.rememberChoice=Jegyezze meg a választást és ne mutassa többet
unlock.success.revealBtn=Széf megjelenítése unlock.success.revealBtn=Széf megjelenítése
## Failure ## Failure
@@ -158,9 +159,11 @@ preferences.updates.currentVersion=Jelenlegi verzió: %s
preferences.updates.autoUpdateCheck=Frissítések autómatikus keresése preferences.updates.autoUpdateCheck=Frissítések autómatikus keresése
preferences.updates.checkNowBtn=Ellenőrzés most preferences.updates.checkNowBtn=Ellenőrzés most
preferences.updates.updateAvailable=Frissítés a %s verzióra elérhető. preferences.updates.updateAvailable=Frissítés a %s verzióra elérhető.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> 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
## About ## About
preferences.about=Rólunk preferences.about=Rólunk
@@ -204,10 +207,7 @@ 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. main.dropZone.unknownDragboardContent=Ha egy széfet szeretne hozzáadni, akkor húzza át erre az ablakra.
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Kattintson ide egy széf hozzáadásához main.vaultlist.emptyList.onboardingInstruction=Kattintson ide egy széf hozzáadásához
main.vaultlist.contextMenu.lock=Zárolás main.vaultlist.contextMenu.remove=Széf eltávolítása…
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 main.vaultlist.addVaultBtn=Széf hozzáadása
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,9 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Sesuaikan Lokasi addvaultwizard.new.directoryPickerLabel=Sesuaikan Lokasi
addvaultwizard.new.directoryPickerButton=Pilih… addvaultwizard.new.directoryPickerButton=Pilih…
addvaultwizard.new.directoryPickerTitle=Pilih Folder addvaultwizard.new.directoryPickerTitle=Pilih Folder
addvaultwizard.new.fileAlreadyExists=Sudah ada file atau direktori dengan nama yang sama addvaultwizard.new.fileAlreadyExists=Brankas tidak dibuat disini karena beberapa item telah ada disini.
addvaultwizard.new.locationDoesNotExist=Direktori pada path yang dipilih tidak ada atau tidak dapat diakses addvaultwizard.new.locationDoesNotExist=Brankas tidak bisa dibuat disini karena setidaknya satu jalur komponen tidak ada.
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. addvaultwizard.new.invalidName=Nama brankas salah. Harap pilih nama folder yang umum.
### Password ### Password
addvaultwizard.new.createVaultBtn=Buat Brankas addvaultwizard.new.createVaultBtn=Buat Brankas
@@ -58,8 +57,6 @@ addvault.new.readme.storageLocation.fileName=PENTING.rtf
addvault.new.readme.storageLocation.1=⚠️ BERKAS BRANKAS ⚠️ addvault.new.readme.storageLocation.1=⚠️ BERKAS BRANKAS ⚠️
addvault.new.readme.storageLocation.2=Ini adalah lokasi penyimpanan brankas kamu. addvault.new.readme.storageLocation.2=Ini adalah lokasi penyimpanan brankas kamu.
addvault.new.readme.storageLocation.3=JANGAN 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.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.7=1. Tambahkan brankas ini ke Cryptomator.
addvault.new.readme.storageLocation.8=2. Buka gembok brankas di Cryptomator. addvault.new.readme.storageLocation.8=2. Buka gembok brankas di Cryptomator.
@@ -68,34 +65,18 @@ addvault.new.readme.storageLocation.10=Jika kamu butuh bantuan, kunjungi dokumen
addvault.new.readme.accessLocation.fileName=SELAMAT_DATANG.rtf addvault.new.readme.accessLocation.fileName=SELAMAT_DATANG.rtf
addvault.new.readme.accessLocation.1=🔐️ ISI TERENKRIPSI 🔐️ addvault.new.readme.accessLocation.1=🔐️ ISI TERENKRIPSI 🔐️
addvault.new.readme.accessLocation.2=Ini adalah lokasi akses brankas kamu. 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 ## Existing
addvaultwizard.existing.instruction=Pilih file "masterkey.cryptomator" dalam vault Anda.
addvaultwizard.existing.chooseBtn=Pilih… addvaultwizard.existing.chooseBtn=Pilih…
addvaultwizard.existing.filePickerTitle=Pilih File Kunci Induk
## Success ## 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 # 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 # 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 # Forget Password
forgetPassword.title=Lupa Kata Sandi
forgetPassword.confirmBtn=Lupa Kata Sandi
# Unlock # Unlock
unlock.unlockBtn=Buka Gembok unlock.unlockBtn=Buka Gembok
##
unlock.chooseMasterkey.filePickerTitle=Pilih File Kunci Induk
## Success ## Success
## Failure ## Failure
### Invalid Mount Point ### Invalid Mount Point
@@ -108,7 +89,6 @@ unlock.chooseMasterkey.filePickerTitle=Pilih File Kunci Induk
## Start ## Start
## Run ## Run
## Sucess ## Sucess
migration.success.unlockNow=Buka Kunci Sekarang
## Missing file system capabilities ## Missing file system capabilities
## Impossible ## Impossible
@@ -117,9 +97,7 @@ preferences.title=Preferensi
## General ## General
## Volume ## Volume
## Updates ## Updates
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
# Vault Statistics # Vault Statistics
@@ -131,13 +109,10 @@ main.closeBtn.tooltip=Tutup
main.preferencesBtn.tooltip=Preferensi main.preferencesBtn.tooltip=Preferensi
## Drag 'n' Drop ## Drag 'n' Drop
## Vault List ## Vault List
main.vaultlist.contextMenu.lock=Gembok
main.vaultlist.contextMenu.unlockNow=Buka Kunci Sekarang
main.vaultlist.addVaultBtn=Tambah Brankas main.vaultlist.addVaultBtn=Tambah Brankas
## Vault Detail ## Vault Detail
### Welcome ### Welcome
### Locked ### Locked
main.vaultDetail.unlockNowBtn=Buka Kunci Sekarang
### Unlocked ### Unlocked
main.vaultDetail.lockBtn=Gembok main.vaultDetail.lockBtn=Gembok
### Missing ### Missing
@@ -151,7 +126,6 @@ vaultOptions.general.vaultName=Nama Brankas
## Mount ## Mount
vaultOptions.mount.mountPoint.directoryPickerButton=Pilih… vaultOptions.mount.mountPoint.directoryPickerButton=Pilih…
## Master Key ## Master Key
vaultOptions.masterkey.changePasswordBtn=Ubah Kata Sandi
# Recovery Key # Recovery Key
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Posizione personalizzata addvaultwizard.new.directoryPickerLabel=Posizione personalizzata
addvaultwizard.new.directoryPickerButton=Scegli… addvaultwizard.new.directoryPickerButton=Scegli…
addvaultwizard.new.directoryPickerTitle=Seleziona cartella addvaultwizard.new.directoryPickerTitle=Seleziona cartella
addvaultwizard.new.fileAlreadyExists=Un file o una cartella con il nome del vault esiste già addvaultwizard.new.fileAlreadyExists=La cassaforte non può essere creata in questo percorso perché un oggetto esiste già.
addvaultwizard.new.locationDoesNotExist=Una cartella nel percorso specificato non esiste o non è accessibile addvaultwizard.new.locationDoesNotExist=La cassaforte non può essere creata in questo percorso perché almeno un componente di percorso non esiste.
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. addvaultwizard.new.invalidName=Nome della cassaforte non valido. Si prega di considerare un nome di directory regolare.
### Password ### Password
addvaultwizard.new.createVaultBtn=Crea Cassaforte addvaultwizard.new.createVaultBtn=Crea Cassaforte
@@ -99,12 +97,10 @@ unlock.title=Sblocca Cassaforte
unlock.passwordPrompt=Inserisci la password per "%s": unlock.passwordPrompt=Inserisci la password per "%s":
unlock.savePassword=Ricorda la Password unlock.savePassword=Ricorda la Password
unlock.unlockBtn=Sblocca unlock.unlockBtn=Sblocca
##
unlock.chooseMasterkey.filePickerTitle=Seleziona file Masterkey
## Success ## Success
unlock.success.message="%s" sbloccato con successo! La tua cassaforte è ora accessibile tramite il suo drive virtuale. unlock.success.message=Sbloccato "%s" con successo! La tua cassaforte è ora accessibile.
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
unlock.success.revealBtn=Rivela Drive unlock.success.revealBtn=Rivela Cassaforte
## Failure ## Failure
unlock.error.heading=Impossibile sbloccare la cassaforte unlock.error.heading=Impossibile sbloccare la cassaforte
### Invalid Mount Point ### Invalid Mount Point
@@ -154,7 +150,6 @@ preferences.general.theme.light=Chiaro
preferences.general.theme.dark=Scuro preferences.general.theme.dark=Scuro
preferences.general.unlockThemes=Sblocca modalità scura preferences.general.unlockThemes=Sblocca modalità scura
preferences.general.showMinimizeButton=Mostra pulsante riduci a icona 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.startHidden=Nascondi la finestra all'avvio di Cryptomator
preferences.general.debugLogging=Abilita i registri di debug preferences.general.debugLogging=Abilita i registri di debug
preferences.general.debugDirectory=Mostra file log preferences.general.debugDirectory=Mostra file log
@@ -178,14 +173,11 @@ preferences.updates.currentVersion=Versione attuale %s
preferences.updates.autoUpdateCheck=Controlla automaticamente la presenza di aggiornamenti preferences.updates.autoUpdateCheck=Controlla automaticamente la presenza di aggiornamenti
preferences.updates.checkNowBtn=Controlla adesso preferences.updates.checkNowBtn=Controlla adesso
preferences.updates.updateAvailable=Disponibile aggiornamento alla versione %s. preferences.updates.updateAvailable=Disponibile aggiornamento alla versione %s.
## Contribution ## Donation Key
preferences.contribute=Supportaci preferences.donationKey=Donazione
preferences.contribute.registeredFor=Certificato del supporto registrato per %s preferences.donationKey.registeredFor=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.donationKey.noDonationKey=Nessuna chiave di donazione valida. È come una chiave di licenza ma per persone fantastiche che usano software gratuito. ;-)
preferences.contribute.getCertificate=Non ne hai già uno? Impara come puoi ottenerlo. preferences.donationKey.getDonationKey=Ottieni una chiave di donazione
preferences.contribute.promptText=Incolla qui il codice del certificato di supporter
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=Informazioni preferences.about=Informazioni
@@ -229,12 +221,7 @@ main.dropZone.dropVault=Aggiungi questa cassaforte
main.dropZone.unknownDragboardContent=Se vuoi aggiungere una cassaforte, trascinala in questa finestra main.dropZone.unknownDragboardContent=Se vuoi aggiungere una cassaforte, trascinala in questa finestra
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Clicca qui per aggiungere una cassaforte main.vaultlist.emptyList.onboardingInstruction=Clicca qui per aggiungere una cassaforte
main.vaultlist.contextMenu.remove=Rimuovi… main.vaultlist.contextMenu.remove=Rimuovi Cassaforte
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 main.vaultlist.addVaultBtn=Aggiungi Cassaforte
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=ユーザー設定 addvaultwizard.new.directoryPickerLabel=ユーザー設定
addvaultwizard.new.directoryPickerButton=選択... addvaultwizard.new.directoryPickerButton=選択...
addvaultwizard.new.directoryPickerTitle=ディレクトリを選択 addvaultwizard.new.directoryPickerTitle=ディレクトリを選択
addvaultwizard.new.fileAlreadyExists=金庫名と同じ名前のファイルまたはディレクトリがすでに存在しています addvaultwizard.new.fileAlreadyExists=いくつかのオブジェクトが既に存在しているため、このパスに金庫を作成することはできません。
addvaultwizard.new.locationDoesNotExist=指定されたパスのディレクトリが存在しないかアクセスできません addvaultwizard.new.locationDoesNotExist=このパスには階層がないため金庫を作成することができません
addvaultwizard.new.locationIsNotWritable=指定されたパスに書き込み権限がありません
addvaultwizard.new.locationIsOk=金庫に適した場所
addvaultwizard.new.invalidName=無効な金庫の名前です。一般的なディレクトリの名前を検討してください。 addvaultwizard.new.invalidName=無効な金庫の名前です。一般的なディレクトリの名前を検討してください。
### Password ### Password
addvaultwizard.new.createVaultBtn=金庫を作成 addvaultwizard.new.createVaultBtn=金庫を作成
@@ -99,12 +97,10 @@ unlock.title=金庫を解錠
unlock.passwordPrompt="%s" のパスワードを入力してください: unlock.passwordPrompt="%s" のパスワードを入力してください:
unlock.savePassword=パスワードを記憶させる unlock.savePassword=パスワードを記憶させる
unlock.unlockBtn=解錠 unlock.unlockBtn=解錠
##
unlock.chooseMasterkey.filePickerTitle=Masterkey ファイルを選択
## Success ## Success
unlock.success.message="%s" の解錠に成功しました! 仮想ドライブから金庫にアクセス可能です。 unlock.success.message="%s" の解錠に成功しました! 金庫にアクセス可能です。
unlock.success.rememberChoice=選択を記憶させて、再度表示しない unlock.success.rememberChoice=選択を記憶させて、再度表示しない
unlock.success.revealBtn=ドライブを表示 unlock.success.revealBtn=金庫を表示
## Failure ## Failure
unlock.error.heading=金庫の解錠に失敗 unlock.error.heading=金庫の解錠に失敗
### Invalid Mount Point ### Invalid Mount Point
@@ -178,14 +174,11 @@ preferences.updates.currentVersion=現在のバージョン: %s
preferences.updates.autoUpdateCheck=自動的に更新を確認する preferences.updates.autoUpdateCheck=自動的に更新を確認する
preferences.updates.checkNowBtn=今すぐ確認 preferences.updates.checkNowBtn=今すぐ確認
preferences.updates.updateAvailable=利用可能なバージョン %s に更新します。 preferences.updates.updateAvailable=利用可能なバージョン %s に更新します。
## Contribution ## Donation Key
preferences.contribute=サポートする preferences.donationKey=寄付
preferences.contribute.registeredFor=サポーター証明書が %s 登録されました preferences.donationKey.registeredFor=%s 登録されています
preferences.contribute.noCertificate=Cryptomator を支援し、サポーター証明書を受け取りましょう。ライセンスキーに似ていますがフリーソフトを使う寄付者向けのキーです。 ;-) preferences.donationKey.noDonationKey=有効な Donation Key が見つかりませんでした。ライセンスキーに似ていますがフリーソフトを使う寄付者向けのキーです。 ;-)
preferences.contribute.getCertificate=まだ証明書を手に入れていませんか? 詳細を確認しましょう。 preferences.donationKey.getDonationKey=Donation Key を入手する
preferences.contribute.promptText=サポーター証明書をここに張り付けてください
#<-- Add entries for donations and code/translation/documentation contribution -->
## About ## About
preferences.about=詳細情報 preferences.about=詳細情報
@@ -230,12 +223,7 @@ main.dropZone.dropVault=この金庫を追加
main.dropZone.unknownDragboardContent=このウィンドウにドラッグして、金庫を追加 main.dropZone.unknownDragboardContent=このウィンドウにドラッグして、金庫を追加
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=ここをクリックして金庫を追加 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=金庫を追加 main.vaultlist.addVaultBtn=金庫を追加
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,10 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=사용자 지정 위치 addvaultwizard.new.directoryPickerLabel=사용자 지정 위치
addvaultwizard.new.directoryPickerButton=선택 addvaultwizard.new.directoryPickerButton=선택
addvaultwizard.new.directoryPickerTitle=디렉터리 선택 addvaultwizard.new.directoryPickerTitle=디렉터리 선택
addvaultwizard.new.fileAlreadyExists=Vault 내에 이미 존재하는 파일 또는 디렉터리 이름입니다. addvaultwizard.new.fileAlreadyExists=이미 다른 객체가 존재하고 있어 해당 경로에 Vault를 생성할 수 없습니다.
addvaultwizard.new.locationDoesNotExist=지정된 디렉터리가 존재하지 않거나 접근 할 수 없습니다. addvaultwizard.new.locationDoesNotExist=하나 이상의 경로 구성 요소가 없기 때문에 이 경로에 Vault가 생성 될 수 없습니다.
addvaultwizard.new.locationIsNotWritable=지정된 경로에 쓰기 권한이 없습니다.
addvaultwizard.new.locationIsOk=Vault에 적당한 위치
addvaultwizard.new.invalidName=유효하지 않은 Vault 이름입니다. 일반적인 디렉터리 이름으로 지정해주십시요. addvaultwizard.new.invalidName=유효하지 않은 Vault 이름입니다. 일반적인 디렉터리 이름으로 지정해주십시요.
### Password ### Password
addvaultwizard.new.createVaultBtn=Vault 생성 addvaultwizard.new.createVaultBtn=Vault 생성
@@ -69,7 +67,7 @@ addvault.new.readme.storageLocation.10=만일 도움이 필요하신 경우, 다
addvault.new.readme.accessLocation.fileName=WELCOME.rtf addvault.new.readme.accessLocation.fileName=WELCOME.rtf
addvault.new.readme.accessLocation.1=🔐️ 암호화 된 볼륨 🔐️ addvault.new.readme.accessLocation.1=🔐️ 암호화 된 볼륨 🔐️
addvault.new.readme.accessLocation.2=이것은 당신의 Vault 접근 위치입니다. addvault.new.readme.accessLocation.2=이것은 당신의 Vault 접근 위치입니다.
addvault.new.readme.accessLocation.3=이 볼륨에 추가된 모든 파일은 Cryptomator로 암호화됩니다. 다른 드라이브/폴더처럼 작업할 수 있습니다. 볼륨의 내용은 복호화 된 것 처럼 보여지지만, 모든 파일은 항상 암호화되어 하드디스크에 저장됩니다. addvault.new.readme.accessLocation.3=이 볼륨에 추가된 모든 파일은 Cryptomator로 암호화됩니다. 다른 드라이브/폴더처럼 작업할 수 있습니다. 볼륨의 내용은 복호화 된 것 처럼 보여질 뿐이며, 모든 파일은 항상 암호화되어 하드디스크에 저장됩니다.
addvault.new.readme.accessLocation.4=이 파일은 지우셔도 무방합니다. addvault.new.readme.accessLocation.4=이 파일은 지우셔도 무방합니다.
## Existing ## Existing
addvaultwizard.existing.instruction=기존 Vault 의 "masterkey.cryptomator" 파일을 선택하여 주십시요. addvaultwizard.existing.instruction=기존 Vault 의 "masterkey.cryptomator" 파일을 선택하여 주십시요.
@@ -99,12 +97,10 @@ unlock.title=Vault 잠금해제
unlock.passwordPrompt="%s"의 비밀번호를 입력하십시요. unlock.passwordPrompt="%s"의 비밀번호를 입력하십시요.
unlock.savePassword=비밀번호 기억 unlock.savePassword=비밀번호 기억
unlock.unlockBtn=잠금해제 unlock.unlockBtn=잠금해제
##
unlock.chooseMasterkey.filePickerTitle=마스터키 파일 선택
## Success ## Success
unlock.success.message="%s"이(가) 성공적으로 잠금해제되었습니다. 이제 이 Vault를 가상드라이브로 접근할 수 있습니다. unlock.success.message="%s"의 잠금해제가 성공적으로 수행되었습니다! 이제 이 Vault의 접근이 가능합니다.
unlock.success.rememberChoice=선택 기억함, 다시 묻지 않음 unlock.success.rememberChoice=선택 기억함, 다시 묻지 않음
unlock.success.revealBtn=드라이브 표시 unlock.success.revealBtn=Vault 표시
## Failure ## Failure
unlock.error.heading=Vault 잠금을 해제 할 수 없습니다. unlock.error.heading=Vault 잠금을 해제 할 수 없습니다.
### Invalid Mount Point ### Invalid Mount Point
@@ -178,9 +174,11 @@ preferences.updates.currentVersion=현재 버전: %s
preferences.updates.autoUpdateCheck=자동으로 업데이트 확인 preferences.updates.autoUpdateCheck=자동으로 업데이트 확인
preferences.updates.checkNowBtn=지금 확인 preferences.updates.checkNowBtn=지금 확인
preferences.updates.updateAvailable=버전 %s의 업데이트가 가능합니다. preferences.updates.updateAvailable=버전 %s의 업데이트가 가능합니다.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=기부하기
preferences.donationKey.registeredFor=%s (으)로 등록되어 있습니다.
preferences.donationKey.noDonationKey=유효한 도네이션(기부) 키를 찾지 못했습니다. 라이센스 키와 비슷하나, 무료 소프트웨어를 사용하는 멋진 사람들을 위한 것입니다. ;-)
preferences.donationKey.getDonationKey=도네이션(기부) 키 얻기
## About ## About
preferences.about=제품 정보 preferences.about=제품 정보
@@ -225,12 +223,7 @@ main.dropZone.dropVault=이 Vault를 추가
main.dropZone.unknownDragboardContent=Vault를 추가하려면, 이 창에 드래그 하십시요. main.dropZone.unknownDragboardContent=Vault를 추가하려면, 이 창에 드래그 하십시요.
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Vault를 추가하기 위해 이곳을 클릭합니다. main.vaultlist.emptyList.onboardingInstruction=Vault를 추가하기 위해 이곳을 클릭합니다.
main.vaultlist.contextMenu.remove=제거... main.vaultlist.contextMenu.remove=Vault 제거...
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 추가 main.vaultlist.addVaultBtn=Vault 추가
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -301,7 +294,7 @@ vaultOptions.masterkey.recoverPasswordBtn=비밀번호 복구
# Recovery Key # Recovery Key
recoveryKey.title=복구 키 recoveryKey.title=복구 키
recoveryKey.enterPassword.prompt="%s"의 복구 키를 표시하려면 비밀번호를 입력해 주세요. recoveryKey.enterPassword.prompt="%s"의 복구 키를 표시하려면 비밀번호를 입력하여 주십시요.
recoveryKey.display.message="%s" 데이터 접근을 복원하는데 사용 할 수 있는 복구 키 입니다: recoveryKey.display.message="%s" 데이터 접근을 복원하는데 사용 할 수 있는 복구 키 입니다:
recoveryKey.display.StorageHints=매우 안전한곳에 보관하십시요. 예시:\n • 비밀번호 관리자를 사용하여 저장\n • USB 플래시 드라이브에 저장\n • 종이에 출력 recoveryKey.display.StorageHints=매우 안전한곳에 보관하십시요. 예시:\n • 비밀번호 관리자를 사용하여 저장\n • USB 플래시 드라이브에 저장\n • 종이에 출력
recoveryKey.recover.prompt="%s"의 복구키를 입력하십시요: recoveryKey.recover.prompt="%s"의 복구키를 입력하십시요:
@@ -309,7 +302,7 @@ recoveryKey.recover.validKey=올바른 복구 키 입니다.
recoveryKey.printout.heading=Cryptomator 복구 키\n"%s"\n recoveryKey.printout.heading=Cryptomator 복구 키\n"%s"\n
# New Password # New Password
newPassword.promptText=새 비밀번호를 입력하세요 newPassword.promptText=새 비밀번호를 입력하십시요.
newPassword.reenterPassword=새 비밀번호를 확인하여 주십시요. newPassword.reenterPassword=새 비밀번호를 확인하여 주십시요.
newPassword.passwordsMatch=비밀번호가 일치합니다! newPassword.passwordsMatch=비밀번호가 일치합니다!
newPassword.passwordsDoNotMatch=비밀번호가 일치하지 않습니다. newPassword.passwordsDoNotMatch=비밀번호가 일치하지 않습니다.
@@ -43,6 +43,7 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Pielāgota atrašanās vieta addvaultwizard.new.directoryPickerLabel=Pielāgota atrašanās vieta
addvaultwizard.new.directoryPickerButton=Izvēlies... addvaultwizard.new.directoryPickerButton=Izvēlies...
addvaultwizard.new.directoryPickerTitle=Izvēlēties mapi addvaultwizard.new.directoryPickerTitle=Izvēlēties mapi
addvaultwizard.new.fileAlreadyExists=Glabātuvi nevar izveidot šajā ceļā, jo kāds objekts jau pastāv.
addvaultwizard.new.invalidName=Nederīgs glabātuves nosaukums. Lūdzu izvēlieties vienkāršu mapes nosaukumu. addvaultwizard.new.invalidName=Nederīgs glabātuves nosaukums. Lūdzu izvēlieties vienkāršu mapes nosaukumu.
### Password ### Password
addvaultwizard.new.createVaultBtn=Izveidot glabātuvi addvaultwizard.new.createVaultBtn=Izveidot glabātuvi
@@ -93,10 +94,9 @@ forgetPassword.confirmBtn=Aizmirst paroli
unlock.title=Atslēgt glabātuvi unlock.title=Atslēgt glabātuvi
unlock.passwordPrompt=Ievadiet "%s" paroli: unlock.passwordPrompt=Ievadiet "%s" paroli:
unlock.unlockBtn=Atslēgt unlock.unlockBtn=Atslēgt
##
unlock.chooseMasterkey.filePickerTitle=Atlasīt galveno atslēgas datni
## Success ## Success
unlock.success.revealBtn=Atklāt disku unlock.success.message="%s" sekmīgi atslēgts! Jūsu glabātuve tagad ir pieejama.
unlock.success.revealBtn=Atklāt glabātuvi
## Failure ## Failure
### Invalid Mount Point ### Invalid Mount Point
@@ -149,9 +149,11 @@ preferences.updates.currentVersion=Pašreizējā versija: %s
preferences.updates.autoUpdateCheck=Automātiski pārbaudīt atjauninājumus preferences.updates.autoUpdateCheck=Automātiski pārbaudīt atjauninājumus
preferences.updates.checkNowBtn=Pārbaudīt tagad preferences.updates.checkNowBtn=Pārbaudīt tagad
preferences.updates.updateAvailable=Pieejams atjauninājums uz versiju %s. preferences.updates.updateAvailable=Pieejams atjauninājums uz versiju %s.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=Ziedojums
preferences.donationKey.registeredFor=Reģistrēts uz %s
preferences.donationKey.noDonationKey=Nav atrasta derīga ziedojuma atslēga. Tā ir kā licences atslēga , bet priekš satriecošiem cilvēkiem, kas izmanto bezmaksas programmatūru. ;-)
preferences.donationKey.getDonationKey=Iegūt ziedojuma atslēgu
## About ## About
preferences.about=Par lietotni preferences.about=Par lietotni
@@ -169,9 +171,6 @@ main.dropZone.dropVault=Pievienot šo glabātuvi
main.dropZone.unknownDragboardContent=Ja jūs vēlaties pievienot glabātuvi, velciet to uz šo logu main.dropZone.unknownDragboardContent=Ja jūs vēlaties pievienot glabātuvi, velciet to uz šo logu
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Spied šeit, lai pievienotu glabātuvi main.vaultlist.emptyList.onboardingInstruction=Spied šeit, lai pievienotu glabātuvi
main.vaultlist.contextMenu.lock=Aizslēgt
main.vaultlist.contextMenu.unlockNow=Atslēgt tagad
main.vaultlist.contextMenu.reveal=Atklāt disku
main.vaultlist.addVaultBtn=Pievienot glabātuvi main.vaultlist.addVaultBtn=Pievienot glabātuvi
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Tilpasset lagringssted addvaultwizard.new.directoryPickerLabel=Tilpasset lagringssted
addvaultwizard.new.directoryPickerButton=Velg… addvaultwizard.new.directoryPickerButton=Velg…
addvaultwizard.new.directoryPickerTitle=Velg mappe addvaultwizard.new.directoryPickerTitle=Velg mappe
addvaultwizard.new.fileAlreadyExists=Hvelvet kan ikke opprettes på denne søkestien fordi et objekt allerede eksisterer.
addvaultwizard.new.locationDoesNotExist=Hvelvet kan ikke opprettes på denne søkestien fordi minst én søkestikomponent ikke finnes.
addvaultwizard.new.invalidName=Ugyldig navn på hvelvet. Vennligst vurder et vanlig mappenavn. addvaultwizard.new.invalidName=Ugyldig navn på hvelvet. Vennligst vurder et vanlig mappenavn.
### Password ### Password
addvaultwizard.new.createVaultBtn=Opprett hvelv addvaultwizard.new.createVaultBtn=Opprett hvelv
@@ -93,13 +95,11 @@ forgetPassword.confirmBtn=Glem passord
# Unlock # Unlock
unlock.title=Lås opp hvelvet unlock.title=Lås opp hvelvet
unlock.passwordPrompt=Skriv inn passordet for "%s": unlock.passwordPrompt=Skriv inn passordet for "%s":
unlock.savePassword=Husk passord
unlock.unlockBtn=Lås opp unlock.unlockBtn=Lås opp
##
unlock.chooseMasterkey.filePickerTitle=Velg hovednøkkelfil
## Success ## Success
unlock.success.message=Vellykket opplåsning av "%s"! Hvelvet ditt er nå tilgjengelig.
unlock.success.rememberChoice=Husk valget - ikke vis dette igjen unlock.success.rememberChoice=Husk valget - ikke vis dette igjen
unlock.success.revealBtn=Gjør enheten synlig unlock.success.revealBtn=Gjør hvelvet synlig
## Failure ## Failure
unlock.error.heading=Klarer ikke å låse opp hvelvet unlock.error.heading=Klarer ikke å låse opp hvelvet
### Invalid Mount Point ### Invalid Mount Point
@@ -173,9 +173,11 @@ preferences.updates.currentVersion=Gjeldende versjon: %s
preferences.updates.autoUpdateCheck=Se etter oppdateringer automatisk preferences.updates.autoUpdateCheck=Se etter oppdateringer automatisk
preferences.updates.checkNowBtn=Sjekk nå preferences.updates.checkNowBtn=Sjekk nå
preferences.updates.updateAvailable=Oppdatering til versjon %s er tilgjengelig. preferences.updates.updateAvailable=Oppdatering til versjon %s er tilgjengelig.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=Donasjon
preferences.donationKey.registeredFor=Registrert for %s
preferences.donationKey.noDonationKey=Ingen gyldig donasjonsnøkkel funnet. Det er som en lisensnøkkel, men for fantastiske mennesker som bruker gratis programvare. ;-)
preferences.donationKey.getDonationKey=Få en donasjonsnøkkel
## About ## About
preferences.about=Om preferences.about=Om
@@ -199,7 +201,6 @@ stats.read.accessCount=Lesninger totalt: %d
stats.write.throughput.idle=Skrive: inaktiv stats.write.throughput.idle=Skrive: inaktiv
stats.write.throughput.kibs=Skriver: %.2f kiB/s stats.write.throughput.kibs=Skriver: %.2f kiB/s
stats.write.throughput.mibs=Skriver: %.2f MiB/s stats.write.throughput.mibs=Skriver: %.2f MiB/s
stats.write.total.data.none=Data skrevet: -
stats.write.total.data.kib=Data skrevet: %.1f kiB stats.write.total.data.kib=Data skrevet: %.1f kiB
stats.write.total.data.mib=Data skrevet: %.1f MiB stats.write.total.data.mib=Data skrevet: %.1f MiB
stats.write.total.data.gib=Data skrevet: %.1f GiB stats.write.total.data.gib=Data skrevet: %.1f GiB
@@ -220,10 +221,7 @@ main.dropZone.dropVault=Legg til dette hvelvet
main.dropZone.unknownDragboardContent=Hvis du vil legge til et hvelv, kan du dra det til dette vinduet main.dropZone.unknownDragboardContent=Hvis du vil legge til et hvelv, kan du dra det til dette vinduet
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klikk her for å legge til et hvelv main.vaultlist.emptyList.onboardingInstruction=Klikk her for å legge til et hvelv
main.vaultlist.contextMenu.lock=Lås main.vaultlist.contextMenu.remove=Fjern hvelvet…
main.vaultlist.contextMenu.unlock=Lås opp…
main.vaultlist.contextMenu.unlockNow=Lås opp nå
main.vaultlist.contextMenu.reveal=Gjør enheten synlig
main.vaultlist.addVaultBtn=Legg til hvelv main.vaultlist.addVaultBtn=Legg til hvelv
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Andere locatie addvaultwizard.new.directoryPickerLabel=Andere locatie
addvaultwizard.new.directoryPickerButton=Kies… addvaultwizard.new.directoryPickerButton=Kies…
addvaultwizard.new.directoryPickerTitle=Selecteer map addvaultwizard.new.directoryPickerTitle=Selecteer map
addvaultwizard.new.fileAlreadyExists=Er kan op deze locatie geen kluis aangemaakt worden, omdat er een bepaald object bestaat.
addvaultwizard.new.locationDoesNotExist=Er kan geen kluis op dit pad aangemaakt worden omdat ten minste één onderdeel van het pad niet bestaat.
addvaultwizard.new.invalidName=Ongeldige kluisnaam. Overweeg een standaard mapnaam. addvaultwizard.new.invalidName=Ongeldige kluisnaam. Overweeg een standaard mapnaam.
### Password ### Password
addvaultwizard.new.createVaultBtn=Kluis aanmaken addvaultwizard.new.createVaultBtn=Kluis aanmaken
@@ -94,11 +96,10 @@ forgetPassword.confirmBtn=Wachtwoord vergeten
unlock.title=Kluis ontgrendelen unlock.title=Kluis ontgrendelen
unlock.passwordPrompt=Voer wachtwoord voor "%s" in: unlock.passwordPrompt=Voer wachtwoord voor "%s" in:
unlock.unlockBtn=Ontgrendel unlock.unlockBtn=Ontgrendel
##
unlock.chooseMasterkey.filePickerTitle=Selecteer het Masterkey-bestand
## Success ## Success
unlock.success.message="%s" is met succes ontgrendeld! Uw kluis is nu toegankelijk.
unlock.success.rememberChoice=Keuze onthouden en dit niet opnieuw tonen unlock.success.rememberChoice=Keuze onthouden en dit niet opnieuw tonen
unlock.success.revealBtn=Toon Schijf unlock.success.revealBtn=Toon kluis
## Failure ## Failure
unlock.error.heading=Kan kluis niet ontgrendelen unlock.error.heading=Kan kluis niet ontgrendelen
### Invalid Mount Point ### Invalid Mount Point
@@ -172,9 +173,11 @@ preferences.updates.currentVersion=Huidige Versie: "%s"
preferences.updates.autoUpdateCheck=Automatisch controleren op updates preferences.updates.autoUpdateCheck=Automatisch controleren op updates
preferences.updates.checkNowBtn=Controleer nu preferences.updates.checkNowBtn=Controleer nu
preferences.updates.updateAvailable=Update naar versie "%s" beschikbaar. preferences.updates.updateAvailable=Update naar versie "%s" beschikbaar.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=Donatie
preferences.donationKey.registeredFor=Geregistreerd voor %s
preferences.donationKey.noDonationKey=Geen geldige donatiesleutel gevonden. Het is als een licentiesleutel, maar dan voor geweldige mensen die gratis software gebruiken. ;-)
preferences.donationKey.getDonationKey=Verkrijg een donatiesleutel
## About ## About
preferences.about=Over preferences.about=Over
@@ -218,10 +221,7 @@ main.dropZone.dropVault=Voeg deze kluis toe
main.dropZone.unknownDragboardContent=Als u een kluis wilt toevoegen, sleep deze dan naar dit venster main.dropZone.unknownDragboardContent=Als u een kluis wilt toevoegen, sleep deze dan naar dit venster
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klik hier om een kluis toe te voegen main.vaultlist.emptyList.onboardingInstruction=Klik hier om een kluis toe te voegen
main.vaultlist.contextMenu.lock=Vergrendel main.vaultlist.contextMenu.remove=Verwijder Kluis…
main.vaultlist.contextMenu.unlock=Ontgrendelen…
main.vaultlist.contextMenu.unlockNow=Nu Ontgrendelen
main.vaultlist.contextMenu.reveal=Toon Schijf
main.vaultlist.addVaultBtn=Kluis toevoegen main.vaultlist.addVaultBtn=Kluis toevoegen
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -43,6 +43,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=Tilpassa lagringsstad addvaultwizard.new.directoryPickerLabel=Tilpassa lagringsstad
addvaultwizard.new.directoryPickerButton=Vel… addvaultwizard.new.directoryPickerButton=Vel…
addvaultwizard.new.directoryPickerTitle=Vel mappe addvaultwizard.new.directoryPickerTitle=Vel mappe
addvaultwizard.new.fileAlreadyExists=Kvelven kan ikkje opprettast på denne søkestien fordi eit objekt allereie eksisterer her.
addvaultwizard.new.locationDoesNotExist=Kvelven kan ikkje opprettast på denne søkjestien fordi minst éin søkjestikomponent ikkje finst.
addvaultwizard.new.invalidName=Ugyldig namn på kvelven. Ver vennleg og vurder eit vanleg mappenamn. addvaultwizard.new.invalidName=Ugyldig namn på kvelven. Ver vennleg og vurder eit vanleg mappenamn.
### Password ### Password
addvaultwizard.new.createVaultBtn=Opprett kvelven addvaultwizard.new.createVaultBtn=Opprett kvelven
@@ -93,11 +95,10 @@ forgetPassword.confirmBtn=Gløym passord
unlock.title=Lås opp kvelven unlock.title=Lås opp kvelven
unlock.passwordPrompt=Skriv inn passordet for "%s": unlock.passwordPrompt=Skriv inn passordet for "%s":
unlock.unlockBtn=Låse opp unlock.unlockBtn=Låse opp
##
unlock.chooseMasterkey.filePickerTitle=Vel hovudnøkkelfil
## Success ## Success
unlock.success.message=Vellykka opplåsning av "%s"! Kvelven din er no tilgjengeleg.
unlock.success.rememberChoice=Hugs valet - ikkje vis dette igjen unlock.success.rememberChoice=Hugs valet - ikkje vis dette igjen
unlock.success.revealBtn=Gjer eininga synleg unlock.success.revealBtn=Gjer kvelven synleg
## Failure ## Failure
### Invalid Mount Point ### Invalid Mount Point
@@ -155,9 +156,11 @@ preferences.updates.currentVersion=Gjeldande versjon: %s
preferences.updates.autoUpdateCheck=Sjå etter oppdateringar automatisk preferences.updates.autoUpdateCheck=Sjå etter oppdateringar automatisk
preferences.updates.checkNowBtn=Sjekk no preferences.updates.checkNowBtn=Sjekk no
preferences.updates.updateAvailable=Oppdatering til versjon %s er tilgjengeleg. preferences.updates.updateAvailable=Oppdatering til versjon %s er tilgjengeleg.
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=Donasjon
preferences.donationKey.registeredFor=Registrert for %s
preferences.donationKey.noDonationKey=Ingen gyldig donasjonsnøkkel funne. Det er som ein lisensnøkkel, men for fantastiske menneske som bruker gratis programvare. ;-)
preferences.donationKey.getDonationKey=Få ein donasjonsnøkkel
## About ## About
preferences.about=Om preferences.about=Om
@@ -175,10 +178,6 @@ main.dropZone.dropVault=Legg til denne kvelven
main.dropZone.unknownDragboardContent=Viss du vil legga til ein kvelv, kan du dra det til dette vindauget main.dropZone.unknownDragboardContent=Viss du vil legga til ein kvelv, kan du dra det til dette vindauget
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=Klikk her for å legga til ein kvelv main.vaultlist.emptyList.onboardingInstruction=Klikk her for å legga til ein kvelv
main.vaultlist.contextMenu.lock=Lås
main.vaultlist.contextMenu.unlock=Lås opp…
main.vaultlist.contextMenu.unlockNow=Lås opp no
main.vaultlist.contextMenu.reveal=Gjer eininga synleg
main.vaultlist.addVaultBtn=Legg til kvelv main.vaultlist.addVaultBtn=Legg til kvelv
## Vault Detail ## Vault Detail
### Welcome ### Welcome
@@ -44,6 +44,8 @@ addvaultwizard.new.locationPrompt=…
addvaultwizard.new.directoryPickerLabel=ਪਸੰਦੀਦਾ ਟਿਕਾਣਾ addvaultwizard.new.directoryPickerLabel=ਪਸੰਦੀਦਾ ਟਿਕਾਣਾ
addvaultwizard.new.directoryPickerButton=…ਚੁਣੋ addvaultwizard.new.directoryPickerButton=…ਚੁਣੋ
addvaultwizard.new.directoryPickerTitle=ਡਾਇਰੈਕਟਰੀ ਚੁਣੋ addvaultwizard.new.directoryPickerTitle=ਡਾਇਰੈਕਟਰੀ ਚੁਣੋ
addvaultwizard.new.fileAlreadyExists=ਇਸ ਮਾਰਗ ਉੱਤੇ ਵਾਲਟ ਨਹੀਂ ਬਣਾਇਆ ਜਾ ਸਕਦਾ ਹੈ, ਕਿਉਂਕਿ ਕੁਝ ਚੀਜ਼ਾਂ ਪਹਿਲਾਂ ਹੀ ਮੌਜੂਦ ਹਨ।
addvaultwizard.new.locationDoesNotExist=ਇਸ ਮਾਰਗ ਉੱਤੇ ਵਾਲੇਟ ਨਹੀਂ ਬਣਾਇਆ ਜਾ ਸਕਦਾ, ਕਿਉਂਕਿ ਮਾਰਗ ਦਾ ਇੱਕ ਭਾਗ ਮੌਜੂਦ ਨਹੀਂ ਹੈ।
addvaultwizard.new.invalidName=ਵਾਲਟ ਦਾ ਨਾਂ ਗਲਤ ਹੈ। ਨਿਯਮਤ ਡਾਇਰੈਕਟਰੀ ਨਾਂ ਦੇਣ ਬਾਰੇ ਵਿਚਾਰੋ। addvaultwizard.new.invalidName=ਵਾਲਟ ਦਾ ਨਾਂ ਗਲਤ ਹੈ। ਨਿਯਮਤ ਡਾਇਰੈਕਟਰੀ ਨਾਂ ਦੇਣ ਬਾਰੇ ਵਿਚਾਰੋ।
### Password ### Password
addvaultwizard.new.createVaultBtn=ਵਾਲਟ ਬਣਾਓ addvaultwizard.new.createVaultBtn=ਵਾਲਟ ਬਣਾਓ
@@ -93,13 +95,11 @@ forgetPassword.confirmBtn=ਪਾਸਵਰਡ ਭੁੱਲ ਗਏ
# Unlock # Unlock
unlock.title=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰੋ unlock.title=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰੋ
unlock.passwordPrompt="%s" ਲਈ ਪਾਸਵਰਡ ਦਿਓ: unlock.passwordPrompt="%s" ਲਈ ਪਾਸਵਰਡ ਦਿਓ:
unlock.savePassword=ਪਾਸਵਰਡ ਯਾਦ ਰੱਖੋ
unlock.unlockBtn=ਅਣ-ਲਾਕ ਕਰੋ unlock.unlockBtn=ਅਣ-ਲਾਕ ਕਰੋ
##
unlock.chooseMasterkey.filePickerTitle=ਮਾਸਟਰ-ਕੁੰਜੀ ਫਾਇਲ ਚੁਣੋ
## Success ## Success
unlock.success.message="%s" ਕਾਮਯਾਬੀ ਨਾਲ ਅਣ-ਲਾਕ ਕੀਤਾ! ਤੁਹਾਡਾ ਵਾਲਟ ਹੁਣ ਵਰਤਿਆ ਜਾ ਸਕਦਾ ਹੈ।
unlock.success.rememberChoice=ਚੋਣਾਂ ਯਾਦ ਰੱਖੋ, ਇਹ ਮੁੜ ਕੇ ਨਾ ਵੇਖਾਓ unlock.success.rememberChoice=ਚੋਣਾਂ ਯਾਦ ਰੱਖੋ, ਇਹ ਮੁੜ ਕੇ ਨਾ ਵੇਖਾਓ
unlock.success.revealBtn=ਡਰਾਇਵ ਦਿਖਾਓ unlock.success.revealBtn=ਾਲਟ ਦਿਖਾਓ
## Failure ## Failure
unlock.error.heading=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰਨ ਲਈ ਅਸਮਰੱਥ unlock.error.heading=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰਨ ਲਈ ਅਸਮਰੱਥ
### Invalid Mount Point ### Invalid Mount Point
@@ -173,9 +173,11 @@ preferences.updates.currentVersion=ਮੌਜੂਦਾ ਵਰਜ਼ਨ: %s
preferences.updates.autoUpdateCheck=ਅੱਪਡੇਟ ਲਈ ਆਪਣੇ-ਆਪ ਜਾਂਚ ਕਰੋ preferences.updates.autoUpdateCheck=ਅੱਪਡੇਟ ਲਈ ਆਪਣੇ-ਆਪ ਜਾਂਚ ਕਰੋ
preferences.updates.checkNowBtn=ਹੁਣੇ ਜਾਂਚ ਕਰੋ preferences.updates.checkNowBtn=ਹੁਣੇ ਜਾਂਚ ਕਰੋ
preferences.updates.updateAvailable=ਉਪਲੱਬਧ %s ਵਰਜ਼ਨ ਲਈ ਅੱਪਡੇਟ ਕਰੋ। preferences.updates.updateAvailable=ਉਪਲੱਬਧ %s ਵਰਜ਼ਨ ਲਈ ਅੱਪਡੇਟ ਕਰੋ।
## Contribution ## Donation Key
#<-- Add entries for donations and code/translation/documentation contribution --> preferences.donationKey=ਦਾਨ ਦਿਓ
preferences.donationKey.registeredFor=%s ਲਈ ਰਜਿਸਟਰ ਕਰੋ
preferences.donationKey.noDonationKey=ਕੋਈ ਵਾਜਬ ਦਾਨ ਕੁੰਜੀ ਨਹੀਂ ਲੱਭੀ ਹੈ। ਇਹ ਲਸੰਸ ਕੁੰਜੀ ਵਾਂਗ ਹੀ ਹੈ, ਪਰ ਮੁਫ਼ਤ ਸਾਫਟਵੇਅਰ ਵਰਤਣ ਵਾਲੇ ਬੇਮਿਸਾਲ ਲੋਕਾਂ ਲਈ। ;-)
preferences.donationKey.getDonationKey=ਦਾਨ ਕੁੰਜੀ ਦਿਓ
## About ## About
preferences.about=ਇਸ ਬਾਰੇ preferences.about=ਇਸ ਬਾਰੇ
@@ -199,7 +201,6 @@ stats.read.accessCount=ਕੁੱਲ ਪੜ੍ਹੇ: %d
stats.write.throughput.idle=ਲਿਖੇ: ਵੇਹਲ stats.write.throughput.idle=ਲਿਖੇ: ਵੇਹਲ
stats.write.throughput.kibs=ਲਿਖੇ: %.2f kiB/s stats.write.throughput.kibs=ਲਿਖੇ: %.2f kiB/s
stats.write.throughput.mibs=ਲਿਖੇ: %.2f MiB/s stats.write.throughput.mibs=ਲਿਖੇ: %.2f MiB/s
stats.write.total.data.none=ਲਿਖਿਆ ਡਾਟਾ: -
stats.write.total.data.kib=ਡਾਟਾ ਲਿਖਿਆ: %.1f kiB stats.write.total.data.kib=ਡਾਟਾ ਲਿਖਿਆ: %.1f kiB
stats.write.total.data.mib=ਡਾਟਾ ਲਿਖਿਆ: %.1f MiB stats.write.total.data.mib=ਡਾਟਾ ਲਿਖਿਆ: %.1f MiB
stats.write.total.data.gib=ਡਾਟਾ ਲਿਖਿਆ: %.1f GiB stats.write.total.data.gib=ਡਾਟਾ ਲਿਖਿਆ: %.1f GiB
@@ -220,10 +221,7 @@ main.dropZone.dropVault=ਇਹ ਵਾਲਟ ਜੋੜੋ
main.dropZone.unknownDragboardContent=ਜੇ ਤੁਸੀਂ ਵਾਲਟ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ ਤਾਂ ਇਸ ਨੂੰ ਬੰਦ ਵਿੰਡੋ ਉੱਤੇ ਖਿੱਚੋ main.dropZone.unknownDragboardContent=ਜੇ ਤੁਸੀਂ ਵਾਲਟ ਜੋੜਨਾ ਚਾਹੁੰਦੇ ਹੋ ਤਾਂ ਇਸ ਨੂੰ ਬੰਦ ਵਿੰਡੋ ਉੱਤੇ ਖਿੱਚੋ
## Vault List ## Vault List
main.vaultlist.emptyList.onboardingInstruction=ਵਾਲਟ ਜੋੜਨ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ main.vaultlist.emptyList.onboardingInstruction=ਵਾਲਟ ਜੋੜਨ ਲਈ ਇੱਥੇ ਕਲਿੱਕ ਕਰੋ
main.vaultlist.contextMenu.lock=ਲਾਕ ਕਰੋ main.vaultlist.contextMenu.remove=…ਵਾਲਟ ਹਟਾਓ
main.vaultlist.contextMenu.unlock=ਅਣ-ਲਾਕ ਕਰੋ…
main.vaultlist.contextMenu.unlockNow=ਹੁਣੇ ਅਣ-ਲਾਕ ਕਰੋ
main.vaultlist.contextMenu.reveal=ਡਰਾਇਵ ਦਿਖਾਓ
main.vaultlist.addVaultBtn=ਵਾਲਟ ਜੋੜੋ main.vaultlist.addVaultBtn=ਵਾਲਟ ਜੋੜੋ
## Vault Detail ## Vault Detail
### Welcome ### Welcome

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