mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-22 07:54:19 +00:00
Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fce262acb | ||
|
|
1159681824 | ||
|
|
c868ac32cc | ||
|
|
f85f953668 | ||
|
|
5185df631d | ||
|
|
617feec111 | ||
|
|
eb196165c5 | ||
|
|
8c37ab3675 | ||
|
|
7eaade1555 | ||
|
|
186c129a30 | ||
|
|
f74b9d3347 | ||
|
|
6038559172 | ||
|
|
98a899c7e2 | ||
|
|
783c8718c1 | ||
|
|
b47382811f | ||
|
|
62f279b9f7 | ||
|
|
ab961f1a8b | ||
|
|
9562db7e13 | ||
|
|
1ca2e029dc | ||
|
|
944e3e6c50 | ||
|
|
e134d2f422 | ||
|
|
04e5170403 | ||
|
|
ea008df9f7 | ||
|
|
e25dfaad35 | ||
|
|
e98f92c63f | ||
|
|
85b761a9d7 | ||
|
|
be8dcd4dc4 | ||
|
|
c01c405f20 | ||
|
|
b4f268f551 | ||
|
|
21f9e72cc5 | ||
|
|
4845984a35 | ||
|
|
b6f5ddaf30 | ||
|
|
1abf0e1bfa | ||
|
|
a8364a8c1a | ||
|
|
13c95868fa | ||
|
|
85ff7542f1 | ||
|
|
432be6dd80 | ||
|
|
ca0402aaf0 | ||
|
|
c7ef0056f9 | ||
|
|
cf13311665 | ||
|
|
a7133dbebe | ||
|
|
168f9b9fb9 | ||
|
|
2b332c3831 | ||
|
|
66e3625a0b | ||
|
|
20f88e203a | ||
|
|
66a8dc9811 | ||
|
|
9d89efc98c | ||
|
|
dfbe017316 | ||
|
|
d2680b9857 | ||
|
|
8e1be4c67b | ||
|
|
749e0ac02d | ||
|
|
a2ad7d69ab | ||
|
|
a47c05c87d | ||
|
|
6b871e56c8 | ||
|
|
40dbc0ba48 | ||
|
|
2e52afed51 | ||
|
|
64d54133f8 | ||
|
|
ac289cc3f7 | ||
|
|
189699d359 | ||
|
|
8e5035f506 | ||
|
|
0ce41e7ac3 | ||
|
|
c1a249fd7f | ||
|
|
c266c7583b | ||
|
|
00331d4857 | ||
|
|
8fd484e2bb | ||
|
|
165d740acd | ||
|
|
fd4010c6c9 | ||
|
|
aa2900fa9e | ||
|
|
76d1875e01 | ||
|
|
bc83e23a34 | ||
|
|
6a326bcbce | ||
|
|
573ad03d68 | ||
|
|
07efb3e94d | ||
|
|
89e680fb45 | ||
|
|
54128a0016 | ||
|
|
e2565097cd | ||
|
|
f827b1fc89 | ||
|
|
c0baab2329 | ||
|
|
70baa4d09f | ||
|
|
d710d406d0 | ||
|
|
554f245f5d | ||
|
|
34995088ba | ||
|
|
69c63702f2 | ||
|
|
db7e731554 | ||
|
|
e08b43bf17 | ||
|
|
58b45bacd1 | ||
|
|
4f942bc23f | ||
|
|
09a8618fa0 | ||
|
|
59f91267ae | ||
|
|
b3ff9423b4 | ||
|
|
c654951dc1 | ||
|
|
f3a03c71ec | ||
|
|
1fab246fcd | ||
|
|
69186b916c | ||
|
|
13db1aa9b9 | ||
|
|
d2cd2c923d | ||
|
|
c1000d9958 | ||
|
|
2810c044ea | ||
|
|
e09bd160b7 | ||
|
|
e6d1e4697c | ||
|
|
497e5ce73f | ||
|
|
2cdde54db6 | ||
|
|
6941603cdd | ||
|
|
cdca4e047d | ||
|
|
e32ce22d24 | ||
|
|
e75c415b46 | ||
|
|
0ab28602d1 | ||
|
|
62c8edff04 | ||
|
|
d01c6268f8 | ||
|
|
557aaa2480 | ||
|
|
78e43d401d | ||
|
|
7b08c5d287 | ||
|
|
9ad217ca55 | ||
|
|
1a0bc92bde | ||
|
|
21038212c1 | ||
|
|
b15471b4ff | ||
|
|
ff17b60f56 | ||
|
|
3284578445 | ||
|
|
4b670a59a3 | ||
|
|
3f928cf958 | ||
|
|
efebbc059a | ||
|
|
6e860d293a | ||
|
|
c0a9a95e4f |
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>buildkit</artifactId>
|
<artifactId>buildkit</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commons</artifactId>
|
<artifactId>commons</artifactId>
|
||||||
<name>Cryptomator Commons</name>
|
<name>Cryptomator Commons</name>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ 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;
|
||||||
@@ -25,6 +26,8 @@ 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;
|
||||||
@@ -55,6 +58,22 @@ 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,5 +3,8 @@ 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,8 +24,6 @@ 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.
|
||||||
@@ -37,7 +35,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_FILENAME_LENGTH_LIMIT = -1;
|
public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -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();
|
||||||
@@ -52,7 +50,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 filenameLengthLimit = new SimpleIntegerProperty(DEFAULT_FILENAME_LENGTH_LIMIT);
|
private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
|
||||||
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;
|
||||||
@@ -63,7 +61,7 @@ public class VaultSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Observable[] observables() {
|
Observable[] observables() {
|
||||||
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, filenameLengthLimit, actionAfterUnlock};
|
return new Observable[]{path, displayName, winDriveLetter, unlockAfterStartup, revealAfterMount, useCustomMountPath, customMountPath, usesReadOnlyMode, mountFlags, maxCleartextFilenameLength, actionAfterUnlock};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static VaultSettings withRandomId() {
|
public static VaultSettings withRandomId() {
|
||||||
@@ -152,8 +150,8 @@ public class VaultSettings {
|
|||||||
return mountFlags;
|
return mountFlags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IntegerProperty filenameLengthLimit() {
|
public IntegerProperty maxCleartextFilenameLength() {
|
||||||
return filenameLengthLimit;
|
return maxCleartextFilenameLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||||
|
|||||||
+4
-4
@@ -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("filenameLengthLimit").value(value.filenameLengthLimit().get());
|
out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().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 filenameLengthLimit = VaultSettings.DEFAULT_FILENAME_LENGTH_LIMIT;
|
int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
|
||||||
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 "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
|
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = 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.filenameLengthLimit().set(filenameLengthLimit);
|
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||||
return vaultSettings;
|
return vaultSettings;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ 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.common.Constants;
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
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.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -35,7 +37,8 @@ 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.file.NoSuchFileException;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
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;
|
||||||
@@ -45,13 +48,12 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.TimeUnit;
|
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;
|
||||||
@@ -100,32 +102,31 @@ public class Vault {
|
|||||||
// Commands
|
// Commands
|
||||||
// ********************************************************************************/
|
// ********************************************************************************/
|
||||||
|
|
||||||
private CryptoFileSystem createCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException {
|
private CryptoFileSystem createCryptoFileSystem(MasterkeyLoader keyLoader) throws IOException, MasterkeyLoadingFailedException {
|
||||||
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().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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int usedFilenameLengthLimit;
|
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
|
||||||
var fileSystemCapabilityChecker = new FileSystemCapabilityChecker();
|
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
|
||||||
if (flags.contains(FileSystemFlags.READONLY)) {
|
|
||||||
usedFilenameLengthLimit = Constants.MAX_CIPHERTEXT_NAME_LENGTH;
|
|
||||||
} else if (vaultSettings.filenameLengthLimit().get() == -1) {
|
|
||||||
LOG.debug("Determining file name length limitations...");
|
|
||||||
usedFilenameLengthLimit = fileSystemCapabilityChecker.determineSupportedFileNameLength(getPath());
|
|
||||||
vaultSettings.filenameLengthLimit().set(usedFilenameLengthLimit);
|
|
||||||
LOG.info("Storing file name length limit of {}", usedFilenameLengthLimit);
|
|
||||||
} else {
|
|
||||||
usedFilenameLengthLimit = vaultSettings.filenameLengthLimit().get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
assert usedFilenameLengthLimit > 0;
|
|
||||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||||
.withPassphrase(passphrase) //
|
.withKeyLoader(keyLoader) //
|
||||||
.withFlags(flags) //
|
.withFlags(flags) //
|
||||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||||
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
|
|
||||||
.withMaxNameLength(usedFilenameLengthLimit) //
|
|
||||||
.build();
|
.build();
|
||||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||||
}
|
}
|
||||||
@@ -142,20 +143,22 @@ public class Vault {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
public synchronized void unlock(MasterkeyLoader keyLoader) 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(), this::lockOnVolumeExit);
|
|
||||||
} 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) {
|
private void lockOnVolumeExit(Throwable t) {
|
||||||
@@ -324,6 +327,12 @@ public class Vault {
|
|||||||
return stats;
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
|
||||||
|
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||||
|
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||||
|
return VaultConfig.decode(token);
|
||||||
|
}
|
||||||
|
|
||||||
public Observable[] observables() {
|
public Observable[] observables() {
|
||||||
return new Observable[]{state};
|
return new Observable[]{state};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ 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;
|
||||||
@@ -20,6 +21,7 @@ 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;
|
||||||
@@ -27,6 +29,8 @@ import java.util.Optional;
|
|||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
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 {
|
||||||
@@ -51,19 +55,18 @@ public class VaultListManager {
|
|||||||
return vaultList;
|
return vaultList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Vault add(Path pathToVault) throws NoSuchFileException {
|
public Vault add(Path pathToVault) throws IOException {
|
||||||
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
|
||||||
if (!CryptoFileSystemProvider.containsVault(normalizedPathToVault, MASTERKEY_FILENAME)) {
|
if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {
|
||||||
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);
|
|
||||||
if (alreadyExistingVault.isPresent()) {
|
return get(normalizedPathToVault) //
|
||||||
return alreadyExistingVault.get();
|
.orElseGet(() -> {
|
||||||
} else {
|
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
vaultList.add(newVault);
|
||||||
vaultList.add(newVault);
|
return newVault;
|
||||||
return newVault;
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private VaultSettings newVaultSettings(Path path) {
|
private VaultSettings newVaultSettings(Path path) {
|
||||||
@@ -97,7 +100,7 @@ public class VaultListManager {
|
|||||||
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(VaultState.Value.ERROR);
|
compBuilder.initialVaultState(ERROR);
|
||||||
compBuilder.initialErrorCause(e);
|
compBuilder.initialErrorCause(e);
|
||||||
}
|
}
|
||||||
return compBuilder.build().vault();
|
return compBuilder.build().vault();
|
||||||
@@ -109,14 +112,14 @@ public class VaultListManager {
|
|||||||
return switch (previousState) {
|
return switch (previousState) {
|
||||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||||
try {
|
try {
|
||||||
VaultState.Value determinedState = determineVaultState(vault.getPath());
|
var determinedState = determineVaultState(vault.getPath());
|
||||||
state.set(determinedState);
|
state.set(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(VaultState.Value.ERROR);
|
state.set(ERROR);
|
||||||
vault.setLastKnownException(e);
|
vault.setLastKnownException(e);
|
||||||
yield VaultState.Value.ERROR;
|
yield ERROR;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||||
@@ -124,13 +127,14 @@ public class VaultListManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
||||||
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
if (!Files.exists(pathToVault)) {
|
||||||
return VaultState.Value.MISSING;
|
return VaultState.Value.MISSING;
|
||||||
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
|
||||||
return VaultState.Value.NEEDS_MIGRATION;
|
|
||||||
} else {
|
|
||||||
return VaultState.Value.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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ 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();
|
||||||
@@ -99,7 +98,6 @@ public class VaultModule {
|
|||||||
flags.append(" -oatomic_o_trunc");
|
flags.append(" -oatomic_o_trunc");
|
||||||
flags.append(" -oauto_xattr");
|
flags.append(" -oauto_xattr");
|
||||||
flags.append(" -oauto_cache");
|
flags.append(" -oauto_cache");
|
||||||
flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding
|
|
||||||
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
flags.append(" -onoappledouble"); // vastly impacts performance for some reason...
|
||||||
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>launcher</artifactId>
|
<artifactId>launcher</artifactId>
|
||||||
<name>Cryptomator Launcher</name>
|
<name>Cryptomator Launcher</name>
|
||||||
|
|||||||
+9
-9
@@ -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.5.15</version>
|
<version>1.6.0</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>1.9.14</cryptomator.cryptofs.version>
|
<cryptomator.cryptofs.version>2.1.0-beta5</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.3.1</cryptomator.fuse.version>
|
||||||
<cryptomator.dokany.version>1.3.0</cryptomator.dokany.version>
|
<cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
|
||||||
<cryptomator.webdav.version>1.2.0</cryptomator.webdav.version>
|
<cryptomator.webdav.version>1.2.2</cryptomator.webdav.version>
|
||||||
|
|
||||||
<!-- 3rd party dependencies -->
|
<!-- 3rd party dependencies -->
|
||||||
<javafx.version>16</javafx.version>
|
<javafx.version>16</javafx.version>
|
||||||
<commons-lang3.version>3.11</commons-lang3.version>
|
<commons-lang3.version>3.11</commons-lang3.version>
|
||||||
<jwt.version>3.13.0</jwt.version>
|
<jwt.version>3.15.0</jwt.version>
|
||||||
<easybind.version>2.1.0</easybind.version>
|
<easybind.version>2.1.0</easybind.version>
|
||||||
<guava.version>30.0-jre</guava.version>
|
<guava.version>30.1.1-jre</guava.version>
|
||||||
<dagger.version>2.32</dagger.version>
|
<dagger.version>2.35.1</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.0</junit.jupiter.version>
|
<junit.jupiter.version>5.7.1</junit.jupiter.version>
|
||||||
<mockito.version>3.6.0</mockito.version>
|
<mockito.version>3.9.0</mockito.version>
|
||||||
<hamcrest.version>2.2</hamcrest.version>
|
<hamcrest.version>2.2</hamcrest.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>1.5.15</version>
|
<version>1.6.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>ui</artifactId>
|
<artifactId>ui</artifactId>
|
||||||
<name>Cryptomator GUI</name>
|
<name>Cryptomator GUI</name>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import dagger.Provides;
|
|||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
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.NewPasswordController;
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
@@ -33,13 +33,6 @@ import java.util.ResourceBundle;
|
|||||||
@Module
|
@Module
|
||||||
public abstract class AddVaultModule {
|
public abstract class AddVaultModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@AddVaultWizardScoped
|
|
||||||
@Named("newPassword")
|
|
||||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
|
||||||
return new SimpleObjectProperty<>("");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@AddVaultWizardWindow
|
@AddVaultWizardWindow
|
||||||
@AddVaultWizardScoped
|
@AddVaultWizardScoped
|
||||||
@@ -167,8 +160,8 @@ public abstract class AddVaultModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(NewPasswordController.class)
|
@FxControllerKey(NewPasswordController.class)
|
||||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
return new NewPasswordController(resourceBundle, strengthRater);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
+1
-1
@@ -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 (NoSuchFileException e) {
|
} catch (IOException 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();
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-23
@@ -5,11 +5,18 @@ 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.NewPasswordController;
|
||||||
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;
|
||||||
@@ -17,7 +24,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.binding.ObjectBinding;
|
import javafx.beans.binding.ObjectBinding;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
@@ -31,13 +37,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.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
import java.nio.file.StandardOpenOption;
|
||||||
import java.util.Collections;
|
import java.security.SecureRandom;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
@@ -48,6 +54,7 @@ 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;
|
||||||
@@ -62,8 +69,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
private final StringProperty recoveryKeyProperty;
|
private final StringProperty recoveryKeyProperty;
|
||||||
private final VaultListManager vaultListManager;
|
private final VaultListManager vaultListManager;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
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;
|
||||||
@@ -71,9 +79,10 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
public ToggleGroup recoveryKeyChoice;
|
public ToggleGroup recoveryKeyChoice;
|
||||||
public Toggle showRecoveryKey;
|
public Toggle showRecoveryKey;
|
||||||
public Toggle skipRecoveryKey;
|
public Toggle skipRecoveryKey;
|
||||||
|
public NewPasswordController newPasswordSceneController;
|
||||||
|
|
||||||
@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) {
|
CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy<Scene> chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy<Scene> recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.chooseLocationScene = chooseLocationScene;
|
this.chooseLocationScene = chooseLocationScene;
|
||||||
this.recoveryKeyScene = recoveryKeyScene;
|
this.recoveryKeyScene = recoveryKeyScene;
|
||||||
@@ -87,8 +96,9 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
this.recoveryKeyProperty = recoveryKey;
|
this.recoveryKeyProperty = recoveryKey;
|
||||||
this.vaultListManager = vaultListManager;
|
this.vaultListManager = vaultListManager;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
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);
|
||||||
@@ -96,8 +106,11 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password);
|
readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
||||||
readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not()));
|
window.setOnHiding(event -> {
|
||||||
|
newPasswordSceneController.passwordField.wipe();
|
||||||
|
newPasswordSceneController.reenterField.wipe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -130,8 +143,8 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
Path pathToVault = vaultPathProperty.get();
|
Path pathToVault = vaultPathProperty.get();
|
||||||
processing.set(true);
|
processing.set(true);
|
||||||
Tasks.create(() -> {
|
Tasks.create(() -> {
|
||||||
initializeVault(pathToVault, password.get());
|
initializeVault(pathToVault);
|
||||||
return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get());
|
return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters());
|
||||||
}).onSuccess(recoveryKey -> {
|
}).onSuccess(recoveryKey -> {
|
||||||
initializationSucceeded(pathToVault);
|
initializationSucceeded(pathToVault);
|
||||||
recoveryKeyProperty.set(recoveryKey);
|
recoveryKeyProperty.set(recoveryKey);
|
||||||
@@ -148,7 +161,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
Path pathToVault = vaultPathProperty.get();
|
Path pathToVault = vaultPathProperty.get();
|
||||||
processing.set(true);
|
processing.set(true);
|
||||||
Tasks.create(() -> {
|
Tasks.create(() -> {
|
||||||
initializeVault(pathToVault, password.get());
|
initializeVault(pathToVault);
|
||||||
}).onSuccess(() -> {
|
}).onSuccess(() -> {
|
||||||
initializationSucceeded(pathToVault);
|
initializationSucceeded(pathToVault);
|
||||||
window.setScene(successScene.get());
|
window.setScene(successScene.get());
|
||||||
@@ -160,24 +173,35 @@ public class CreateNewVaultPasswordController implements FxController {
|
|||||||
}).runOnce(executor);
|
}).runOnce(executor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
private void initializeVault(Path path) throws IOException {
|
||||||
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
// 1. write masterkey:
|
||||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||||
.withPassphrase(passphrase) //
|
try (Masterkey masterkey = Masterkey.generate(csprng)) {
|
||||||
.withFlags(Collections.emptySet()) //
|
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters());
|
||||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
|
||||||
.build();
|
|
||||||
|
|
||||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
// 2. initialize vault:
|
||||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
try {
|
||||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||||
|
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||||
|
|
||||||
|
// 3. write vault-internal readme file:
|
||||||
|
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||||
|
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||||
|
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||||
|
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||||
|
}
|
||||||
|
} catch (CryptoException e) {
|
||||||
|
throw new IOException("Failed initialize vault.", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. write vault-external readme file:
|
||||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +209,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 (NoSuchFileException e) {
|
} catch (IOException e) {
|
||||||
throw new UncheckedIOException(e);
|
throw new UncheckedIOException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-17
@@ -2,29 +2,33 @@ 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.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||||
|
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;
|
||||||
import org.cryptomator.ui.common.FxController;
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||||
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.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
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.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
|
||||||
@@ -34,29 +38,36 @@ public class ChangePasswordController implements FxController {
|
|||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
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;
|
||||||
|
public NewPasswordController newPasswordController;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) {
|
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.newPassword = newPassword;
|
|
||||||
this.errorComponent = errorComponent;
|
this.errorComponent = errorComponent;
|
||||||
this.keychain = keychain;
|
this.keychain = keychain;
|
||||||
|
this.csprng = csprng;
|
||||||
|
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not();
|
||||||
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty();
|
||||||
BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword);
|
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not()));
|
||||||
finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid));
|
window.setOnHiding(event -> {
|
||||||
|
oldPasswordField.wipe();
|
||||||
|
newPasswordController.passwordField.wipe();
|
||||||
|
newPasswordController.reenterField.wipe();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -67,24 +78,31 @@ public class ChangePasswordController implements FxController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void finish() {
|
public void finish() {
|
||||||
try {
|
try {
|
||||||
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
|
CharSequence oldPassphrase = oldPasswordField.getCharacters();
|
||||||
|
CharSequence newPassphrase = newPasswordController.passwordField.getCharacters();
|
||||||
|
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||||
|
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||||
|
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||||
|
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||||
|
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||||
window.close();
|
|
||||||
updatePasswordInSystemkeychain();
|
updatePasswordInSystemkeychain();
|
||||||
} catch (IOException e) {
|
window.close();
|
||||||
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePasswordInSystemkeychain() {
|
private void updatePasswordInSystemkeychain() {
|
||||||
if (keychain.isSupported()) {
|
if (keychain.isSupported()) {
|
||||||
try {
|
try {
|
||||||
keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get()));
|
keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters());
|
||||||
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
|
LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName());
|
||||||
} catch (KeychainAccessException e) {
|
} catch (KeychainAccessException e) {
|
||||||
LOG.error("Failed to update password in system keychain.", e);
|
LOG.error("Failed to update password in system keychain.", e);
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
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.NewPasswordController;
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
@@ -16,8 +16,6 @@ import org.cryptomator.ui.common.StageFactory;
|
|||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -27,13 +25,6 @@ import java.util.ResourceBundle;
|
|||||||
@Module
|
@Module
|
||||||
abstract class ChangePasswordModule {
|
abstract class ChangePasswordModule {
|
||||||
|
|
||||||
@Provides
|
|
||||||
@ChangePasswordScoped
|
|
||||||
@Named("newPassword")
|
|
||||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
|
||||||
return new SimpleObjectProperty<>("");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@ChangePasswordWindow
|
@ChangePasswordWindow
|
||||||
@ChangePasswordScoped
|
@ChangePasswordScoped
|
||||||
@@ -71,8 +62,8 @@ abstract class ChangePasswordModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(NewPasswordController.class)
|
@FxControllerKey(NewPasswordController.class)
|
||||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
return new NewPasswordController(resourceBundle, strengthRater);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ public enum FxmlFile {
|
|||||||
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||||
ERROR("/fxml/error.fxml"), //
|
ERROR("/fxml/error.fxml"), //
|
||||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||||
|
HEALTH_START("/fxml/health_start.fxml"), //
|
||||||
|
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||||
@@ -26,8 +28,9 @@ 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("/fxml/unlock.fxml"),
|
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.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"), //
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import javafx.beans.Observable;
|
|||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.IntegerProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||||
|
import javafx.beans.property.ReadOnlyBooleanWrapper;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -18,8 +19,8 @@ public class NewPasswordController implements FxController {
|
|||||||
|
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
private final PasswordStrengthUtil strengthRater;
|
private final PasswordStrengthUtil strengthRater;
|
||||||
private final ObjectProperty<CharSequence> password;
|
|
||||||
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1);
|
||||||
|
private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper();
|
||||||
|
|
||||||
public NiceSecurePasswordField passwordField;
|
public NiceSecurePasswordField passwordField;
|
||||||
public NiceSecurePasswordField reenterField;
|
public NiceSecurePasswordField reenterField;
|
||||||
@@ -31,10 +32,9 @@ public class NewPasswordController implements FxController {
|
|||||||
public FontAwesome5IconView passwordMatchCheckmark;
|
public FontAwesome5IconView passwordMatchCheckmark;
|
||||||
public FontAwesome5IconView passwordMatchCross;
|
public FontAwesome5IconView passwordMatchCross;
|
||||||
|
|
||||||
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty<CharSequence> password) {
|
public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
this.strengthRater = strengthRater;
|
this.strengthRater = strengthRater;
|
||||||
this.password = password;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -44,7 +44,7 @@ public class NewPasswordController implements FxController {
|
|||||||
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength));
|
||||||
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
|
||||||
|
|
||||||
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty());
|
BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty());
|
||||||
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty();
|
||||||
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty);
|
||||||
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross));
|
||||||
@@ -54,6 +54,7 @@ public class NewPasswordController implements FxController {
|
|||||||
reenterField.textProperty().addListener(this::passwordsDidChange);
|
reenterField.textProperty().addListener(this::passwordsDidChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
private FontAwesome5IconView getIconViewForPasswordStrengthLabel() {
|
||||||
if (passwordField.getCharacters().length() == 0) {
|
if (passwordField.getCharacters().length() == 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -67,17 +68,19 @@ public class NewPasswordController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) {
|
||||||
if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) {
|
||||||
password.set(passwordField.getCharacters());
|
passwordsMatchAndSufficient.setValue(true);
|
||||||
} else {
|
|
||||||
password.set("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean hasSamePasswordInBothFields() {
|
private boolean passwordFieldsMatch() {
|
||||||
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() {
|
||||||
|
return passwordsMatchAndSufficient.getReadOnlyProperty();
|
||||||
|
}
|
||||||
|
|
||||||
/* Getter/Setter */
|
/* Getter/Setter */
|
||||||
|
|
||||||
public IntegerProperty passwordStrengthProperty() {
|
public IntegerProperty passwordStrengthProperty() {
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ package org.cryptomator.ui.controls;
|
|||||||
public enum FontAwesome5Icon {
|
public enum FontAwesome5Icon {
|
||||||
ANCHOR("\uF13D"), //
|
ANCHOR("\uF13D"), //
|
||||||
ARROW_UP("\uF062"), //
|
ARROW_UP("\uF062"), //
|
||||||
|
BAN("\uF05E"), //
|
||||||
BUG("\uF188"), //
|
BUG("\uF188"), //
|
||||||
CHECK("\uF00C"), //
|
CHECK("\uF00C"), //
|
||||||
|
CLOCK("\uF017"), //
|
||||||
COG("\uF013"), //
|
COG("\uF013"), //
|
||||||
COGS("\uF085"), //
|
COGS("\uF085"), //
|
||||||
COPY("\uF0C5"), //
|
COPY("\uF0C5"), //
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public class FormattedLabel extends Label {
|
|||||||
|
|
||||||
private final StringProperty format = new SimpleStringProperty("");
|
private final StringProperty format = new SimpleStringProperty("");
|
||||||
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
private final ObjectProperty<Object> arg1 = new SimpleObjectProperty<>();
|
||||||
|
private final ObjectProperty<Object> arg2 = new SimpleObjectProperty<>();
|
||||||
// add arg2, arg3, ... on demand
|
// add arg2, arg3, ... on demand
|
||||||
|
|
||||||
public FormattedLabel() {
|
public FormattedLabel() {
|
||||||
@@ -19,11 +20,11 @@ public class FormattedLabel extends Label {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected StringBinding createStringBinding() {
|
protected StringBinding createStringBinding() {
|
||||||
return Bindings.createStringBinding(this::updateText, format, arg1);
|
return Bindings.createStringBinding(this::updateText, format, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String updateText() {
|
private String updateText() {
|
||||||
return String.format(format.get(), arg1.get());
|
return String.format(format.get(), arg1.get(), arg2.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Observables */
|
/* Observables */
|
||||||
@@ -51,4 +52,16 @@ public class FormattedLabel extends Label {
|
|||||||
public void setArg1(Object arg1) {
|
public void setArg1(Object arg1) {
|
||||||
this.arg1.set(arg1);
|
this.arg1.set(arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<Object> arg2Property() {
|
||||||
|
return arg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getArg2() {
|
||||||
|
return arg2.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setArg2(Object arg2) {
|
||||||
|
this.arg2.set(arg2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ public class SecurePasswordField extends TextField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateCapsLocked() {
|
private void updateCapsLocked() {
|
||||||
// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
|
//TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680)
|
||||||
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Suppliers;
|
||||||
|
import dagger.Lazy;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.concurrent.Service;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
public class BatchService extends Service<Void> {
|
||||||
|
|
||||||
|
private final Iterator<HealthCheckTask> remainingTasks;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public BatchService(Iterable<HealthCheckTask> tasks) {
|
||||||
|
this.remainingTasks = tasks.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Task<Void> createTask() {
|
||||||
|
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
|
||||||
|
return remainingTasks.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void succeeded() {
|
||||||
|
if (remainingTasks.hasNext()) {
|
||||||
|
this.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import com.tobiasdiez.easybind.EasyObservableList;
|
||||||
|
import com.tobiasdiez.easybind.Subscription;
|
||||||
|
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class CheckDetailController implements FxController {
|
||||||
|
|
||||||
|
private final EasyObservableList<DiagnosticResult> results;
|
||||||
|
private final OptionalBinding<Worker.State> taskState;
|
||||||
|
private final Binding<String> taskName;
|
||||||
|
private final Binding<Number> taskDuration;
|
||||||
|
private final ResultListCellFactory resultListCellFactory;
|
||||||
|
private final Binding<Boolean> taskRunning;
|
||||||
|
private final Binding<Boolean> taskScheduled;
|
||||||
|
private final Binding<Boolean> taskFinished;
|
||||||
|
private final Binding<Boolean> taskNotStarted;
|
||||||
|
private final Binding<Boolean> taskSucceeded;
|
||||||
|
private final Binding<Boolean> taskFailed;
|
||||||
|
private final Binding<Boolean> taskCancelled;
|
||||||
|
private final Binding<Number> countOfWarnSeverity;
|
||||||
|
private final Binding<Number> countOfCritSeverity;
|
||||||
|
|
||||||
|
public ListView<DiagnosticResult> resultsListView;
|
||||||
|
private Subscription resultSubscription;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory) {
|
||||||
|
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||||
|
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
|
||||||
|
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
|
||||||
|
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L);
|
||||||
|
this.resultListCellFactory = resultListCellFactory;
|
||||||
|
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
|
||||||
|
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
|
||||||
|
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
|
||||||
|
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
|
||||||
|
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
|
||||||
|
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
|
||||||
|
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
|
||||||
|
this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
|
||||||
|
this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
|
||||||
|
selectedTask.addListener(this::selectedTaskChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
|
||||||
|
if (resultSubscription != null) {
|
||||||
|
resultSubscription.unsubscribe();
|
||||||
|
}
|
||||||
|
if (newValue != null) {
|
||||||
|
resultSubscription = EasyBind.bindContent(results, newValue.results());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
|
||||||
|
return stream -> stream.filter(item -> severity.equals(item.getServerity())).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
resultsListView.setItems(results);
|
||||||
|
resultsListView.setCellFactory(resultListCellFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
|
||||||
|
public String getTaskName() {
|
||||||
|
return taskName.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<String> taskNameProperty() {
|
||||||
|
return taskName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Number getTaskDuration() {
|
||||||
|
return taskDuration.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> taskDurationProperty() {
|
||||||
|
return taskDuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCountOfWarnSeverity() {
|
||||||
|
return countOfWarnSeverity.getValue().longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> countOfWarnSeverityProperty() {
|
||||||
|
return countOfWarnSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getCountOfCritSeverity() {
|
||||||
|
return countOfCritSeverity.getValue().longValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Number> countOfCritSeverityProperty() {
|
||||||
|
return countOfCritSeverity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskRunning() {
|
||||||
|
return taskRunning.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskRunningProperty() {
|
||||||
|
return taskRunning;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskFinished() {
|
||||||
|
return taskFinished.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskFinishedProperty() {
|
||||||
|
return taskFinished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskScheduled() {
|
||||||
|
return taskScheduled.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskScheduledProperty() {
|
||||||
|
return taskScheduled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskNotStarted() {
|
||||||
|
return taskNotStarted.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskNotStartedProperty() {
|
||||||
|
return taskNotStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskSucceeded() {
|
||||||
|
return taskSucceeded.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskSucceededProperty() {
|
||||||
|
return taskSucceeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskFailed() {
|
||||||
|
return taskFailed.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskFailedProperty() {
|
||||||
|
return taskFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTaskCancelled() {
|
||||||
|
return taskCancelled.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> taskCancelledProperty() {
|
||||||
|
return taskCancelled;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
|
||||||
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.geometry.Insets;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
|
||||||
|
class CheckListCell extends ListCell<HealthCheckTask> {
|
||||||
|
|
||||||
|
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
|
||||||
|
|
||||||
|
CheckListCell() {
|
||||||
|
setPadding(new Insets(6));
|
||||||
|
setAlignment(Pos.CENTER_LEFT);
|
||||||
|
setContentDisplay(ContentDisplay.LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(HealthCheckTask item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
|
||||||
|
if (item != null) {
|
||||||
|
textProperty().bind(item.titleProperty());
|
||||||
|
item.stateProperty().addListener(this::stateChanged);
|
||||||
|
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty()));
|
||||||
|
stateIcon.setGlyph(glyphForState(item.getState()));
|
||||||
|
} else {
|
||||||
|
textProperty().unbind();
|
||||||
|
graphicProperty().unbind();
|
||||||
|
setGraphic(null);
|
||||||
|
setText(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
|
||||||
|
stateIcon.setGlyph(glyphForState(newState));
|
||||||
|
stateIcon.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node graphicForState(Worker.State state) {
|
||||||
|
return switch (state) {
|
||||||
|
case READY -> null;
|
||||||
|
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private FontAwesome5Icon glyphForState(Worker.State state) {
|
||||||
|
return switch (state) {
|
||||||
|
case READY -> FontAwesome5Icon.COG; //just a placeholder
|
||||||
|
case SCHEDULED -> FontAwesome5Icon.CLOCK;
|
||||||
|
case RUNNING -> FontAwesome5Icon.SPINNER;
|
||||||
|
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||||
|
case CANCELLED -> FontAwesome5Icon.BAN;
|
||||||
|
case SUCCEEDED -> FontAwesome5Icon.CHECK;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.IntegerProperty;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleIntegerProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.cell.CheckBoxListCell;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class CheckListController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
|
||||||
|
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final ObservableList<HealthCheckTask> tasks;
|
||||||
|
private final ReportWriter reportWriter;
|
||||||
|
private final ExecutorService executorService;
|
||||||
|
private final ObjectProperty<HealthCheckTask> selectedTask;
|
||||||
|
private final Lazy<ErrorComponent.Builder> errorComponenBuilder;
|
||||||
|
private final SimpleObjectProperty<Worker<?>> runningTask;
|
||||||
|
private final Binding<Boolean> running;
|
||||||
|
private final Binding<Boolean> finished;
|
||||||
|
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
|
||||||
|
private final IntegerProperty numberOfPickedChecks;
|
||||||
|
private final BooleanBinding anyCheckSelected;
|
||||||
|
private final BooleanProperty showResultScreen;
|
||||||
|
|
||||||
|
/* FXML */
|
||||||
|
public ListView<HealthCheckTask> checksListView;
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponenBuilder) {
|
||||||
|
this.window = window;
|
||||||
|
this.tasks = FXCollections.observableArrayList(tasks.get());
|
||||||
|
this.reportWriter = reportWriteTask;
|
||||||
|
this.executorService = executorService;
|
||||||
|
this.selectedTask = selectedTask;
|
||||||
|
this.errorComponenBuilder = errorComponenBuilder;
|
||||||
|
this.runningTask = new SimpleObjectProperty<>();
|
||||||
|
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
|
||||||
|
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
|
||||||
|
this.listPickIndicators = new HashMap<>();
|
||||||
|
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
|
||||||
|
this.tasks.forEach(task -> {
|
||||||
|
var entrySelectedProp = new SimpleBooleanProperty(false);
|
||||||
|
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
|
||||||
|
listPickIndicators.put(task, entrySelectedProp);
|
||||||
|
});
|
||||||
|
this.anyCheckSelected = selectedTask.isNotNull();
|
||||||
|
this.showResultScreen = new SimpleBooleanProperty(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void initialize() {
|
||||||
|
checksListView.setItems(tasks);
|
||||||
|
checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter<HealthCheckTask>() {
|
||||||
|
@Override
|
||||||
|
public String toString(HealthCheckTask object) {
|
||||||
|
return object.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HealthCheckTask fromString(String string) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void toggleSelectAll(ActionEvent event) {
|
||||||
|
if (event.getSource() instanceof CheckBox c) {
|
||||||
|
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void runSelectedChecks() {
|
||||||
|
Preconditions.checkState(runningTask.get() == null);
|
||||||
|
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
|
||||||
|
var batchService = new BatchService(batch);
|
||||||
|
batchService.setExecutor(executorService);
|
||||||
|
batchService.start();
|
||||||
|
runningTask.set(batchService);
|
||||||
|
showResultScreen.set(true);
|
||||||
|
checksListView.getSelectionModel().select(batch.get(0));
|
||||||
|
checksListView.setCellFactory(view -> new CheckListCell());
|
||||||
|
window.sizeToScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public synchronized void cancelCheck() {
|
||||||
|
Preconditions.checkState(runningTask.get() != null);
|
||||||
|
runningTask.get().cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void exportResults() {
|
||||||
|
try {
|
||||||
|
reportWriter.writeReport(tasks);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOG.error("Failed to write health check report.", e);
|
||||||
|
errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter/Setter */
|
||||||
|
public boolean isRunning() {
|
||||||
|
return running.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> runningProperty() {
|
||||||
|
return running;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isFinished() {
|
||||||
|
return finished.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<Boolean> finishedProperty() {
|
||||||
|
return finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAnyCheckSelected() {
|
||||||
|
return anyCheckSelected.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanBinding anyCheckSelectedProperty() {
|
||||||
|
return anyCheckSelected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getShowResultScreen() {
|
||||||
|
return showResultScreen.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BooleanProperty showResultScreenProperty() {
|
||||||
|
return showResultScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getNumberOfPickedChecks() {
|
||||||
|
return numberOfPickedChecks.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IntegerProperty numberOfPickedChecksProperty() {
|
||||||
|
return numberOfPickedChecks;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
@Subcomponent(modules = {HealthCheckModule.class})
|
||||||
|
public interface HealthCheckComponent {
|
||||||
|
|
||||||
|
@HealthCheckWindow
|
||||||
|
Stage window();
|
||||||
|
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_START)
|
||||||
|
Lazy<Scene> scene();
|
||||||
|
|
||||||
|
default Stage showHealthCheckWindow() {
|
||||||
|
Stage stage = window();
|
||||||
|
stage.setScene(scene().get());
|
||||||
|
stage.show();
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@HealthCheckWindow Vault vault);
|
||||||
|
|
||||||
|
HealthCheckComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.StageFactory;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Modality;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||||
|
abstract class HealthCheckModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static AtomicReference<Masterkey> provideMasterkeyRef() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static AtomicReference<VaultConfig> provideVaultConfigRef() {
|
||||||
|
return new AtomicReference<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Collection<HealthCheck> provideAvailableHealthChecks() {
|
||||||
|
return HealthCheck.allChecks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
|
||||||
|
return new SimpleObjectProperty<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Only inject with Lazy-Wrapper!*/
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
|
||||||
|
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
|
||||||
|
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckWindow
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
|
||||||
|
Stage stage = factory.create();
|
||||||
|
stage.setTitle(resourceBundle.getString("health.title"));
|
||||||
|
stage.setResizable(true);
|
||||||
|
stage.initModality(Modality.WINDOW_MODAL);
|
||||||
|
stage.initOwner(owner);
|
||||||
|
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
|
||||||
|
return stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@HealthCheckScoped
|
||||||
|
static ChangeListener<Boolean> provideWindowShowingChangeListener(AtomicReference<Masterkey> masterkey) {
|
||||||
|
return (observable, wasShowing, isShowing) -> {
|
||||||
|
if (!isShowing) {
|
||||||
|
Optional.ofNullable(masterkey.getAndSet(null)).ifPresent(Masterkey::destroy);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_START)
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Scene provideHealthStartScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
|
||||||
|
@HealthCheckScoped
|
||||||
|
static Scene provideHealthCheckListScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
return fxmlLoaders.createScene(FxmlFile.HEALTH_CHECK_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(StartController.class)
|
||||||
|
abstract FxController bindStartController(StartController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CheckListController.class)
|
||||||
|
abstract FxController bindCheckController(CheckListController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(CheckDetailController.class)
|
||||||
|
abstract FxController bindCheckDetailController(CheckDetailController controller);
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FxControllerKey(ResultListCellController.class)
|
||||||
|
abstract FxController bindResultListCellController(ResultListCellController controller);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@interface HealthCheckScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.property.LongProperty;
|
||||||
|
import javafx.beans.property.SimpleLongProperty;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.concurrent.Task;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.MissingResourceException;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
|
||||||
|
class HealthCheckTask extends Task<Void> {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
|
||||||
|
|
||||||
|
private final Path vaultPath;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
private final Masterkey masterkey;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final HealthCheck check;
|
||||||
|
private final ObservableList<DiagnosticResult> results;
|
||||||
|
private final LongProperty durationInMillis;
|
||||||
|
|
||||||
|
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
|
||||||
|
this.vaultPath = Objects.requireNonNull(vaultPath);
|
||||||
|
this.vaultConfig = Objects.requireNonNull(vaultConfig);
|
||||||
|
this.masterkey = Objects.requireNonNull(masterkey);
|
||||||
|
this.csprng = Objects.requireNonNull(csprng);
|
||||||
|
this.check = Objects.requireNonNull(check);
|
||||||
|
this.results = FXCollections.observableArrayList();
|
||||||
|
try {
|
||||||
|
updateTitle(resourceBundle.getString("health." + check.identifier()));
|
||||||
|
} catch (MissingResourceException e) {
|
||||||
|
LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier());
|
||||||
|
updateTitle(check.identifier());
|
||||||
|
}
|
||||||
|
this.durationInMillis = new SimpleLongProperty(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void call() {
|
||||||
|
Instant start = Instant.now();
|
||||||
|
try (var masterkeyClone = masterkey.clone(); //
|
||||||
|
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||||
|
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
|
||||||
|
if (isCancelled()) {
|
||||||
|
throw new CancellationException();
|
||||||
|
}
|
||||||
|
// FIXME: slowdown for demonstration purposes only:
|
||||||
|
try {
|
||||||
|
Thread.sleep(2000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Platform.runLater(() -> results.add(result));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void scheduled() {
|
||||||
|
LOG.info("starting {}", check.identifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void done() {
|
||||||
|
LOG.info("finished {}", check.identifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Getter */
|
||||||
|
|
||||||
|
public ObservableList<DiagnosticResult> results() {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HealthCheck getCheck() {
|
||||||
|
return check;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LongProperty durationInMillisProperty() {
|
||||||
|
return durationInMillis;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getDurationInMillis() {
|
||||||
|
return durationInMillis.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
@interface HealthCheckWindow {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||||
|
import org.cryptomator.common.Environment;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class ReportWriter {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
|
||||||
|
private static final String REPORT_HEADER = """
|
||||||
|
**************************************
|
||||||
|
* Cryptomator Vault Health Report *
|
||||||
|
**************************************
|
||||||
|
Analyzed vault: %s (Current name "%s")
|
||||||
|
Vault storage path: %s
|
||||||
|
""";
|
||||||
|
private static final String REPORT_CHECK_HEADER = """
|
||||||
|
|
||||||
|
|
||||||
|
Check %s
|
||||||
|
------------------------------
|
||||||
|
""";
|
||||||
|
private static final String REPORT_CHECK_RESULT = "%8s - %s\n";
|
||||||
|
private static final DateTimeFormatter TIME_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault());
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
private final Application application;
|
||||||
|
private final Path exportDestination;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReportWriter(@HealthCheckWindow Vault vault, AtomicReference<VaultConfig> vaultConfigRef, Application application, Environment env) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get());
|
||||||
|
this.application = application;
|
||||||
|
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
|
||||||
|
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||||
|
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||||
|
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
|
||||||
|
for (var task : tasks) {
|
||||||
|
if (task.getState() == Worker.State.READY) {
|
||||||
|
LOG.debug("Skipping not performed check {}.", task.getCheck().identifier());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier()));
|
||||||
|
switch (task.getState()) {
|
||||||
|
case SUCCEEDED -> {
|
||||||
|
writer.write("STATUS: SUCCESS\nRESULTS:\n");
|
||||||
|
for (var result : task.results()) {
|
||||||
|
writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case CANCELLED -> writer.write("STATUS: CANCELED\n");
|
||||||
|
case FAILED -> {
|
||||||
|
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier());
|
||||||
|
writer.write(prepareFailureMsg(task));
|
||||||
|
}
|
||||||
|
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reveal();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String prepareFailureMsg(HealthCheckTask task) {
|
||||||
|
if (task.getException() != null) {
|
||||||
|
return ExceptionUtils.getStackTrace(task.getException()) //
|
||||||
|
.lines() //
|
||||||
|
.map(line -> "\t\t" + line + "\n") //
|
||||||
|
.collect(Collectors.joining());
|
||||||
|
} else {
|
||||||
|
return "Unknown reason of failure.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reveal() {
|
||||||
|
application.getHostServices().showDocument(exportDestination.getParent().toUri().toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
class ResultFixApplier {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
|
||||||
|
|
||||||
|
private final Path vaultPath;
|
||||||
|
private final SecureRandom csprng;
|
||||||
|
private final Masterkey masterkey;
|
||||||
|
private final VaultConfig vaultConfig;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
|
||||||
|
this.vaultPath = vault.getPath();
|
||||||
|
this.masterkey = masterkeyRef.get();
|
||||||
|
this.vaultConfig = vaultConfigRef.get();
|
||||||
|
this.csprng = csprng;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fix(DiagnosticResult result) {
|
||||||
|
Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result");
|
||||||
|
try (var masterkeyClone = masterkey.clone(); //
|
||||||
|
var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) {
|
||||||
|
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
|
||||||
|
} catch (Exception e) {
|
||||||
|
LOG.error("Failed to apply fix", e);
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
|
||||||
|
alert.showAndWait();
|
||||||
|
//TODO: real error/not supported handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import com.tobiasdiez.easybind.EasyBind;
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||||
|
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.beans.binding.Binding;
|
||||||
|
import javafx.beans.property.ObjectProperty;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
|
||||||
|
// unscoped because each cell needs its own controller
|
||||||
|
public class ResultListCellController implements FxController {
|
||||||
|
|
||||||
|
private final ResultFixApplier fixApplier;
|
||||||
|
private final ObjectProperty<DiagnosticResult> result;
|
||||||
|
private final Binding<String> description;
|
||||||
|
|
||||||
|
public FontAwesome5IconView iconView;
|
||||||
|
public Button actionButton;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ResultListCellController(ResultFixApplier fixApplier) {
|
||||||
|
this.result = new SimpleObjectProperty<>(null);
|
||||||
|
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
|
||||||
|
this.fixApplier = fixApplier;
|
||||||
|
result.addListener(this::updateCellContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
|
||||||
|
iconView.getStyleClass().clear();
|
||||||
|
actionButton.setVisible(false);
|
||||||
|
//TODO: see comment in case WARN
|
||||||
|
actionButton.setManaged(false);
|
||||||
|
switch (newVal.getServerity()) {
|
||||||
|
case INFO -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-muted");
|
||||||
|
}
|
||||||
|
case GOOD -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.CHECK);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-primary");
|
||||||
|
}
|
||||||
|
case WARN -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-orange");
|
||||||
|
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
|
||||||
|
// before both are not fix, do not show the button
|
||||||
|
//actionButton.setVisible(true);
|
||||||
|
}
|
||||||
|
case CRITICAL -> {
|
||||||
|
iconView.setGlyph(FontAwesome5Icon.TIMES);
|
||||||
|
iconView.getStyleClass().add("glyph-icon-red");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void runResultAction() {
|
||||||
|
final var realResult = result.get();
|
||||||
|
if (realResult != null) {
|
||||||
|
fixApplier.fix(realResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Getter & Setter */
|
||||||
|
|
||||||
|
|
||||||
|
public DiagnosticResult getResult() {
|
||||||
|
return result.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResult(DiagnosticResult result) {
|
||||||
|
this.result.set(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObjectProperty<DiagnosticResult> resultProperty() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Binding<String> descriptionProperty() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
|
||||||
|
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.control.ContentDisplay;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.util.Callback;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
|
||||||
|
|
||||||
|
private final FxmlLoaderFactory fxmlLoaders;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||||
|
this.fxmlLoaders = fxmlLoaders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
|
||||||
|
try {
|
||||||
|
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
|
||||||
|
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new UncheckedIOException("Failed to load /fxml/health_result_listcell.fxml.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class Cell extends ListCell<DiagnosticResult> {
|
||||||
|
|
||||||
|
private final Parent node;
|
||||||
|
private final ResultListCellController controller;
|
||||||
|
|
||||||
|
public Cell(Parent node, ResultListCellController controller) {
|
||||||
|
this.node = node;
|
||||||
|
this.controller = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(DiagnosticResult item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (item == null || empty) {
|
||||||
|
setText(null);
|
||||||
|
setGraphic(null);
|
||||||
|
} else {
|
||||||
|
controller.setResult(item);
|
||||||
|
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||||
|
setGraphic(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.cryptomator.ui.health;
|
||||||
|
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||||
|
import org.cryptomator.cryptofs.VaultKeyInvalidException;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
import org.cryptomator.ui.common.ErrorComponent;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@HealthCheckScoped
|
||||||
|
public class StartController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||||
|
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||||
|
private final ExecutorService executor;
|
||||||
|
private final AtomicReference<Masterkey> masterkeyRef;
|
||||||
|
private final AtomicReference<VaultConfig> vaultConfigRef;
|
||||||
|
private final Lazy<Scene> checkScene;
|
||||||
|
private final Lazy<ErrorComponent.Builder> errorComponent;
|
||||||
|
|
||||||
|
/* FXML */
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
|
||||||
|
this.window = window;
|
||||||
|
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||||
|
this.executor = executor;
|
||||||
|
this.masterkeyRef = masterkeyRef;
|
||||||
|
this.vaultConfigRef = vaultConfigRef;
|
||||||
|
this.checkScene = checkScene;
|
||||||
|
this.errorComponent = errorComponent;
|
||||||
|
|
||||||
|
//TODO: this is ugly
|
||||||
|
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
|
||||||
|
//or: load vault config in a previous step to see if it is loadable.
|
||||||
|
VaultConfig.UnverifiedVaultConfig tmp;
|
||||||
|
try {
|
||||||
|
tmp = vault.getUnverifiedVaultConfig();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
tmp = null;
|
||||||
|
}
|
||||||
|
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void close() {
|
||||||
|
LOG.trace("StartController.close()");
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void next() {
|
||||||
|
LOG.trace("StartController.next()");
|
||||||
|
executor.submit(this::loadKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadKey() {
|
||||||
|
assert !Platform.isFxApplicationThread();
|
||||||
|
assert unverifiedVaultConfig.isPresent();
|
||||||
|
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
|
||||||
|
var unverifiedCfg = unverifiedVaultConfig.get();
|
||||||
|
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
|
||||||
|
vaultConfigRef.set(verifiedCfg);
|
||||||
|
var old = masterkeyRef.getAndSet(masterkey.clone());
|
||||||
|
if (old != null) {
|
||||||
|
old.destroy();
|
||||||
|
}
|
||||||
|
Platform.runLater(this::loadedKey);
|
||||||
|
} catch (MasterkeyLoadingFailedException e) {
|
||||||
|
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||||
|
// retry
|
||||||
|
loadKey();
|
||||||
|
} else {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
}
|
||||||
|
} catch (VaultKeyInvalidException e) {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
} catch (VaultConfigLoadException e) {
|
||||||
|
Platform.runLater(() -> loadingKeyFailed(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadedKey() {
|
||||||
|
LOG.debug("Loaded valid key");
|
||||||
|
window.setScene(checkScene.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadingKeyFailed(Exception e) {
|
||||||
|
if (e instanceof UnlockCancelledException) {
|
||||||
|
// ok
|
||||||
|
} else if (e instanceof VaultKeyInvalidException) {
|
||||||
|
LOG.error("Invalid key"); //TODO: specific error screen
|
||||||
|
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||||
|
} else {
|
||||||
|
LOG.error("Failed to load key.", e);
|
||||||
|
errorComponent.get().window(window).cause(e).build().showErrorScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInvalidConfig() {
|
||||||
|
return unverifiedVaultConfig.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import javax.inject.Qualifier;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||||
|
|
||||||
|
@Qualifier
|
||||||
|
@Documented
|
||||||
|
@Retention(RUNTIME)
|
||||||
|
public @interface KeyLoading {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import dagger.BindsInstance;
|
||||||
|
import dagger.Subcomponent;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
@KeyLoadingScoped
|
||||||
|
@Subcomponent(modules = {KeyLoadingModule.class})
|
||||||
|
public interface KeyLoadingComponent {
|
||||||
|
|
||||||
|
@KeyLoading
|
||||||
|
KeyLoadingStrategy keyloadingStrategy();
|
||||||
|
|
||||||
|
@Subcomponent.Builder
|
||||||
|
interface Builder {
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder vault(@KeyLoading Vault vault);
|
||||||
|
|
||||||
|
@BindsInstance
|
||||||
|
Builder window(@KeyLoading Stage window);
|
||||||
|
|
||||||
|
KeyLoadingComponent build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||||
|
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||||
|
|
||||||
|
import javax.inject.Provider;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
|
@Module(includes = {MasterkeyFileLoadingModule.class})
|
||||||
|
abstract class KeyLoadingModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoading
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||||
|
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoading
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||||
|
try {
|
||||||
|
String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme();
|
||||||
|
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||||
|
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||||
|
} catch (IOException e) {
|
||||||
|
return KeyLoadingStrategy.failed(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import javax.inject.Scope;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
|
@Scope
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface KeyLoadingScoped {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.cryptomator.ui.keyloading;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable, stateful {@link MasterkeyLoader}, that can deal with certain exceptions.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface KeyLoadingStrategy extends MasterkeyLoader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
|
||||||
|
* <p>
|
||||||
|
* If loading fails exceptionally, this strategy might be able to {@link #recoverFromException(MasterkeyLoadingFailedException) recover from this exception}, so it can be used in a further attempt.
|
||||||
|
*
|
||||||
|
* @param keyId An URI uniquely identifying the source and identity of the key
|
||||||
|
* @return The raw key bytes. Must not be null
|
||||||
|
* @throws MasterkeyLoadingFailedException Thrown when it is impossible to fulfill the request
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows the loader to try and recover from an exception thrown during the last attempt.
|
||||||
|
*
|
||||||
|
* @param exception An exception thrown by {@link #loadKey(URI)}.
|
||||||
|
* @return <code>true</code> if this component was able to handle the exception and another attempt can be made to load a masterkey
|
||||||
|
*/
|
||||||
|
default boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release any ressources or do follow-up tasks after loading a key.
|
||||||
|
*
|
||||||
|
* @param unlockedSuccessfully <code>true</code> if successfully unlocked a vault with the loaded key
|
||||||
|
* @implNote This method might be invoked multiple times, depending on whether multiple attempts to load a key are started.
|
||||||
|
*/
|
||||||
|
default void cleanup(boolean unlockedSuccessfully) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A key loading strategy that will always fail by throwing a {@link MasterkeyLoadingFailedException}.
|
||||||
|
*
|
||||||
|
* @param exception The cause of the failure. If not alreay an {@link MasterkeyLoadingFailedException}, it will get wrapped.
|
||||||
|
* @return A new KeyLoadingStrategy that will always fail with an {@link MasterkeyLoadingFailedException}.
|
||||||
|
*/
|
||||||
|
static KeyLoadingStrategy failed(Exception exception) {
|
||||||
|
return keyid -> {
|
||||||
|
if (exception instanceof MasterkeyLoadingFailedException e) {
|
||||||
|
throw e;
|
||||||
|
} else {
|
||||||
|
throw new MasterkeyLoadingFailedException("Can not load key", exception);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@KeyLoadingScoped
|
||||||
|
class MasterkeyFileLoadingFinisher {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingFinisher.class);
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final Optional<char[]> storedPassword;
|
||||||
|
private final AtomicReference<char[]> enteredPassword;
|
||||||
|
private final AtomicBoolean shouldSavePassword;
|
||||||
|
private final KeychainManager keychain;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional<char[]> storedPassword, AtomicReference<char[]> enteredPassword, @Named("savePassword") AtomicBoolean shouldSavePassword, KeychainManager keychain) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.storedPassword = storedPassword;
|
||||||
|
this.enteredPassword = enteredPassword;
|
||||||
|
this.shouldSavePassword = shouldSavePassword;
|
||||||
|
this.keychain = keychain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanup(boolean successfullyUnlocked) {
|
||||||
|
if (successfullyUnlocked && shouldSavePassword.get()) {
|
||||||
|
savePasswordToSystemkeychain();
|
||||||
|
}
|
||||||
|
wipePassword(storedPassword.orElse(null));
|
||||||
|
wipePassword(enteredPassword.getAndSet(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePasswordToSystemkeychain() {
|
||||||
|
if (keychain.isSupported()) {
|
||||||
|
try {
|
||||||
|
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(enteredPassword.get()));
|
||||||
|
} catch (KeychainAccessException e) {
|
||||||
|
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void wipePassword(char[] pw) {
|
||||||
|
if (pw != null) {
|
||||||
|
Arrays.fill(pw, ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
|
import dagger.Binds;
|
||||||
|
import dagger.Module;
|
||||||
|
import dagger.Provides;
|
||||||
|
import dagger.multibindings.IntoMap;
|
||||||
|
import dagger.multibindings.StringKey;
|
||||||
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.FxControllerKey;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.UserInteractionLock;
|
||||||
|
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Named;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@Module(subcomponents = {ForgetPasswordComponent.class})
|
||||||
|
public abstract class MasterkeyFileLoadingModule {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(MasterkeyFileLoadingModule.class);
|
||||||
|
|
||||||
|
public enum PasswordEntry {
|
||||||
|
PASSWORD_ENTERED,
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum MasterkeyFileProvision {
|
||||||
|
MASTERKEYFILE_PROVIDED,
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
|
||||||
|
return new UserInteractionLock<>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
|
||||||
|
return new UserInteractionLock<>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("savedPassword")
|
||||||
|
@KeyLoadingScoped
|
||||||
|
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
|
||||||
|
if (!keychain.isSupported()) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import dagger.Lazy;
|
||||||
|
import org.cryptomator.common.vaults.Vault;
|
||||||
|
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||||
|
import org.cryptomator.cryptolib.api.Masterkey;
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||||
|
import org.cryptomator.ui.common.Animations;
|
||||||
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
|
import org.cryptomator.ui.common.FxmlScene;
|
||||||
|
import org.cryptomator.ui.common.UserInteractionLock;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
|
import org.cryptomator.ui.unlock.UnlockCancelledException;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@KeyLoading
|
||||||
|
public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||||
|
|
||||||
|
public static final String SCHEME = "masterkeyfile";
|
||||||
|
|
||||||
|
private final Vault vault;
|
||||||
|
private final MasterkeyFileAccess masterkeyFileAcccess;
|
||||||
|
private final Stage window;
|
||||||
|
private final Lazy<Scene> passphraseEntryScene;
|
||||||
|
private final Lazy<Scene> selectMasterkeyFileScene;
|
||||||
|
private final UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock;
|
||||||
|
private final UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock;
|
||||||
|
private final AtomicReference<char[]> password;
|
||||||
|
private final AtomicReference<Path> filePath;
|
||||||
|
private final MasterkeyFileLoadingFinisher finisher;
|
||||||
|
|
||||||
|
private boolean wrongPassword;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAcccess, @KeyLoading Stage window, @FxmlScene(FxmlFile.UNLOCK_ENTER_PASSWORD) Lazy<Scene> passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, UserInteractionLock<MasterkeyFileLoadingModule.PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileLoadingModule.MasterkeyFileProvision> masterkeyFileProvisionLock, AtomicReference<char[]> password, AtomicReference<Path> filePath, MasterkeyFileLoadingFinisher finisher) {
|
||||||
|
this.vault = vault;
|
||||||
|
this.masterkeyFileAcccess = masterkeyFileAcccess;
|
||||||
|
this.window = window;
|
||||||
|
this.passphraseEntryScene = passphraseEntryScene;
|
||||||
|
this.selectMasterkeyFileScene = selectMasterkeyFileScene;
|
||||||
|
this.passwordEntryLock = passwordEntryLock;
|
||||||
|
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||||
|
this.password = password;
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.finisher = finisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Masterkey loadKey(URI keyId) throws MasterkeyLoadingFailedException {
|
||||||
|
Preconditions.checkArgument(SCHEME.equalsIgnoreCase(keyId.getScheme()), "Only supports keys with scheme " + SCHEME);
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path filePath = vault.getPath().resolve(keyId.getSchemeSpecificPart());
|
||||||
|
if (!Files.exists(filePath)) {
|
||||||
|
filePath = getAlternateMasterkeyFilePath();
|
||||||
|
}
|
||||||
|
CharSequence passphrase = getPassphrase();
|
||||||
|
return masterkeyFileAcccess.load(filePath, passphrase);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new UnlockCancelledException("Unlock interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean recoverFromException(MasterkeyLoadingFailedException exception) {
|
||||||
|
if (exception instanceof InvalidPassphraseException) {
|
||||||
|
this.wrongPassword = true;
|
||||||
|
password.set(null);
|
||||||
|
return true; // reattempting key load
|
||||||
|
} else {
|
||||||
|
return false; // nothing we can do
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup(boolean unlockedSuccessfully) {
|
||||||
|
finisher.cleanup(unlockedSuccessfully);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException {
|
||||||
|
if (filePath.get() == null) {
|
||||||
|
return switch (askUserForMasterkeyFilePath()) {
|
||||||
|
case MASTERKEYFILE_PROVIDED -> filePath.get();
|
||||||
|
case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled.");
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return filePath.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MasterkeyFileLoadingModule.MasterkeyFileProvision askUserForMasterkeyFilePath() throws InterruptedException {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
window.setScene(selectMasterkeyFileScene.get());
|
||||||
|
window.show();
|
||||||
|
Window owner = window.getOwner();
|
||||||
|
if (owner != null) {
|
||||||
|
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||||
|
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||||
|
} else {
|
||||||
|
window.centerOnScreen();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return masterkeyFileProvisionLock.awaitInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence getPassphrase() throws UnlockCancelledException, InterruptedException {
|
||||||
|
if (password.get() == null) {
|
||||||
|
return switch (askForPassphrase()) {
|
||||||
|
case PASSWORD_ENTERED -> CharBuffer.wrap(password.get());
|
||||||
|
case CANCELED -> throw new UnlockCancelledException("Password entry cancelled.");
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// e.g. pre-filled from keychain or previous unlock attempt
|
||||||
|
return CharBuffer.wrap(password.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MasterkeyFileLoadingModule.PasswordEntry askForPassphrase() throws InterruptedException {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
window.setScene(passphraseEntryScene.get());
|
||||||
|
window.show();
|
||||||
|
Window owner = window.getOwner();
|
||||||
|
if (owner != null) {
|
||||||
|
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||||
|
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||||
|
} else {
|
||||||
|
window.centerOnScreen();
|
||||||
|
}
|
||||||
|
if (wrongPassword) {
|
||||||
|
Animations.createShakeWindowAnimation(window).play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return passwordEntryLock.awaitInteraction();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
-8
@@ -1,4 +1,4 @@
|
|||||||
package org.cryptomator.ui.unlock;
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
import org.cryptomator.common.keychain.KeychainManager;
|
import org.cryptomator.common.keychain.KeychainManager;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
@@ -7,6 +7,9 @@ import org.cryptomator.ui.common.UserInteractionLock;
|
|||||||
import org.cryptomator.ui.common.WeakBindings;
|
import org.cryptomator.ui.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;
|
||||||
|
|
||||||
@@ -38,17 +41,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;
|
||||||
|
|
||||||
@UnlockScoped
|
@KeyLoadingScoped
|
||||||
public class UnlockController implements FxController {
|
public class PassphraseEntryController implements FxController {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
|
||||||
|
|
||||||
private final Stage window;
|
private final 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<UnlockModule.PasswordEntry> passwordEntryLock;
|
private final UserInteractionLock<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;
|
||||||
@@ -66,7 +69,7 @@ public class UnlockController implements FxController {
|
|||||||
public Animation unlockAnimation;
|
public Animation unlockAnimation;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
@@ -138,7 +141,7 @@ public class UnlockController 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(UnlockModule.PasswordEntry.CANCELED);
|
passwordEntryLock.interacted(PasswordEntry.CANCELED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +157,7 @@ public class UnlockController implements FxController {
|
|||||||
if (oldPw != null) {
|
if (oldPw != null) {
|
||||||
Arrays.fill(oldPw, ' ');
|
Arrays.fill(oldPw, ' ');
|
||||||
}
|
}
|
||||||
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
|
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
|
||||||
startUnlockAnimation();
|
startUnlockAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
+67
@@ -0,0 +1,67 @@
|
|||||||
|
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||||
|
|
||||||
|
import org.cryptomator.ui.common.FxController;
|
||||||
|
import org.cryptomator.ui.common.UserInteractionLock;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||||
|
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||||
|
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.MasterkeyFileProvision;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import javafx.stage.WindowEvent;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@KeyLoadingScoped
|
||||||
|
public class SelectMasterkeyFileController implements FxController {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SelectMasterkeyFileController.class);
|
||||||
|
|
||||||
|
private final Stage window;
|
||||||
|
private final AtomicReference<Path> masterkeyPath;
|
||||||
|
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
|
||||||
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, ResourceBundle resourceBundle) {
|
||||||
|
this.window = window;
|
||||||
|
this.masterkeyPath = masterkeyPath;
|
||||||
|
this.masterkeyFileProvisionLock = masterkeyFileProvisionLock;
|
||||||
|
this.resourceBundle = resourceBundle;
|
||||||
|
this.window.setOnHiding(this::windowClosed);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void cancel() {
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void windowClosed(WindowEvent windowEvent) {
|
||||||
|
// if not already interacted, mark this workflow as cancelled:
|
||||||
|
if (masterkeyFileProvisionLock.awaitingInteraction().get()) {
|
||||||
|
LOG.debug("Unlock canceled by user.");
|
||||||
|
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void proceed() {
|
||||||
|
LOG.trace("proceed()");
|
||||||
|
FileChooser fileChooser = new FileChooser();
|
||||||
|
fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle"));
|
||||||
|
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||||
|
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||||
|
if (masterkeyFile != null) {
|
||||||
|
LOG.debug("Chose masterkey file: {}", masterkeyFile);
|
||||||
|
masterkeyPath.set(masterkeyFile.toPath());
|
||||||
|
masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import javax.inject.Inject;
|
|||||||
import javax.inject.Named;
|
import javax.inject.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;
|
||||||
@@ -78,7 +79,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 (NoSuchFileException e) {
|
} catch (IOException e) {
|
||||||
LOG.error("Failed to add vault " + potentialVaultPath, e);
|
LOG.error("Failed to add vault " + potentialVaultPath, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ 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;
|
||||||
@@ -20,6 +22,7 @@ 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;
|
||||||
@@ -27,6 +30,7 @@ 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 {
|
||||||
@@ -91,23 +95,21 @@ public class MainWindowController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean containsVault(Path path) {
|
private boolean containsVault(Path path) {
|
||||||
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
try {
|
||||||
return true;
|
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
|
||||||
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
|
} catch (IOException e) {
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addVault(Path pathToVault) {
|
private void addVault(Path pathToVault) {
|
||||||
try {
|
try {
|
||||||
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
|
||||||
vaultListManager.add(pathToVault.getParent());
|
vaultListManager.add(pathToVault.getParent());
|
||||||
} else {
|
} else {
|
||||||
vaultListManager.add(pathToVault);
|
vaultListManager.add(pathToVault);
|
||||||
}
|
}
|
||||||
} catch (NoSuchFileException e) {
|
} catch (IOException e) {
|
||||||
LOG.debug("Not a vault: {}", pathToVault);
|
LOG.debug("Not a vault: {}", pathToVault);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.cryptomator.ui.common.FxControllerKey;
|
|||||||
import org.cryptomator.ui.common.FxmlFile;
|
import org.cryptomator.ui.common.FxmlFile;
|
||||||
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.health.HealthCheckComponent;
|
||||||
import org.cryptomator.ui.migration.MigrationComponent;
|
import org.cryptomator.ui.migration.MigrationComponent;
|
||||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||||
@@ -27,7 +28,7 @@ import javafx.stage.StageStyle;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
|
@Module(subcomponents = {AddVaultWizardComponent.class, HealthCheckComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class})
|
||||||
abstract class MainWindowModule {
|
abstract class MainWindowModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
+3
-1
@@ -5,6 +5,7 @@ import org.cryptomator.common.keychain.KeychainManager;
|
|||||||
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.fxapp.FxApplication;
|
import org.cryptomator.ui.fxapp.FxApplication;
|
||||||
|
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||||
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
|
||||||
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ import javafx.beans.binding.BooleanExpression;
|
|||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -28,7 +30,7 @@ public class VaultDetailLockedController implements FxController {
|
|||||||
private final BooleanExpression passwordSaved;
|
private final BooleanExpression passwordSaved;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
VaultDetailLockedController(ObjectProperty<Vault> vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) {
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.vaultOptionsWindow = vaultOptionsWindow;
|
this.vaultOptionsWindow = vaultOptionsWindow;
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ 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 {
|
||||||
@@ -94,6 +95,8 @@ public class MigrationRunController implements FxController {
|
|||||||
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
|
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
|
||||||
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
|
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
|
||||||
.or(passwordField.textProperty().isEmpty()));
|
.or(passwordField.textProperty().isEmpty()));
|
||||||
|
|
||||||
|
window.setOnHiding(event -> passwordField.wipe());
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -114,8 +117,8 @@ 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(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
||||||
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
|
return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, 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());
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@ 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;
|
||||||
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String call() throws IOException {
|
protected String call() throws IOException, CryptoException {
|
||||||
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
|
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,28 +2,34 @@ package org.cryptomator.ui.recoverykey;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.hash.Hashing;
|
import com.google.common.hash.Hashing;
|
||||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||||
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.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) {
|
public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) {
|
||||||
this.wordEncoder = wordEncoder;
|
this.wordEncoder = wordEncoder;
|
||||||
|
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<String> getDictionary() {
|
public Collection<String> getDictionary() {
|
||||||
@@ -36,11 +42,14 @@ 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 {
|
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
|
||||||
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
|
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||||
try {
|
byte[] rawKey = new byte[0];
|
||||||
|
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);
|
||||||
@@ -72,8 +81,15 @@ 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 {
|
try (var masterkey = new Masterkey(rawKey)) {
|
||||||
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
|
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||||
|
if (Files.exists(masterkeyPath)) {
|
||||||
|
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||||
|
// TODO: deduplicate with ChangePasswordController:
|
||||||
|
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||||
|
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
}
|
||||||
|
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
|
||||||
} finally {
|
} finally {
|
||||||
Arrays.fill(rawKey, (byte) 0x00);
|
Arrays.fill(rawKey, (byte) 0x00);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import dagger.Provides;
|
|||||||
import dagger.multibindings.IntoMap;
|
import dagger.multibindings.IntoMap;
|
||||||
import org.cryptomator.common.vaults.Vault;
|
import org.cryptomator.common.vaults.Vault;
|
||||||
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.NewPasswordController;
|
import org.cryptomator.ui.common.NewPasswordController;
|
||||||
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
import org.cryptomator.ui.common.PasswordStrengthUtil;
|
||||||
@@ -17,8 +17,6 @@ import org.cryptomator.ui.common.StageFactory;
|
|||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
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.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
@@ -56,14 +54,6 @@ abstract class RecoveryKeyModule {
|
|||||||
return new SimpleStringProperty();
|
return new SimpleStringProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@RecoveryKeyScoped
|
|
||||||
@Named("newPassword")
|
|
||||||
static ObjectProperty<CharSequence> provideNewPasswordProperty() {
|
|
||||||
return new SimpleObjectProperty<>("");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -126,8 +116,8 @@ abstract class RecoveryKeyModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(NewPasswordController.class)
|
@FxControllerKey(NewPasswordController.class)
|
||||||
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty<CharSequence> password) {
|
static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) {
|
||||||
return new NewPasswordController(resourceBundle, strengthRater, password);
|
return new NewPasswordController(resourceBundle, strengthRater);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-14
@@ -6,14 +6,12 @@ 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.NewPasswordController;
|
||||||
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.beans.property.ReadOnlyBooleanProperty;
|
||||||
import javafx.beans.binding.Bindings;
|
|
||||||
import javafx.beans.binding.BooleanBinding;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
@@ -32,21 +30,19 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
|||||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||||
private final ExecutorService executor;
|
private final ExecutorService executor;
|
||||||
private final StringProperty recoveryKey;
|
private final StringProperty recoveryKey;
|
||||||
private final ObjectProperty<CharSequence> newPassword;
|
|
||||||
private final Lazy<Scene> recoverScene;
|
private final Lazy<Scene> recoverScene;
|
||||||
private final BooleanBinding invalidNewPassword;
|
|
||||||
private final ErrorComponent.Builder errorComponent;
|
private final ErrorComponent.Builder errorComponent;
|
||||||
|
|
||||||
|
public NewPasswordController newPasswordController;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword") ObjectProperty<CharSequence> newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
|
public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene, ErrorComponent.Builder errorComponent) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||||
this.executor = executor;
|
this.executor = executor;
|
||||||
this.recoveryKey = recoveryKey;
|
this.recoveryKey = recoveryKey;
|
||||||
this.newPassword = newPassword;
|
|
||||||
this.recoverScene = recoverScene;
|
this.recoverScene = recoverScene;
|
||||||
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
|
|
||||||
this.errorComponent = errorComponent;
|
this.errorComponent = errorComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +77,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Void call() throws IOException, IllegalArgumentException {
|
protected Void call() throws IOException, IllegalArgumentException {
|
||||||
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get());
|
recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +85,12 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
|||||||
|
|
||||||
/* Getter/Setter */
|
/* Getter/Setter */
|
||||||
|
|
||||||
public BooleanBinding invalidNewPasswordProperty() {
|
public ReadOnlyBooleanProperty validPasswordProperty() {
|
||||||
return invalidNewPassword;
|
return newPasswordController.passwordsMatchAndSufficientProperty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isInvalidNewPassword() {
|
public boolean isValidPassword() {
|
||||||
return newPassword.get() == null || newPassword.get().length() == 0;
|
return newPasswordController.passwordsMatchAndSufficientProperty().get();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.cryptomator.ui.unlock;
|
||||||
|
|
||||||
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
|
|
||||||
|
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
|
||||||
|
|
||||||
|
public UnlockCancelledException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UnlockCancelledException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-9
@@ -1,15 +1,11 @@
|
|||||||
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.
|
||||||
@@ -17,19 +13,17 @@ 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, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @UnlockWindow Vault vault) {
|
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.unlockScene = unlockScene;
|
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void back() {
|
public void close() {
|
||||||
window.setScene(unlockScene.get());
|
window.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Getter/Setter */
|
/* Getter/Setter */
|
||||||
|
|||||||
@@ -4,20 +4,16 @@ 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.common.UserInteractionLock;
|
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Provider;
|
import javax.inject.Provider;
|
||||||
@@ -27,54 +23,10 @@ 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 = {ForgetPasswordComponent.class})
|
@Module(subcomponents = {KeyLoadingComponent.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
|
||||||
@@ -99,10 +51,10 @@ abstract class UnlockModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@FxmlScene(FxmlFile.UNLOCK)
|
@UnlockWindow
|
||||||
@UnlockScoped
|
@UnlockScoped
|
||||||
static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
|
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
||||||
return fxmlLoaders.createScene(FxmlFile.UNLOCK);
|
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@@ -121,11 +73,6 @@ abstract class UnlockModule {
|
|||||||
|
|
||||||
// ------------------
|
// ------------------
|
||||||
|
|
||||||
@Binds
|
|
||||||
@IntoMap
|
|
||||||
@FxControllerKey(UnlockController.class)
|
|
||||||
abstract FxController bindUnlockController(UnlockController controller);
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FxControllerKey(UnlockSuccessController.class)
|
@FxControllerKey(UnlockSuccessController.class)
|
||||||
|
|||||||
@@ -1,40 +1,31 @@
|
|||||||
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.InvalidPassphraseException;
|
import org.cryptomator.cryptolib.api.CryptoException;
|
||||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||||
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.unlock.UnlockModule.PasswordEntry;
|
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||||
|
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.
|
||||||
@@ -49,88 +40,47 @@ 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, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent) {
|
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
|
||||||
this.window = window;
|
this.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||||
try {
|
try {
|
||||||
if (attemptUnlock()) {
|
attemptUnlock();
|
||||||
if (savePassword.get()) {
|
return true;
|
||||||
savePasswordToSystemkeychain(); //savePassword will be wiped on method return, so it must be set here
|
} catch (UnlockCancelledException e) {
|
||||||
}
|
cancel(false); // set Tasks state to cancelled
|
||||||
return true;
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
vault.unlock(keyLoadingStrategy);
|
||||||
|
success = true;
|
||||||
|
} catch (MasterkeyLoadingFailedException e) {
|
||||||
|
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||||
|
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||||
|
attemptUnlock();
|
||||||
} else {
|
} else {
|
||||||
cancel(false); // set Tasks state to cancelled
|
throw e;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
wipePassword(password.get());
|
keyLoadingStrategy.cleanup(success);
|
||||||
wipePassword(savedPassword.orElse(null));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
|
||||||
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
|
|
||||||
while (proceed) {
|
|
||||||
try {
|
|
||||||
vault.unlock(CharBuffer.wrap(password.get()));
|
|
||||||
return true;
|
|
||||||
} catch (InvalidPassphraseException e) {
|
|
||||||
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
window.setScene(unlockScene.get());
|
|
||||||
window.show();
|
|
||||||
Window owner = window.getOwner();
|
|
||||||
if (owner != null) {
|
|
||||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
|
||||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
|
||||||
} else {
|
|
||||||
window.centerOnScreen();
|
|
||||||
}
|
|
||||||
if (animateShake) {
|
|
||||||
Animations.createShakeWindowAnimation(window).play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return passwordEntryLock.awaitInteraction();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +121,6 @@ public class UnlockWorkflow extends Task<Boolean> {
|
|||||||
errorComponent.cause(e).window(window).build().showErrorScene();
|
errorComponent.cause(e).window(window).build().showErrorScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void wipePassword(char[] pw) {
|
|
||||||
if (pw != null) {
|
|
||||||
Arrays.fill(pw, ' ');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void succeeded() {
|
protected void succeeded() {
|
||||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
||||||
|
|||||||
+10
-1
@@ -3,6 +3,7 @@ package org.cryptomator.ui.vaultoptions;
|
|||||||
import org.cryptomator.common.settings.WhenUnlocked;
|
import org.cryptomator.common.settings.WhenUnlocked;
|
||||||
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.health.HealthCheckComponent;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
@@ -22,6 +23,7 @@ public class GeneralVaultOptionsController implements FxController {
|
|||||||
|
|
||||||
private final Stage window;
|
private final Stage window;
|
||||||
private final Vault vault;
|
private final Vault vault;
|
||||||
|
private final HealthCheckComponent.Builder healthCheckWindow;
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
|
|
||||||
public TextField vaultName;
|
public TextField vaultName;
|
||||||
@@ -29,9 +31,10 @@ public class GeneralVaultOptionsController implements FxController {
|
|||||||
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
|
public ChoiceBox<WhenUnlocked> actionAfterUnlockChoiceBox;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
|
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.vault = vault;
|
this.vault = vault;
|
||||||
|
this.healthCheckWindow = healthCheckWindow;
|
||||||
this.resourceBundle = resourceBundle;
|
this.resourceBundle = resourceBundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +64,12 @@ public class GeneralVaultOptionsController implements FxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
public void showHealthCheck() {
|
||||||
|
healthCheckWindow.vault(vault).build().showHealthCheckWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
|
private static class WhenUnlockedConverter extends StringConverter<WhenUnlocked> {
|
||||||
|
|
||||||
private final ResourceBundle resourceBundle;
|
private final ResourceBundle resourceBundle;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<Insets topRightBottomLeft="24"/>
|
<Insets topRightBottomLeft="24"/>
|
||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<fx:include source="/fxml/new_password.fxml"/>
|
<fx:include fx:id="newPasswordScene" source="/fxml/new_password.fxml"/>
|
||||||
|
|
||||||
<Region VBox.vgrow="ALWAYS"/>
|
<Region VBox.vgrow="ALWAYS"/>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
|
|
||||||
<Region prefHeight="12" VBox.vgrow="NEVER"/>
|
<Region prefHeight="12" VBox.vgrow="NEVER"/>
|
||||||
|
|
||||||
<fx:include source="/fxml/new_password.fxml"/>
|
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
|
||||||
|
|
||||||
<CheckBox fx:id="finalConfirmationCheckbox" text="%changepassword.finalConfirmation" wrapText="true"/>
|
<CheckBox fx:id="finalConfirmationCheckbox" text="%changepassword.finalConfirmation" wrapText="true"/>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
|
xmlns="http://javafx.com/javafx"
|
||||||
|
fx:controller="org.cryptomator.ui.health.CheckDetailController"
|
||||||
|
prefWidth="500"
|
||||||
|
spacing="6">
|
||||||
|
<FormattedLabel fx:id="checkTitle" styleClass="label-large" format="%health.check.detail.header" arg1="${controller.taskName}"/>
|
||||||
|
|
||||||
|
<Label text="%health.check.detail.taskNotStarted" visible="${controller.taskNotStarted}" managed="${controller.taskNotStarted}"/>
|
||||||
|
<Label text="%health.check.detail.taskRunning" visible="${controller.taskRunning}" managed="${controller.taskRunning}"/>
|
||||||
|
<Label text="%health.check.detail.taskScheduled" visible="${controller.taskScheduled}" managed="${controller.taskScheduled}"/>
|
||||||
|
<Label text="%health.check.detail.taskCancelled" visible="${controller.taskCancelled}" managed="${controller.taskCancelled}"/>
|
||||||
|
<Label text="%health.check.detail.taskFailed" visible="${controller.taskFailed}" managed="${controller.taskFailed}"/>
|
||||||
|
<FormattedLabel styleClass="label" format="%health.check.detail.taskSucceeded" arg1="${controller.taskDuration}" visible="${controller.taskSucceeded}" managed="${controller.taskSucceeded}"/>
|
||||||
|
|
||||||
|
<FormattedLabel styleClass="label" format="%health.check.detail.problemCount" arg1="${controller.countOfWarnSeverity}" arg2="${controller.countOfCritSeverity}" visible="${!controller.taskNotStarted}"
|
||||||
|
managed="${!controller.taskNotStarted}" />
|
||||||
|
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ButtonBar?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ListView?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import java.lang.Integer?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.control.CheckBox?>
|
||||||
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
|
xmlns="http://javafx.com/javafx"
|
||||||
|
fx:controller="org.cryptomator.ui.health.CheckListController"
|
||||||
|
minHeight="145"
|
||||||
|
spacing="12">
|
||||||
|
<padding>
|
||||||
|
<Insets topRightBottomLeft="12"/>
|
||||||
|
</padding>
|
||||||
|
<fx:define>
|
||||||
|
<Integer fx:id="ZERO" fx:value="0"/>
|
||||||
|
</fx:define>
|
||||||
|
<children>
|
||||||
|
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||||
|
<VBox minWidth="80" maxWidth="200" spacing="6" HBox.hgrow="ALWAYS" >
|
||||||
|
<Label fx:id="listHeading" text="%health.checkList.header"/>
|
||||||
|
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
|
||||||
|
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
<StackPane visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
|
||||||
|
<VBox minWidth="300" alignment="CENTER" visible="${!controller.anyCheckSelected}" managed="${!controller.anyCheckSelected}" >
|
||||||
|
<Label text="%health.check.detail.noSelectedCheck" wrapText="true" alignment="CENTER" />
|
||||||
|
</VBox>
|
||||||
|
<fx:include source="/fxml/health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" />
|
||||||
|
</StackPane>
|
||||||
|
</HBox>
|
||||||
|
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||||
|
<buttons>
|
||||||
|
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" />
|
||||||
|
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${!controller.finished}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" onAction="#exportResults"/>
|
||||||
|
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.numberOfPickedChecks == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
|
||||||
|
</buttons>
|
||||||
|
</ButtonBar>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.Region?>
|
||||||
|
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||||
|
xmlns="http://javafx.com/javafx"
|
||||||
|
fx:controller="org.cryptomator.ui.health.ResultListCellController"
|
||||||
|
prefHeight="25"
|
||||||
|
prefWidth="200"
|
||||||
|
spacing="6"
|
||||||
|
alignment="CENTER_LEFT">
|
||||||
|
<!-- Remark: Check the containing list view for a fixed cell size before editing height properties -->
|
||||||
|
<padding>
|
||||||
|
<Insets topRightBottomLeft="6"/>
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16"/>
|
||||||
|
<Label text="${controller.description}"/>
|
||||||
|
<Region HBox.hgrow="ALWAYS"/>
|
||||||
|
<!-- TODO: setting the minWidth of the button is just a workaround.
|
||||||
|
What we actually want to do is to prevent shrinking the button more than the text
|
||||||
|
-> own subclass of HBox is needed -->
|
||||||
|
<Button fx:id="actionButton" text="%health.check.fixBtn" onAction="#runResultAction" alignment="CENTER" visible="false" minWidth="-Infinity"/>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ButtonBar?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
|
xmlns="http://javafx.com/javafx"
|
||||||
|
fx:controller="org.cryptomator.ui.health.StartController"
|
||||||
|
minWidth="400"
|
||||||
|
maxWidth="400"
|
||||||
|
minHeight="145"
|
||||||
|
spacing="12">
|
||||||
|
<padding>
|
||||||
|
<Insets topRightBottomLeft="12"/>
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<Label text="%health.start.introduction" wrapText="true"/>
|
||||||
|
|
||||||
|
<!-- TODO: combine the two below labels to one and bind the properties accordingly or, preferably think about a new flow -->
|
||||||
|
<Label text="%health.start.configInvalid" visible="${controller.invalidConfig}" managed="${controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
|
||||||
|
<graphic>
|
||||||
|
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red" />
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
<Label text="%health.start.configValid" visible="${!controller.invalidConfig}" managed="${!controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
|
||||||
|
<graphic>
|
||||||
|
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary" />
|
||||||
|
</graphic>
|
||||||
|
</Label>
|
||||||
|
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||||
|
<buttons>
|
||||||
|
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||||
|
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${controller.invalidConfig}" defaultButton="true" onAction="#next"/>
|
||||||
|
</buttons>
|
||||||
|
</ButtonBar>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<Insets topRightBottomLeft="12"/>
|
<Insets topRightBottomLeft="12"/>
|
||||||
</padding>
|
</padding>
|
||||||
<children>
|
<children>
|
||||||
<fx:include source="/fxml/new_password.fxml"/>
|
<fx:include fx:id="newPassword" source="/fxml/new_password.fxml"/>
|
||||||
|
|
||||||
<Region VBox.vgrow="ALWAYS"/>
|
<Region VBox.vgrow="ALWAYS"/>
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+I">
|
<ButtonBar buttonMinWidth="120" buttonOrder="B+I">
|
||||||
<buttons>
|
<buttons>
|
||||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
|
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
|
||||||
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${controller.invalidNewPassword}"/>
|
<Button text="%generic.button.done" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#done" disable="${!controller.validPassword}"/>
|
||||||
</buttons>
|
</buttons>
|
||||||
</ButtonBar>
|
</ButtonBar>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
+1
-1
@@ -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.unlock.UnlockController"
|
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.PassphraseEntryController"
|
||||||
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="B+U">
|
<ButtonBar buttonMinWidth="120" buttonOrder="U+C">
|
||||||
<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>
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||||
|
<?import javafx.geometry.Insets?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.ButtonBar?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.shape.Circle?>
|
||||||
|
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||||
|
xmlns="http://javafx.com/javafx"
|
||||||
|
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.SelectMasterkeyFileController"
|
||||||
|
minWidth="400"
|
||||||
|
maxWidth="400"
|
||||||
|
minHeight="145"
|
||||||
|
spacing="12">
|
||||||
|
<padding>
|
||||||
|
<Insets topRightBottomLeft="12"/>
|
||||||
|
</padding>
|
||||||
|
<children>
|
||||||
|
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
|
||||||
|
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
|
||||||
|
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||||
|
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="24"/>
|
||||||
|
</StackPane>
|
||||||
|
<VBox spacing="6">
|
||||||
|
<Label text="%unlock.chooseMasterkey.prompt" wrapText="true" HBox.hgrow="ALWAYS"/>
|
||||||
|
</VBox>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
|
||||||
|
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||||
|
<buttons>
|
||||||
|
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
|
||||||
|
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
|
||||||
|
</buttons>
|
||||||
|
</ButtonBar>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
<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.vaultoptions.GeneralVaultOptionsController"
|
fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"
|
||||||
@@ -26,5 +27,6 @@
|
|||||||
<Label text="%vaultOptions.general.actionAfterUnlock"/>
|
<Label text="%vaultOptions.general.actionAfterUnlock"/>
|
||||||
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
|
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
<Button text="%vaultOptions.general.healthBtn" onAction="#showHealthCheck"/>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ 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 via its virtual drive.
|
||||||
unlock.success.rememberChoice=Remember choice, don't show this again
|
unlock.success.rememberChoice=Remember choice, don't show this again
|
||||||
@@ -143,6 +146,29 @@ migration.impossible.heading=Unable to migrate vault
|
|||||||
migration.impossible.reason=The vault cannot be automatically migrated because its storage location or access point is not compatible.
|
migration.impossible.reason=The vault cannot be automatically migrated because its storage location or access point is not compatible.
|
||||||
migration.impossible.moreInfo=The vault can still be opened with an older version. For instructions on how to manually migrate a vault, visit
|
migration.impossible.moreInfo=The vault can still be opened with an older version. For instructions on how to manually migrate a vault, visit
|
||||||
|
|
||||||
|
# Health Check
|
||||||
|
health.title=Vault Health Check
|
||||||
|
health.start.introduction=The Vault Health Check is a collection of checks to detect and possilby fix problems in the internal structure of your vault. Please note, that not all problems are fixable. You need the vault password to perform the checks.
|
||||||
|
health.start.configValid=Reading and parsing vault configuration file was successful. Proceed to select checks.
|
||||||
|
health.start.configInvalid=Error while reading and parsing the vault configuration file.
|
||||||
|
health.checkList.header=Available Health Checks
|
||||||
|
health.checkList.selectAllBox=Select All
|
||||||
|
health.check.runBatchBtn=Run selected Checks
|
||||||
|
## Detail view
|
||||||
|
health.check.detail.noSelectedCheck=For results select a finished health check in the left list.
|
||||||
|
health.check.detail.header=Results of %s
|
||||||
|
health.check.detail.taskNotStarted=The check was not selected to run.
|
||||||
|
health.check.detail.taskScheduled=The check is scheduled.
|
||||||
|
health.check.detail.taskRunning=The check is currently running…
|
||||||
|
health.check.detail.taskSucceeded=The check finished successfully after %d milliseconds.
|
||||||
|
health.check.detail.taskFailed=The check exited due to an error.
|
||||||
|
health.check.detail.taskCancelled=The check was cancelled.
|
||||||
|
health.check.detail.problemCount=Found %d problems and %d unfixable errors.
|
||||||
|
health.check.exportBtn=Export Report
|
||||||
|
health.check.fixBtn=Fix
|
||||||
|
## Checks
|
||||||
|
health.org.cryptomator.cryptofs.health.dirid.DirIdCheck=Directory Check
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
preferences.title=Preferences
|
preferences.title=Preferences
|
||||||
## General
|
## General
|
||||||
@@ -284,6 +310,7 @@ vaultOptions.general.actionAfterUnlock=After successful unlock
|
|||||||
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
|
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
|
||||||
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
|
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
|
||||||
vaultOptions.general.actionAfterUnlock.ask=Ask
|
vaultOptions.general.actionAfterUnlock.ask=Ask
|
||||||
|
vaultOptions.general.healthBtn=Start Health Check
|
||||||
## Mount
|
## Mount
|
||||||
vaultOptions.mount=Mounting
|
vaultOptions.mount=Mounting
|
||||||
vaultOptions.mount.readonly=Read-Only
|
vaultOptions.mount.readonly=Read-Only
|
||||||
|
|||||||
@@ -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,15 +38,19 @@ 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.invalidName=اسم المخزن غير صالح. يرجى النظر في اسم الدليل المعتاد.
|
addvaultwizard.new.fileAlreadyExists=ملف أو مجلد بنفس اسم المخزن موجود مسبقاً
|
||||||
|
addvaultwizard.new.locationDoesNotExist=المجلد في المسار المحدد غير موجود أو لا يمكن الوصول إليه
|
||||||
|
addvaultwizard.new.locationIsNotWritable=لا يوجد صلاحيات للكتابة على المسار المحدد
|
||||||
|
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=لا شكراً، لن أفقد كلمة المرور الخاصة بي
|
||||||
@@ -93,8 +97,12 @@ 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.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
|
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
|
||||||
unlock.success.revealBtn=اظهار القرص
|
unlock.success.revealBtn=اظهار القرص
|
||||||
## Failure
|
## Failure
|
||||||
@@ -105,9 +113,12 @@ 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=ترقية الحافظة
|
||||||
@@ -142,6 +153,8 @@ 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=عرض ملفات السجل
|
||||||
@@ -166,12 +179,18 @@ preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوم
|
|||||||
preferences.updates.checkNowBtn=تحقق الان
|
preferences.updates.checkNowBtn=تحقق الان
|
||||||
preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح.
|
preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح.
|
||||||
## Contribution
|
## Contribution
|
||||||
|
preferences.contribute=ادعمنا
|
||||||
|
preferences.contribute.registeredFor=شهادة الداعم مسجلة لـ %s
|
||||||
|
preferences.contribute.noCertificate=ادعم Cryptomator واحصل على شهادة الداعم. إنها مثل مفتاح الترخيص لكن للأشخاص الرائعين الذين يستخدمون البرامج المجانية ؛-)
|
||||||
|
preferences.contribute.getCertificate=ليس لديك واحدة بعد؟ تعلم كيف يمكنك الحصول عليها.
|
||||||
|
preferences.contribute.promptText=قم بلصق رمز شهادة الداعم هنا
|
||||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
#<-- 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=قراءة: خامل
|
||||||
@@ -190,9 +209,11 @@ 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.lock=قفل
|
main.vaultlist.contextMenu.lock=قفل
|
||||||
main.vaultlist.contextMenu.unlock=فتح…
|
main.vaultlist.contextMenu.unlock=فتح…
|
||||||
main.vaultlist.contextMenu.unlockNow=افتح الان
|
main.vaultlist.contextMenu.unlockNow=افتح الان
|
||||||
|
main.vaultlist.contextMenu.vaultoptions=إظهار خيارات المخزن
|
||||||
main.vaultlist.contextMenu.reveal=اظهار القرص
|
main.vaultlist.contextMenu.reveal=اظهار القرص
|
||||||
main.vaultlist.addVaultBtn=أضِف مخزنًا
|
main.vaultlist.addVaultBtn=أضِف مخزنًا
|
||||||
## Vault Detail
|
## Vault Detail
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ unlock.title=Otključaj sef
|
|||||||
unlock.passwordPrompt=Unesite lozinku za "%s":
|
unlock.passwordPrompt=Unesite lozinku za "%s":
|
||||||
unlock.savePassword=Zapamti šifru
|
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 putem svog virtualnog diska.
|
||||||
unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo
|
unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ 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'ha desblocat %s correctament! Podeu accedir a la vostra caixa forta a través de la unitat virtual.
|
||||||
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
|
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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ý jako virtuální jednotka.
|
||||||
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
|
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
|
||||||
|
|||||||
@@ -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 die folgende Fehlermeldung und beschreibe kurz, durch welche Schritte er aufgetreten ist.
|
generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde den folgenden Fehlertext und beschreibe kurz, durch welche Schritte der Fehler aufgetreten ist.
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
defaults.vault.vaultName=Tresor
|
defaults.vault.vaultName=Tresor
|
||||||
@@ -38,7 +38,7 @@ 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 Daten deines Tresors ablegen?
|
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Dateien 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
|
||||||
@@ -81,7 +81,7 @@ addvaultwizard.success.unlockNow=Jetzt entsperren
|
|||||||
|
|
||||||
# Remove Vault
|
# Remove Vault
|
||||||
removeVault.title=Tresor entfernen
|
removeVault.title=Tresor entfernen
|
||||||
removeVault.information=Dein Tresor wird nicht gelöscht, sondern lediglich aus Cryptomators Tresorliste entfernt. Du kannst ihn daher später jederzeit wieder hinzufügen.
|
removeVault.information=Dieser Tresor wird nur aus der Tresorliste entfernt. Du kannst den Tresor später jederzeit wieder hinzufügen. Es werden keine verschlüsselten Daten gelöscht.
|
||||||
removeVault.confirmBtn=Tresor entfernen
|
removeVault.confirmBtn=Tresor entfernen
|
||||||
|
|
||||||
# Change Password
|
# Change Password
|
||||||
@@ -99,6 +99,8 @@ 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 über das virtuelle Laufwerk auf deinen Tresor zugreifen.
|
||||||
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
|
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
|
||||||
@@ -174,7 +176,7 @@ 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 suchen
|
preferences.updates.checkNowBtn=Jetzt prüfen
|
||||||
preferences.updates.updateAvailable=Update auf Version %s verfügbar.
|
preferences.updates.updateAvailable=Update auf Version %s verfügbar.
|
||||||
## Contribution
|
## Contribution
|
||||||
preferences.contribute=Unterstütze uns
|
preferences.contribute=Unterstütze uns
|
||||||
@@ -256,7 +258,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=Mit diesem Pfad konnte Cryptomator keinen Tresor finden.
|
main.vaultDetail.missing.info=Cryptomator konnte keinen Tresor mit diesem Pfad finden.
|
||||||
main.vaultDetail.missing.recheck=Erneut prüfen
|
main.vaultDetail.missing.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…
|
||||||
@@ -271,8 +273,8 @@ wrongFileAlert.header.lead=Für diesen Zweck stellt Cryptomator ein Laufwerk in
|
|||||||
wrongFileAlert.instruction.0=Folge diesen Schritten, um Dateien zu verschlüsseln:
|
wrongFileAlert.instruction.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 diesem Laufwerk hinzu.
|
wrongFileAlert.instruction.3=3. Füge deine Dateien zu diesem Laufwerk hinzu.
|
||||||
wrongFileAlert.link=Für weitere Unterstützung besuche
|
wrongFileAlert.link=Besuche für weitere Hilfe
|
||||||
|
|
||||||
# Vault Options
|
# Vault Options
|
||||||
## General
|
## General
|
||||||
@@ -286,26 +288,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 Mount-Flags
|
vaultOptions.mount.customMountFlags=Benutzerdefinierte Einhänge-Optionen
|
||||||
vaultOptions.mount.winDriveLetterOccupied=belegt
|
vaultOptions.mount.winDriveLetterOccupied=belegt
|
||||||
vaultOptions.mount.mountPoint=Einhängepunkt
|
vaultOptions.mount.mountPoint=Einhängepunkt
|
||||||
vaultOptions.mount.mountPoint.auto=Automatisch einen passenden Ort auswählen
|
vaultOptions.mount.mountPoint.auto=Wähle automatisch einen geeigneten Ort aus
|
||||||
vaultOptions.mount.mountPoint.driveLetter=Laufwerksbuchstaben zuweisen
|
vaultOptions.mount.mountPoint.driveLetter=Laufwerksbuchstaben zuweisen
|
||||||
vaultOptions.mount.mountPoint.custom=Pfad selbst wählen
|
vaultOptions.mount.mountPoint.custom=Eigener Pfad
|
||||||
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=Bei Verlust deines Passworts ist ein Wiederherstellungsschlüssel deine einzige Möglichkeit, den Zugriff auf einen Tresor wiederherzustellen.
|
vaultOptions.masterkey.recoveryKeyExpanation=Dein Wiederherstellungsschlüssel gehört nur dir! Dieser wird benötigt, um deinen Tresor wiederherzustellen, für den Fall das du dein Passwort verloren hast.
|
||||||
vaultOptions.masterkey.showRecoveryKeyBtn=Wiederherstellungsschlüssel anzeigen
|
vaultOptions.masterkey.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 folgendem Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
|
recoveryKey.display.message=Mit dem folgenden Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
|
||||||
recoveryKey.display.StorageHints=Bewahre ihn möglichst sicher auf, z. B.\n • in einem Passwortmanager\n • auf einem USB-Speicherstick\n • auf Papier ausgedruckt
|
recoveryKey.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
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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=¡Desbloqueo de "%s" exitoso! Su bóveda ahora es accesible a través de su unidad virtual.
|
||||||
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
|
unlock.success.rememberChoice=Recordar opción y no mostrar de nuevo
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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 par son lecteur virtuel.
|
||||||
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
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ changepassword.title=पासवर्ड बदलें
|
|||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
unlock.unlockBtn=अनलॉक करें
|
unlock.unlockBtn=अनलॉक करें
|
||||||
|
##
|
||||||
## Success
|
## Success
|
||||||
## Failure
|
## Failure
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
# Forget Password
|
# Forget Password
|
||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
|
##
|
||||||
## Success
|
## Success
|
||||||
## Failure
|
## Failure
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
|
|||||||
@@ -93,6 +93,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.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
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ 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.locationDoesNotExist=Direktori pada path yang dipilih tidak ada atau tidak dapat diakses
|
||||||
|
addvaultwizard.new.locationIsNotWritable=Anda tidak memiliki hak akses untuk menulis pada path yang dipilih
|
||||||
addvaultwizard.new.invalidName=Nama brankas salah. Harap pilih nama folder yang umum.
|
addvaultwizard.new.invalidName=Nama brankas salah. Harap pilih nama folder yang umum.
|
||||||
### Password
|
### Password
|
||||||
addvaultwizard.new.createVaultBtn=Buat Brankas
|
addvaultwizard.new.createVaultBtn=Buat Brankas
|
||||||
@@ -55,6 +58,8 @@ 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.
|
||||||
@@ -63,18 +68,34 @@ addvault.new.readme.storageLocation.10=Jika kamu butuh bantuan, kunjungi dokumen
|
|||||||
addvault.new.readme.accessLocation.fileName=SELAMAT_DATANG.rtf
|
addvault.new.readme.accessLocation.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
|
||||||
@@ -87,6 +108,7 @@ unlock.unlockBtn=Buka Gembok
|
|||||||
## Start
|
## Start
|
||||||
## Run
|
## Run
|
||||||
## Sucess
|
## Sucess
|
||||||
|
migration.success.unlockNow=Buka Kunci Sekarang
|
||||||
## Missing file system capabilities
|
## Missing file system capabilities
|
||||||
## Impossible
|
## Impossible
|
||||||
|
|
||||||
@@ -110,10 +132,12 @@ main.preferencesBtn.tooltip=Preferensi
|
|||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
## Vault List
|
## Vault List
|
||||||
main.vaultlist.contextMenu.lock=Gembok
|
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
|
||||||
@@ -127,6 +151,7 @@ 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,6 +44,10 @@ 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.locationDoesNotExist=Una cartella nel percorso specificato non esiste o non è accessibile
|
||||||
|
addvaultwizard.new.locationIsNotWritable=Non c'è accesso in scrittura nel percorso specificato
|
||||||
|
addvaultwizard.new.locationIsOk=Posizione adatta per il tuo vault
|
||||||
addvaultwizard.new.invalidName=Nome della cassaforte non valido. Si prega di considerare un nome di directory regolare.
|
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
|
||||||
@@ -95,6 +99,8 @@ 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="%s" sbloccato con successo! La tua cassaforte è ora accessibile tramite il suo drive virtuale.
|
||||||
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
|
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
|
||||||
@@ -148,6 +154,7 @@ 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
|
||||||
@@ -172,6 +179,11 @@ preferences.updates.autoUpdateCheck=Controlla automaticamente la presenza di agg
|
|||||||
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
|
## Contribution
|
||||||
|
preferences.contribute=Supportaci
|
||||||
|
preferences.contribute.registeredFor=Certificato del supporto registrato per %s
|
||||||
|
preferences.contribute.noCertificate=Supporta Cryptomator e ricevi un certificato di supporter. È come una chiave di licenza, ma per persone fantastiche che utilizzano software libero. ;-)
|
||||||
|
preferences.contribute.getCertificate=Non ne hai già uno? Impara come puoi ottenerlo.
|
||||||
|
preferences.contribute.promptText=Incolla qui il codice del certificato di supporter
|
||||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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=選択を記憶させて、再度表示しない
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ 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=선택 기억함, 다시 묻지 않음
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ 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.revealBtn=Atklāt disku
|
||||||
## Failure
|
## Failure
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ 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.savePassword=Husk passord
|
||||||
unlock.unlockBtn=Lås opp
|
unlock.unlockBtn=Lås opp
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Velg hovednøkkelfil
|
||||||
## Success
|
## Success
|
||||||
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 enheten synlig
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ 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.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 Schijf
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ 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.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 eininga synleg
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ unlock.title=ਵਾਲਟ ਅਣ-ਲਾਕ ਕਰੋ
|
|||||||
unlock.passwordPrompt="%s" ਲਈ ਪਾਸਵਰਡ ਦਿਓ:
|
unlock.passwordPrompt="%s" ਲਈ ਪਾਸਵਰਡ ਦਿਓ:
|
||||||
unlock.savePassword=ਪਾਸਵਰਡ ਯਾਦ ਰੱਖੋ
|
unlock.savePassword=ਪਾਸਵਰਡ ਯਾਦ ਰੱਖੋ
|
||||||
unlock.unlockBtn=ਅਣ-ਲਾਕ ਕਰੋ
|
unlock.unlockBtn=ਅਣ-ਲਾਕ ਕਰੋ
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=ਮਾਸਟਰ-ਕੁੰਜੀ ਫਾਇਲ ਚੁਣੋ
|
||||||
## Success
|
## Success
|
||||||
unlock.success.rememberChoice=ਚੋਣਾਂ ਯਾਦ ਰੱਖੋ, ਇਹ ਮੁੜ ਕੇ ਨਾ ਵੇਖਾਓ
|
unlock.success.rememberChoice=ਚੋਣਾਂ ਯਾਦ ਰੱਖੋ, ਇਹ ਮੁੜ ਕੇ ਨਾ ਵੇਖਾਓ
|
||||||
unlock.success.revealBtn=ਡਰਾਇਵ ਦਿਖਾਓ
|
unlock.success.revealBtn=ਡਰਾਇਵ ਦਿਖਾਓ
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ unlock.title=Odblokuj sejf
|
|||||||
unlock.passwordPrompt=Wprowadź hasło dla "%s":
|
unlock.passwordPrompt=Wprowadź hasło dla "%s":
|
||||||
unlock.savePassword=Zapamiętaj hasło
|
unlock.savePassword=Zapamiętaj hasło
|
||||||
unlock.unlockBtn=Odblokuj
|
unlock.unlockBtn=Odblokuj
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Wybierz plik Masterkey
|
||||||
## Success
|
## Success
|
||||||
unlock.success.message=Odblokowano "%s" pomyślnie! Twój sejf jest teraz dostępny za pomocą dysku wirtualnego.
|
unlock.success.message=Odblokowano "%s" pomyślnie! Twój sejf jest teraz dostępny za pomocą dysku wirtualnego.
|
||||||
unlock.success.rememberChoice=Zapamiętaj wybór, nie pokazuj tego ponownie
|
unlock.success.rememberChoice=Zapamiętaj wybór, nie pokazuj tego ponownie
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ generic.error.title=Ocorreu um erro inesperado
|
|||||||
generic.error.instruction=Isto não devia ter acontecido. Por favor reporte o texto do erro abaixo e descreva os passos que levaram a este problema.
|
generic.error.instruction=Isto não devia ter acontecido. Por favor reporte o texto do erro abaixo e descreva os passos que levaram a este problema.
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
|
defaults.vault.vaultName=Cofre
|
||||||
|
|
||||||
# Tray Menu
|
# Tray Menu
|
||||||
traymenu.showMainWindow=Mostrar
|
traymenu.showMainWindow=Mostrar
|
||||||
@@ -43,6 +44,7 @@ addvaultwizard.new.locationPrompt=…
|
|||||||
addvaultwizard.new.directoryPickerLabel=Outro Local
|
addvaultwizard.new.directoryPickerLabel=Outro Local
|
||||||
addvaultwizard.new.directoryPickerButton=Escolher…
|
addvaultwizard.new.directoryPickerButton=Escolher…
|
||||||
addvaultwizard.new.directoryPickerTitle=Selecionar diretório
|
addvaultwizard.new.directoryPickerTitle=Selecionar diretório
|
||||||
|
addvaultwizard.new.fileAlreadyExists=Aviso: Já existe um Ficheiro ou Diretório com o mesmo nome
|
||||||
addvaultwizard.new.invalidName=Nome de cofre inválido. Por favor considere um nome de diretório regular.
|
addvaultwizard.new.invalidName=Nome de cofre inválido. Por favor considere um nome de diretório regular.
|
||||||
### Password
|
### Password
|
||||||
addvaultwizard.new.createVaultBtn=Criar Cofre
|
addvaultwizard.new.createVaultBtn=Criar Cofre
|
||||||
@@ -85,6 +87,8 @@ forgetPassword.confirmBtn=Esqueci a Senha
|
|||||||
unlock.title=Destrancar Cofre
|
unlock.title=Destrancar Cofre
|
||||||
unlock.passwordPrompt=Insira a senha para "%s":
|
unlock.passwordPrompt=Insira a senha para "%s":
|
||||||
unlock.unlockBtn=Destrancar
|
unlock.unlockBtn=Destrancar
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Selecionar ficheiro MasterKey
|
||||||
## Success
|
## Success
|
||||||
unlock.success.rememberChoice=Lembrar escolha, não mostrar isto novamente
|
unlock.success.rememberChoice=Lembrar escolha, não mostrar isto novamente
|
||||||
## Failure
|
## Failure
|
||||||
@@ -92,6 +96,7 @@ unlock.success.rememberChoice=Lembrar escolha, não mostrar isto novamente
|
|||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
## Force
|
## Force
|
||||||
|
lock.forced.confirmBtn=Forçar Bloqueio
|
||||||
## Failure
|
## Failure
|
||||||
|
|
||||||
# Migration
|
# Migration
|
||||||
@@ -181,5 +186,8 @@ vaultOptions.masterkey.recoverPasswordBtn=Recuperar Senha
|
|||||||
recoveryKey.title=Chave de Recuperação
|
recoveryKey.title=Chave de Recuperação
|
||||||
|
|
||||||
# New Password
|
# New Password
|
||||||
|
passwordStrength.messageLabel.3=Forte
|
||||||
|
passwordStrength.messageLabel.4=Muito forte
|
||||||
|
|
||||||
# Quit
|
# Quit
|
||||||
|
quit.lockAndQuit=Bloquear e Sair
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ unlock.title=Desbloquear Cofre
|
|||||||
unlock.passwordPrompt=Digite a senha para "%s":
|
unlock.passwordPrompt=Digite a senha para "%s":
|
||||||
unlock.savePassword=Lembrar Senha
|
unlock.savePassword=Lembrar Senha
|
||||||
unlock.unlockBtn=Desbloquear
|
unlock.unlockBtn=Desbloquear
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Selecionar Arquivo Masterkey
|
||||||
## Success
|
## Success
|
||||||
unlock.success.message="%s" desbloqueado com êxito! Seu cofre agora está acessível na unidade virtual.
|
unlock.success.message="%s" desbloqueado com êxito! Seu cofre agora está acessível na unidade virtual.
|
||||||
unlock.success.rememberChoice=Lembrar opção escolhida, não mostrar isto novamente
|
unlock.success.rememberChoice=Lembrar opção escolhida, não mostrar isto novamente
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
# Forget Password
|
# Forget Password
|
||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
|
##
|
||||||
## Success
|
## Success
|
||||||
## Failure
|
## Failure
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ generic.button.copy=Скопировать
|
|||||||
generic.button.copied=Скопировано!
|
generic.button.copied=Скопировано!
|
||||||
generic.button.done=Готово
|
generic.button.done=Готово
|
||||||
generic.button.next=Далее
|
generic.button.next=Далее
|
||||||
generic.button.print=Распечатать
|
generic.button.print=Печать
|
||||||
## Error
|
## Error
|
||||||
generic.error.title=Неизвестная ошибка
|
generic.error.title=Неизвестная ошибка
|
||||||
generic.error.instruction=Этого не должно было произойти. Создайте отчёт об ошибке ниже и опишите, какие шаги к ней привели.
|
generic.error.instruction=Этого не должно было произойти. Создайте отчёт об ошибке ниже и опишите, какие шаги к ней привели.
|
||||||
@@ -45,8 +45,8 @@ 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.locationIsNotWritable=Указанный адрес - только для чтения
|
||||||
addvaultwizard.new.locationIsOk=Подходящее расположение для вашего хранилища
|
addvaultwizard.new.locationIsOk=Подходящее расположение для вашего хранилища
|
||||||
addvaultwizard.new.invalidName=Неверное имя хранилища. Укажите корректное имя папки.
|
addvaultwizard.new.invalidName=Неверное имя хранилища. Укажите корректное имя папки.
|
||||||
### Password
|
### Password
|
||||||
@@ -59,8 +59,8 @@ addvault.new.readme.storageLocation.fileName=ВАЖНО.rtf
|
|||||||
addvault.new.readme.storageLocation.1=⚠️ ФАЙЛЫ ХРАНИЛИЩА ⚠️
|
addvault.new.readme.storageLocation.1=⚠️ ФАЙЛЫ ХРАНИЛИЩА ⚠️
|
||||||
addvault.new.readme.storageLocation.2=Это место, где находится ваше хранилище.
|
addvault.new.readme.storageLocation.2=Это место, где находится ваше хранилище.
|
||||||
addvault.new.readme.storageLocation.3=НЕЛЬЗЯ
|
addvault.new.readme.storageLocation.3=НЕЛЬЗЯ
|
||||||
addvault.new.readme.storageLocation.4=• изменять любые файлы в этой папке или
|
addvault.new.readme.storageLocation.4=• изменять любые файлы в этой папке
|
||||||
addvault.new.readme.storageLocation.5=• добавляйте в эту папку любые файлы для шифрования.
|
addvault.new.readme.storageLocation.5=• добавлять в эту папку любые файлы для шифрования
|
||||||
addvault.new.readme.storageLocation.6=Чтобы зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
|
addvault.new.readme.storageLocation.6=Чтобы зашифровать файлы и просмотреть содержимое хранилища, сделайте следующее:
|
||||||
addvault.new.readme.storageLocation.7=1. Добавьте это хранилище в Cryptomator.
|
addvault.new.readme.storageLocation.7=1. Добавьте это хранилище в Cryptomator.
|
||||||
addvault.new.readme.storageLocation.8=2. Разблокируйте хранилище в Cryptomator.
|
addvault.new.readme.storageLocation.8=2. Разблокируйте хранилище в Cryptomator.
|
||||||
@@ -99,14 +99,16 @@ 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
|
||||||
unlock.error.invalidMountPoint.notExisting=Точка монтирования "%s" не является пустой папкой или не существует.
|
unlock.error.invalidMountPoint.notExisting=Точка монтирования %s - не папка, не пуста или не существует.
|
||||||
unlock.error.invalidMountPoint.existing=Точка монтирования %s уже существует, либо отсутствует родительская папка.
|
unlock.error.invalidMountPoint.existing=Точка монтирования %s уже существует, либо отсутствует родительская папка.
|
||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
@@ -146,7 +148,7 @@ migration.impossible.moreInfo=Хранилище по-прежнему можн
|
|||||||
preferences.title=Настройки
|
preferences.title=Настройки
|
||||||
## General
|
## General
|
||||||
preferences.general=Общие
|
preferences.general=Общие
|
||||||
preferences.general.theme=Оформление
|
preferences.general.theme=Тема
|
||||||
preferences.general.theme.automatic=Автоматически
|
preferences.general.theme.automatic=Автоматически
|
||||||
preferences.general.theme.light=Светлая
|
preferences.general.theme.light=Светлая
|
||||||
preferences.general.theme.dark=Тёмная
|
preferences.general.theme.dark=Тёмная
|
||||||
@@ -157,7 +159,7 @@ preferences.general.startHidden=Скрывать окно при запуске
|
|||||||
preferences.general.debugLogging=Вести журнал отладки
|
preferences.general.debugLogging=Вести журнал отладки
|
||||||
preferences.general.debugDirectory=Показать файлы журнала
|
preferences.general.debugDirectory=Показать файлы журнала
|
||||||
preferences.general.autoStart=Запускать Cryptomator при старте системы
|
preferences.general.autoStart=Запускать Cryptomator при старте системы
|
||||||
preferences.general.keychainBackend=Хранить пароли в
|
preferences.general.keychainBackend=Хранение паролей
|
||||||
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Связка ключей Gnome
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Связка ключей Gnome
|
||||||
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=Хранилище ключей KDE
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=Хранилище ключей KDE
|
||||||
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=Доступ к связке ключей macOS
|
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=Доступ к связке ключей macOS
|
||||||
@@ -230,7 +232,7 @@ main.dropZone.unknownDragboardContent=Если вы хотите добавит
|
|||||||
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь, чтобы добавить хранилище
|
main.vaultlist.emptyList.onboardingInstruction=Нажмите здесь, чтобы добавить хранилище
|
||||||
main.vaultlist.contextMenu.remove=Удалить…
|
main.vaultlist.contextMenu.remove=Удалить…
|
||||||
main.vaultlist.contextMenu.lock=Заблокировать
|
main.vaultlist.contextMenu.lock=Заблокировать
|
||||||
main.vaultlist.contextMenu.unlock=Разблокировка…
|
main.vaultlist.contextMenu.unlock=Разблокировать…
|
||||||
main.vaultlist.contextMenu.unlockNow=Разблокировать
|
main.vaultlist.contextMenu.unlockNow=Разблокировать
|
||||||
main.vaultlist.contextMenu.vaultoptions=Параметры хранилища
|
main.vaultlist.contextMenu.vaultoptions=Параметры хранилища
|
||||||
main.vaultlist.contextMenu.reveal=Показать диск
|
main.vaultlist.contextMenu.reveal=Показать диск
|
||||||
@@ -240,7 +242,7 @@ main.vaultlist.addVaultBtn=Добавить хранилище
|
|||||||
main.vaultDetail.welcomeOnboarding=Благодарим за выбор Cryptomator для защиты ваших файлов. Если требуется помощь, ознакомьтесь с документацией по началу работы:
|
main.vaultDetail.welcomeOnboarding=Благодарим за выбор Cryptomator для защиты ваших файлов. Если требуется помощь, ознакомьтесь с документацией по началу работы:
|
||||||
### Locked
|
### Locked
|
||||||
main.vaultDetail.lockedStatus=ЗАБЛОКИРОВАНО
|
main.vaultDetail.lockedStatus=ЗАБЛОКИРОВАНО
|
||||||
main.vaultDetail.unlockBtn=Разблокировка…
|
main.vaultDetail.unlockBtn=Разблокировать…
|
||||||
main.vaultDetail.unlockNowBtn=Разблокировать
|
main.vaultDetail.unlockNowBtn=Разблокировать
|
||||||
main.vaultDetail.optionsBtn=Параметры хранилища
|
main.vaultDetail.optionsBtn=Параметры хранилища
|
||||||
main.vaultDetail.passwordSavedInKeychain=Пароль сохранён
|
main.vaultDetail.passwordSavedInKeychain=Пароль сохранён
|
||||||
@@ -289,7 +291,7 @@ vaultOptions.mount.readonly=Только чтение
|
|||||||
vaultOptions.mount.customMountFlags=Свои флаги монтирования
|
vaultOptions.mount.customMountFlags=Свои флаги монтирования
|
||||||
vaultOptions.mount.winDriveLetterOccupied=занято
|
vaultOptions.mount.winDriveLetterOccupied=занято
|
||||||
vaultOptions.mount.mountPoint=Точка монтирования
|
vaultOptions.mount.mountPoint=Точка монтирования
|
||||||
vaultOptions.mount.mountPoint.auto=Автоматически выбрать подходящее расположение
|
vaultOptions.mount.mountPoint.auto=Автоматически выбирать подходящее расположение
|
||||||
vaultOptions.mount.mountPoint.driveLetter=Использовать назначенную букву диска
|
vaultOptions.mount.mountPoint.driveLetter=Использовать назначенную букву диска
|
||||||
vaultOptions.mount.mountPoint.custom=Свой путь
|
vaultOptions.mount.mountPoint.custom=Свой путь
|
||||||
vaultOptions.mount.mountPoint.directoryPickerButton=Выбрать…
|
vaultOptions.mount.mountPoint.directoryPickerButton=Выбрать…
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ unlock.title=Odomknúť trezor
|
|||||||
unlock.passwordPrompt=Zadajte heslo pre "%s":
|
unlock.passwordPrompt=Zadajte heslo pre "%s":
|
||||||
unlock.savePassword=Odomknúť.uložiťHeslo
|
unlock.savePassword=Odomknúť.uložiťHeslo
|
||||||
unlock.unlockBtn=Odomknúť
|
unlock.unlockBtn=Odomknúť
|
||||||
|
##
|
||||||
## Success
|
## Success
|
||||||
unlock.success.message=Odomknutie "%s" úspešné! Vaša peňaženka je už prístupná cez jej virtuálny disk.
|
unlock.success.message=Odomknutie "%s" úspešné! Vaša peňaženka je už prístupná cez jej virtuálny disk.
|
||||||
unlock.success.revealBtn=Odkry disk
|
unlock.success.revealBtn=Odkry disk
|
||||||
|
|||||||
@@ -2,76 +2,329 @@
|
|||||||
|
|
||||||
# Generics
|
# Generics
|
||||||
## Button
|
## Button
|
||||||
|
generic.button.apply=Примени
|
||||||
|
generic.button.back=Назад
|
||||||
|
generic.button.cancel=Откажи
|
||||||
|
generic.button.change=Измени
|
||||||
|
generic.button.close=Затвори
|
||||||
|
generic.button.copy=Копирај
|
||||||
|
generic.button.copied=Копирано!
|
||||||
|
generic.button.done=Завршено
|
||||||
|
generic.button.next=Даље
|
||||||
|
generic.button.print=Штампај
|
||||||
## Error
|
## Error
|
||||||
|
generic.error.title=Догодила се неочекивана грешка
|
||||||
|
generic.error.instruction=Ово није требало да се догоди. Молимо вас пријавите текст грешке испод и додајте опис корака који су довели до ове грешке.
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
|
defaults.vault.vaultName=Сеф
|
||||||
|
|
||||||
# Tray Menu
|
# Tray Menu
|
||||||
|
traymenu.showMainWindow=Прикажи
|
||||||
|
traymenu.showPreferencesWindow=Подешавања
|
||||||
|
traymenu.lockAllVaults=Закључај све
|
||||||
|
traymenu.quitApplication=Излаз
|
||||||
|
traymenu.vault.unlock=Откључај
|
||||||
|
traymenu.vault.lock=Закључај
|
||||||
|
traymenu.vault.reveal=Откриј
|
||||||
|
|
||||||
# Add Vault Wizard
|
# Add Vault Wizard
|
||||||
|
addvaultwizard.title=Додавање сефа
|
||||||
## Welcome
|
## Welcome
|
||||||
|
addvaultwizard.welcome.newButton=Направи нови сеф
|
||||||
|
addvaultwizard.welcome.existingButton=Отвори постојећи сеф
|
||||||
## New
|
## New
|
||||||
### Name
|
### Name
|
||||||
|
addvaultwizard.new.nameInstruction=Одабери назив за сеф
|
||||||
|
addvaultwizard.new.namePrompt=Назив сефа
|
||||||
### Location
|
### Location
|
||||||
|
addvaultwizard.new.locationInstruction=Где Cryptomator да складишти шифроване датотеке вашег сефа?
|
||||||
|
addvaultwizard.new.locationLabel=Локација складишта
|
||||||
|
addvaultwizard.new.locationPrompt=…
|
||||||
|
addvaultwizard.new.directoryPickerLabel=Друга локација
|
||||||
|
addvaultwizard.new.directoryPickerButton=Изабери…
|
||||||
|
addvaultwizard.new.directoryPickerTitle=Изабери директоријум
|
||||||
|
addvaultwizard.new.fileAlreadyExists=Датотека или директоријум са овим именом сефа већ постоји
|
||||||
|
addvaultwizard.new.locationDoesNotExist=Директоријум на датој локацији не постоји или му се не може приступити
|
||||||
|
addvaultwizard.new.locationIsNotWritable=Немате приступ писању на датој локацији
|
||||||
|
addvaultwizard.new.locationIsOk=Прикладна локација за ваш сеф
|
||||||
|
addvaultwizard.new.invalidName=Неважеће име сефа. Молимо вас за одабир обичног имена директоријума.
|
||||||
### Password
|
### Password
|
||||||
|
addvaultwizard.new.createVaultBtn=Направи сеф
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice=Нећете моћи да приступите вашим подацима без ваше лозинке. Да ли желите резервни кључ у случају губитка лозинке?
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice.yes=Да молим, увек је добро имати резерву
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice.no=Не хвала, нећу изгубити своју лозинку
|
||||||
### Information
|
### Information
|
||||||
|
addvault.new.readme.storageLocation.fileName=ВАЖНО.rtf
|
||||||
|
addvault.new.readme.storageLocation.1=⚠️ ДОКУМЕНТИ СЕФА ⚠️
|
||||||
|
addvault.new.readme.storageLocation.2=Ово је локација где је складиштен ваш сеф.
|
||||||
|
addvault.new.readme.storageLocation.3=НИ У КОМ СЛУЧАЈУ НЕМОЈТЕ
|
||||||
|
addvault.new.readme.storageLocation.4=• мењати било коју датотеку у овом директоријуму или
|
||||||
|
addvault.new.readme.storageLocation.5=• стављати било коју датотеку који желите да заштитите директно у овај директоријум.
|
||||||
|
addvault.new.readme.storageLocation.6=Ако желите да заштитите документе или прегледате садржај сефа, урадите следеће:
|
||||||
|
addvault.new.readme.storageLocation.7=1. Додајте овај сеф у Cryptomator.
|
||||||
|
addvault.new.readme.storageLocation.8=2. Откључајте сеф у Cryptomator.
|
||||||
|
addvault.new.readme.storageLocation.9=3. Отворите локацију сефа притиском на дугме "Откриј".
|
||||||
|
addvault.new.readme.storageLocation.10=Ако вам је потребна помоћ, прочитајте документацију: %s
|
||||||
|
addvault.new.readme.accessLocation.fileName=ДОБРОДОШЛИ.rtf
|
||||||
|
addvault.new.readme.accessLocation.1=🔐️ ШИФРОВАНА ПАРТИЦИЈА 🔐️
|
||||||
|
addvault.new.readme.accessLocation.2=Ово је приступна локација вашег сефа.
|
||||||
|
addvault.new.readme.accessLocation.3=Било која датотека додата у ову партицију биће шифровани са Cryptomator. Можете да радите у њој баш као у било којој другој партицији или директоријуму. Ово је само дешифрован преглед њеног садржаја, ваше датотеке остају шифроване на вашем тврдом диску све време.
|
||||||
|
addvault.new.readme.accessLocation.4=Слободно можете да обришете ову датотеку.
|
||||||
## Existing
|
## Existing
|
||||||
|
addvaultwizard.existing.instruction=Изабери "masterkey.cryptomator" датотеку свог постојећег сефа.
|
||||||
|
addvaultwizard.existing.chooseBtn=Изабери…
|
||||||
|
addvaultwizard.existing.filePickerTitle=Изабери "Masterkey" датотеку
|
||||||
## Success
|
## Success
|
||||||
|
addvaultwizard.success.nextStepsInstructions=Додат је сеф "%s".\nМорате да откључате овај сеф да бисте приступили или додали садржај. Такође можете да га откључате касније у било ком тренутку.
|
||||||
|
addvaultwizard.success.unlockNow=Откључај сада
|
||||||
|
|
||||||
# Remove Vault
|
# Remove Vault
|
||||||
|
removeVault.title=Уклони сеф
|
||||||
|
removeVault.information=Ово ће урадити да Cryptomator заборави ваш сеф. Увек га поново можете додати касније. Шифроване датотеке неће бити обрисане са вашег тврдог диска.
|
||||||
|
removeVault.confirmBtn=Уклони сеф
|
||||||
|
|
||||||
# Change Password
|
# Change Password
|
||||||
|
changepassword.title=Промена лозинке
|
||||||
|
changepassword.enterOldPassword=Унеси тренутну лозинку за "%s"
|
||||||
|
changepassword.finalConfirmation=Разумем да нећу моћи приступити својим подацима уколико заборавим лозинку
|
||||||
|
|
||||||
# Forget Password
|
# Forget Password
|
||||||
|
forgetPassword.title=Заборави лозинку
|
||||||
|
forgetPassword.information=Ово ће избрисати сачувану лозинку овог сефа из вашег системског менаџера лозинки.
|
||||||
|
forgetPassword.confirmBtn=Заборави лозинку
|
||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
|
unlock.title=Откључавање сефа
|
||||||
|
unlock.passwordPrompt=Унесите лозинку за "%s":
|
||||||
|
unlock.savePassword=Запамти лозинку
|
||||||
|
unlock.unlockBtn=Откључај
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Изабери "Masterkey" датотеку
|
||||||
## Success
|
## Success
|
||||||
|
unlock.success.message=Успешно сте откључали "%s"! Ваш сеф је сада доступан путем свог виртуелног диска.
|
||||||
|
unlock.success.rememberChoice=Запамти избор, не приказуј ово поново
|
||||||
|
unlock.success.revealBtn=Отвори диск
|
||||||
## Failure
|
## Failure
|
||||||
|
unlock.error.heading=Сеф није могуће откључати
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
|
unlock.error.invalidMountPoint.notExisting=Тачка везивања "%s" није директоријум, није празна или не постоји.
|
||||||
|
unlock.error.invalidMountPoint.existing=Тачка везивања "%s" већ постоји или је надређени директоријум непостојећи.
|
||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
## Force
|
## Force
|
||||||
|
lock.forced.heading=Нормално закључавање није успело
|
||||||
|
lock.forced.message=Закључавање "%s" је блокирано операцијама на чекању или отвореним датотекама. Можете присилно закључати овај сеф, међутим прекид "I/O" операција може резултирати губитком несачуваних података.
|
||||||
|
lock.forced.confirmBtn=Присилно закључај
|
||||||
## Failure
|
## Failure
|
||||||
|
lock.fail.heading=Закључавање сефа није успело.
|
||||||
|
lock.fail.message=Сеф "%s" није могуће закључати. Будите сигурни да је несачувани рад сачуван негде другде и да су важне операције читања/писања завршене. Како бисте затворили сеф, зауставите Cryptomator процес у систему.
|
||||||
|
|
||||||
# Migration
|
# Migration
|
||||||
|
migration.title=Надогради сеф
|
||||||
## Start
|
## Start
|
||||||
|
migration.start.prompt=Ваш сеф "%s" мора бити ажуриран на новији формат. Пре наставка, проверите да нема синхронизације на чекању која утиче на овај сеф.
|
||||||
|
migration.start.confirm=Да, мој сеф је у потпуности синхронизован
|
||||||
## Run
|
## Run
|
||||||
|
migration.run.enterPassword=Унесите лозинку за "%s"
|
||||||
|
migration.run.startMigrationBtn=Преместите сеф
|
||||||
|
migration.run.progressHint=Ово би могло потрајати…
|
||||||
## Sucess
|
## Sucess
|
||||||
|
migration.success.nextStepsInstructions="%s" је успешно премештен. Сада можете да откључате ваш сеф.
|
||||||
|
migration.success.unlockNow=Откључај сада
|
||||||
## Missing file system capabilities
|
## Missing file system capabilities
|
||||||
|
migration.error.missingFileSystemCapabilities.title=Неподржан фајл систем
|
||||||
|
migration.error.missingFileSystemCapabilities.description=Премештање није започето, јер се ваш сеф налази на неадекватном фајл систему.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Фајл систем не подржава дугачка имена датотека.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Фајл систем не подржава дугачке адресе локација.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Фајл систем не дозвољава читање.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Фајл систем не дозвољава писање.
|
||||||
## Impossible
|
## Impossible
|
||||||
|
migration.impossible.heading=Сеф не може да се премести
|
||||||
|
migration.impossible.reason=Сеф се не може аутоматски преместити јер је његова складишна локација или тачка приступа некомпатибилна.
|
||||||
|
migration.impossible.moreInfo=Сеф се и даље може отворити са старијом верзијом. За упутства како ручно преместити сеф, посетите
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
|
preferences.title=Подешавања
|
||||||
## General
|
## General
|
||||||
|
preferences.general=Опште
|
||||||
|
preferences.general.theme=Тема
|
||||||
|
preferences.general.theme.automatic=Аутоматска
|
||||||
|
preferences.general.theme.light=Светла
|
||||||
|
preferences.general.theme.dark=Тамна
|
||||||
|
preferences.general.unlockThemes=Откључај тамну тему
|
||||||
|
preferences.general.showMinimizeButton=Прикажи дугме за умањење програма
|
||||||
|
preferences.general.showTrayIcon=Прикажи "tray" иконицу (потребан рестарт)
|
||||||
|
preferences.general.startHidden=Сакриј прозор приликом покретања Cryptomator
|
||||||
|
preferences.general.debugLogging=Омогући евиденцију отклањања грешака
|
||||||
|
preferences.general.debugDirectory=Прикажи датотеке евиденције
|
||||||
|
preferences.general.autoStart=Покрени Cryptomator при покретању система
|
||||||
|
preferences.general.keychainBackend=Похрани лозинке са
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Gnome Keyring
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=KDE Wallet
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=macOS Keychain Access
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Windows Data Protection
|
||||||
|
preferences.general.interfaceOrientation=Оријентација интерфејса
|
||||||
|
preferences.general.interfaceOrientation.ltr=Са лева на десно
|
||||||
|
preferences.general.interfaceOrientation.rtl=Са десна на лево
|
||||||
## Volume
|
## Volume
|
||||||
|
preferences.volume=Виртуелни Диск
|
||||||
|
preferences.volume.type=Тип партиције
|
||||||
|
preferences.volume.webdav.port=WebDAV Порт
|
||||||
|
preferences.volume.webdav.scheme=WebDAV Шема
|
||||||
## Updates
|
## Updates
|
||||||
|
preferences.updates=Ажурирања
|
||||||
|
preferences.updates.currentVersion=Тренутна верзија: %s
|
||||||
|
preferences.updates.autoUpdateCheck=Аутоматска провера доступних ажурирања
|
||||||
|
preferences.updates.checkNowBtn=Провери сада
|
||||||
|
preferences.updates.updateAvailable=Ажурирање на верзију %s је доступно.
|
||||||
## Contribution
|
## Contribution
|
||||||
|
preferences.contribute=Подржите Нас
|
||||||
|
preferences.contribute.registeredFor=Сертификат "Пријатељ Пројекта" регистрован на %s
|
||||||
|
preferences.contribute.noCertificate=Подржите Cryptomator и добићете сертификат "Пријатељ Пројекта". Нешто као лиценцни кључ али за сјајне људе који користе и подржавају бесплатан софтвер. ;-)
|
||||||
|
preferences.contribute.getCertificate=Немате сертификат? Сазнајте како га можете прибавити.
|
||||||
|
preferences.contribute.promptText=Унесите ваш "Пријатељ Пројекта" код овде
|
||||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
preferences.about=О програму
|
||||||
|
|
||||||
# Vault Statistics
|
# Vault Statistics
|
||||||
|
stats.title=Статистика за %s
|
||||||
|
stats.cacheHitRate=Фреквентност кеш меморије
|
||||||
## Read
|
## Read
|
||||||
|
stats.read.throughput.idle=Читање: у приправности
|
||||||
|
stats.read.throughput.kibs=Читање: %.2f kiB/s
|
||||||
|
stats.read.throughput.mibs=Читање: %.2f MiB/s
|
||||||
|
stats.read.total.data.none=Учитано података: -
|
||||||
|
stats.read.total.data.kib=Учитано података: %.1f kiB
|
||||||
|
stats.read.total.data.mib=Учитано података: %.1f MiB
|
||||||
|
stats.read.total.data.gib=Учитано података: %.1f GiB
|
||||||
|
stats.decr.total.data.none=Дешифровано података: -
|
||||||
|
stats.decr.total.data.kib=Дешифровано података: %.1f kiB
|
||||||
|
stats.decr.total.data.mib=Дешифровано података: %.1f MiB
|
||||||
|
stats.decr.total.data.gib=Дешифровано података: %.1f GiB
|
||||||
|
stats.read.accessCount=Укупно учитано: %d
|
||||||
## Write
|
## Write
|
||||||
|
stats.write.throughput.idle=Писање: у приправности
|
||||||
|
stats.write.throughput.kibs=Писање: %.2f kiB/s
|
||||||
|
stats.write.throughput.mibs=Писање: %.2f MiB/s
|
||||||
|
stats.write.total.data.none=Уписано података: -
|
||||||
|
stats.write.total.data.kib=Уписано података: %.1f kiB
|
||||||
|
stats.write.total.data.mib=Уписано података: %.1f MiB
|
||||||
|
stats.write.total.data.gib=Уписано података: %.1f GiB
|
||||||
|
stats.encr.total.data.none=Шифровано података: -
|
||||||
|
stats.encr.total.data.kib=Шифровано података: %.1f kiB
|
||||||
|
stats.encr.total.data.mib=Шифровано података: %.1f MiB
|
||||||
|
stats.encr.total.data.gib=Шифровано података: %.1f GiB
|
||||||
|
stats.write.accessCount=Укупно уписано: %d
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
|
main.closeBtn.tooltip=Затвори
|
||||||
|
main.minimizeBtn.tooltip=Умањи
|
||||||
|
main.preferencesBtn.tooltip=Подешавања
|
||||||
|
main.debugModeEnabled.tooltip=Омогућен је режим отклањања грешака
|
||||||
|
main.donationKeyMissing.tooltip=Молимо Вас размислите о донацији пројекту
|
||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
|
main.dropZone.dropVault=Додај овај сеф
|
||||||
|
main.dropZone.unknownDragboardContent=Ако желите додати сеф, превуците га у овај прозор
|
||||||
## Vault List
|
## Vault List
|
||||||
|
main.vaultlist.emptyList.onboardingInstruction=Кликните овде да додате сеф
|
||||||
|
main.vaultlist.contextMenu.remove=Уклони…
|
||||||
|
main.vaultlist.contextMenu.lock=Закључај
|
||||||
|
main.vaultlist.contextMenu.unlock=Откључај…
|
||||||
|
main.vaultlist.contextMenu.unlockNow=Откључај сада
|
||||||
|
main.vaultlist.contextMenu.vaultoptions=Прикажи опције сефа
|
||||||
|
main.vaultlist.contextMenu.reveal=Прикажи диск
|
||||||
|
main.vaultlist.addVaultBtn=Додај сеф
|
||||||
## Vault Detail
|
## Vault Detail
|
||||||
### Welcome
|
### Welcome
|
||||||
|
main.vaultDetail.welcomeOnboarding=Хвала вам што сте одабрали Cryptomator за заштиту ваших података. Ако вам је потребна помоћ, прегледајте наше корисничко упутство:
|
||||||
### Locked
|
### Locked
|
||||||
|
main.vaultDetail.lockedStatus=ЗАКЉУЧАНО
|
||||||
|
main.vaultDetail.unlockBtn=Откључај…
|
||||||
|
main.vaultDetail.unlockNowBtn=Откључај сада
|
||||||
|
main.vaultDetail.optionsBtn=Опције сефа
|
||||||
|
main.vaultDetail.passwordSavedInKeychain=Лозинка је сачувана
|
||||||
### Unlocked
|
### Unlocked
|
||||||
|
main.vaultDetail.unlockedStatus=ОТКЉУЧАНО
|
||||||
|
main.vaultDetail.accessLocation=Садржај Вашег сефа је доступан овде:
|
||||||
|
main.vaultDetail.revealBtn=Прикажи диск
|
||||||
|
main.vaultDetail.lockBtn=Закључај
|
||||||
|
main.vaultDetail.bytesPerSecondRead=Читање:
|
||||||
|
main.vaultDetail.bytesPerSecondWritten=Писање:
|
||||||
|
main.vaultDetail.throughput.idle=у стању мировања
|
||||||
|
main.vaultDetail.throughput.kbps=%.1f kiB/s
|
||||||
|
main.vaultDetail.throughput.mbps=%.1f MiB/s
|
||||||
|
main.vaultDetail.stats=Статистика сефа
|
||||||
### Missing
|
### Missing
|
||||||
|
main.vaultDetail.missing.info=Cryptomator није пронашао сеф на овој локацији.
|
||||||
|
main.vaultDetail.missing.recheck=Провери поново
|
||||||
|
main.vaultDetail.missing.remove=Удаљи са листе сефова…
|
||||||
|
main.vaultDetail.missing.changeLocation=Промени локацију сефа…
|
||||||
### Needs Migration
|
### Needs Migration
|
||||||
|
main.vaultDetail.migrateButton=Надогради сеф
|
||||||
|
main.vaultDetail.migratePrompt=Да бисте приступили вашем сефу, он мора бити надограђен на нови формат
|
||||||
|
|
||||||
# Wrong File Alert
|
# Wrong File Alert
|
||||||
|
wrongFileAlert.title=Како шифровати датотеке
|
||||||
|
wrongFileAlert.header.title=Да ли сте покушали да шифрујете ове датотеке?
|
||||||
|
wrongFileAlert.header.lead=У ту сврху, Cryptomator вам пружа партицију унутар вашег системског менаџера датотека.
|
||||||
|
wrongFileAlert.instruction.0=Да бисте шифрирали датотеке, пратите следеће кораке:
|
||||||
|
wrongFileAlert.instruction.1=1. Откључајте Ваш сеф.
|
||||||
|
wrongFileAlert.instruction.2=2. Кликните на "Откриј" да бисте отворили партицију унутар вашег менаџера датотека.
|
||||||
|
wrongFileAlert.instruction.3=3. Додајте Ваше датотеке у ову партицију.
|
||||||
|
wrongFileAlert.link=За даљу подршку, посетите
|
||||||
|
|
||||||
# Vault Options
|
# Vault Options
|
||||||
## General
|
## General
|
||||||
|
vaultOptions.general=Опште
|
||||||
|
vaultOptions.general.vaultName=Назив сефа
|
||||||
|
vaultOptions.general.unlockAfterStartup=Откључај сеф при покретању Cryptomator
|
||||||
|
vaultOptions.general.actionAfterUnlock=Након успешног откључавања
|
||||||
|
vaultOptions.general.actionAfterUnlock.ignore=Не ради ништа
|
||||||
|
vaultOptions.general.actionAfterUnlock.reveal=Прикажи диск
|
||||||
|
vaultOptions.general.actionAfterUnlock.ask=Питај
|
||||||
## Mount
|
## Mount
|
||||||
|
vaultOptions.mount=Монтирање
|
||||||
|
vaultOptions.mount.readonly=Само читање
|
||||||
|
vaultOptions.mount.customMountFlags=Прилагођена обележја монтирања
|
||||||
|
vaultOptions.mount.winDriveLetterOccupied=заузето
|
||||||
|
vaultOptions.mount.mountPoint=Монтажна тачка
|
||||||
|
vaultOptions.mount.mountPoint.auto=Аутоматски одабери погодну локацију
|
||||||
|
vaultOptions.mount.mountPoint.driveLetter=Користи додељено слово диска
|
||||||
|
vaultOptions.mount.mountPoint.custom=Друга локација
|
||||||
|
vaultOptions.mount.mountPoint.directoryPickerButton=Изабери…
|
||||||
|
vaultOptions.mount.mountPoint.directoryPickerTitle=Изаберите празан директоријум
|
||||||
## Master Key
|
## Master Key
|
||||||
|
vaultOptions.masterkey=Лозинка
|
||||||
|
vaultOptions.masterkey.changePasswordBtn=Промена лозинке
|
||||||
|
vaultOptions.masterkey.forgetSavedPasswordBtn=Заборави сачувану лозинку
|
||||||
|
vaultOptions.masterkey.recoveryKeyExpanation=Резервни кључ је Ваш једини начин да вратите приступ сефу уколико изгубите лозинку.
|
||||||
|
vaultOptions.masterkey.showRecoveryKeyBtn=Прикажи Резервни Кључ
|
||||||
|
vaultOptions.masterkey.recoverPasswordBtn=Обнови лозинку
|
||||||
|
|
||||||
# Recovery Key
|
# Recovery Key
|
||||||
|
recoveryKey.title=Резервни Кључ
|
||||||
|
recoveryKey.enterPassword.prompt=Унесите Вашу лозинку како бисте приказали резервни кључ за %s:
|
||||||
|
recoveryKey.display.message=Следећи резервни кључ може се користити за враћање приступа "%s":
|
||||||
|
recoveryKey.display.StorageHints=Чувајте га на веома сигурном месту, нпр.:\n • Похраните га унутар менаџера лозинки\n • Сачувајте га на екстерни диск\n • Одштампајте га на папиру
|
||||||
|
recoveryKey.recover.prompt=Унесите резервни кључ за "%s":
|
||||||
|
recoveryKey.recover.validKey=Ово је исправан резервни кључ
|
||||||
|
recoveryKey.printout.heading=Cryptomator Резервни Кључ\n"%s"\n
|
||||||
|
|
||||||
# New Password
|
# New Password
|
||||||
|
newPassword.promptText=Унесите нову лозинку
|
||||||
|
newPassword.reenterPassword=Потврдите нову лозинку
|
||||||
|
newPassword.passwordsMatch=Лозинке се подударају!
|
||||||
|
newPassword.passwordsDoNotMatch=Лозинке се не подударају
|
||||||
|
passwordStrength.messageLabel.tooShort=Користите најмање %d карактера
|
||||||
|
passwordStrength.messageLabel.0=Веома слаба
|
||||||
|
passwordStrength.messageLabel.1=Слаба
|
||||||
|
passwordStrength.messageLabel.2=Осредња
|
||||||
|
passwordStrength.messageLabel.3=Jakа
|
||||||
|
passwordStrength.messageLabel.4=Веома јака
|
||||||
|
|
||||||
# Quit
|
# Quit
|
||||||
|
quit.prompt=Затворити програм? Имате откључаних сефова.
|
||||||
|
quit.lockAndQuit=Закључај и Изађи
|
||||||
|
|||||||
@@ -2,76 +2,245 @@
|
|||||||
|
|
||||||
# Generics
|
# Generics
|
||||||
## Button
|
## Button
|
||||||
|
generic.button.apply=Primeni
|
||||||
|
generic.button.back=Nazad
|
||||||
|
generic.button.cancel=Otkaži
|
||||||
|
generic.button.change=Izmeni
|
||||||
|
generic.button.close=Zatvori
|
||||||
|
generic.button.copy=Kopiraj
|
||||||
|
generic.button.copied=Kopirano!
|
||||||
|
generic.button.done=Završeno
|
||||||
|
generic.button.next=Dalje
|
||||||
|
generic.button.print=Štampaj
|
||||||
## Error
|
## Error
|
||||||
|
generic.error.title=Dogodila se neočekivana greška
|
||||||
|
generic.error.instruction=Ovo nije trebalo da se dogodi. Molimo vas prijavite tekst greške ispod i dodajte opis koraka koji su doveli do ove greške.
|
||||||
|
|
||||||
# Defaults
|
# Defaults
|
||||||
|
defaults.vault.vaultName=Sef
|
||||||
|
|
||||||
# Tray Menu
|
# Tray Menu
|
||||||
|
traymenu.showMainWindow=Prikaži
|
||||||
|
traymenu.showPreferencesWindow=Podešavanja
|
||||||
|
traymenu.lockAllVaults=Zaključaj sve
|
||||||
|
traymenu.quitApplication=Izlaz
|
||||||
|
traymenu.vault.unlock=Otključaj
|
||||||
|
traymenu.vault.lock=Zaključaj
|
||||||
|
traymenu.vault.reveal=Otkrij
|
||||||
|
|
||||||
# Add Vault Wizard
|
# Add Vault Wizard
|
||||||
|
addvaultwizard.title=Dodavanje safa
|
||||||
## Welcome
|
## Welcome
|
||||||
|
addvaultwizard.welcome.newButton=Napravi novi sef
|
||||||
|
addvaultwizard.welcome.existingButton=Otvori postojeći sef
|
||||||
## New
|
## New
|
||||||
### Name
|
### Name
|
||||||
|
addvaultwizard.new.nameInstruction=Odaberi naziv za sef
|
||||||
|
addvaultwizard.new.namePrompt=Naziv sefa
|
||||||
### Location
|
### Location
|
||||||
|
addvaultwizard.new.locationInstruction=Gde Cryptomator da skladišti šifrovane datoteke vašeg sefa?
|
||||||
|
addvaultwizard.new.locationLabel=Lokacija skladišta
|
||||||
|
addvaultwizard.new.locationPrompt=…
|
||||||
|
addvaultwizard.new.directoryPickerLabel=Druga lokacija
|
||||||
|
addvaultwizard.new.directoryPickerButton=Izaberi…
|
||||||
|
addvaultwizard.new.directoryPickerTitle=Izaberi direktorijum
|
||||||
|
addvaultwizard.new.fileAlreadyExists=Datoteka ili direktorijum sa ovim imenom sefa već postoji
|
||||||
|
addvaultwizard.new.locationDoesNotExist=Direktorijum na datoj lokaciji ne postoji ili mu se ne može pristupiti
|
||||||
|
addvaultwizard.new.locationIsNotWritable=Nemate pristup pisanju na datoj lokaciji
|
||||||
|
addvaultwizard.new.locationIsOk=Prikladna lokacija za vaš sef
|
||||||
|
addvaultwizard.new.invalidName=Nevažeće ime sefa. Molimo vas za odabir običnog imena direktorijuma.
|
||||||
### Password
|
### Password
|
||||||
|
addvaultwizard.new.createVaultBtn=Napravi sef
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice=Nećete moći da pristupite vašim podacima bez vaše lozinke. Da li želite rezervni ključ u slučaju gubitka lozinke?
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice.yes=Da molim, uvek je dobro imati rezervu
|
||||||
|
addvaultwizard.new.generateRecoveryKeyChoice.no=Ne hvala, neću izgubiti svoju lozinku
|
||||||
### Information
|
### Information
|
||||||
|
addvault.new.readme.storageLocation.fileName=VAŽNO.rtf
|
||||||
|
addvault.new.readme.storageLocation.1=⚠️ DOKUMENTI SEFA ⚠️
|
||||||
|
addvault.new.readme.storageLocation.2=Ovo je lokacija gde je skladišten vaš sef.
|
||||||
|
addvault.new.readme.storageLocation.3=NI U KOM SLUČAJU NEMOJTE
|
||||||
|
addvault.new.readme.storageLocation.4=• menjati bilo koju datoteku u ovom direktorijumu ili
|
||||||
|
addvault.new.readme.storageLocation.5=• stavljati bilo koju datoteku koji želite da zaštitite direktno u ovaj direktorijum.
|
||||||
|
addvault.new.readme.storageLocation.6=Ako želite da zaštitite dokumente ili pregledate sadržaj sefa, uradite sledeće:
|
||||||
|
addvault.new.readme.storageLocation.7=1. Dodajte ovaj sef u Cryptomator.
|
||||||
|
addvault.new.readme.storageLocation.8=2. Otključajte sef u Cryptomator.
|
||||||
|
addvault.new.readme.storageLocation.9=3. Otvorite lokaciju sefa pritiskom na dugme "Otkrij".
|
||||||
|
addvault.new.readme.storageLocation.10=Ako vam je potrebna pomoć, pročitajte dokumentaciju: %s
|
||||||
|
addvault.new.readme.accessLocation.fileName=DOBRODOŠLI.rtf
|
||||||
|
addvault.new.readme.accessLocation.1=🔐️ ŠIFROVANA PARTICIJA 🔐️
|
||||||
|
addvault.new.readme.accessLocation.2=Ovo je pristupna lokacija vašeg sefa.
|
||||||
|
addvault.new.readme.accessLocation.3=Bilo koja datoteka dodata u ovu particiju biće šifrovana sa Cryptomator. Možete da radite u njoj baš kao i u bilo kojoj drugoj particiji ili direktorijumu. Ovo je samo dešifrovan pregled njenog sadržaja, vaše datoteke ostaju šifrovane na vašem tvrdom disku sve vreme.
|
||||||
|
addvault.new.readme.accessLocation.4=Slobodno možete da obrišete ovu datoteku.
|
||||||
## Existing
|
## Existing
|
||||||
|
addvaultwizard.existing.instruction=Izaberi "masterkey.cryptomator" datoteku svog postojećeg sefa.
|
||||||
|
addvaultwizard.existing.chooseBtn=Izaberi…
|
||||||
|
addvaultwizard.existing.filePickerTitle=Izaberi "Masterkey" datoteku
|
||||||
## Success
|
## Success
|
||||||
|
addvaultwizard.success.nextStepsInstructions=Dodat je sef "%s".\nMorate da otključate ovaj sef da biste pristupili ili dodali sadržaj. Takođe možete da ga otključate kasnije u bilo kom trenutku.
|
||||||
|
addvaultwizard.success.unlockNow=Otključaj sada
|
||||||
|
|
||||||
# Remove Vault
|
# Remove Vault
|
||||||
|
removeVault.title=Ukloni sef
|
||||||
|
removeVault.information=Ovo će uraditi da Cryptomator zaboravi vaš sef. Uvek ga ponovo možete dodati kasnije. Šifrovane datoteke neće biti obrisane sa vašeg tvrdog diska.
|
||||||
|
removeVault.confirmBtn=Ukloni sef
|
||||||
|
|
||||||
# Change Password
|
# Change Password
|
||||||
|
changepassword.title=Promena lozinke
|
||||||
|
changepassword.enterOldPassword=Unesi trenutnu lozinku za "%s"
|
||||||
|
changepassword.finalConfirmation=Razumem da neću moći pristupiti svojim podacima ukoliko zaboravim lozinku
|
||||||
|
|
||||||
# Forget Password
|
# Forget Password
|
||||||
|
forgetPassword.title=Zaboravi lozinku
|
||||||
|
forgetPassword.information=Ovo će izbrisati sačuvanu lozinku ovog sefa iz vašeg sistemskog menadžera lozinki.
|
||||||
|
forgetPassword.confirmBtn=Zaboravi lozinku
|
||||||
|
|
||||||
# Unlock
|
# Unlock
|
||||||
|
unlock.title=Otključavanje sefa
|
||||||
|
unlock.passwordPrompt=Unesite lozinku za "%s":
|
||||||
|
unlock.savePassword=Zapamti lozinku
|
||||||
|
unlock.unlockBtn=Otključaj
|
||||||
|
##
|
||||||
|
unlock.chooseMasterkey.filePickerTitle=Izaberi "Masterkey" datoteku
|
||||||
## Success
|
## Success
|
||||||
|
unlock.success.message=Uspešno ste otključali "%s"! Vaš sef je sada dostupan putem ovog virtuelnog diska.
|
||||||
|
unlock.success.rememberChoice=Zapamti izbor, ne prikazuj ovo ponovo
|
||||||
|
unlock.success.revealBtn=Otvori disk
|
||||||
## Failure
|
## Failure
|
||||||
|
unlock.error.heading=Sef nije moguće otključati
|
||||||
### Invalid Mount Point
|
### Invalid Mount Point
|
||||||
|
unlock.error.invalidMountPoint.notExisting=Tačka vezivanja "%s" nije direktorijum, nije prazna ili ne postoji.
|
||||||
|
unlock.error.invalidMountPoint.existing=Tačka vezivanja "%s" već postoji ili je nadređeni direktorijum nepostojeći.
|
||||||
|
|
||||||
# Lock
|
# Lock
|
||||||
## Force
|
## Force
|
||||||
|
lock.forced.heading=Normalno zaključavanje nije uspelo
|
||||||
|
lock.forced.message=Zaključavanje "%s" je blokirano operacijama na čekanju ili otvorenim datotekama. Možete prisilno zaključati ovaj sef, međutim prekid "I/O" operacija može rezultirati gubitkom nesačuvanih podataka.
|
||||||
|
lock.forced.confirmBtn=Prisilno zaključaj
|
||||||
## Failure
|
## Failure
|
||||||
|
lock.fail.heading=Zaključavanje sefa nije uspelo.
|
||||||
|
lock.fail.message=Sef "%s" nije moguće zaključati. Budite sigurni da je nesačuvani rad sačuvan negde drugde i da su važne operacije čitanja/pisanja završene. Kako biste zatvorili sef, zaustavite Cryptomator proces u sistemu.
|
||||||
|
|
||||||
# Migration
|
# Migration
|
||||||
|
migration.title=Nadogradi sef
|
||||||
## Start
|
## Start
|
||||||
|
migration.start.prompt=Vaš sef "%s" mora biti ažuriran na noviji format. Pre nastavka, proverite da nema sinhronizacije na čekanju koja utiče na ovaj sef.
|
||||||
|
migration.start.confirm=Da moj sef je u potpunosti sinhronizovan
|
||||||
## Run
|
## Run
|
||||||
|
migration.run.enterPassword=Unesite lozinku za "%s"
|
||||||
|
migration.run.startMigrationBtn=Premestite sef
|
||||||
|
migration.run.progressHint=Ovo bi moglo potrajati…
|
||||||
## Sucess
|
## Sucess
|
||||||
|
migration.success.nextStepsInstructions="%s" je uspešno premešten. Sada možete da otključate vaš šef.
|
||||||
|
migration.success.unlockNow=Otključaj sada
|
||||||
## Missing file system capabilities
|
## Missing file system capabilities
|
||||||
|
migration.error.missingFileSystemCapabilities.title=Nepodržan fajl sistem
|
||||||
|
migration.error.missingFileSystemCapabilities.description=Premeštanje nije započeto, jer se vaš sef nalazi na neadekvatnom fajl sistemu.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.LONG_FILENAMES=Fajl sistem ne podržava dugačka imena datoteka.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.LONG_PATHS=Fajl sistem ne podržava dugačke adrese lokacija.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.READ_ACCESS=Fajl sistem ne dozvoljava čitanje.
|
||||||
|
migration.error.missingFileSystemCapabilities.reason.WRITE_ACCESS=Fajl sistem ne dozvoljava pisanje.
|
||||||
## Impossible
|
## Impossible
|
||||||
|
migration.impossible.heading=Sef ne može da se premesti
|
||||||
|
migration.impossible.reason=Sef se ne može automatski premestiti jer je njegova skladišna lokacija ili tačka pristupa nekompatibilna.
|
||||||
|
migration.impossible.moreInfo=Sef se i dalje može otvoriti sa starijom verzijom. Za uputstva kako ručno premestiti sef, posetite
|
||||||
|
|
||||||
# Preferences
|
# Preferences
|
||||||
|
preferences.title=Podešavanja
|
||||||
## General
|
## General
|
||||||
|
preferences.general=Opšte
|
||||||
|
preferences.general.theme=Tema
|
||||||
|
preferences.general.theme.automatic=Automatska
|
||||||
|
preferences.general.theme.light=Svetla
|
||||||
|
preferences.general.theme.dark=Tamna
|
||||||
|
preferences.general.unlockThemes=Otključaj tamnu temu
|
||||||
|
preferences.general.showMinimizeButton=Prikaži dugme za umanjenje programa
|
||||||
|
preferences.general.showTrayIcon=Prikaži "tray" ikonicu (potreban restart)
|
||||||
|
preferences.general.startHidden=Sakrij prozor prilikom pokretanja Cryptomator
|
||||||
|
preferences.general.debugLogging=Omogući evidenciju otklanjanja grešaka
|
||||||
|
preferences.general.debugDirectory=Prikaži datoteke evidencije
|
||||||
|
preferences.general.autoStart=Pokreni Cryptomator pri pokretanju sistema
|
||||||
|
preferences.general.keychainBackend=Pohrani lozinke sa
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.SecretServiceKeychainAccess=Gnome Keyring
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.linux.keychain.KDEWalletKeychainAccess=KDE Wallet
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.macos.keychain.MacSystemKeychainAccess=macOS Keychain Access
|
||||||
|
preferences.general.keychainBackend.org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess=Windows Data Protection
|
||||||
|
preferences.general.interfaceOrientation=Orijentacija interfejsa
|
||||||
|
preferences.general.interfaceOrientation.ltr=Sa leva na desno
|
||||||
|
preferences.general.interfaceOrientation.rtl=Sa desna na levo
|
||||||
## Volume
|
## Volume
|
||||||
|
preferences.volume=Virtuelni Disk
|
||||||
|
preferences.volume.type=Tip particije
|
||||||
|
preferences.volume.webdav.port=WebDAV Port
|
||||||
|
preferences.volume.webdav.scheme=WebDAV Šema
|
||||||
## Updates
|
## Updates
|
||||||
|
preferences.updates=Ažuriranja
|
||||||
|
preferences.updates.currentVersion=Trenutna verzija: %s
|
||||||
|
preferences.updates.autoUpdateCheck=Automatska provera dostupnih ažuriranja
|
||||||
|
preferences.updates.checkNowBtn=Proveri sada
|
||||||
|
preferences.updates.updateAvailable=Ažuriranje na verziju %s je dostupno.
|
||||||
## Contribution
|
## Contribution
|
||||||
|
preferences.contribute=Podržite Nas
|
||||||
|
preferences.contribute.registeredFor=Sertifikat "Prijatelj Projekta" registrovan je na %s
|
||||||
|
preferences.contribute.noCertificate=Podržite Cryptomator i dobićete sertifikat "Prijatelj Projekta". Nešto kao licencni ključ ali za sjajne ljude koji koriste i podržavaju besplatan softver. ;-)
|
||||||
|
preferences.contribute.getCertificate=Nemate sertifikat? Saznajte kako ga možete pribaviti.
|
||||||
|
preferences.contribute.promptText=Unesite vaš "Prijatelj Projekta" kod ovde
|
||||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
preferences.about=O programu
|
||||||
|
|
||||||
# Vault Statistics
|
# Vault Statistics
|
||||||
|
stats.title=Statistika za %s
|
||||||
|
stats.cacheHitRate=Frekventnost keš memorije
|
||||||
## Read
|
## Read
|
||||||
|
stats.read.throughput.idle=Čitanje: u pripravnosti
|
||||||
|
stats.read.throughput.kibs=Čitanje: %.2f kiB/s
|
||||||
|
stats.read.throughput.mibs=Čitanje: %.2f MiB/s
|
||||||
|
stats.read.total.data.none=Učitano podataka: -
|
||||||
|
stats.read.total.data.kib=Učitano podataka: %.1f kiB
|
||||||
|
stats.read.total.data.mib=Učitano podataka: %.1f MiB
|
||||||
|
stats.read.total.data.gib=Učitano podataka: %.1f GiB
|
||||||
|
stats.decr.total.data.none=Dešifrovano podataka: -
|
||||||
|
stats.decr.total.data.kib=Dešifrovano podataka: %.1f kiB
|
||||||
|
stats.decr.total.data.mib=Dešifrovano podataka: %.1f MiB
|
||||||
|
stats.decr.total.data.gib=Dešifrovano podataka: %.1f GiB
|
||||||
## Write
|
## Write
|
||||||
|
|
||||||
# Main Window
|
# Main Window
|
||||||
|
main.closeBtn.tooltip=Zatvori
|
||||||
|
main.preferencesBtn.tooltip=Podešavanja
|
||||||
## Drag 'n' Drop
|
## Drag 'n' Drop
|
||||||
## Vault List
|
## Vault List
|
||||||
|
main.vaultlist.contextMenu.lock=Zaključaj
|
||||||
|
main.vaultlist.contextMenu.unlockNow=Otključaj sada
|
||||||
|
main.vaultlist.contextMenu.reveal=Otvori disk
|
||||||
|
main.vaultlist.addVaultBtn=Dodavanje safa
|
||||||
## Vault Detail
|
## Vault Detail
|
||||||
### Welcome
|
### Welcome
|
||||||
### Locked
|
### Locked
|
||||||
|
main.vaultDetail.unlockNowBtn=Otključaj sada
|
||||||
### Unlocked
|
### Unlocked
|
||||||
|
main.vaultDetail.revealBtn=Otvori disk
|
||||||
|
main.vaultDetail.lockBtn=Zaključaj
|
||||||
### Missing
|
### Missing
|
||||||
### Needs Migration
|
### Needs Migration
|
||||||
|
main.vaultDetail.migrateButton=Nadogradi sef
|
||||||
|
|
||||||
# Wrong File Alert
|
# Wrong File Alert
|
||||||
|
|
||||||
# Vault Options
|
# Vault Options
|
||||||
## General
|
## General
|
||||||
|
vaultOptions.general=Opšte
|
||||||
|
vaultOptions.general.vaultName=Naziv sefa
|
||||||
|
vaultOptions.general.actionAfterUnlock.reveal=Otvori disk
|
||||||
## Mount
|
## Mount
|
||||||
|
vaultOptions.mount.mountPoint.directoryPickerButton=Izaberi…
|
||||||
## Master Key
|
## Master Key
|
||||||
|
vaultOptions.masterkey.changePasswordBtn=Promena lozinke
|
||||||
|
|
||||||
# Recovery Key
|
# Recovery Key
|
||||||
|
|
||||||
# New Password
|
# New Password
|
||||||
|
|
||||||
# Quit
|
# Quit
|
||||||
|
quit.lockAndQuit=Zaključaj i Izađi
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user