mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-21 15:34:27 +00:00
Compare commits
81
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e5035f506 | ||
|
|
0ce41e7ac3 | ||
|
|
c1a249fd7f | ||
|
|
c266c7583b | ||
|
|
00331d4857 | ||
|
|
8fd484e2bb | ||
|
|
165d740acd | ||
|
|
fd4010c6c9 | ||
|
|
aa2900fa9e | ||
|
|
76d1875e01 | ||
|
|
bc83e23a34 | ||
|
|
6a326bcbce | ||
|
|
573ad03d68 | ||
|
|
d710d406d0 | ||
|
|
554f245f5d | ||
|
|
34995088ba | ||
|
|
69c63702f2 | ||
|
|
db7e731554 | ||
|
|
e08b43bf17 | ||
|
|
58b45bacd1 | ||
|
|
4f942bc23f | ||
|
|
09a8618fa0 | ||
|
|
52af530d1b | ||
|
|
86a42234c6 | ||
|
|
37c4e78b1d | ||
|
|
9c2c234bee | ||
|
|
59f91267ae | ||
|
|
b3ff9423b4 | ||
|
|
e3f3090341 | ||
|
|
83be6c0864 | ||
|
|
c654951dc1 | ||
|
|
4e177c9ea7 | ||
|
|
dc3a951a1b | ||
|
|
defa9c75eb | ||
|
|
c1f498a114 | ||
|
|
8457c50ebc | ||
|
|
0144cbb99f | ||
|
|
d6e4c7d177 | ||
|
|
3376b16b7b | ||
|
|
673fdcd095 | ||
|
|
0d00520ac1 | ||
|
|
22a0d3a9a5 | ||
|
|
24baa44e70 | ||
|
|
03886f88e8 | ||
|
|
642816b631 | ||
|
|
cd5c55aad7 | ||
|
|
b066b4b045 | ||
|
|
0840695e0a | ||
|
|
8447f105b0 | ||
|
|
c306d8df04 | ||
|
|
beba6490c3 | ||
|
|
fb1078b35b | ||
|
|
be0912e6ca | ||
|
|
2810c044ea | ||
|
|
e09bd160b7 | ||
|
|
e6d1e4697c | ||
|
|
629b6fb97d | ||
|
|
17dc32bb79 | ||
|
|
c05e00d32a | ||
|
|
2cdde54db6 | ||
|
|
6941603cdd | ||
|
|
cdca4e047d | ||
|
|
e32ce22d24 | ||
|
|
e75c415b46 | ||
|
|
0ab28602d1 | ||
|
|
62c8edff04 | ||
|
|
d01c6268f8 | ||
|
|
557aaa2480 | ||
|
|
78e43d401d | ||
|
|
7b08c5d287 | ||
|
|
9ad217ca55 | ||
|
|
1a0bc92bde | ||
|
|
21038212c1 | ||
|
|
b15471b4ff | ||
|
|
ff17b60f56 | ||
|
|
3284578445 | ||
|
|
4b670a59a3 | ||
|
|
3f928cf958 | ||
|
|
efebbc059a | ||
|
|
6e860d293a | ||
|
|
c0a9a95e4f |
@@ -92,6 +92,16 @@ jobs:
|
||||
* [ ] add Linux appimage, zsync file and signature file
|
||||
* [ ] add Windows installer and signature file
|
||||
* [ ] add MacOs disk image and signature file
|
||||
|
||||
## What's new
|
||||
|
||||
## Bugfixes
|
||||
|
||||
## Misc
|
||||
|
||||
---
|
||||
|
||||
:scroll: A complete list of closed issues is available [here](LINK)
|
||||
draft: true
|
||||
prerelease: false
|
||||
- name: Upload buildkit-linux.zip to GitHub Releases
|
||||
|
||||
@@ -48,7 +48,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
|
||||
|
||||
## Features
|
||||
|
||||
- Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||
- Works with Dropbox, Google Drive, OneDrive, MEGA, pCloud, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory
|
||||
- Open Source means: No backdoors, control is better than trust
|
||||
- Client-side: No accounts, no data shared with any online service
|
||||
- Totally transparent: Just work on the virtual drive as if it were a USB flash drive
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.6.0-SNAPSHOT</version>
|
||||
<version>1.6.0</version>
|
||||
</parent>
|
||||
<artifactId>buildkit</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.6.0-SNAPSHOT</version>
|
||||
<version>1.6.0</version>
|
||||
</parent>
|
||||
<artifactId>commons</artifactId>
|
||||
<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.VaultComponent;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.frontend.webdav.WebDavServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -25,6 +26,8 @@ import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
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
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
|
||||
@@ -3,5 +3,8 @@ package org.cryptomator.common;
|
||||
public interface Constants {
|
||||
|
||||
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.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 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_READONLY_MODE = false;
|
||||
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;
|
||||
|
||||
private static final Random RNG = new Random();
|
||||
@@ -52,7 +50,7 @@ public class VaultSettings {
|
||||
private final StringProperty customMountPath = new SimpleStringProperty();
|
||||
private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
|
||||
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 StringBinding mountName;
|
||||
@@ -63,7 +61,7 @@ public class VaultSettings {
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -152,8 +150,8 @@ public class VaultSettings {
|
||||
return mountFlags;
|
||||
}
|
||||
|
||||
public IntegerProperty filenameLengthLimit() {
|
||||
return filenameLengthLimit;
|
||||
public IntegerProperty maxCleartextFilenameLength() {
|
||||
return maxCleartextFilenameLength;
|
||||
}
|
||||
|
||||
public ObjectProperty<WhenUnlocked> actionAfterUnlock() {
|
||||
|
||||
+4
-4
@@ -29,7 +29,7 @@ class VaultSettingsJsonAdapter {
|
||||
out.name("customMountPath").value(value.customMountPath().get());
|
||||
out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().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.endObject();
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class VaultSettingsJsonAdapter {
|
||||
boolean useCustomMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH;
|
||||
boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
|
||||
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;
|
||||
|
||||
in.beginObject();
|
||||
@@ -64,7 +64,7 @@ class VaultSettingsJsonAdapter {
|
||||
case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
|
||||
case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
|
||||
case "mountFlags" -> mountFlags = in.nextString();
|
||||
case "filenameLengthLimit" -> filenameLengthLimit = in.nextInt();
|
||||
case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
|
||||
case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
|
||||
default -> {
|
||||
LOG.warn("Unsupported vault setting found in JSON: " + name);
|
||||
@@ -88,7 +88,7 @@ class VaultSettingsJsonAdapter {
|
||||
vaultSettings.customMountPath().set(customMountPath);
|
||||
vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
|
||||
vaultSettings.mountFlags().set(mountFlags);
|
||||
vaultSettings.filenameLengthLimit().set(filenameLengthLimit);
|
||||
vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
|
||||
vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
|
||||
return vaultSettings;
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.dokany.DokanyMountFailedException;
|
||||
import org.cryptomator.frontend.dokany.Mount;
|
||||
import org.cryptomator.frontend.dokany.MountFactory;
|
||||
import org.cryptomator.frontend.dokany.MountFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class DokanyVolume extends AbstractVolume {
|
||||
|
||||
@@ -22,15 +22,13 @@ public class DokanyVolume extends AbstractVolume {
|
||||
private static final String FS_TYPE_NAME = "CryptomatorFS";
|
||||
|
||||
private final VaultSettings vaultSettings;
|
||||
private final MountFactory mountFactory;
|
||||
|
||||
private Mount mount;
|
||||
|
||||
@Inject
|
||||
public DokanyVolume(VaultSettings vaultSettings, ExecutorService executorService, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
public DokanyVolume(VaultSettings vaultSettings, @Named("orderedMountPointChoosers") Iterable<MountPointChooser> choosers) {
|
||||
super(choosers);
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.mountFactory = new MountFactory(executorService);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -39,11 +37,11 @@ public class DokanyVolume extends AbstractVolume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
try {
|
||||
this.mount = mountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip());
|
||||
} catch (MountFailedException e) {
|
||||
this.mount = MountFactory.mount(fs.getPath("/"), mountPoint, vaultSettings.mountName().get(), FS_TYPE_NAME, mountFlags.strip(), onExitAction);
|
||||
} catch (DokanyMountFailedException e) {
|
||||
if (vaultSettings.getCustomMountPath().isPresent()) {
|
||||
LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPoint);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.mountpoint.MountPointChooser;
|
||||
import org.cryptomator.common.settings.VolumeImpl;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.frontend.fuse.mount.CommandFailedException;
|
||||
import org.cryptomator.frontend.fuse.mount.EnvironmentVariables;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountException;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseMountFactory;
|
||||
import org.cryptomator.frontend.fuse.mount.FuseNotSupportedException;
|
||||
import org.cryptomator.frontend.fuse.mount.Mount;
|
||||
@@ -20,6 +20,7 @@ import javax.inject.Named;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class FuseVolume extends AbstractVolume {
|
||||
@@ -35,13 +36,12 @@ public class FuseVolume extends AbstractVolume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws InvalidMountPointException, VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws InvalidMountPointException, VolumeException {
|
||||
this.mountPoint = determineMountPoint();
|
||||
|
||||
mount(fs.getPath("/"), mountFlags);
|
||||
mount(fs.getPath("/"), mountFlags, onExitAction);
|
||||
}
|
||||
|
||||
private void mount(Path root, String mountFlags) throws VolumeException {
|
||||
private void mount(Path root, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
try {
|
||||
Mounter mounter = FuseMountFactory.getMounter();
|
||||
EnvironmentVariables envVars = EnvironmentVariables.create() //
|
||||
@@ -49,8 +49,8 @@ public class FuseVolume extends AbstractVolume {
|
||||
.withMountPoint(mountPoint) //
|
||||
.withFileNameTranscoder(mounter.defaultFileNameTranscoder()) //
|
||||
.build();
|
||||
this.mount = mounter.mount(root, envVars);
|
||||
} catch (CommandFailedException | FuseNotSupportedException e) {
|
||||
this.mount = mounter.mount(root, envVars, onExitAction);
|
||||
} catch ( FuseMountException | FuseNotSupportedException e) {
|
||||
throw new VolumeException("Unable to mount Filesystem", e);
|
||||
}
|
||||
}
|
||||
@@ -91,8 +91,7 @@ public class FuseVolume extends AbstractVolume {
|
||||
public synchronized void unmountForced() throws VolumeException {
|
||||
try {
|
||||
mount.unmountForced();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
@@ -102,8 +101,7 @@ public class FuseVolume extends AbstractVolume {
|
||||
public synchronized void unmount() throws VolumeException {
|
||||
try {
|
||||
mount.unmount();
|
||||
mount.close();
|
||||
} catch (CommandFailedException e) {
|
||||
} catch (FuseMountException e) {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanupMountPoint();
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public class LockNotCompletedException extends Exception {
|
||||
|
||||
public LockNotCompletedException(String reason) {
|
||||
super(reason);
|
||||
}
|
||||
|
||||
public LockNotCompletedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,12 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
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.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.LoggerFactory;
|
||||
|
||||
@@ -35,28 +37,29 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
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.Paths;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@PerVault
|
||||
public class Vault {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
|
||||
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 Provider<Volume> volumeProvider;
|
||||
private final StringBinding defaultMountFlags;
|
||||
private final AtomicReference<CryptoFileSystem> cryptoFileSystem;
|
||||
private final ObjectProperty<VaultState> state;
|
||||
private final VaultState state;
|
||||
private final ObjectProperty<Exception> lastKnownException;
|
||||
private final VaultStats stats;
|
||||
private final StringBinding displayName;
|
||||
@@ -74,7 +77,7 @@ public class Vault {
|
||||
private volatile Volume volume;
|
||||
|
||||
@Inject
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, ObjectProperty<VaultState> state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
Vault(VaultSettings vaultSettings, Provider<Volume> volumeProvider, @DefaultMountFlags StringBinding defaultMountFlags, AtomicReference<CryptoFileSystem> cryptoFileSystem, VaultState state, @Named("lastKnownException") ObjectProperty<Exception> lastKnownException, VaultStats stats) {
|
||||
this.vaultSettings = vaultSettings;
|
||||
this.volumeProvider = volumeProvider;
|
||||
this.defaultMountFlags = defaultMountFlags;
|
||||
@@ -99,24 +102,31 @@ public class Vault {
|
||||
// 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);
|
||||
if (vaultSettings.usesReadOnlyMode().get()) {
|
||||
flags.add(FileSystemFlags.READONLY);
|
||||
} else if(vaultSettings.maxCleartextFilenameLength().get() == -1) {
|
||||
LOG.debug("Determining cleartext filename length limitations...");
|
||||
var checker = new FileSystemCapabilityChecker();
|
||||
int shorteningThreshold = getUnverifiedVaultConfig().orElseThrow().allegedShorteningThreshold();
|
||||
int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath());
|
||||
if (ciphertextLimit < shorteningThreshold) {
|
||||
int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath());
|
||||
vaultSettings.maxCleartextFilenameLength().set(cleartextLimit);
|
||||
} else {
|
||||
vaultSettings.maxCleartextFilenameLength().setValue(UNLIMITED_FILENAME_LENGTH);
|
||||
}
|
||||
}
|
||||
if (!flags.contains(FileSystemFlags.READONLY) && vaultSettings.filenameLengthLimit().get() == -1) {
|
||||
LOG.debug("Determining file name length limitations...");
|
||||
int limit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(getPath());
|
||||
vaultSettings.filenameLengthLimit().set(limit);
|
||||
LOG.info("Storing file name length limit of {}", limit);
|
||||
|
||||
if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) {
|
||||
LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get());
|
||||
}
|
||||
assert vaultSettings.filenameLengthLimit().get() > 0;
|
||||
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withKeyLoader(keyLoader) //
|
||||
.withFlags(flags) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) //
|
||||
.withMaxNameLength(vaultSettings.filenameLengthLimit().get()) //
|
||||
.withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) //
|
||||
.build();
|
||||
return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps);
|
||||
}
|
||||
@@ -133,29 +143,51 @@ public class Vault {
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() == null) {
|
||||
CryptoFileSystem fs = createCryptoFileSystem(passphrase);
|
||||
cryptoFileSystem.set(fs);
|
||||
try {
|
||||
volume = volumeProvider.get();
|
||||
volume.mount(fs, getEffectiveMountFlags());
|
||||
} catch (Exception e) {
|
||||
destroyCryptoFileSystem();
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
public synchronized void unlock(MasterkeyLoader keyLoader) throws CryptoException, IOException, VolumeException, InvalidMountPointException {
|
||||
if (cryptoFileSystem.get() != null) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException {
|
||||
private void lockOnVolumeExit(Throwable t) {
|
||||
LOG.info("Unmounted vault '{}'", getDisplayName());
|
||||
destroyCryptoFileSystem();
|
||||
state.set(VaultState.Value.LOCKED);
|
||||
if (t != null) {
|
||||
LOG.warn("Unexpected unmount and lock of vault " + getDisplayName(), t);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void lock(boolean forced) throws VolumeException, LockNotCompletedException {
|
||||
//initiate unmount
|
||||
if (forced && volume.supportsForcedUnmount()) {
|
||||
volume.unmountForced();
|
||||
} else {
|
||||
volume.unmount();
|
||||
}
|
||||
destroyCryptoFileSystem();
|
||||
|
||||
//wait for lockOnVolumeExit to be executed
|
||||
try {
|
||||
boolean locked = state.awaitState(VaultState.Value.LOCKED, 3000, TimeUnit.MILLISECONDS);
|
||||
if (!locked) {
|
||||
throw new LockNotCompletedException("Locking of vault " + this.getDisplayName() + " still in progress.");
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new LockNotCompletedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal(Volume.Revealer vaultRevealer) throws VolumeException {
|
||||
@@ -166,16 +198,12 @@ public class Vault {
|
||||
// Observable Properties
|
||||
// *******************************************************************************
|
||||
|
||||
public ObjectProperty<VaultState> stateProperty() {
|
||||
public VaultState stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public VaultState getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
public void setState(VaultState value) {
|
||||
state.setValue(value);
|
||||
public VaultState.Value getState() {
|
||||
return state.getValue();
|
||||
}
|
||||
|
||||
public ObjectProperty<Exception> lastKnownExceptionProperty() {
|
||||
@@ -195,7 +223,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isLocked() {
|
||||
return state.get() == VaultState.LOCKED;
|
||||
return state.get() == VaultState.Value.LOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding processingProperty() {
|
||||
@@ -203,7 +231,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isProcessing() {
|
||||
return state.get() == VaultState.PROCESSING;
|
||||
return state.get() == VaultState.Value.PROCESSING;
|
||||
}
|
||||
|
||||
public BooleanBinding unlockedProperty() {
|
||||
@@ -211,7 +239,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return state.get() == VaultState.UNLOCKED;
|
||||
return state.get() == VaultState.Value.UNLOCKED;
|
||||
}
|
||||
|
||||
public BooleanBinding missingProperty() {
|
||||
@@ -219,7 +247,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isMissing() {
|
||||
return state.get() == VaultState.MISSING;
|
||||
return state.get() == VaultState.Value.MISSING;
|
||||
}
|
||||
|
||||
public BooleanBinding needsMigrationProperty() {
|
||||
@@ -227,7 +255,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isNeedsMigration() {
|
||||
return state.get() == VaultState.NEEDS_MIGRATION;
|
||||
return state.get() == VaultState.Value.NEEDS_MIGRATION;
|
||||
}
|
||||
|
||||
public BooleanBinding unknownErrorProperty() {
|
||||
@@ -235,7 +263,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public boolean isUnknownError() {
|
||||
return state.get() == VaultState.ERROR;
|
||||
return state.get() == VaultState.Value.ERROR;
|
||||
}
|
||||
|
||||
public StringBinding displayNameProperty() {
|
||||
@@ -251,7 +279,7 @@ public class Vault {
|
||||
}
|
||||
|
||||
public String getAccessPoint() {
|
||||
if (state.get() == VaultState.UNLOCKED) {
|
||||
if (state.getValue() == VaultState.Value.UNLOCKED) {
|
||||
assert volume != null;
|
||||
return volume.getMountPoint().orElse(Path.of("")).toString();
|
||||
} else {
|
||||
@@ -299,6 +327,16 @@ public class Vault {
|
||||
return stats;
|
||||
}
|
||||
|
||||
public Optional<UnverifiedVaultConfig> getUnverifiedVaultConfig() {
|
||||
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||
try {
|
||||
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||
return Optional.of(VaultConfig.decode(token));
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
public Observable[] observables() {
|
||||
return new Observable[]{state};
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public interface VaultComponent {
|
||||
Builder vaultSettings(VaultSettings vaultSettings);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialVaultState(VaultState vaultState);
|
||||
Builder initialVaultState(VaultState.Value vaultState);
|
||||
|
||||
@BindsInstance
|
||||
Builder initialErrorCause(@Nullable @Named("lastKnownException") Exception initialErrorCause);
|
||||
|
||||
@@ -11,6 +11,7 @@ package org.cryptomator.common.vaults;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.VaultSettings;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.DirStructure;
|
||||
import org.cryptomator.cryptofs.migration.Migrators;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,14 +21,16 @@ import javax.inject.Singleton;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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
|
||||
public class VaultListManager {
|
||||
@@ -52,19 +55,18 @@ public class VaultListManager {
|
||||
return vaultList;
|
||||
}
|
||||
|
||||
public Vault add(Path pathToVault) throws NoSuchFileException {
|
||||
public Vault add(Path pathToVault) throws IOException {
|
||||
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");
|
||||
}
|
||||
Optional<Vault> alreadyExistingVault = get(normalizedPathToVault);
|
||||
if (alreadyExistingVault.isPresent()) {
|
||||
return alreadyExistingVault.get();
|
||||
} else {
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
}
|
||||
|
||||
return get(normalizedPathToVault) //
|
||||
.orElseGet(() -> {
|
||||
Vault newVault = create(newVaultSettings(normalizedPathToVault));
|
||||
vaultList.add(newVault);
|
||||
return newVault;
|
||||
});
|
||||
}
|
||||
|
||||
private VaultSettings newVaultSettings(Path path) {
|
||||
@@ -94,43 +96,45 @@ public class VaultListManager {
|
||||
private Vault create(VaultSettings vaultSettings) {
|
||||
VaultComponent.Builder compBuilder = vaultComponentBuilder.vaultSettings(vaultSettings);
|
||||
try {
|
||||
VaultState vaultState = determineVaultState(vaultSettings.path().get());
|
||||
VaultState.Value vaultState = determineVaultState(vaultSettings.path().get());
|
||||
compBuilder.initialVaultState(vaultState);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e);
|
||||
compBuilder.initialVaultState(VaultState.ERROR);
|
||||
compBuilder.initialVaultState(ERROR);
|
||||
compBuilder.initialErrorCause(e);
|
||||
}
|
||||
return compBuilder.build().vault();
|
||||
}
|
||||
|
||||
public static VaultState redetermineVaultState(Vault vault) {
|
||||
VaultState previousState = vault.getState();
|
||||
public static VaultState.Value redetermineVaultState(Vault vault) {
|
||||
VaultState state = vault.stateProperty();
|
||||
VaultState.Value previousState = state.getValue();
|
||||
return switch (previousState) {
|
||||
case LOCKED, NEEDS_MIGRATION, MISSING -> {
|
||||
try {
|
||||
VaultState determinedState = determineVaultState(vault.getPath());
|
||||
vault.setState(determinedState);
|
||||
var determinedState = determineVaultState(vault.getPath());
|
||||
state.set(determinedState);
|
||||
yield determinedState;
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to determine vault state for " + vault.getPath(), e);
|
||||
vault.setState(VaultState.ERROR);
|
||||
state.set(ERROR);
|
||||
vault.setLastKnownException(e);
|
||||
yield VaultState.ERROR;
|
||||
yield ERROR;
|
||||
}
|
||||
}
|
||||
case ERROR, UNLOCKED, PROCESSING -> previousState;
|
||||
};
|
||||
}
|
||||
|
||||
private static VaultState determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) {
|
||||
return VaultState.MISSING;
|
||||
} else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) {
|
||||
return VaultState.NEEDS_MIGRATION;
|
||||
} else {
|
||||
return VaultState.LOCKED;
|
||||
private static VaultState.Value determineVaultState(Path pathToVault) throws IOException {
|
||||
if (!Files.exists(pathToVault)) {
|
||||
return VaultState.Value.MISSING;
|
||||
}
|
||||
return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) {
|
||||
case VAULT -> VaultState.Value.LOCKED;
|
||||
case UNRELATED -> VaultState.Value.MISSING;
|
||||
case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,12 +40,6 @@ public class VaultModule {
|
||||
return new AtomicReference<>();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@PerVault
|
||||
public ObjectProperty<VaultState> provideVaultState(VaultState initialState) {
|
||||
return new SimpleObjectProperty<>(initialState);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("lastKnownException")
|
||||
@PerVault
|
||||
@@ -53,7 +47,6 @@ public class VaultModule {
|
||||
return new SimpleObjectProperty<>(initialErrorCause);
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
public Volume provideVolume(Settings settings, WebDavVolume webDavVolume, FuseVolume fuseVolume, DokanyVolume dokanyVolume) {
|
||||
VolumeImpl preferredImpl = settings.preferredVolumeImpl().get();
|
||||
|
||||
@@ -1,34 +1,141 @@
|
||||
package org.cryptomator.common.vaults;
|
||||
|
||||
public enum VaultState {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.beans.value.ObservableValueBase;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@PerVault
|
||||
public class VaultState extends ObservableValueBase<VaultState.Value> implements ObservableObjectValue<VaultState.Value> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultState.class);
|
||||
|
||||
public enum Value {
|
||||
/**
|
||||
* No vault found at the provided path
|
||||
*/
|
||||
MISSING,
|
||||
|
||||
/**
|
||||
* Vault requires migration to a newer vault format
|
||||
*/
|
||||
NEEDS_MIGRATION,
|
||||
|
||||
/**
|
||||
* Vault ready to be unlocked
|
||||
*/
|
||||
LOCKED,
|
||||
|
||||
/**
|
||||
* Vault in transition between two other states
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Vault is unlocked
|
||||
*/
|
||||
UNLOCKED,
|
||||
|
||||
/**
|
||||
* Unknown state due to preceeding unrecoverable exceptions.
|
||||
*/
|
||||
ERROR;
|
||||
}
|
||||
|
||||
private final AtomicReference<Value> value;
|
||||
private final Lock lock = new ReentrantLock();
|
||||
private final Condition valueChanged = lock.newCondition();
|
||||
|
||||
@Inject
|
||||
public VaultState(VaultState.Value initialValue) {
|
||||
this.value = new AtomicReference<>(initialValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value get() {
|
||||
return getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault requires migration to a newer vault format
|
||||
* Transitions from <code>fromState</code> to <code>toState</code>.
|
||||
*
|
||||
* @param fromState Previous state
|
||||
* @param toState New state
|
||||
* @return <code>true</code> if successful
|
||||
*/
|
||||
NEEDS_MIGRATION,
|
||||
public boolean transition(Value fromState, Value toState) {
|
||||
Preconditions.checkArgument(fromState != toState, "fromState must be different than toState");
|
||||
boolean success = value.compareAndSet(fromState, toState);
|
||||
if (success) {
|
||||
fireValueChangedEvent();
|
||||
} else {
|
||||
LOG.debug("Failed transiting into state {}: Expected state was not{}.", fromState, toState);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
public void set(Value newState) {
|
||||
var oldState = value.getAndSet(newState);
|
||||
if (oldState != newState) {
|
||||
fireValueChangedEvent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault ready to be unlocked
|
||||
* Waits for the specified time, until the desired state is reached.
|
||||
*
|
||||
* @param desiredState what state to wait for
|
||||
* @param time the maximum time to wait
|
||||
* @param unit the time unit of the {@code time} argument
|
||||
* @return {@code false} if the waiting time detectably elapsed before reaching {@code desiredState}
|
||||
* @throws InterruptedException if the current thread is interrupted
|
||||
*/
|
||||
LOCKED,
|
||||
public boolean awaitState(Value desiredState, long time, TimeUnit unit) throws InterruptedException {
|
||||
lock.lock();
|
||||
try {
|
||||
long remaining = TimeUnit.NANOSECONDS.convert(time, unit);
|
||||
while (value.get() != desiredState) {
|
||||
if (remaining <= 0L) {
|
||||
return false;
|
||||
}
|
||||
remaining = valueChanged.awaitNanos(remaining);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vault in transition between two other states
|
||||
*/
|
||||
PROCESSING,
|
||||
|
||||
/**
|
||||
* Vault is unlocked
|
||||
*/
|
||||
UNLOCKED,
|
||||
|
||||
/**
|
||||
* Unknown state due to preceeding unrecoverable exceptions.
|
||||
*/
|
||||
ERROR;
|
||||
private void signal() {
|
||||
lock.lock();
|
||||
try {
|
||||
valueChanged.signalAll();
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fireValueChangedEvent() {
|
||||
signal();
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
super.fireValueChangedEvent();
|
||||
} else {
|
||||
Platform.runLater(super::fireValueChangedEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ public class VaultStats {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(VaultStats.class);
|
||||
|
||||
private final AtomicReference<CryptoFileSystem> fs;
|
||||
private final ObjectProperty<VaultState> state;
|
||||
private final VaultState state;
|
||||
private final ScheduledService<Optional<CryptoFileSystemStats>> updateService;
|
||||
private final LongProperty bytesPerSecondRead = new SimpleLongProperty();
|
||||
private final LongProperty bytesPerSecondWritten = new SimpleLongProperty();
|
||||
@@ -41,7 +41,7 @@ public class VaultStats {
|
||||
private final LongProperty filesWritten = new SimpleLongProperty();
|
||||
|
||||
@Inject
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, ObjectProperty<VaultState> state, ExecutorService executor) {
|
||||
VaultStats(AtomicReference<CryptoFileSystem> fs, VaultState state, ExecutorService executor) {
|
||||
this.fs = fs;
|
||||
this.state = state;
|
||||
this.updateService = new UpdateStatsService();
|
||||
@@ -52,13 +52,13 @@ public class VaultStats {
|
||||
}
|
||||
|
||||
private void vaultStateChanged(@SuppressWarnings("unused") Observable observable) {
|
||||
if (VaultState.UNLOCKED.equals(state.get())) {
|
||||
if (VaultState.Value.UNLOCKED == state.get()) {
|
||||
assert fs.get() != null;
|
||||
LOG.debug("start recording stats");
|
||||
updateService.restart();
|
||||
Platform.runLater(() -> updateService.restart());
|
||||
} else {
|
||||
LOG.debug("stop recording stats");
|
||||
updateService.cancel();
|
||||
Platform.runLater(() -> updateService.cancel());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.cryptomator.cryptofs.CryptoFileSystem;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionStage;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
@@ -32,7 +34,7 @@ public interface Volume {
|
||||
* @param fs
|
||||
* @throws IOException
|
||||
*/
|
||||
void mount(CryptoFileSystem fs, String mountFlags) throws IOException, VolumeException, InvalidMountPointException;
|
||||
void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws IOException, VolumeException, InvalidMountPointException;
|
||||
|
||||
/**
|
||||
* Reveals the mounted volume.
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class WebDavVolume implements Volume {
|
||||
@@ -31,6 +32,7 @@ public class WebDavVolume implements Volume {
|
||||
private WebDavServer server;
|
||||
private WebDavServletController servlet;
|
||||
private Mounter.Mount mount;
|
||||
private Consumer<Throwable> onExitAction;
|
||||
|
||||
@Inject
|
||||
public WebDavVolume(Provider<WebDavServer> serverProvider, VaultSettings vaultSettings, Settings settings, WindowsDriveLetters windowsDriveLetters) {
|
||||
@@ -41,12 +43,13 @@ public class WebDavVolume implements Volume {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mount(CryptoFileSystem fs, String mountFlags) throws VolumeException {
|
||||
public void mount(CryptoFileSystem fs, String mountFlags, Consumer<Throwable> onExitAction) throws VolumeException {
|
||||
startServlet(fs);
|
||||
mountServlet();
|
||||
this.onExitAction = onExitAction;
|
||||
}
|
||||
|
||||
private void startServlet(CryptoFileSystem fs){
|
||||
private void startServlet(CryptoFileSystem fs) {
|
||||
if (server == null) {
|
||||
server = serverProvider.get();
|
||||
}
|
||||
@@ -66,7 +69,7 @@ public class WebDavVolume implements Volume {
|
||||
|
||||
//on windows, prevent an automatic drive letter selection in the upstream library. Either we choose already a specifc one or there is no free.
|
||||
Supplier<String> driveLetterSupplier;
|
||||
if(System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
if (System.getProperty("os.name").toLowerCase().contains("windows") && vaultSettings.winDriveLetter().isEmpty().get()) {
|
||||
driveLetterSupplier = () -> windowsDriveLetters.getAvailableDriveLetter().orElse(null);
|
||||
} else {
|
||||
driveLetterSupplier = () -> vaultSettings.winDriveLetter().get();
|
||||
@@ -101,6 +104,7 @@ public class WebDavVolume implements Volume {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,6 +115,7 @@ public class WebDavVolume implements Volume {
|
||||
throw new VolumeException(e);
|
||||
}
|
||||
cleanup();
|
||||
onExitAction.accept(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.6.0-SNAPSHOT</version>
|
||||
<version>1.6.0</version>
|
||||
</parent>
|
||||
<artifactId>launcher</artifactId>
|
||||
<name>Cryptomator Launcher</name>
|
||||
|
||||
+16
-10
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.6.0-SNAPSHOT</version>
|
||||
<version>1.6.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -25,29 +25,29 @@
|
||||
<project.jdk.version>16</project.jdk.version>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>1.9.14</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptofs.version>2.0.0-rc2</cryptomator.cryptofs.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.mac.version>1.0.0-beta2</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.0.0-beta1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>1.3.0</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.2.4</cryptomator.dokany.version>
|
||||
<cryptomator.fuse.version>1.3.1</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.3.1</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.2.0</cryptomator.webdav.version>
|
||||
|
||||
<!-- 3rd party dependencies -->
|
||||
<javafx.version>15</javafx.version>
|
||||
<javafx.version>16</javafx.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>
|
||||
<guava.version>30.0-jre</guava.version>
|
||||
<dagger.version>2.32</dagger.version>
|
||||
<guava.version>30.1.1-jre</guava.version>
|
||||
<dagger.version>2.35.1</dagger.version>
|
||||
<gson.version>2.8.6</gson.version>
|
||||
<slf4j.version>1.7.30</slf4j.version>
|
||||
<logback.version>1.2.3</logback.version>
|
||||
|
||||
<!-- test dependencies -->
|
||||
<junit.jupiter.version>5.7.0</junit.jupiter.version>
|
||||
<mockito.version>3.6.0</mockito.version>
|
||||
<junit.jupiter.version>5.7.1</junit.jupiter.version>
|
||||
<mockito.version>3.9.0</mockito.version>
|
||||
<hamcrest.version>2.2</hamcrest.version>
|
||||
</properties>
|
||||
|
||||
@@ -76,6 +76,12 @@
|
||||
<artifactId>cryptofs</artifactId>
|
||||
<version>${cryptomator.cryptofs.version}</version>
|
||||
</dependency>
|
||||
<!--TODO: only temporary workaround until 1.6.0-beta -->
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>cryptolib</artifactId>
|
||||
<version>2.0.0-rc1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>fuse-nio-adapter</artifactId>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>1.6.0-SNAPSHOT</version>
|
||||
<version>1.6.0</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
+6
-6
@@ -4,7 +4,7 @@ import dagger.Lazy;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
@@ -34,7 +34,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> welcomeScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final ObjectProperty<Vault> vault;
|
||||
private final VaultListManager vaultListManager;
|
||||
@@ -43,11 +43,11 @@ public class ChooseExistingVaultController implements FxController {
|
||||
private Image screenshot;
|
||||
|
||||
@Inject
|
||||
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, GenericErrorComponent.Builder genericErrorBuilder, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
|
||||
ChooseExistingVaultController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_WELCOME) Lazy<Scene> welcomeScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy<Scene> successScene, ErrorComponent.Builder errorComponent, ObjectProperty<Path> vaultPath, @AddVaultWizardWindow ObjectProperty<Vault> vault, VaultListManager vaultListManager, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.welcomeScene = welcomeScene;
|
||||
this.successScene = successScene;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vault = vault;
|
||||
this.vaultListManager = vaultListManager;
|
||||
@@ -81,9 +81,9 @@ public class ChooseExistingVaultController implements FxController {
|
||||
Vault newVault = vaultListManager.add(vaultPath.get());
|
||||
vault.set(newVault);
|
||||
window.setScene(successScene.get());
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to open existing vault.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
-43
@@ -1,10 +1,10 @@
|
||||
package org.cryptomator.ui.addvaultwizard;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -15,21 +15,21 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ResourceBundle;
|
||||
@@ -43,55 +43,74 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseNameScene;
|
||||
private final Lazy<Scene> choosePasswordScene;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final LocationPresets locationPresets;
|
||||
private final ObjectProperty<Path> vaultPath;
|
||||
private final StringProperty vaultName;
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final BooleanBinding validVaultPath;
|
||||
private final BooleanProperty usePresetPath;
|
||||
private final StringProperty warningText;
|
||||
private final StringProperty statusText;
|
||||
private final ObjectProperty<Node> statusGraphic;
|
||||
|
||||
private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH;
|
||||
|
||||
//FXML
|
||||
public ToggleGroup predefinedLocationToggler;
|
||||
public RadioButton iclouddriveRadioButton;
|
||||
public RadioButton dropboxRadioButton;
|
||||
public RadioButton gdriveRadioButton;
|
||||
public RadioButton onedriveRadioButton;
|
||||
public RadioButton megaRadioButton;
|
||||
public RadioButton pcloudRadioButton;
|
||||
public RadioButton customRadioButton;
|
||||
public Label vaultPathStatus;
|
||||
public FontAwesome5IconView goodLocation;
|
||||
public FontAwesome5IconView badLocation;
|
||||
|
||||
@Inject
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, GenericErrorComponent.Builder genericErrorBuilder, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, LocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.chooseNameScene = chooseNameScene;
|
||||
this.choosePasswordScene = choosePasswordScene;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.locationPresets = locationPresets;
|
||||
this.vaultPath = vaultPath;
|
||||
this.vaultName = vaultName;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::isValidVaultPath, vaultPath);
|
||||
this.validVaultPath = Bindings.createBooleanBinding(this::validateVaultPathAndSetStatus, this.vaultPath);
|
||||
this.usePresetPath = new SimpleBooleanProperty();
|
||||
this.warningText = new SimpleStringProperty();
|
||||
this.statusText = new SimpleStringProperty();
|
||||
this.statusGraphic = new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private boolean isValidVaultPath() {
|
||||
return vaultPath.get() != null && Files.notExists(vaultPath.get());
|
||||
private boolean validateVaultPathAndSetStatus() {
|
||||
final Path p = vaultPath.get();
|
||||
if (p == null) {
|
||||
statusText.set("Error: Path is NULL.");
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.exists(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.isWritable(p.getParent())) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsNotWritable"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else if (!Files.notExists(p)) {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
statusGraphic.set(badLocation);
|
||||
return false;
|
||||
} else {
|
||||
statusText.set(resourceBundle.getString("addvaultwizard.new.locationIsOk"));
|
||||
statusGraphic.set(goodLocation);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation);
|
||||
usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton));
|
||||
vaultPath.addListener(this::vaultPathDidChange);
|
||||
}
|
||||
|
||||
private void vaultPathDidChange(@SuppressWarnings("unused") ObservableValue<? extends Path> observable, @SuppressWarnings("unused") Path oldValue, Path newValue) {
|
||||
if (!Files.notExists(newValue)) {
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
} else {
|
||||
warningText.set(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
|
||||
@@ -103,6 +122,10 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get()));
|
||||
} else if (onedriveRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get()));
|
||||
} else if (megaRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get()));
|
||||
} else if (pcloudRadioButton.equals(newValue)) {
|
||||
vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get()));
|
||||
} else if (customRadioButton.equals(newValue)) {
|
||||
vaultPath.set(customVaultPath.resolve(vaultName.get()));
|
||||
}
|
||||
@@ -115,21 +138,10 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void next() {
|
||||
try {
|
||||
// check if we have write access AND the vaultPath doesn't already exist:
|
||||
assert Files.isDirectory(vaultPath.get().getParent());
|
||||
Path createdDir = Files.createDirectory(vaultPath.get());
|
||||
Files.delete(createdDir); // assert: dir exists and is empty
|
||||
if (validateVaultPathAndSetStatus()) {
|
||||
window.setScene(choosePasswordScene.get());
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
LOG.warn("Can not use already existing vault path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.fileAlreadyExists"));
|
||||
} catch (NoSuchFileException e) {
|
||||
LOG.warn("At least one path component does not exist of path {}", vaultPath.get());
|
||||
warningText.set(resourceBundle.getString("addvaultwizard.new.locationDoesNotExist"));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create and delete directory at chosen vault path.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
} else {
|
||||
validVaultPath.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,19 +191,27 @@ public class CreateNewVaultLocationController implements FxController {
|
||||
return usePresetPath.get();
|
||||
}
|
||||
|
||||
public StringProperty warningTextProperty() {
|
||||
return warningText;
|
||||
public BooleanBinding anyRadioButtonSelectedProperty() {
|
||||
return predefinedLocationToggler.selectedToggleProperty().isNotNull();
|
||||
}
|
||||
|
||||
public String getWarningText() {
|
||||
return warningText.get();
|
||||
public boolean isAnyRadioButtonSelected() {
|
||||
return anyRadioButtonSelectedProperty().get();
|
||||
}
|
||||
|
||||
public BooleanBinding showWarningProperty() {
|
||||
return warningText.isNotEmpty();
|
||||
public StringProperty statusTextProperty() {
|
||||
return statusText;
|
||||
}
|
||||
|
||||
public boolean isShowWarning() {
|
||||
return showWarningProperty().get();
|
||||
public String getStatusText() {
|
||||
return statusText.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> statusGraphicProperty() {
|
||||
return statusGraphic;
|
||||
}
|
||||
|
||||
public Node getStatusGraphic() {
|
||||
return statusGraphic.get();
|
||||
}
|
||||
}
|
||||
|
||||
+42
-19
@@ -5,11 +5,17 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProperties;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.cryptofs.VaultCipherCombo;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.Tasks;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -31,13 +37,14 @@ import javafx.scene.control.ToggleGroup;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.URI;
|
||||
import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Collections;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@@ -48,12 +55,13 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
public class CreateNewVaultPasswordController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class);
|
||||
private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place?
|
||||
|
||||
private final Stage window;
|
||||
private final Lazy<Scene> chooseLocationScene;
|
||||
private final Lazy<Scene> recoveryKeyScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final ExecutorService executor;
|
||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||
private final StringProperty vaultNameProperty;
|
||||
@@ -64,6 +72,8 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
private final ResourceBundle resourceBundle;
|
||||
private final ObjectProperty<CharSequence> password;
|
||||
private final ReadmeGenerator readmeGenerator;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
private final BooleanProperty processing;
|
||||
private final BooleanProperty readyToCreateVault;
|
||||
private final ObjectBinding<ContentDisplay> createVaultButtonState;
|
||||
@@ -73,12 +83,12 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
public Toggle skipRecoveryKey;
|
||||
|
||||
@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, GenericErrorComponent.Builder genericErrorBuilder, 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, @Named("newPassword") ObjectProperty<CharSequence> password, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.chooseLocationScene = chooseLocationScene;
|
||||
this.recoveryKeyScene = recoveryKeyScene;
|
||||
this.successScene = successScene;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
this.executor = executor;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
this.vaultNameProperty = vaultName;
|
||||
@@ -89,6 +99,8 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.password = password;
|
||||
this.readmeGenerator = readmeGenerator;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
this.processing = new SimpleBooleanProperty();
|
||||
this.readyToCreateVault = new SimpleBooleanProperty();
|
||||
this.createVaultButtonState = Bindings.createObjectBinding(this::getCreateVaultButtonState, processing);
|
||||
@@ -113,7 +125,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
Files.createDirectory(pathToVault);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create vault directory.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +150,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
window.setScene(recoveryKeyScene.get());
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to initialize vault.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
processing.set(false);
|
||||
}).runOnce(executor);
|
||||
@@ -154,30 +166,41 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
window.setScene(successScene.get());
|
||||
}).onError(IOException.class, e -> {
|
||||
LOG.error("Failed to initialize vault.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
processing.set(false);
|
||||
}).runOnce(executor);
|
||||
}
|
||||
|
||||
private void initializeVault(Path path, CharSequence passphrase) throws IOException {
|
||||
CryptoFileSystemProvider.initialize(path, MASTERKEY_FILENAME, passphrase);
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
|
||||
.withPassphrase(passphrase) //
|
||||
.withFlags(Collections.emptySet()) //
|
||||
.withMasterkeyFilename(MASTERKEY_FILENAME) //
|
||||
.build();
|
||||
// 1. write masterkey:
|
||||
Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME);
|
||||
try (Masterkey masterkey = Masterkey.generate(csprng)) {
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase);
|
||||
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
// 2. initialize vault:
|
||||
try {
|
||||
MasterkeyLoader loader = ignored -> masterkey.clone();
|
||||
CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(VaultCipherCombo.SIV_CTRMAC).withKeyLoader(loader).build();
|
||||
CryptoFileSystemProvider.initialize(path, fsProps, DEFAULT_KEY_ID);
|
||||
|
||||
// 3. write vault-internal readme file:
|
||||
String vaultReadmeFileName = resourceBundle.getString("addvault.new.readme.accessLocation.fileName");
|
||||
try (FileSystem fs = CryptoFileSystemProvider.newFileSystem(path, fsProps); //
|
||||
WritableByteChannel ch = Files.newByteChannel(fs.getPath("/", vaultReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultAccessLocationReadmeRtf()));
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
throw new IOException("Failed initialize vault.", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. write vault-external readme file:
|
||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||
}
|
||||
|
||||
LOG.info("Created vault at {}", path);
|
||||
}
|
||||
|
||||
@@ -185,7 +208,7 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
try {
|
||||
Vault newVault = vaultListManager.add(pathToVault);
|
||||
vaultProperty.set(newVault);
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,21 @@ public class LocationPresets {
|
||||
private static final String[] DROPBOX_LOCATIONS = {"~/Dropbox"};
|
||||
private static final String[] GDRIVE_LOCATIONS = {"~/Google Drive"};
|
||||
private static final String[] ONEDRIVE_LOCATIONS = {"~/OneDrive"};
|
||||
private static final String[] MEGA_LOCATIONS = {"~/MEGA"};
|
||||
private static final String[] PCLOUD_LOCATIONS = {"~/pCloudDrive"};
|
||||
|
||||
private final ReadOnlyObjectProperty<Path> iclouddriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> dropboxLocation;
|
||||
private final ReadOnlyObjectProperty<Path> gdriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> onedriveLocation;
|
||||
private final ReadOnlyObjectProperty<Path> megaLocation;
|
||||
private final ReadOnlyObjectProperty<Path> pcloudLocation;
|
||||
private final BooleanBinding foundIclouddrive;
|
||||
private final BooleanBinding foundDropbox;
|
||||
private final BooleanBinding foundGdrive;
|
||||
private final BooleanBinding foundOnedrive;
|
||||
private final BooleanBinding foundMega;
|
||||
private final BooleanBinding foundPcloud;
|
||||
|
||||
@Inject
|
||||
public LocationPresets() {
|
||||
@@ -32,10 +38,14 @@ public class LocationPresets {
|
||||
this.dropboxLocation = new SimpleObjectProperty<>(existingWritablePath(DROPBOX_LOCATIONS));
|
||||
this.gdriveLocation = new SimpleObjectProperty<>(existingWritablePath(GDRIVE_LOCATIONS));
|
||||
this.onedriveLocation = new SimpleObjectProperty<>(existingWritablePath(ONEDRIVE_LOCATIONS));
|
||||
this.megaLocation = new SimpleObjectProperty<>(existingWritablePath(MEGA_LOCATIONS));
|
||||
this.pcloudLocation = new SimpleObjectProperty<>(existingWritablePath(PCLOUD_LOCATIONS));
|
||||
this.foundIclouddrive = iclouddriveLocation.isNotNull();
|
||||
this.foundDropbox = dropboxLocation.isNotNull();
|
||||
this.foundGdrive = gdriveLocation.isNotNull();
|
||||
this.foundOnedrive = onedriveLocation.isNotNull();
|
||||
this.foundMega = megaLocation.isNotNull();
|
||||
this.foundPcloud = pcloudLocation.isNotNull();
|
||||
}
|
||||
|
||||
private static Path existingWritablePath(String... candidates) {
|
||||
@@ -122,4 +132,36 @@ public class LocationPresets {
|
||||
return foundOnedrive.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
|
||||
return megaLocation;
|
||||
}
|
||||
|
||||
public Path getMegaLocation() {
|
||||
return megaLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundMegaProperty() {
|
||||
return foundMega;
|
||||
}
|
||||
|
||||
public boolean isFoundMega() {
|
||||
return foundMega.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
|
||||
return pcloudLocation;
|
||||
}
|
||||
|
||||
public Path getPcloudLocation() {
|
||||
return pcloudLocation.get();
|
||||
}
|
||||
|
||||
public BooleanBinding foundPcloudProperty() {
|
||||
return foundPcloud;
|
||||
}
|
||||
|
||||
public boolean isFoundPcloud() {
|
||||
return foundPcloud.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
-9
@@ -2,11 +2,13 @@ package org.cryptomator.ui.changepassword;
|
||||
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.slf4j.Logger;
|
||||
@@ -23,7 +25,13 @@ import javafx.scene.control.CheckBox;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@ChangePasswordScoped
|
||||
@@ -34,20 +42,24 @@ public class ChangePasswordController implements FxController {
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final ObjectProperty<CharSequence> newPassword;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final KeychainManager keychain;
|
||||
private final SecureRandom csprng;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
|
||||
public NiceSecurePasswordField oldPasswordField;
|
||||
public CheckBox finalConfirmationCheckbox;
|
||||
public Button finishButton;
|
||||
|
||||
@Inject
|
||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, GenericErrorComponent.Builder genericErrorBuilder, KeychainManager keychain) {
|
||||
public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty<CharSequence> newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.newPassword = newPassword;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
this.keychain = keychain;
|
||||
this.csprng = csprng;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -66,17 +78,26 @@ public class ChangePasswordController implements FxController {
|
||||
@FXML
|
||||
public void finish() {
|
||||
try {
|
||||
CryptoFileSystemProvider.changePassphrase(vault.getPath(), MASTERKEY_FILENAME, oldPasswordField.getCharacters(), newPassword.get());
|
||||
//String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC);
|
||||
//String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC);
|
||||
CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized?
|
||||
CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized?
|
||||
Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME);
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase);
|
||||
Path backupKeyPath = vault.getPath().resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE);
|
||||
LOG.info("Successfully changed password for {}", vault.getDisplayName());
|
||||
window.close();
|
||||
updatePasswordInSystemkeychain();
|
||||
} catch (IOException e) {
|
||||
LOG.error("IO error occured during password change. Unable to perform operation.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
} catch (InvalidPassphraseException e) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
oldPasswordField.selectAll();
|
||||
oldPasswordField.requestFocus();
|
||||
} catch (IOException | CryptoException e) {
|
||||
LOG.error("Password change failed. Unable to perform operation.", e);
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@Subcomponent(modules = {ErrorModule.class})
|
||||
public interface ErrorComponent {
|
||||
|
||||
Stage window();
|
||||
|
||||
@FxmlScene(FxmlFile.ERROR)
|
||||
Scene scene();
|
||||
|
||||
default void showErrorScene() {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
show();
|
||||
} else {
|
||||
Platform.runLater(this::show);
|
||||
}
|
||||
}
|
||||
|
||||
private void show() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder {
|
||||
|
||||
@BindsInstance
|
||||
Builder cause(Throwable cause);
|
||||
|
||||
@BindsInstance
|
||||
Builder window(Stage window);
|
||||
|
||||
@BindsInstance
|
||||
Builder returnToScene(@Nullable Scene previousScene);
|
||||
|
||||
ErrorComponent build();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class ErrorController implements FxController {
|
||||
|
||||
private final String stackTrace;
|
||||
private final Scene previousScene;
|
||||
private final Stage window;
|
||||
|
||||
@Inject
|
||||
ErrorController(@Named("stackTrace") String stackTrace, @Nullable Scene previousScene, Stage window) {
|
||||
this.stackTrace = stackTrace;
|
||||
this.previousScene = previousScene;
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
if (previousScene != null) {
|
||||
window.setScene(previousScene);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public boolean isPreviousScenePresent() {
|
||||
return previousScene != null;
|
||||
}
|
||||
|
||||
public String getStackTrace() {
|
||||
return stackTrace;
|
||||
}
|
||||
}
|
||||
+14
-9
@@ -1,11 +1,9 @@
|
||||
package org.cryptomator.ui.error;
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import dagger.multibindings.IntoMap;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@@ -26,16 +24,23 @@ abstract class ErrorModule {
|
||||
|
||||
@Provides
|
||||
@Named("stackTrace")
|
||||
static String provideStackTrace(@ErrorReport Throwable cause) {
|
||||
static String provideStackTrace(Throwable cause) {
|
||||
// TODO deduplicate VaultDetailUnknownErrorController.java
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
cause.printStackTrace(new PrintStream(baos));
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(ErrorController.class)
|
||||
abstract FxController bindErrorController(ErrorController controller);
|
||||
|
||||
@Provides
|
||||
@Named("errorScene")
|
||||
static Scene provideErrorScene(FxmlLoaderFactory fxmlLoaders, @ErrorReport FxmlFile file) {
|
||||
return fxmlLoaders.createScene(file);
|
||||
@FxmlScene(FxmlFile.ERROR)
|
||||
static Scene provideErrorScene(FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.ERROR);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -9,8 +9,8 @@ public enum FxmlFile {
|
||||
ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), //
|
||||
ADDVAULT_WELCOME("/fxml/addvault_welcome.fxml"), //
|
||||
CHANGEPASSWORD("/fxml/changepassword.fxml"), //
|
||||
ERROR("/fxml/error.fxml"), //
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
GENERIC_ERROR("/fxml/generic_error.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
MAIN_WINDOW("/fxml/main_window.fxml"), //
|
||||
@@ -26,8 +26,9 @@ public enum FxmlFile {
|
||||
RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
|
||||
RECOVERYKEY_SUCCESS("/fxml/recoverykey_success.fxml"), //
|
||||
REMOVE_VAULT("/fxml/remove_vault.fxml"), //
|
||||
UNLOCK("/fxml/unlock.fxml"),
|
||||
UNLOCK_ENTER_PASSWORD("/fxml/unlock_enter_password.fxml"),
|
||||
UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), //
|
||||
UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), //
|
||||
UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), //
|
||||
VAULT_OPTIONS("/fxml/vault_options.fxml"), //
|
||||
VAULT_STATISTICS("/fxml/stats.fxml"), //
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.common;
|
||||
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
@@ -175,24 +176,29 @@ public class VaultService {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Vault call() throws Volume.VolumeException {
|
||||
protected Vault call() throws Volume.VolumeException, LockNotCompletedException {
|
||||
vault.lock(forced);
|
||||
return vault;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.cryptomator.ui.error;
|
||||
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class AbstractErrorController implements FxController {
|
||||
|
||||
protected final Stage window;
|
||||
protected final Scene returnScene;
|
||||
|
||||
public AbstractErrorController(@ErrorReport Stage window, @ErrorReport @Nullable Scene returnScene) {
|
||||
this.returnScene = returnScene;
|
||||
this.window = window;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void back() {
|
||||
if (this.returnScene != null) {
|
||||
this.window.setScene(this.returnScene);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
this.window.close();
|
||||
}
|
||||
|
||||
public boolean isReturnScenePresent() {
|
||||
return this.returnScene != null;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package org.cryptomator.ui.error;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public interface ErrorComponentBase {
|
||||
|
||||
@ErrorReport
|
||||
Stage window();
|
||||
|
||||
@Named("errorScene")
|
||||
Scene errorScene();
|
||||
|
||||
@ErrorReport
|
||||
Throwable cause();
|
||||
|
||||
@ErrorReport
|
||||
@Nullable
|
||||
Scene previousScene();
|
||||
|
||||
default void showErrorScene() {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
show();
|
||||
} else {
|
||||
Platform.runLater(this::show);
|
||||
}
|
||||
}
|
||||
|
||||
private void show() {
|
||||
Stage stage = window();
|
||||
stage.setScene(errorScene());
|
||||
stage.show();
|
||||
}
|
||||
|
||||
interface BuilderBase<B, C> {
|
||||
|
||||
@BindsInstance
|
||||
B window(@ErrorReport Stage window);
|
||||
|
||||
@BindsInstance
|
||||
B cause(@ErrorReport Throwable cause);
|
||||
|
||||
@BindsInstance
|
||||
B returnToScene(@ErrorReport @Nullable Scene returnScene);
|
||||
|
||||
C build();
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.cryptomator.ui.error;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.Subcomponent;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
|
||||
@Subcomponent(modules = {ErrorModule.class, GenericErrorComponent.GenericErrorModule.class})
|
||||
public interface GenericErrorComponent extends ErrorComponentBase {
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder extends BuilderBase<Builder, GenericErrorComponent> { /* Handled by base interface */ }
|
||||
|
||||
@Module
|
||||
abstract class GenericErrorModule {
|
||||
|
||||
@Provides
|
||||
@ErrorReport
|
||||
static FxmlFile provideFxmlFile() {
|
||||
return FxmlFile.GENERIC_ERROR;
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(GenericErrorController.class)
|
||||
abstract FxController bindController(GenericErrorController controller);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.cryptomator.ui.error;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class GenericErrorController extends AbstractErrorController {
|
||||
|
||||
private final String stackTrace;
|
||||
|
||||
@Inject
|
||||
GenericErrorController(@ErrorReport Stage window, @ErrorReport @Nullable Scene previousScene, @Named("stackTrace") String stackTrace) {
|
||||
super(window, previousScene);
|
||||
this.stackTrace = stackTrace;
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getStackTrace() {
|
||||
return this.stackTrace;
|
||||
}
|
||||
}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
package org.cryptomator.ui.error;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.Subcomponent;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.unlock.UnlockInvalidMountPointController;
|
||||
import org.cryptomator.ui.unlock.UnlockScoped;
|
||||
|
||||
@Subcomponent(modules = {ErrorModule.class, InvalidMountPointExceptionComponent.InvalidMountPointExceptionModule.class})
|
||||
@UnlockScoped
|
||||
public interface InvalidMountPointExceptionComponent extends ErrorComponentBase {
|
||||
|
||||
@Subcomponent.Builder
|
||||
interface Builder extends BuilderBase<Builder, InvalidMountPointExceptionComponent> {
|
||||
|
||||
@BindsInstance
|
||||
Builder vault(@ErrorReport Vault vault);
|
||||
|
||||
}
|
||||
|
||||
@Module
|
||||
abstract class InvalidMountPointExceptionModule {
|
||||
|
||||
@Provides
|
||||
@ErrorReport
|
||||
static FxmlFile provideFxmlFile() {
|
||||
return FxmlFile.UNLOCK_INVALID_MOUNT_POINT;
|
||||
}
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnlockInvalidMountPointController.class)
|
||||
abstract FxController bindController(UnlockInvalidMountPointController controller);
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import org.cryptomator.common.LicenseHolder;
|
||||
import org.cryptomator.common.settings.Settings;
|
||||
import org.cryptomator.common.settings.UiTheme;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.integrations.tray.TrayIntegrationProvider;
|
||||
import org.cryptomator.integrations.uiappearance.Theme;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceListener;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
@@ -44,8 +46,9 @@ public class FxApplication extends Application {
|
||||
private final Lazy<MainWindowComponent> mainWindow;
|
||||
private final Lazy<PreferencesComponent> preferencesWindow;
|
||||
private final Lazy<QuitComponent> quitWindow;
|
||||
private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
|
||||
private final Provider<LockComponent.Builder> lockWindowBuilderProvider;
|
||||
private final Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider;
|
||||
private final Provider<LockComponent.Builder> lockWorkflowBuilderProvider;
|
||||
private final ErrorComponent.Builder errorWindowBuilder;
|
||||
private final Optional<TrayIntegrationProvider> trayIntegration;
|
||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||
private final VaultService vaultService;
|
||||
@@ -55,13 +58,14 @@ public class FxApplication extends Application {
|
||||
private final UiAppearanceListener systemInterfaceThemeListener = this::systemInterfaceThemeChanged;
|
||||
|
||||
@Inject
|
||||
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<LockComponent.Builder> lockWindowBuilderProvider, Lazy<QuitComponent> quitWindow, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
|
||||
FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWorkflowBuilderProvider, Provider<LockComponent.Builder> lockWorkflowBuilderProvider, Lazy<QuitComponent> quitWindow, ErrorComponent.Builder errorWindowBuilder, Optional<TrayIntegrationProvider> trayIntegration, Optional<UiAppearanceProvider> appearanceProvider, VaultService vaultService, LicenseHolder licenseHolder) {
|
||||
this.settings = settings;
|
||||
this.mainWindow = mainWindow;
|
||||
this.preferencesWindow = preferencesWindow;
|
||||
this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
|
||||
this.lockWindowBuilderProvider = lockWindowBuilderProvider;
|
||||
this.unlockWorkflowBuilderProvider = unlockWorkflowBuilderProvider;
|
||||
this.lockWorkflowBuilderProvider = lockWorkflowBuilderProvider;
|
||||
this.quitWindow = quitWindow;
|
||||
this.errorWindowBuilder = errorWindowBuilder;
|
||||
this.trayIntegration = trayIntegration;
|
||||
this.appearanceProvider = appearanceProvider;
|
||||
this.vaultService = vaultService;
|
||||
@@ -113,15 +117,23 @@ public class FxApplication extends Application {
|
||||
|
||||
public void startUnlockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||
Platform.runLater(() -> {
|
||||
unlockWindowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
||||
LOG.debug("Showing UnlockWindow for {}", vault.getDisplayName());
|
||||
if (vault.stateProperty().transition(VaultState.Value.LOCKED, VaultState.Value.PROCESSING)) {
|
||||
unlockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startUnlockWorkflow();
|
||||
LOG.debug("Start unlock workflow for {}", vault.getDisplayName());
|
||||
} else {
|
||||
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to unlock vault in non-locked state.")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void startLockWorkflow(Vault vault, Optional<Stage> owner) {
|
||||
Platform.runLater(() -> {
|
||||
lockWindowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
|
||||
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
|
||||
if (vault.stateProperty().transition(VaultState.Value.UNLOCKED, VaultState.Value.PROCESSING)) {
|
||||
lockWorkflowBuilderProvider.get().vault(vault).owner(owner).build().startLockWorkflow();
|
||||
LOG.debug("Start lock workflow for {}", vault.getDisplayName());
|
||||
} else {
|
||||
showMainWindow().thenAccept(mainWindow -> errorWindowBuilder.window(mainWindow).cause(new IllegalStateException("Unable to lock vault in non-unlocked state.")));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,8 @@ import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.error.InvalidMountPointExceptionComponent;
|
||||
import org.cryptomator.ui.lock.LockComponent;
|
||||
import org.cryptomator.ui.mainwindow.MainWindowComponent;
|
||||
import org.cryptomator.ui.preferences.PreferencesComponent;
|
||||
@@ -20,15 +19,17 @@ import org.cryptomator.ui.unlock.UnlockComponent;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Application;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, GenericErrorComponent.class, InvalidMountPointExceptionComponent.class})
|
||||
public abstract class FxApplicationModule {
|
||||
@Module(includes = {UpdateCheckerModule.class}, subcomponents = {MainWindowComponent.class, PreferencesComponent.class, UnlockComponent.class, LockComponent.class, QuitComponent.class, ErrorComponent.class})
|
||||
abstract class FxApplicationModule {
|
||||
|
||||
@Provides
|
||||
@Named("windowIcons")
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package org.cryptomator.ui.error;
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import javax.inject.Qualifier;
|
||||
import java.lang.annotation.Documented;
|
||||
@@ -9,6 +9,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
|
||||
@Qualifier
|
||||
@Documented
|
||||
@Retention(RUNTIME)
|
||||
public @interface ErrorReport {
|
||||
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,48 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@Module(includes = {MasterkeyFileLoadingModule.class})
|
||||
abstract class KeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static FxmlLoaderFactory provideFxmlLoaderFactory(Map<Class<? extends FxController>, Provider<FxController>> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static Optional<URI> provideKeyId(@KeyLoading Vault vault) {
|
||||
return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional<URI> keyId, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||
if (keyId.isEmpty()) {
|
||||
return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided"));
|
||||
} else {
|
||||
String scheme = keyId.get().getScheme();
|
||||
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 == 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.vaults.Vault;
|
||||
@@ -7,6 +7,9 @@ import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.common.WeakBindings;
|
||||
import org.cryptomator.ui.controls.NiceSecurePasswordField;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule.PasswordEntry;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -38,17 +41,17 @@ import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@UnlockScoped
|
||||
public class UnlockController implements FxController {
|
||||
@KeyLoadingScoped
|
||||
public class PassphraseEntryController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PassphraseEntryController.class);
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicBoolean savePassword;
|
||||
private final Optional<char[]> savedPassword;
|
||||
private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
|
||||
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
|
||||
private final ForgetPasswordComponent.Builder forgetPassword;
|
||||
private final KeychainManager keychain;
|
||||
private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
|
||||
@@ -66,7 +69,7 @@ public class UnlockController implements FxController {
|
||||
public Animation unlockAnimation;
|
||||
|
||||
@Inject
|
||||
public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.password = password;
|
||||
@@ -138,7 +141,7 @@ public class UnlockController implements FxController {
|
||||
// if not already interacted, mark this workflow as cancelled:
|
||||
if (passwordEntryLock.awaitingInteraction().get()) {
|
||||
LOG.debug("Unlock canceled by user.");
|
||||
passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
|
||||
passwordEntryLock.interacted(PasswordEntry.CANCELED);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +157,7 @@ public class UnlockController implements FxController {
|
||||
if (oldPw != null) {
|
||||
Arrays.fill(oldPw, ' ');
|
||||
}
|
||||
passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
|
||||
passwordEntryLock.interacted(PasswordEntry.PASSWORD_ENTERED);
|
||||
startUnlockAnimation();
|
||||
}
|
||||
|
||||
+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.Singleton;
|
||||
import javafx.application.Platform;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
@@ -78,7 +79,7 @@ class AppLaunchEventHandler {
|
||||
fxApplicationStarter.get().thenAccept(app -> app.getVaultService().reveal(v));
|
||||
}
|
||||
LOG.debug("Added vault {}", potentialVaultPath);
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to add vault " + potentialVaultPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.cryptomator.ui.launcher;
|
||||
|
||||
import org.cryptomator.common.ShutdownHook;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
@@ -24,11 +25,13 @@ import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
|
||||
@Singleton
|
||||
public class AppLifecycleListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AppLifecycleListener.class);
|
||||
public static final Set<VaultState> STATES_ALLOWING_TERMINATION = EnumSet.of(VaultState.LOCKED, VaultState.NEEDS_MIGRATION, VaultState.MISSING, VaultState.ERROR);
|
||||
public static final Set<VaultState.Value> STATES_ALLOWING_TERMINATION = EnumSet.of(LOCKED, NEEDS_MIGRATION, MISSING, ERROR);
|
||||
|
||||
private final FxApplicationStarter fxApplicationStarter;
|
||||
private final CountDownLatch shutdownLatch;
|
||||
@@ -127,6 +130,8 @@ public class AppLifecycleListener {
|
||||
vault.lock(true);
|
||||
} catch (Volume.VolumeException e) {
|
||||
LOG.error("Failed to unmount vault " + vault.getPath(), e);
|
||||
} catch (LockNotCompletedException e) {
|
||||
LOG.error("Failed to lock vault " + vault.getPath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.cryptomator.ui.lock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.LockNotCompletedException;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
@@ -35,21 +37,23 @@ public class LockWorkflow extends Task<Void> {
|
||||
private final UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock;
|
||||
private final Lazy<Scene> lockForcedScene;
|
||||
private final Lazy<Scene> lockFailedScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
|
||||
@Inject
|
||||
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene) {
|
||||
public LockWorkflow(@LockWindow Stage lockWindow, @LockWindow Vault vault, UserInteractionLock<LockModule.ForceLockDecision> forceLockDecisionLock, @FxmlScene(FxmlFile.LOCK_FORCED) Lazy<Scene> lockForcedScene, @FxmlScene(FxmlFile.LOCK_FAILED) Lazy<Scene> lockFailedScene, ErrorComponent.Builder errorComponent) {
|
||||
this.lockWindow = lockWindow;
|
||||
this.vault = vault;
|
||||
this.forceLockDecisionLock = forceLockDecisionLock;
|
||||
this.lockForcedScene = lockForcedScene;
|
||||
this.lockFailedScene = lockFailedScene;
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException {
|
||||
protected Void call() throws Volume.VolumeException, InterruptedException, LockNotCompletedException {
|
||||
try {
|
||||
vault.lock(false);
|
||||
} catch (Volume.VolumeException e) {
|
||||
} catch (Volume.VolumeException | LockNotCompletedException e) {
|
||||
LOG.debug("Regular lock of {} failed.", vault.getDisplayName(), e);
|
||||
var decision = askUserForAction();
|
||||
switch (decision) {
|
||||
@@ -77,29 +81,29 @@ public class LockWorkflow extends Task<Void> {
|
||||
return forceLockDecisionLock.awaitInteraction();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
LOG.info("Lock of {} succeeded.", vault.getDisplayName());
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
LOG.warn("Failed to lock {}.", vault.getDisplayName());
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
lockWindow.setScene(lockFailedScene.get());
|
||||
lockWindow.show();
|
||||
final var throwable = super.getException();
|
||||
LOG.warn("Lock of {} failed.", vault.getDisplayName(), throwable);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
if (throwable instanceof Volume.VolumeException) {
|
||||
lockWindow.setScene(lockFailedScene.get());
|
||||
lockWindow.show();
|
||||
} else {
|
||||
errorComponent.cause(throwable).window(lockWindow).build().showErrorScene();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
LOG.debug("Lock of {} canceled.", vault.getDisplayName());
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package org.cryptomator.ui.mainwindow;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.DirStructure;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.wrongfilealert.WrongFileAlertComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -20,6 +22,7 @@ import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
@@ -27,6 +30,7 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
|
||||
@MainWindowScoped
|
||||
public class MainWindowController implements FxController {
|
||||
@@ -91,23 +95,21 @@ public class MainWindowController implements FxController {
|
||||
}
|
||||
|
||||
private boolean containsVault(Path path) {
|
||||
if (path.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
||||
return true;
|
||||
} else if (Files.isDirectory(path) && Files.exists(path.resolve(MASTERKEY_FILENAME))) {
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
return CryptoFileSystemProvider.checkDirStructureForVault(path, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) != DirStructure.UNRELATED;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void addVault(Path pathToVault) {
|
||||
try {
|
||||
if (pathToVault.getFileName().toString().equals(MASTERKEY_FILENAME)) {
|
||||
if (pathToVault.getFileName().toString().equals(VAULTCONFIG_FILENAME)) {
|
||||
vaultListManager.add(pathToVault.getParent());
|
||||
} else {
|
||||
vaultListManager.add(pathToVault);
|
||||
}
|
||||
} catch (NoSuchFileException e) {
|
||||
} catch (IOException e) {
|
||||
LOG.debug("Not a vault: {}", pathToVault);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ public class MainWindowTitleController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void showDonationKeyPreferences() {
|
||||
application.showPreferencesWindow(SelectedPreferencesTab.DONATION_KEY);
|
||||
application.showPreferencesWindow(SelectedPreferencesTab.CONTRIBUTE);
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
@@ -32,7 +32,8 @@ public class VaultDetailController implements FxController {
|
||||
this.anyVaultSelected = vault.isNotNull();
|
||||
}
|
||||
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
|
||||
// TODO deduplicate w/ VaultListCellController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
return switch (state) {
|
||||
case LOCKED -> FontAwesome5Icon.LOCK;
|
||||
|
||||
@@ -24,7 +24,8 @@ public class VaultListCellController implements FxController {
|
||||
.map(this::getGlyphForVaultState);
|
||||
}
|
||||
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState state) {
|
||||
// TODO deduplicate w/ VaultDetailController
|
||||
private FontAwesome5Icon getGlyphForVaultState(VaultState.Value state) {
|
||||
if (state != null) {
|
||||
return switch (state) {
|
||||
case LOCKED -> FontAwesome5Icon.LOCK;
|
||||
|
||||
+2
-9
@@ -14,19 +14,13 @@ import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.Arrays;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.ERROR;
|
||||
import static org.cryptomator.common.vaults.VaultState.LOCKED;
|
||||
import static org.cryptomator.common.vaults.VaultState.MISSING;
|
||||
import static org.cryptomator.common.vaults.VaultState.NEEDS_MIGRATION;
|
||||
import static org.cryptomator.common.vaults.VaultState.UNLOCKED;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.*;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultListContextMenuController implements FxController {
|
||||
@@ -37,7 +31,7 @@ public class VaultListContextMenuController implements FxController {
|
||||
private final KeychainManager keychain;
|
||||
private final RemoveVaultComponent.Builder removeVault;
|
||||
private final VaultOptionsComponent.Builder vaultOptionsWindow;
|
||||
private final OptionalBinding<VaultState> selectedVaultState;
|
||||
private final OptionalBinding<VaultState.Value> selectedVaultState;
|
||||
private final Binding<Boolean> selectedVaultPassphraseStored;
|
||||
private final Binding<Boolean> selectedVaultRemovable;
|
||||
private final Binding<Boolean> selectedVaultUnlockable;
|
||||
@@ -57,7 +51,6 @@ public class VaultListContextMenuController implements FxController {
|
||||
this.selectedVaultRemovable = selectedVaultState.map(EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION)::contains).orElse(false);
|
||||
this.selectedVaultUnlockable = selectedVaultState.map(LOCKED::equals).orElse(false);
|
||||
this.selectedVaultLockable = selectedVaultState.map(UNLOCKED::equals).orElse(false);
|
||||
|
||||
}
|
||||
|
||||
private boolean isPasswordStored(Vault vault) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.cryptomator.ui.mainwindow;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultListManager;
|
||||
import org.cryptomator.ui.addvaultwizard.AddVaultWizardComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.removevault.RemoveVaultComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -15,26 +17,39 @@ import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.stage.Stage;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.MISSING;
|
||||
import static org.cryptomator.common.vaults.VaultState.Value.NEEDS_MIGRATION;
|
||||
|
||||
@MainWindowScoped
|
||||
public class VaultListController implements FxController {
|
||||
|
||||
|
||||
private final Stage mainWindow;
|
||||
private final ObservableList<Vault> vaults;
|
||||
private final ObjectProperty<Vault> selectedVault;
|
||||
private final VaultListCellFactory cellFactory;
|
||||
private final AddVaultWizardComponent.Builder addVaultWizard;
|
||||
private final BooleanBinding emptyVaultList;
|
||||
private final RemoveVaultComponent.Builder removeVaultDialogue;
|
||||
|
||||
public ListView<Vault> vaultList;
|
||||
|
||||
@Inject
|
||||
VaultListController(ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard) {
|
||||
VaultListController(@MainWindow Stage mainWindow, ObservableList<Vault> vaults, ObjectProperty<Vault> selectedVault, VaultListCellFactory cellFactory, AddVaultWizardComponent.Builder addVaultWizard, RemoveVaultComponent.Builder removeVaultDialogue) {
|
||||
this.mainWindow = mainWindow;
|
||||
this.vaults = vaults;
|
||||
this.selectedVault = selectedVault;
|
||||
this.cellFactory = cellFactory;
|
||||
this.addVaultWizard = addVaultWizard;
|
||||
this.removeVaultDialogue = removeVaultDialogue;
|
||||
|
||||
this.emptyVaultList = Bindings.isEmpty(vaults);
|
||||
|
||||
@@ -59,6 +74,28 @@ public class VaultListController implements FxController {
|
||||
request.consume();
|
||||
}
|
||||
});
|
||||
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
if (keyEvent.getCode() == KeyCode.DELETE) {
|
||||
pressedShortcutToRemoveVault();
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
vaultList.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
|
||||
if (keyEvent.getCode() == KeyCode.BACK_SPACE) {
|
||||
pressedShortcutToRemoveVault();
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//register vault selection shortcut to the main window
|
||||
mainWindow.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
|
||||
if (keyEvent.isShortcutDown() && keyEvent.getCode().isDigitKey()) {
|
||||
vaultList.getSelectionModel().select(Integer.parseInt(keyEvent.getText()) - 1);
|
||||
keyEvent.consume();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deselect(MouseEvent released) {
|
||||
@@ -80,6 +117,13 @@ public class VaultListController implements FxController {
|
||||
addVaultWizard.build().showAddVaultWizard();
|
||||
}
|
||||
|
||||
private void pressedShortcutToRemoveVault() {
|
||||
final var vault = selectedVault.get();
|
||||
if (vault != null && EnumSet.of(LOCKED, MISSING, ERROR, NEEDS_MIGRATION).contains(vault.getState())) {
|
||||
removeVaultDialogue.vault(vault).build().showRemoveVault();
|
||||
}
|
||||
}
|
||||
|
||||
// Getter and Setter
|
||||
|
||||
public BooleanBinding emptyVaultListProperty() {
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
@@ -26,6 +26,7 @@ import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.ObjectExpression;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -43,6 +44,7 @@ import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
|
||||
|
||||
@MigrationScoped
|
||||
public class MigrationRunController implements FxController {
|
||||
@@ -56,7 +58,7 @@ public class MigrationRunController implements FxController {
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final KeychainManager keychain;
|
||||
private final ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final Lazy<Scene> startScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final Lazy<Scene> impossibleScene;
|
||||
@@ -68,14 +70,14 @@ public class MigrationRunController implements FxController {
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, KeychainManager keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, GenericErrorComponent.Builder genericErrorBuilder) {
|
||||
public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, ScheduledExecutorService scheduler, KeychainManager keychain, @Named("capabilityErrorCause") ObjectProperty<FileSystemCapabilityChecker.Capability> missingCapability, @FxmlScene(FxmlFile.MIGRATION_START) Lazy<Scene> startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.MIGRATION_CAPABILITY_ERROR) Lazy<Scene> capabilityErrorScene, @FxmlScene(FxmlFile.MIGRATION_IMPOSSIBLE) Lazy<Scene> impossibleScene, ErrorComponent.Builder errorComponent) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.executor = executor;
|
||||
this.scheduler = scheduler;
|
||||
this.keychain = keychain;
|
||||
this.missingCapability = missingCapability;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
this.startScene = startScene;
|
||||
this.successScene = successScene;
|
||||
this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty());
|
||||
@@ -89,7 +91,12 @@ public class MigrationRunController implements FxController {
|
||||
if (keychain.isSupported()) {
|
||||
loadStoredPassword();
|
||||
}
|
||||
migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty()));
|
||||
|
||||
migrationButtonDisabled.bind(ObjectExpression.objectExpression(vault.stateProperty())
|
||||
.isNotEqualTo(VaultState.Value.NEEDS_MIGRATION)
|
||||
.or(passwordField.textProperty().isEmpty()));
|
||||
|
||||
window.setOnHiding(event -> passwordField.wipe());
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -101,7 +108,7 @@ public class MigrationRunController implements FxController {
|
||||
public void migrate() {
|
||||
LOG.info("Migrating vault {}", vault.getPath());
|
||||
CharSequence password = passwordField.getCharacters();
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
vault.stateProperty().transition(VaultState.Value.NEEDS_MIGRATION, VaultState.Value.PROCESSING);
|
||||
passwordField.setDisable(true);
|
||||
ScheduledFuture<?> progressSyncTask = scheduler.scheduleAtFixedRate(() -> {
|
||||
Platform.runLater(() -> {
|
||||
@@ -110,15 +117,15 @@ public class MigrationRunController implements FxController {
|
||||
}, 0, MIGRATION_PROGRESS_UPDATE_MILLIS, TimeUnit.MILLISECONDS);
|
||||
Tasks.create(() -> {
|
||||
Migrators migrators = Migrators.get();
|
||||
migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
||||
return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME);
|
||||
migrators.migrate(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME, password, this::migrationProgressChanged, this::migrationRequiresInput);
|
||||
return migrators.needsMigration(vault.getPath(), VAULTCONFIG_FILENAME, MASTERKEY_FILENAME);
|
||||
}).onSuccess(needsAnotherMigration -> {
|
||||
if (needsAnotherMigration) {
|
||||
LOG.info("Migration of '{}' succeeded, but another migration is required.", vault.getDisplayName());
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
} else {
|
||||
LOG.info("Migration of '{}' succeeded.", vault.getDisplayName());
|
||||
vault.setState(VaultState.LOCKED);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
passwordField.wipe();
|
||||
window.setScene(successScene.get());
|
||||
}
|
||||
@@ -127,21 +134,21 @@ public class MigrationRunController implements FxController {
|
||||
passwordField.setDisable(false);
|
||||
passwordField.selectAll();
|
||||
passwordField.requestFocus();
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
}).onError(FileSystemCapabilityChecker.MissingCapabilityException.class, e -> {
|
||||
LOG.error("Underlying file system not supported.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
missingCapability.set(e.getMissingCapability());
|
||||
window.setScene(capabilityErrorScene.get());
|
||||
}).onError(FileNameTooLongException.class, e -> {
|
||||
LOG.error("Migration failed because the underlying file system does not support long filenames.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
window.setScene(impossibleScene.get());
|
||||
}).onError(Exception.class, e -> { // including RuntimeExceptions
|
||||
LOG.error("Migration failed for technical reasons.", e);
|
||||
vault.setState(VaultState.NEEDS_MIGRATION);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.NEEDS_MIGRATION);
|
||||
errorComponent.cause(e).window(window).returnToScene(startScene.get()).build().showErrorScene();
|
||||
}).andFinally(() -> {
|
||||
passwordField.setDisable(false);
|
||||
progressSyncTask.cancel(true);
|
||||
|
||||
+7
-7
@@ -8,7 +8,7 @@ import org.cryptomator.common.settings.UiTheme;
|
||||
import org.cryptomator.integrations.autostart.AutoStartProvider;
|
||||
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.traymenu.TrayMenuComponent;
|
||||
import org.slf4j.Logger;
|
||||
@@ -49,7 +49,7 @@ public class GeneralPreferencesController implements FxController {
|
||||
private final Application application;
|
||||
private final Environment environment;
|
||||
private final Set<KeychainAccessProvider> keychainAccessProviders;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
public ChoiceBox<UiTheme> themeChoiceBox;
|
||||
public ChoiceBox<KeychainBackend> keychainBackendChoiceBox;
|
||||
public CheckBox showMinimizeButtonCheckbox;
|
||||
@@ -63,7 +63,7 @@ public class GeneralPreferencesController implements FxController {
|
||||
|
||||
|
||||
@Inject
|
||||
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, GenericErrorComponent.Builder genericErrorBuilder) {
|
||||
GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, Set<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, ErrorComponent.Builder errorComponent) {
|
||||
this.window = window;
|
||||
this.settings = settings;
|
||||
this.trayMenuInitialized = trayMenu.isInitialized();
|
||||
@@ -75,7 +75,7 @@ public class GeneralPreferencesController implements FxController {
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.application = application;
|
||||
this.environment = environment;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -146,7 +146,7 @@ public class GeneralPreferencesController implements FxController {
|
||||
} catch (ToggleAutoStartFailedException e) {
|
||||
autoStartCheckbox.setSelected(!enableAutoStart); // restore previous state
|
||||
LOG.error("Failed to toggle autostart.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -157,8 +157,8 @@ public class GeneralPreferencesController implements FxController {
|
||||
|
||||
|
||||
@FXML
|
||||
public void showDonationTab() {
|
||||
selectedTabProperty.set(SelectedPreferencesTab.DONATION_KEY);
|
||||
public void showContributeTab() {
|
||||
selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
|
||||
}
|
||||
|
||||
@FXML
|
||||
|
||||
@@ -26,7 +26,7 @@ public class PreferencesController implements FxController {
|
||||
public Tab generalTab;
|
||||
public Tab volumeTab;
|
||||
public Tab updatesTab;
|
||||
public Tab donationKeyTab;
|
||||
public Tab contributeTab;
|
||||
public Tab aboutTab;
|
||||
|
||||
@Inject
|
||||
@@ -52,7 +52,7 @@ public class PreferencesController implements FxController {
|
||||
return switch (selectedTab) {
|
||||
case UPDATES -> updatesTab;
|
||||
case VOLUME -> volumeTab;
|
||||
case DONATION_KEY -> donationKeyTab;
|
||||
case CONTRIBUTE -> contributeTab;
|
||||
case GENERAL -> generalTab;
|
||||
case ABOUT -> aboutTab;
|
||||
case ANY -> updateAvailable.get() ? updatesTab : generalTab;
|
||||
|
||||
@@ -77,8 +77,8 @@ abstract class PreferencesModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(DonationKeyPreferencesController.class)
|
||||
abstract FxController bindDonationKeyPreferencesController(DonationKeyPreferencesController controller);
|
||||
@FxControllerKey(SupporterCertificateController.class)
|
||||
abstract FxController bindSupporterCertificatePreferencesController(SupporterCertificateController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
||||
@@ -22,9 +22,9 @@ public enum SelectedPreferencesTab {
|
||||
UPDATES,
|
||||
|
||||
/**
|
||||
* Show donation key tab
|
||||
* Show contribute tab
|
||||
*/
|
||||
DONATION_KEY,
|
||||
CONTRIBUTE,
|
||||
|
||||
/**
|
||||
* Show about tab
|
||||
|
||||
+9
-9
@@ -14,17 +14,17 @@ import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
|
||||
@PreferencesScoped
|
||||
public class DonationKeyPreferencesController implements FxController {
|
||||
public class SupporterCertificateController implements FxController {
|
||||
|
||||
private static final String DONATION_URI = "https://store.cryptomator.org/desktop";
|
||||
private static final String SUPPORTER_URI = "https://store.cryptomator.org/desktop";
|
||||
|
||||
private final Application application;
|
||||
private final LicenseHolder licenseHolder;
|
||||
private final Settings settings;
|
||||
public TextArea donationKeyField;
|
||||
public TextArea supporterCertificateField;
|
||||
|
||||
@Inject
|
||||
DonationKeyPreferencesController(Application application, LicenseHolder licenseHolder, Settings settings) {
|
||||
SupporterCertificateController(Application application, LicenseHolder licenseHolder, Settings settings) {
|
||||
this.application = application;
|
||||
this.licenseHolder = licenseHolder;
|
||||
this.settings = settings;
|
||||
@@ -32,9 +32,9 @@ public class DonationKeyPreferencesController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
donationKeyField.setText(licenseHolder.getLicenseKey().orElse(null));
|
||||
donationKeyField.textProperty().addListener(this::registrationKeyChanged);
|
||||
donationKeyField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
supporterCertificateField.setText(licenseHolder.getLicenseKey().orElse(null));
|
||||
supporterCertificateField.textProperty().addListener(this::registrationKeyChanged);
|
||||
supporterCertificateField.setTextFormatter(new TextFormatter<>(this::checkVaultNameLength));
|
||||
}
|
||||
|
||||
private TextFormatter.Change checkVaultNameLength(TextFormatter.Change change) {
|
||||
@@ -53,8 +53,8 @@ public class DonationKeyPreferencesController implements FxController {
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void getDonationKey() {
|
||||
application.getHostServices().showDocument(DONATION_URI);
|
||||
public void getSupporterCertificate() {
|
||||
application.getHostServices().showDocument(SUPPORTER_URI);
|
||||
}
|
||||
|
||||
public LicenseHolder getLicenseHolder() {
|
||||
+7
-6
@@ -2,9 +2,10 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
@@ -32,18 +33,18 @@ public class RecoveryKeyCreationController implements FxController {
|
||||
private final ExecutorService executor;
|
||||
private final RecoveryKeyFactory recoveryKeyFactory;
|
||||
private final StringProperty recoveryKeyProperty;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
public NiceSecurePasswordField passwordField;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, GenericErrorComponent.Builder genericErrorBuilder) {
|
||||
public RecoveryKeyCreationController(@RecoveryKeyWindow Stage window, @FxmlScene(FxmlFile.RECOVERYKEY_SUCCESS) Lazy<Scene> successScene, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, ErrorComponent.Builder errorComponent) {
|
||||
this.window = window;
|
||||
this.successScene = successScene;
|
||||
this.vault = vault;
|
||||
this.executor = executor;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
this.recoveryKeyProperty = recoveryKey;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -62,7 +63,7 @@ public class RecoveryKeyCreationController implements FxController {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
} else {
|
||||
LOG.error("Creation of recovery key failed.", task.getException());
|
||||
genericErrorBuilder.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
});
|
||||
executor.submit(task);
|
||||
@@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String call() throws IOException {
|
||||
protected String call() throws IOException, CryptoException {
|
||||
return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters());
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,34 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.hash.Hashing;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
|
||||
@Singleton
|
||||
public class RecoveryKeyFactory {
|
||||
|
||||
private static final byte[] PEPPER = new byte[0];
|
||||
|
||||
private final WordEncoder wordEncoder;
|
||||
private final MasterkeyFileAccess masterkeyFileAccess;
|
||||
|
||||
@Inject
|
||||
public RecoveryKeyFactory(WordEncoder wordEncoder) {
|
||||
public RecoveryKeyFactory(WordEncoder wordEncoder, MasterkeyFileAccess masterkeyFileAccess) {
|
||||
this.wordEncoder = wordEncoder;
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
}
|
||||
|
||||
public Collection<String> getDictionary() {
|
||||
@@ -36,11 +42,14 @@ public class RecoveryKeyFactory {
|
||||
* @return The recovery key of the vault at the given path
|
||||
* @throws IOException If the masterkey file could not be read
|
||||
* @throws InvalidPassphraseException If the provided password is wrong
|
||||
* @throws CryptoException In case of other cryptographic errors
|
||||
* @apiNote This is a long-running operation and should be invoked in a background thread
|
||||
*/
|
||||
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException {
|
||||
byte[] rawKey = CryptoFileSystemProvider.exportRawKey(vaultPath, MASTERKEY_FILENAME, PEPPER, password);
|
||||
try {
|
||||
public String createRecoveryKey(Path vaultPath, CharSequence password) throws IOException, InvalidPassphraseException, CryptoException {
|
||||
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||
byte[] rawKey = new byte[0];
|
||||
try (var masterkey = masterkeyFileAccess.load(masterkeyPath, password)) {
|
||||
rawKey = masterkey.getEncoded();
|
||||
return createRecoveryKey(rawKey);
|
||||
} finally {
|
||||
Arrays.fill(rawKey, (byte) 0x00);
|
||||
@@ -72,8 +81,15 @@ public class RecoveryKeyFactory {
|
||||
*/
|
||||
public void resetPasswordWithRecoveryKey(Path vaultPath, String recoveryKey, CharSequence newPassword) throws IOException, IllegalArgumentException {
|
||||
final byte[] rawKey = decodeRecoveryKey(recoveryKey);
|
||||
try {
|
||||
CryptoFileSystemProvider.restoreRawKey(vaultPath, MASTERKEY_FILENAME, rawKey, PEPPER, newPassword);
|
||||
try (var masterkey = new Masterkey(rawKey)) {
|
||||
Path masterkeyPath = vaultPath.resolve(MASTERKEY_FILENAME);
|
||||
if (Files.exists(masterkeyPath)) {
|
||||
byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath);
|
||||
// TODO: deduplicate with ChangePasswordController:
|
||||
Path backupKeyPath = vaultPath.resolve(MASTERKEY_FILENAME + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + MASTERKEY_BACKUP_SUFFIX);
|
||||
Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
}
|
||||
masterkeyFileAccess.persist(masterkey, masterkeyPath, newPassword);
|
||||
} finally {
|
||||
Arrays.fill(rawKey, (byte) 0x00);
|
||||
}
|
||||
|
||||
+5
-5
@@ -2,7 +2,7 @@ package org.cryptomator.ui.recoverykey;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
@@ -35,10 +35,10 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
private final ObjectProperty<CharSequence> newPassword;
|
||||
private final Lazy<Scene> recoverScene;
|
||||
private final BooleanBinding invalidNewPassword;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
|
||||
@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, GenericErrorComponent.Builder genericErrorBuilder) {
|
||||
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) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.recoveryKeyFactory = recoveryKeyFactory;
|
||||
@@ -47,7 +47,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
this.newPassword = newPassword;
|
||||
this.recoverScene = recoverScene;
|
||||
this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword);
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.errorComponent = errorComponent;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -68,7 +68,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
|
||||
});
|
||||
task.setOnFailed(event -> {
|
||||
LOG.error("Resetting password failed.", task.getException());
|
||||
genericErrorBuilder.cause(task.getException()).window(window).returnToScene(recoverScene.get()).build().showErrorScene();
|
||||
errorComponent.cause(task.getException()).window(window).returnToScene(recoverScene.get()).build().showErrorScene();
|
||||
});
|
||||
executor.submit(task);
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ abstract class VaultStatisticsModule {
|
||||
var weakStage = new WeakReference<>(stage);
|
||||
vault.stateProperty().addListener(new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends VaultState> observable, VaultState oldValue, VaultState newValue) {
|
||||
if (newValue != VaultState.UNLOCKED) {
|
||||
public void changed(ObservableValue<? extends VaultState.Value> observable, VaultState.Value oldValue, VaultState.Value newValue) {
|
||||
if (newValue != VaultState.Value.UNLOCKED) {
|
||||
Stage stage = weakStage.get();
|
||||
if (stage != null) {
|
||||
stage.hide();
|
||||
|
||||
@@ -44,6 +44,9 @@ class TrayMenuController {
|
||||
|
||||
public void initTrayMenu() {
|
||||
vaults.addListener(this::vaultListChanged);
|
||||
vaults.forEach(v -> {
|
||||
v.displayNameProperty().addListener(this::vaultListChanged);
|
||||
});
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
public class UnlockCancelledException extends MasterkeyLoadingFailedException {
|
||||
|
||||
public UnlockCancelledException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnlockCancelledException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
+14
-9
@@ -2,37 +2,42 @@ package org.cryptomator.ui.unlock;
|
||||
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.error.AbstractErrorController;
|
||||
import org.cryptomator.ui.error.ErrorReport;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
//At the current point in time only the CustomMountPointChooser may cause this window to be shown.
|
||||
@UnlockScoped
|
||||
public class UnlockInvalidMountPointController extends AbstractErrorController {
|
||||
public class UnlockInvalidMountPointController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
|
||||
@Inject
|
||||
UnlockInvalidMountPointController(@ErrorReport Stage window, @ErrorReport @Nullable Scene previousScene, @ErrorReport Vault vault) {
|
||||
super(window, previousScene);
|
||||
UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getMountPoint() {
|
||||
return this.vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
|
||||
return vault.getVaultSettings().getCustomMountPath().orElse("AUTO");
|
||||
}
|
||||
|
||||
public boolean getMustExist() {
|
||||
MountPointRequirement requirement = this.vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
|
||||
MountPointRequirement requirement = vault.getVolume().orElseThrow(() -> new IllegalStateException("Invalid Mountpoint without a Volume?!")).getMountPointRequirement();
|
||||
assert requirement != MountPointRequirement.NONE; //An invalid MountPoint with no required MountPoint doesn't seem sensible
|
||||
assert requirement != MountPointRequirement.PARENT_OPT_MOUNT_POINT; //Not implemented anywhere (yet)
|
||||
|
||||
return requirement == MountPointRequirement.EMPTY_MOUNT_POINT;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,20 +4,16 @@ import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
@@ -27,54 +23,10 @@ import javafx.stage.Stage;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Module(subcomponents = {ForgetPasswordComponent.class})
|
||||
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||
abstract class UnlockModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
|
||||
|
||||
public enum PasswordEntry {
|
||||
PASSWORD_ENTERED,
|
||||
CANCELED
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
|
||||
return new UserInteractionLock<>(null);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savedPassword")
|
||||
@UnlockScoped
|
||||
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @UnlockWindow Vault vault) {
|
||||
if (!keychain.isSupported()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
try {
|
||||
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to load entry from system keychain.", e);
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockScoped
|
||||
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicReference(storedPassword.orElse(null));
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("savePassword")
|
||||
@UnlockScoped
|
||||
static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
|
||||
return new AtomicBoolean(storedPassword.isPresent());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@UnlockWindow
|
||||
@UnlockScoped
|
||||
@@ -99,10 +51,10 @@ abstract class UnlockModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.UNLOCK)
|
||||
@UnlockWindow
|
||||
@UnlockScoped
|
||||
static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.UNLOCK);
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@@ -123,11 +75,12 @@ abstract class UnlockModule {
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnlockController.class)
|
||||
abstract FxController bindUnlockController(UnlockController controller);
|
||||
@FxControllerKey(UnlockSuccessController.class)
|
||||
abstract FxController bindUnlockSuccessController(UnlockSuccessController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(UnlockSuccessController.class)
|
||||
abstract FxController bindUnlockSuccessController(UnlockSuccessController controller);
|
||||
@FxControllerKey(UnlockInvalidMountPointController.class)
|
||||
abstract FxController bindUnlockInvalidMountPointController(UnlockInvalidMountPointController controller);
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import java.lang.annotation.RetentionPolicy;
|
||||
@Scope
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface UnlockScoped {
|
||||
@interface UnlockScoped {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,41 +1,31 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
import org.cryptomator.ui.common.Animations;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.UserInteractionLock;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.error.GenericErrorComponent;
|
||||
import org.cryptomator.ui.error.InvalidMountPointExceptionComponent;
|
||||
import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import java.io.IOException;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* A multi-step task that consists of background activities as well as user interaction.
|
||||
@@ -50,113 +40,47 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final VaultService vaultService;
|
||||
private final AtomicReference<char[]> password;
|
||||
private final AtomicBoolean savePassword;
|
||||
private final Optional<char[]> savedPassword;
|
||||
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
|
||||
private final KeychainManager keychain;
|
||||
private final Lazy<Scene> unlockScene;
|
||||
private final Lazy<Scene> successScene;
|
||||
private final GenericErrorComponent.Builder genericErrorBuilder;
|
||||
private final InvalidMountPointExceptionComponent.Builder invalidMountPointExceptionBuilder;
|
||||
private final Lazy<Scene> invalidMountPointScene;
|
||||
private final ErrorComponent.Builder errorComponent;
|
||||
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||
|
||||
@Inject
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, GenericErrorComponent.Builder genericErrorBuilder, InvalidMountPointExceptionComponent.Builder invalidMountPointExceptionBuilder) {
|
||||
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vaultService = vaultService;
|
||||
this.password = password;
|
||||
this.savePassword = savePassword;
|
||||
this.savedPassword = savedPassword;
|
||||
this.passwordEntryLock = passwordEntryLock;
|
||||
this.keychain = keychain;
|
||||
this.unlockScene = unlockScene;
|
||||
this.successScene = successScene;
|
||||
this.genericErrorBuilder = genericErrorBuilder;
|
||||
this.invalidMountPointExceptionBuilder = invalidMountPointExceptionBuilder;
|
||||
|
||||
setOnFailed(event -> {
|
||||
Throwable throwable = event.getSource().getException();
|
||||
if (throwable instanceof InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
} else {
|
||||
handleGenericError(throwable);
|
||||
}
|
||||
});
|
||||
this.invalidMountPointScene = invalidMountPointScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
try {
|
||||
if (attemptUnlock()) {
|
||||
handleSuccess();
|
||||
return true;
|
||||
attemptUnlock();
|
||||
return true;
|
||||
} catch (UnlockCancelledException e) {
|
||||
cancel(false); // set Tasks state to cancelled
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
boolean success = false;
|
||||
try {
|
||||
vault.unlock(keyLoadingStrategy);
|
||||
success = true;
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||
attemptUnlock();
|
||||
} else {
|
||||
cancel(false); // set Tasks state to cancelled
|
||||
return false;
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
wipePassword(password.get());
|
||||
wipePassword(savedPassword.orElse(null));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptUnlock() throws InterruptedException, IOException, VolumeException, InvalidMountPointException {
|
||||
boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
|
||||
while (proceed) {
|
||||
try {
|
||||
vault.unlock(CharBuffer.wrap(password.get()));
|
||||
return true;
|
||||
} catch (InvalidPassphraseException e) {
|
||||
proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(unlockScene.get());
|
||||
window.show();
|
||||
Window owner = window.getOwner();
|
||||
if (owner != null) {
|
||||
window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2);
|
||||
window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2);
|
||||
} else {
|
||||
window.centerOnScreen();
|
||||
}
|
||||
if (animateShake) {
|
||||
Animations.createShakeWindowAnimation(window).play();
|
||||
}
|
||||
});
|
||||
return passwordEntryLock.awaitInteraction();
|
||||
}
|
||||
|
||||
private void handleSuccess() {
|
||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
||||
if (savePassword.get()) {
|
||||
savePasswordToSystemkeychain();
|
||||
}
|
||||
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
|
||||
case ASK -> Platform.runLater(() -> {
|
||||
window.setScene(successScene.get());
|
||||
window.show();
|
||||
});
|
||||
case REVEAL -> {
|
||||
Platform.runLater(window::close);
|
||||
vaultService.reveal(vault);
|
||||
}
|
||||
case IGNORE -> Platform.runLater(window::close);
|
||||
}
|
||||
}
|
||||
|
||||
private void savePasswordToSystemkeychain() {
|
||||
if (keychain.isSupported()) {
|
||||
try {
|
||||
keychain.storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LOG.error("Failed to store passphrase in system keychain.", e);
|
||||
}
|
||||
keyLoadingStrategy.cleanup(success);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,54 +97,65 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
} else {
|
||||
LOG.error("Unlock failed. Mountpoint doesn't exist (needs to be a folder): {}", cause.getMessage());
|
||||
}
|
||||
showInvalidMountPointScene(cause);
|
||||
return;
|
||||
showInvalidMountPointScene();
|
||||
} else if (cause instanceof FileAlreadyExistsException) {
|
||||
LOG.error("Unlock failed. Mountpoint already exists: {}", cause.getMessage());
|
||||
showInvalidMountPointScene(cause);
|
||||
return;
|
||||
showInvalidMountPointScene();
|
||||
} else if (cause instanceof DirectoryNotEmptyException) {
|
||||
LOG.error("Unlock failed. Mountpoint not an empty directory: {}", cause.getMessage());
|
||||
showInvalidMountPointScene(cause);
|
||||
return;
|
||||
showInvalidMountPointScene();
|
||||
} else {
|
||||
handleGenericError(impExc);
|
||||
}
|
||||
}
|
||||
|
||||
private void showInvalidMountPointScene(Throwable e) {
|
||||
invalidMountPointExceptionBuilder.cause(e).window(window).returnToScene(window.getScene()).vault(vault).build().showErrorScene();
|
||||
private void showInvalidMountPointScene() {
|
||||
Platform.runLater(() -> {
|
||||
window.setScene(invalidMountPointScene.get());
|
||||
window.show();
|
||||
});
|
||||
}
|
||||
|
||||
private void handleGenericError(Throwable e) {
|
||||
LOG.error("Unlock failed for technical reasons.", e);
|
||||
genericErrorBuilder.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
}
|
||||
|
||||
private void wipePassword(char[] pw) {
|
||||
if (pw != null) {
|
||||
Arrays.fill(pw, ' ');
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
vault.setState(VaultState.PROCESSING);
|
||||
errorComponent.cause(e).window(window).build().showErrorScene();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
vault.setState(VaultState.UNLOCKED);
|
||||
LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName());
|
||||
|
||||
switch (vault.getVaultSettings().actionAfterUnlock().get()) {
|
||||
case ASK -> Platform.runLater(() -> {
|
||||
window.setScene(successScene.get());
|
||||
window.show();
|
||||
});
|
||||
case REVEAL -> {
|
||||
Platform.runLater(window::close);
|
||||
vaultService.reveal(vault);
|
||||
}
|
||||
case IGNORE -> Platform.runLater(window::close);
|
||||
}
|
||||
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
LOG.info("Unlock of '{}' failed.", vault.getDisplayName());
|
||||
Throwable throwable = super.getException();
|
||||
if (throwable instanceof InvalidMountPointException e) {
|
||||
handleInvalidMountPoint(e);
|
||||
} else {
|
||||
handleGenericError(throwable);
|
||||
}
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
vault.setState(VaultState.LOCKED);
|
||||
LOG.debug("Unlock of '{}' canceled.", vault.getDisplayName());
|
||||
vault.stateProperty().transition(VaultState.Value.PROCESSING, VaultState.Value.LOCKED);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
alignment="CENTER_LEFT">
|
||||
<fx:define>
|
||||
<ToggleGroup fx:id="predefinedLocationToggler"/>
|
||||
<FontAwesome5IconView fx:id="badLocation" styleClass="glyph-icon-red" glyph="TIMES" />
|
||||
<FontAwesome5IconView fx:id="goodLocation" styleClass="glyph-icon-primary" glyph="CHECK" />
|
||||
</fx:define>
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="24"/>
|
||||
@@ -33,6 +35,8 @@
|
||||
<RadioButton fx:id="dropboxRadioButton" toggleGroup="${predefinedLocationToggler}" text="Dropbox" visible="${controller.locationPresets.foundDropbox}" managed="${controller.locationPresets.foundDropbox}"/>
|
||||
<RadioButton fx:id="gdriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="Google Drive" visible="${controller.locationPresets.foundGdrive}" managed="${controller.locationPresets.foundGdrive}"/>
|
||||
<RadioButton fx:id="onedriveRadioButton" toggleGroup="${predefinedLocationToggler}" text="OneDrive" visible="${controller.locationPresets.foundOnedrive}" managed="${controller.locationPresets.foundOnedrive}"/>
|
||||
<RadioButton fx:id="megaRadioButton" toggleGroup="${predefinedLocationToggler}" text="MEGA" visible="${controller.locationPresets.foundMega}" managed="${controller.locationPresets.foundMega}"/>
|
||||
<RadioButton fx:id="pcloudRadioButton" toggleGroup="${predefinedLocationToggler}" text="pCloud" visible="${controller.locationPresets.foundPcloud}" managed="${controller.locationPresets.foundPcloud}"/>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT">
|
||||
<RadioButton fx:id="customRadioButton" toggleGroup="${predefinedLocationToggler}" text="%addvaultwizard.new.directoryPickerLabel"/>
|
||||
<Button contentDisplay="LEFT" text="%addvaultwizard.new.directoryPickerButton" onAction="#chooseCustomVaultPath" disable="${controller.usePresetPath}">
|
||||
@@ -47,12 +51,8 @@
|
||||
|
||||
<VBox spacing="6">
|
||||
<Label text="%addvaultwizard.new.locationLabel" labelFor="$locationTextField"/>
|
||||
<TextField fx:id="locationTextField" promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" disable="true" HBox.hgrow="ALWAYS"/>
|
||||
<Label text="${controller.warningText}" wrapText="true" visible="${controller.showWarning}">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="EXCLAMATION_TRIANGLE"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<TextField promptText="%addvaultwizard.new.locationPrompt" text="${controller.vaultPath}" editable="false" disable="${!controller.anyRadioButtonSelected}" HBox.hgrow="ALWAYS"/>
|
||||
<Label fx:id="vaultPathStatus" styleClass="label-muted" alignment="CENTER_RIGHT" wrapText="true" visible="${controller.anyRadioButtonSelected}" maxWidth="Infinity" graphicTextGap="6" text="${controller.statusText}" graphic="${controller.statusGraphic}" />
|
||||
</VBox>
|
||||
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.error.GenericErrorController"
|
||||
fx:controller="org.cryptomator.ui.common.ErrorController"
|
||||
prefWidth="450"
|
||||
prefHeight="450"
|
||||
spacing="12"
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+C">
|
||||
<buttons>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" onAction="#back" visible="${controller.returnScenePresent}"/>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" onAction="#back" visible="${controller.previousScenePresent}"/>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#close"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
@@ -38,12 +38,12 @@
|
||||
<fx:include source="/fxml/preferences_updates.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="donationKeyTab" id="DONATION_KEY" text="%preferences.donationKey">
|
||||
<Tab fx:id="contributeTab" id="CONTRIBUTE" text="%preferences.contribute">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="HEART"/>
|
||||
</graphic>
|
||||
<content>
|
||||
<fx:include source="/fxml/preferences_donationkey.fxml"/>
|
||||
<fx:include source="/fxml/preferences_contribute.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="aboutTab" id="ABOUT" text="%preferences.about">
|
||||
|
||||
+5
-5
@@ -12,7 +12,7 @@
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.preferences.DonationKeyPreferencesController"
|
||||
fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController"
|
||||
spacing="18">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
@@ -24,7 +24,7 @@
|
||||
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="CROWN" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<FormattedLabel format="%preferences.donationKey.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
|
||||
<FormattedLabel format="%preferences.contribute.registeredFor" arg1="${controller.licenseHolder.licenseSubject}" wrapText="true"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" visible="${!controller.licenseHolder.validLicense}">
|
||||
@@ -33,8 +33,8 @@
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="HAND_HOLDING_HEART" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<VBox HBox.hgrow="ALWAYS" spacing="6">
|
||||
<Label text="%preferences.donationKey.noDonationKey" wrapText="true" VBox.vgrow="ALWAYS"/>
|
||||
<Hyperlink text="%preferences.donationKey.getDonationKey" onAction="#getDonationKey" contentDisplay="LEFT">
|
||||
<Label text="%preferences.contribute.noCertificate" wrapText="true" VBox.vgrow="ALWAYS"/>
|
||||
<Hyperlink text="%preferences.contribute.getCertificate" onAction="#getSupporterCertificate" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="LINK"/>
|
||||
</graphic>
|
||||
@@ -43,6 +43,6 @@
|
||||
</HBox>
|
||||
</StackPane>
|
||||
|
||||
<TextArea fx:id="donationKeyField" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
|
||||
<TextArea fx:id="supporterCertificateField" promptText="%preferences.contribute.promptText" wrapText="true" VBox.vgrow="ALWAYS" prefRowCount="6"/>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -23,7 +23,7 @@
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
<Label text="%preferences.general.theme"/>
|
||||
<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
|
||||
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showDonationTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
|
||||
<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
|
||||
</HBox>
|
||||
|
||||
<HBox spacing="6" alignment="CENTER_LEFT">
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.unlock.UnlockController"
|
||||
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.PassphraseEntryController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
@@ -37,10 +37,10 @@
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
|
||||
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="B+U">
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="U+C">
|
||||
<buttons>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="BACK_PREVIOUS" cancelButton="true" onAction="#back"/>
|
||||
<Region ButtonBar.buttonData="OTHER"/>
|
||||
<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.shape.Circle?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.keyloading.masterkeyfile.SelectMasterkeyFileController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
|
||||
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
|
||||
<Circle styleClass="glyph-icon-primary" radius="24"/>
|
||||
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="24"/>
|
||||
</StackPane>
|
||||
<VBox spacing="6">
|
||||
<Label text="%unlock.chooseMasterkey.prompt" wrapText="true" HBox.hgrow="ALWAYS"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
|
||||
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -45,8 +45,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Custom Location
|
||||
addvaultwizard.new.directoryPickerButton=Choose…
|
||||
addvaultwizard.new.directoryPickerTitle=Select Directory
|
||||
addvaultwizard.new.fileAlreadyExists=Vault can not be created at this path because some object already exists.
|
||||
addvaultwizard.new.locationDoesNotExist=Vault can not be created at this path because at least one path component does not exist.
|
||||
addvaultwizard.new.fileAlreadyExists=A file or directory with the vault name already exists
|
||||
addvaultwizard.new.locationDoesNotExist=A directory in the specified path does not exist or cannot be accessed
|
||||
addvaultwizard.new.locationIsNotWritable=No write access at the specified path
|
||||
addvaultwizard.new.locationIsOk=Suitable location for your vault
|
||||
addvaultwizard.new.invalidName=Invalid vault name. Please consider a regular directory name.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Create Vault
|
||||
@@ -98,6 +100,9 @@ unlock.title=Unlock Vault
|
||||
unlock.passwordPrompt=Enter password for "%s":
|
||||
unlock.savePassword=Remember Password
|
||||
unlock.unlockBtn=Unlock
|
||||
##
|
||||
unlock.chooseMasterkey.prompt=Could not find the masterkey file for this vault at its expected location. Please choose the key file manually.
|
||||
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
|
||||
## Success
|
||||
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible via its virtual drive.
|
||||
unlock.success.rememberChoice=Remember choice, don't show this again
|
||||
@@ -175,11 +180,14 @@ preferences.updates.currentVersion=Current Version: %s
|
||||
preferences.updates.autoUpdateCheck=Check for updates automatically
|
||||
preferences.updates.checkNowBtn=Check Now
|
||||
preferences.updates.updateAvailable=Update to version %s available.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donation
|
||||
preferences.donationKey.registeredFor=Registered for %s
|
||||
preferences.donationKey.noDonationKey=No valid donation key found. It's like a license key but for awesome people using free software. ;-)
|
||||
preferences.donationKey.getDonationKey=Get a donation key
|
||||
## Contribution
|
||||
preferences.contribute=Support Us
|
||||
preferences.contribute.registeredFor=Supporter certificate registered for %s
|
||||
preferences.contribute.noCertificate=Support Cryptomator and receive a supporter certificate. It's like a license key but for awesome people using free software. ;-)
|
||||
preferences.contribute.getCertificate=Don't have one already? Learn how you can obtain it.
|
||||
preferences.contribute.promptText=Paste supporter certificate code here
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=About
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ generic.error.title=حدث خطأ غير متوقع
|
||||
generic.error.instruction=ما كان ينبغي أن يحدث هذا. يرجى الإبلاغ عن نص الخطأ أدناه وإدراج وصف للخطوات التي أدت إلى هذا الخطأ.
|
||||
|
||||
# Defaults
|
||||
defaults.vault.vaultName=خزنة
|
||||
defaults.vault.vaultName=مخزن
|
||||
|
||||
# Tray Menu
|
||||
traymenu.showMainWindow=اظهار
|
||||
@@ -38,17 +38,19 @@ addvaultwizard.welcome.existingButton=افتح مخزن موجود
|
||||
addvaultwizard.new.nameInstruction=اختر اسم للمخزن
|
||||
addvaultwizard.new.namePrompt=اسم المخزن
|
||||
### Location
|
||||
addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة من قبو الخاص بك؟
|
||||
addvaultwizard.new.locationInstruction=أين يجب على Cryptomator تخزين الملفات المشفرة للمخزن الخاص بك؟
|
||||
addvaultwizard.new.locationLabel=موقع التخزين
|
||||
addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=موقع مخصص
|
||||
addvaultwizard.new.directoryPickerButton=اختر…
|
||||
addvaultwizard.new.directoryPickerTitle=اختر القاموس
|
||||
addvaultwizard.new.fileAlreadyExists=لا يمكن إنشاء مخزن على هذا المسار لأن بعض الملفات موجودة مسبقا.
|
||||
addvaultwizard.new.locationDoesNotExist=لا يمكن إنشاء المخزن على هذا المسار لأن جزء من هذا المسار غير موجود.
|
||||
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى النظر في اسم الدليل المعتاد.
|
||||
addvaultwizard.new.fileAlreadyExists=ملف أو مجلد بنفس اسم المخزن موجود مسبقاً
|
||||
addvaultwizard.new.locationDoesNotExist=المجلد في المسار المحدد غير موجود أو لا يمكن الوصول إليه
|
||||
addvaultwizard.new.locationIsNotWritable=لا يوجد صلاحيات للكتابة على المسار المحدد
|
||||
addvaultwizard.new.locationIsOk=الموقع المناسب للمخزن الخاص بك
|
||||
addvaultwizard.new.invalidName=اسم المخزن غير صالح. يرجى إعادة تسمية المجلد بشكل عادي.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=انشئ حافظة
|
||||
addvaultwizard.new.createVaultBtn=إنشاء المخزن
|
||||
addvaultwizard.new.generateRecoveryKeyChoice=لن تتمكن من الوصول إلى بياناتك بدون كلمة المرور الخاصة بك. هل تريد مفتاح استرداد في حالة فقدان كلمة المرور الخاصة بك؟
|
||||
addvaultwizard.new.generateRecoveryKeyChoice.yes=نعم من فضلك، أن تكون آمناً أفضل من الندم
|
||||
addvaultwizard.new.generateRecoveryKeyChoice.no=لا شكراً، لن أفقد كلمة المرور الخاصة بي
|
||||
@@ -95,8 +97,12 @@ forgetPassword.confirmBtn=نسيت كلمة المرور
|
||||
# Unlock
|
||||
unlock.title=افتح الحافظة
|
||||
unlock.passwordPrompt=أدخل كلمة السر لـ "%s":
|
||||
unlock.savePassword=تذكر كلمة المرور
|
||||
unlock.unlockBtn=افتح
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=اختر ملف الـ Masterkey
|
||||
## Success
|
||||
unlock.success.message=تم فتح "%s" بنجاح! يمكنك الآن الوصول لمخزنك عن طريق القرص الافتراضي الخاص به.
|
||||
unlock.success.rememberChoice=تذكر اختياري ولا تظهر هذا مرة أخرى
|
||||
unlock.success.revealBtn=اظهار القرص
|
||||
## Failure
|
||||
@@ -107,9 +113,12 @@ unlock.error.invalidMountPoint.existing=نقطة/مجلد التحميل موج
|
||||
|
||||
# Lock
|
||||
## Force
|
||||
lock.forced.heading=فشل عملية القفل
|
||||
lock.forced.message=تم حظر قفل "%s" بواسطة العمليات المعلقة أو الملفات المفتوحة. يمكنك فرض قفل هذا المخزن، ولكن مقاطعة عمليات الادخال والاخراج I/O قد تؤدي لفقدان البيانات غير المحفوظة.
|
||||
lock.forced.confirmBtn=فرض القفل
|
||||
## Failure
|
||||
lock.fail.heading=فشلت عملية اقفال الخزنة.
|
||||
lock.fail.message=فشل عملية قفل %s". تأكد من حفظ العمل غير المحفوظ في مكان آخر وأن العمليات الهامة للقراءة/الكتابة قد انتهت. من أجل إغلاق المخزن، اقتل تطبيق Cryptomator.
|
||||
|
||||
# Migration
|
||||
migration.title=ترقية الحافظة
|
||||
@@ -144,6 +153,8 @@ preferences.general.theme.automatic=تلقائي
|
||||
preferences.general.theme.light=فاتح (أبيض)
|
||||
preferences.general.theme.dark=مظلم (أسود)
|
||||
preferences.general.unlockThemes=تفعيل الوضع المظلم
|
||||
preferences.general.showMinimizeButton=إظهار زر التصغير
|
||||
preferences.general.showTrayIcon=إظهار أيقونة بجنب الساعة (يتطلب إعادة تشغيل)
|
||||
preferences.general.startHidden=إخفاء النافذة عند بدء تشغيل Cryptomator
|
||||
preferences.general.debugLogging=تمكين سجلات التصحيح
|
||||
preferences.general.debugDirectory=عرض ملفات السجل
|
||||
@@ -167,15 +178,19 @@ preferences.updates.currentVersion=الإصدار الحالي: %s
|
||||
preferences.updates.autoUpdateCheck=تحقق من التحديثات اوتوماتيكيا
|
||||
preferences.updates.checkNowBtn=تحقق الان
|
||||
preferences.updates.updateAvailable=التحديث إلى الإصدار %s متاح.
|
||||
## Donation Key
|
||||
preferences.donationKey=التبرعات
|
||||
preferences.donationKey.registeredFor=مسجل لـ %s
|
||||
preferences.donationKey.noDonationKey=لم يتم العثور على مفتاح تبرع صحيح. إنه مثل مفتاح الترخيص إلا أنه للأشخاص الرائعين الذين يستخدمون البرمجيات المجانية؛-)
|
||||
preferences.donationKey.getDonationKey=الحصول على مفتاح تبرع
|
||||
## Contribution
|
||||
preferences.contribute=ادعمنا
|
||||
preferences.contribute.registeredFor=شهادة الداعم مسجلة لـ %s
|
||||
preferences.contribute.noCertificate=ادعم Cryptomator واحصل على شهادة الداعم. إنها مثل مفتاح الترخيص لكن للأشخاص الرائعين الذين يستخدمون البرامج المجانية ؛-)
|
||||
preferences.contribute.getCertificate=ليس لديك واحدة بعد؟ تعلم كيف يمكنك الحصول عليها.
|
||||
preferences.contribute.promptText=قم بلصق رمز شهادة الداعم هنا
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=حول البرنامج
|
||||
|
||||
# Vault Statistics
|
||||
stats.title=إحصائيات عن %s
|
||||
stats.cacheHitRate=معدل استخدام الكاش
|
||||
## Read
|
||||
stats.read.throughput.idle=قراءة: خامل
|
||||
@@ -194,9 +209,11 @@ main.dropZone.dropVault=أضف هذا المخزن
|
||||
main.dropZone.unknownDragboardContent=إذا كنت ترغب في إضافة مخزن، قم بسحبه إلى هذه النافذة
|
||||
## 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
|
||||
|
||||
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Prilagođena lokacija
|
||||
addvaultwizard.new.directoryPickerButton=Odaberi…
|
||||
addvaultwizard.new.directoryPickerTitle=Izaberi folder
|
||||
addvaultwizard.new.fileAlreadyExists=Sef ne može biti kreiran na odabranoj lokaciji jer neki objekti tu već postoje.
|
||||
addvaultwizard.new.locationDoesNotExist=Sef ne može biti kreiran na odabranoj lokaciji jer najmanje jedna komponenta lokacije ne postoji.
|
||||
addvaultwizard.new.invalidName=Naziv sefa neispravan. Molimo razmislite o drugom nazivu.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Kreiraj novi sef
|
||||
@@ -95,8 +93,12 @@ forgetPassword.confirmBtn=Zaboravili ste šifru
|
||||
# Unlock
|
||||
unlock.title=Otključaj sef
|
||||
unlock.passwordPrompt=Unesite lozinku za "%s":
|
||||
unlock.savePassword=Zapamti šifru
|
||||
unlock.unlockBtn=Otključaj
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Odaberite Masterkey Datoteku
|
||||
## Success
|
||||
unlock.success.message=Uspješno ste otključali "%s"! Vaš sef je sada dostupan putem svog virtualnog diska.
|
||||
unlock.success.rememberChoice=Zapamtite izbor, ne pokazujte ovo ponovo
|
||||
unlock.success.revealBtn=Otkrij pogon
|
||||
## Failure
|
||||
@@ -172,11 +174,9 @@ preferences.updates.currentVersion=Trenutna verzija: %s
|
||||
preferences.updates.autoUpdateCheck=Automatski provjeri ima li ažuriranja
|
||||
preferences.updates.checkNowBtn=Provjeri sada
|
||||
preferences.updates.updateAvailable=Dostupno ažuriranje na verziju %s.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donacija
|
||||
preferences.donationKey.registeredFor=Registrovan za %s
|
||||
preferences.donationKey.noDonationKey=Nije pronađen važeći ključ za donaciju. To je poput licence, ali za sjajne ljude koji koriste besplatni softver. ;-)
|
||||
preferences.donationKey.getDonationKey=Nabavite ključ za donaciju
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=O programu
|
||||
|
||||
@@ -221,9 +221,11 @@ main.dropZone.dropVault=Dodajte ovaj sef
|
||||
main.dropZone.unknownDragboardContent=Ako želite dodati sef, povucite ga u ovaj prozor
|
||||
## Vault List
|
||||
main.vaultlist.emptyList.onboardingInstruction=Kliknite ovdje da dodate sef
|
||||
main.vaultlist.contextMenu.remove=Ukloni…
|
||||
main.vaultlist.contextMenu.lock=Zaključaj
|
||||
main.vaultlist.contextMenu.unlock=Otključaj…
|
||||
main.vaultlist.contextMenu.unlockNow=Otključaj sada
|
||||
main.vaultlist.contextMenu.vaultoptions=Pokaži opcije sefa
|
||||
main.vaultlist.contextMenu.reveal=Otkrij pogon
|
||||
main.vaultlist.addVaultBtn=Dodaj sef
|
||||
## Vault Detail
|
||||
|
||||
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Ubicació personalitzada
|
||||
addvaultwizard.new.directoryPickerButton=Trieu…
|
||||
addvaultwizard.new.directoryPickerTitle=Seleccioneu el directori
|
||||
addvaultwizard.new.fileAlreadyExists=No es pot crear una caixa forta en aquest camí perquè ja hi ha algun objecte.
|
||||
addvaultwizard.new.locationDoesNotExist=No ha estat possible crear la caixa forta en aquest camí perquè, si més no, un component no existeix.
|
||||
addvaultwizard.new.invalidName=El nom de la caixa forta no és vàlid. Si us plau, escribiu un mom de directori amb caràcters estàndard.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Crea la caixa forta
|
||||
@@ -97,7 +95,10 @@ unlock.title=Desbloquejar la caixa forta
|
||||
unlock.passwordPrompt=Introduïu la contrasenya de "%s":
|
||||
unlock.savePassword=Recorda la contrasenya
|
||||
unlock.unlockBtn=Desbloqueja
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Seleccioneu el fitxer Masterkey
|
||||
## Success
|
||||
unlock.success.message=S'ha desblocat %s correctament! Podeu accedir a la vostra caixa forta a través de la unitat virtual.
|
||||
unlock.success.rememberChoice=Recorda l'elecció. No ho tornis a mostrar.
|
||||
unlock.success.revealBtn=Mostra la unitat
|
||||
## Failure
|
||||
@@ -173,11 +174,14 @@ preferences.updates.currentVersion=Versió actual: %s
|
||||
preferences.updates.autoUpdateCheck=Comprova automàticament si hi ha actualitzacions
|
||||
preferences.updates.checkNowBtn=Comprova-ho ara
|
||||
preferences.updates.updateAvailable=L'actualització a la versió %s es troba disponible.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donacions
|
||||
preferences.donationKey.registeredFor=Registrat per %s
|
||||
preferences.donationKey.noDonationKey=No s'ha trobar una clau de donació vàlida. És sembla a una clau de llicència però per gent meravellosa qui utilitza programari lliure. ;-)
|
||||
preferences.donationKey.getDonationKey=Obtingau una clau de donació
|
||||
## Contribution
|
||||
preferences.contribute=Doneu-nos suport
|
||||
preferences.contribute.registeredFor=Certificat de col·laborador registrat per a %s
|
||||
preferences.contribute.noCertificate=Doneu suport a Cryptomator i rebeu un certificat de col·laborador. És com una clau de llicència però per a gent meravellosa que fa servir programari lliure. ;-)
|
||||
preferences.contribute.getCertificate=Encara no en teniu cap? Sapigueu com aconseguir-ne un.
|
||||
preferences.contribute.promptText=Enganxeu aquí el certificat de col·laborador
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Quant a
|
||||
|
||||
@@ -222,9 +226,11 @@ main.dropZone.dropVault=Afegeix aquesta caixa forta
|
||||
main.dropZone.unknownDragboardContent=Si voleu afegir una caixa forta, arrossegueu-la a aquesta finestra
|
||||
## Vault List
|
||||
main.vaultlist.emptyList.onboardingInstruction=Feu clic aquí per afegir una caixa forta
|
||||
main.vaultlist.contextMenu.remove=Elimina…
|
||||
main.vaultlist.contextMenu.lock=Bloqueja
|
||||
main.vaultlist.contextMenu.unlock=Desbloca…
|
||||
main.vaultlist.contextMenu.unlockNow=Desbloqueja ara
|
||||
main.vaultlist.contextMenu.vaultoptions=Opcions de la caixa forta
|
||||
main.vaultlist.contextMenu.reveal=Mostra la unitat
|
||||
main.vaultlist.addVaultBtn=Afegir una caixa forta
|
||||
## Vault Detail
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=...
|
||||
addvaultwizard.new.directoryPickerLabel=Vlastní umístění
|
||||
addvaultwizard.new.directoryPickerButton=Vybrat...
|
||||
addvaultwizard.new.directoryPickerTitle=Vyberte adresář
|
||||
addvaultwizard.new.fileAlreadyExists=Trezor nemůže být vytvořen na tomto umístění, protože stejný objekt již existuje.
|
||||
addvaultwizard.new.locationDoesNotExist=Trezor nemůže být vytvořen v tomto umístění, protože přinejmenším jedna položka z daného umístění neexistuje.
|
||||
addvaultwizard.new.fileAlreadyExists=Soubor nebo složka s tímto názvem trezoru již existuje
|
||||
addvaultwizard.new.locationDoesNotExist=Složka v zadané cestě neexistuje nebo není přístupná
|
||||
addvaultwizard.new.locationIsNotWritable=Není možné zapisovat na zadané cestě
|
||||
addvaultwizard.new.locationIsOk=Vhodné umístění pro váš trezor
|
||||
addvaultwizard.new.invalidName=Neplatné jméno trezoru. Prosím změňte jméno adresáře.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Vytvořit trezor
|
||||
@@ -97,7 +99,10 @@ unlock.title=Odemknout trezor
|
||||
unlock.passwordPrompt=Zadejte heslo pro "%s":
|
||||
unlock.savePassword=Zapamatovat heslo
|
||||
unlock.unlockBtn=Odemknout
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Vyberte Masterkey soubor
|
||||
## Success
|
||||
unlock.success.message=Trezor "%s" byl úspěšně odemčen a nyní je dostupný jako virtuální jednotka.
|
||||
unlock.success.rememberChoice=Pamatovat si volbu, nezobrazovat to znovu
|
||||
unlock.success.revealBtn=Zobrazit jednotku
|
||||
## Failure
|
||||
@@ -109,8 +114,11 @@ unlock.error.invalidMountPoint.existing=Připojovací bod %s již existuje nebo
|
||||
# Lock
|
||||
## Force
|
||||
lock.forced.heading=Běžné uzamčení selhalo
|
||||
lock.forced.message=Uzamčení "%s" bylo zablokováno nevyřízenými operacemi nebo otevřenými soubory. Můžete vynutit uzamčení tohoto trezoru, ale přerušení I/O může mít za následek ztrátu neuložených dat.
|
||||
lock.forced.confirmBtn=Přesto uzamknout
|
||||
## Failure
|
||||
lock.fail.heading=Uzamčení trezoru selhalo.
|
||||
lock.fail.message=Trezor "%s" nelze uzamknout. Ujistěte se, že je neuložená práce uložena jinde a že jsou dokončeny důležité operace čtení/zápis. Za účelem uzavření trezoru ukončte proces Cryptomatoru.
|
||||
|
||||
# Migration
|
||||
migration.title=Upgrade trezoru
|
||||
@@ -170,11 +178,14 @@ preferences.updates.currentVersion=Aktuální verze: %s
|
||||
preferences.updates.autoUpdateCheck=Automaticky kontrolovat aktualizace
|
||||
preferences.updates.checkNowBtn=Zkontrolovat nyní
|
||||
preferences.updates.updateAvailable=Dostupná nová verze %s.
|
||||
## Donation Key
|
||||
preferences.donationKey=Příspěvek
|
||||
preferences.donationKey.registeredFor=Registrováno na: %s
|
||||
preferences.donationKey.noDonationKey=Nebyl nalezen žádný platný darovací klíč. Je to jako licenční klíč, ale pro úžasné lidi využívající software zadarmo. ;-)
|
||||
preferences.donationKey.getDonationKey=Získat darovací klíč
|
||||
## Contribution
|
||||
preferences.contribute=Podpořte nás
|
||||
preferences.contribute.registeredFor=Certifikát podporovatele byl registrován pro %s
|
||||
preferences.contribute.noCertificate=Podpořte Cryptomator a získejte certifikát podporovatele. Je to jako licenční klíč, ale pro skvělé lidi, kteří používají svobodný software. ;-)
|
||||
preferences.contribute.getCertificate=Ještě žádný nemáte? Naučte se, jak ho můžete získat.
|
||||
preferences.contribute.promptText=Sem vložte kód certifikátu podporovatele
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=O aplikaci
|
||||
|
||||
@@ -223,6 +234,7 @@ main.vaultlist.contextMenu.remove=Odstranit…
|
||||
main.vaultlist.contextMenu.lock=Zamknout
|
||||
main.vaultlist.contextMenu.unlock=Odemknout…
|
||||
main.vaultlist.contextMenu.unlockNow=Odemknout nyní
|
||||
main.vaultlist.contextMenu.vaultoptions=Zobrazit možnosti trezoru
|
||||
main.vaultlist.contextMenu.reveal=Zobrazit jednotku
|
||||
main.vaultlist.addVaultBtn=Přidat trezor
|
||||
## Vault Detail
|
||||
|
||||
@@ -14,7 +14,7 @@ generic.button.next=Weiter
|
||||
generic.button.print=Drucken
|
||||
## Error
|
||||
generic.error.title=Ein unerwarteter Fehler ist aufgetreten
|
||||
generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde die folgende Fehlermeldung und beschreibe kurz, durch welche Schritte er aufgetreten ist.
|
||||
generic.error.instruction=Das hätte nicht passieren dürfen. Bitte melde den folgenden Fehlertext und beschreibe kurz, durch welche Schritte der Fehler aufgetreten ist.
|
||||
|
||||
# Defaults
|
||||
defaults.vault.vaultName=Tresor
|
||||
@@ -38,14 +38,16 @@ addvaultwizard.welcome.existingButton=Existierenden Tresor öffnen
|
||||
addvaultwizard.new.nameInstruction=Wähle einen Namen für den Tresor
|
||||
addvaultwizard.new.namePrompt=Tresorname
|
||||
### Location
|
||||
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Daten deines Tresors ablegen?
|
||||
addvaultwizard.new.locationInstruction=Wo soll Cryptomator die verschlüsselten Dateien deines Tresors ablegen?
|
||||
addvaultwizard.new.locationLabel=Speicherort
|
||||
addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Eigener Ort
|
||||
addvaultwizard.new.directoryPickerButton=Durchsuchen …
|
||||
addvaultwizard.new.directoryPickerTitle=Verzeichnis auswählen
|
||||
addvaultwizard.new.fileAlreadyExists=Der Tresor konnte nicht erstellt werden, da am Speicherort so eine Datei bereits existiert.
|
||||
addvaultwizard.new.locationDoesNotExist=Der Tresor kann nicht erstellt werden, da mindestens ein Speicherort nicht existiert.
|
||||
addvaultwizard.new.fileAlreadyExists=Eine Datei oder ein Ordner mit diesem Namen ist bereits vorhanden
|
||||
addvaultwizard.new.locationDoesNotExist=Der Ordner im angegebenen Pfad existiert nicht oder kann nicht geöffnet werden
|
||||
addvaultwizard.new.locationIsNotWritable=Kein Schreibzugriff auf den angegebenen Pfad
|
||||
addvaultwizard.new.locationIsOk=Geeigneter Ort für deinen Tresor
|
||||
addvaultwizard.new.invalidName=Ungültiger Tresorname. Bitte wähle einen regulären Namen, mit dem auch Verzeichnisse erstellt werden können.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Tresor erstellen
|
||||
@@ -79,7 +81,7 @@ addvaultwizard.success.unlockNow=Jetzt entsperren
|
||||
|
||||
# Remove Vault
|
||||
removeVault.title=Tresor entfernen
|
||||
removeVault.information=Dein Tresor wird nicht gelöscht, sondern lediglich aus Cryptomators Tresorliste entfernt. Du kannst ihn daher später jederzeit wieder hinzufügen.
|
||||
removeVault.information=Dieser Tresor wird nur aus der Tresorliste entfernt. Du kannst den Tresor später jederzeit wieder hinzufügen. Es werden keine verschlüsselten Daten gelöscht.
|
||||
removeVault.confirmBtn=Tresor entfernen
|
||||
|
||||
# Change Password
|
||||
@@ -97,6 +99,8 @@ unlock.title=Tresor entsperren
|
||||
unlock.passwordPrompt=Gib das Passwort für „%s“ ein:
|
||||
unlock.savePassword=Passwort merken
|
||||
unlock.unlockBtn=Entsperren
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Masterkey-Datei auswählen
|
||||
## Success
|
||||
unlock.success.message=„%s“ erfolgreich entsperrt! Nun kannst du über das virtuelle Laufwerk auf deinen Tresor zugreifen.
|
||||
unlock.success.rememberChoice=Auswahl speichern und nicht mehr anzeigen
|
||||
@@ -172,13 +176,16 @@ preferences.volume.webdav.scheme=WebDAV-Schema
|
||||
preferences.updates=Updates
|
||||
preferences.updates.currentVersion=Aktuelle Version: %s
|
||||
preferences.updates.autoUpdateCheck=Automatisch nach Updates suchen
|
||||
preferences.updates.checkNowBtn=Jetzt suchen
|
||||
preferences.updates.checkNowBtn=Jetzt prüfen
|
||||
preferences.updates.updateAvailable=Update auf Version %s verfügbar.
|
||||
## Donation Key
|
||||
preferences.donationKey=Spende
|
||||
preferences.donationKey.registeredFor=Registriert für %s
|
||||
preferences.donationKey.noDonationKey=Kein gültiger Spendenschlüssel gefunden. Er ist wie ein Lizenzschlüssel, aber für tolle Leute, die freie Software verwenden. ;-)
|
||||
preferences.donationKey.getDonationKey=Hol dir einen Spendenschlüssel
|
||||
## Contribution
|
||||
preferences.contribute=Unterstütze uns
|
||||
preferences.contribute.registeredFor=Supporter-Zertifikat registriert für %s
|
||||
preferences.contribute.noCertificate=Unterstütze Cryptomator und erhalte ein Supporter-Zertifikat. Es ist wie ein Lizenzschlüssel, aber für großartige Menschen, die freie Software verwenden. ;-)
|
||||
preferences.contribute.getCertificate=Du hast noch keines? Erfahre, wie du es erhalten kannst.
|
||||
preferences.contribute.promptText=Code des Supporter-Zertifikats hier einfügen
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Über
|
||||
|
||||
@@ -251,7 +258,7 @@ main.vaultDetail.throughput.kbps=%.1f kiB/s
|
||||
main.vaultDetail.throughput.mbps=%.1f MiB/s
|
||||
main.vaultDetail.stats=Tresorstatistik
|
||||
### Missing
|
||||
main.vaultDetail.missing.info=Mit diesem Pfad konnte Cryptomator keinen Tresor finden.
|
||||
main.vaultDetail.missing.info=Cryptomator konnte keinen Tresor mit diesem Pfad finden.
|
||||
main.vaultDetail.missing.recheck=Erneut prüfen
|
||||
main.vaultDetail.missing.remove=Aus Tresorliste entfernen…
|
||||
main.vaultDetail.missing.changeLocation=Speicherort des Tresors ändern…
|
||||
@@ -266,8 +273,8 @@ wrongFileAlert.header.lead=Für diesen Zweck stellt Cryptomator ein Laufwerk in
|
||||
wrongFileAlert.instruction.0=Folge diesen Schritten, um Dateien zu verschlüsseln:
|
||||
wrongFileAlert.instruction.1=1. Entsperre deinen Tresor.
|
||||
wrongFileAlert.instruction.2=2. Klicke auf „Anzeigen“, um das Laufwerk in deinem Dateimanager zu öffnen.
|
||||
wrongFileAlert.instruction.3=3. Füge deine Dateien diesem Laufwerk hinzu.
|
||||
wrongFileAlert.link=Für weitere Unterstützung besuche
|
||||
wrongFileAlert.instruction.3=3. Füge deine Dateien zu diesem Laufwerk hinzu.
|
||||
wrongFileAlert.link=Besuche für weitere Hilfe
|
||||
|
||||
# Vault Options
|
||||
## General
|
||||
@@ -281,26 +288,26 @@ vaultOptions.general.actionAfterUnlock.ask=Nachfragen
|
||||
## Mount
|
||||
vaultOptions.mount=Laufwerk
|
||||
vaultOptions.mount.readonly=Schreibgeschützt
|
||||
vaultOptions.mount.customMountFlags=Benutzerdefinierte Mount-Flags
|
||||
vaultOptions.mount.customMountFlags=Benutzerdefinierte Einhänge-Optionen
|
||||
vaultOptions.mount.winDriveLetterOccupied=belegt
|
||||
vaultOptions.mount.mountPoint=Einhängepunkt
|
||||
vaultOptions.mount.mountPoint.auto=Automatisch einen passenden Ort auswählen
|
||||
vaultOptions.mount.mountPoint.auto=Wähle automatisch einen geeigneten Ort aus
|
||||
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.directoryPickerTitle=Wähle ein leeres Verzeichnis
|
||||
## Master Key
|
||||
vaultOptions.masterkey=Passwort
|
||||
vaultOptions.masterkey.changePasswordBtn=Passwort ändern
|
||||
vaultOptions.masterkey.forgetSavedPasswordBtn=Gespeichertes Passwort vergessen
|
||||
vaultOptions.masterkey.recoveryKeyExpanation=Bei Verlust deines Passworts ist ein Wiederherstellungsschlüssel deine einzige Möglichkeit, den Zugriff auf einen Tresor wiederherzustellen.
|
||||
vaultOptions.masterkey.recoveryKeyExpanation=Dein Wiederherstellungsschlüssel gehört nur dir! Dieser wird benötigt, um deinen Tresor wiederherzustellen, für den Fall das du dein Passwort verloren hast.
|
||||
vaultOptions.masterkey.showRecoveryKeyBtn=Wiederherstellungsschlüssel anzeigen
|
||||
vaultOptions.masterkey.recoverPasswordBtn=Passwort wiederherstellen
|
||||
|
||||
# Recovery Key
|
||||
recoveryKey.title=Wiederherstellungsschlüssel
|
||||
recoveryKey.enterPassword.prompt=Gib dein Passwort ein, um den Wiederherstellungsschlüssel für „%s“ anzuzeigen:
|
||||
recoveryKey.display.message=Mit folgendem Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
|
||||
recoveryKey.display.message=Mit dem folgenden Wiederherstellungsschlüssel kannst du den Zugriff auf „%s“ wiederherstellen:
|
||||
recoveryKey.display.StorageHints=Bewahre ihn möglichst sicher auf, z. B.\n • in einem Passwortmanager\n • auf einem USB-Speicherstick\n • auf Papier ausgedruckt
|
||||
recoveryKey.recover.prompt=Gib deinen Wiederherstellungsschlüssel für „%s“ ein:
|
||||
recoveryKey.recover.validKey=Dies ist ein gültiger Wiederherstellungsschlüssel
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Προσαρμοσμένη τοποθεσία
|
||||
addvaultwizard.new.directoryPickerButton=Επιλογή…
|
||||
addvaultwizard.new.directoryPickerTitle=Επιλογή φακέλου
|
||||
addvaultwizard.new.fileAlreadyExists=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς κάποια αντικείμενα υπάρχουν ήδη.
|
||||
addvaultwizard.new.locationDoesNotExist=Το vault δεν μπορεί να δημιουργηθεί σε αυτό το μονοπάτι καθώς τουλάχιστον ένα στοιχείο του μονοπατιού δεν υπάρχει.
|
||||
addvaultwizard.new.fileAlreadyExists=Ένα αρχείο ή φάκελος με το όνομα του vault υπάρχει ήδη
|
||||
addvaultwizard.new.locationDoesNotExist=Στην καθορισμένη διαδρομή δεν υπάρχει ή δεν μπορεί να προσπελαστεί ένας φάκελος
|
||||
addvaultwizard.new.locationIsNotWritable=Δεν υπάρχει πρόσβαση εγγραφής στην καθορισμένη διαδρομή
|
||||
addvaultwizard.new.locationIsOk=Κατάλληλη τοποθεσία για το vault σας
|
||||
addvaultwizard.new.invalidName=Λάθος όνομα vault. Παρακαλώ χρησιμοποιείστε ένα κανονικό όνομα φακέλου.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Δημιουργία Vault
|
||||
@@ -97,7 +99,10 @@ unlock.title=Ξεκλείδωμα Vault
|
||||
unlock.passwordPrompt=Εισάγετε τον κωδικό για "%s":
|
||||
unlock.savePassword=Απομνημόνευση κωδικού πρόσβασης
|
||||
unlock.unlockBtn=Ξεκλείδωμα
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Επιλέξτε το αρχείο Masterkey
|
||||
## Success
|
||||
unlock.success.message=Ξεκλειδώθηκε "%s" επιτυχώς! Το vault σας είναι διαθέσιμο μέσω του εικονικού δίσκου του.
|
||||
unlock.success.rememberChoice=Απομνημόνευση επιλογής, μην ρωτήσεις ξανά
|
||||
unlock.success.revealBtn=Αποκάλυψη εικονικού δίσκου
|
||||
## Failure
|
||||
@@ -173,11 +178,14 @@ preferences.updates.currentVersion=Τρέχουσα έκδοση: %s
|
||||
preferences.updates.autoUpdateCheck=Αυτόματος έλεγχος για ενημερώσεις
|
||||
preferences.updates.checkNowBtn=Έλεγχος τώρα
|
||||
preferences.updates.updateAvailable=Η ενημέρωση για την έκδοση %s είναι διαθέσιμη.
|
||||
## Donation Key
|
||||
preferences.donationKey=Δωρεά
|
||||
preferences.donationKey.registeredFor=Καταχωρημένο σε %s
|
||||
preferences.donationKey.noDonationKey=Δεν βρέθηκε ενεργό κλειδί δωρεάς. Είναι σαν κλειδί αγοράς αλλά για απίθανους ανθρώπους που χρησιμοποιούν δωρεάν λογισμικό. ;-)
|
||||
preferences.donationKey.getDonationKey=Πάρε ένα κλειδί δωρεάς
|
||||
## Contribution
|
||||
preferences.contribute=Υποστηρίξτε μας
|
||||
preferences.contribute.registeredFor=Το πιστοποιητικό υποστήριξης καταχωρήθηκε για %s
|
||||
preferences.contribute.noCertificate=Υποστηρίξτε το Cryptomator και λάβετε ένα πιστοποιητικό υποστηρικτή. Είναι σαν κλειδί άδειας χρήσης, αλλά για φοβερά άτομα που χρησιμοποιούν ελεύθερο λογισμικό. ;-)
|
||||
preferences.contribute.getCertificate=Δεν έχετε ένα ήδη; Μάθετε πώς μπορείτε να το αποκτήσετε.
|
||||
preferences.contribute.promptText=Επικολλήστε τον κωδικό πιστοποιητικού υποστηρικτή εδώ
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Σχετικά με
|
||||
|
||||
@@ -222,9 +230,11 @@ main.dropZone.dropVault=Προσθήκη vault
|
||||
main.dropZone.unknownDragboardContent=Αν θέλετε να προσθέσετε ένα vault, σύρετε το σε αυτό το παράθυρο
|
||||
## Vault List
|
||||
main.vaultlist.emptyList.onboardingInstruction=Κάντε κλικ εδώ για να προσθέσετε ένα vault
|
||||
main.vaultlist.contextMenu.remove=Αφαίρεση…
|
||||
main.vaultlist.contextMenu.lock=Κλείδωμα
|
||||
main.vaultlist.contextMenu.unlock=Ξεκλείδωμα…
|
||||
main.vaultlist.contextMenu.unlockNow=Ξεκλείδωμα τώρα
|
||||
main.vaultlist.contextMenu.vaultoptions=Εμφάνιση επιλογών Vault
|
||||
main.vaultlist.contextMenu.reveal=Αποκάλυψη εικονικού δίσκου
|
||||
main.vaultlist.addVaultBtn=Προσθήκη Vault
|
||||
## Vault Detail
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Ubicación personalizada
|
||||
addvaultwizard.new.directoryPickerButton=Elegir…
|
||||
addvaultwizard.new.directoryPickerTitle=Seleccionar directorio
|
||||
addvaultwizard.new.fileAlreadyExists=La bóveda no puede crearse en esta ruta porque ya existe un objeto.
|
||||
addvaultwizard.new.locationDoesNotExist=La bóveda no puede ser creada en esta ruta porque al menos un componente no existe.
|
||||
addvaultwizard.new.fileAlreadyExists=Ya existe un archivo o directorio con el nombre de la bóveda
|
||||
addvaultwizard.new.locationDoesNotExist=No existe un directorio en la ruta especificada o no se puede acceder
|
||||
addvaultwizard.new.locationIsNotWritable=Sin acceso de escritura en la ruta especificada
|
||||
addvaultwizard.new.locationIsOk=Ubicación adecuada para la bóveda
|
||||
addvaultwizard.new.invalidName=Nombre de bóveda inválido. Considere un nombre de directorio regular.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Crear bóveda
|
||||
@@ -97,6 +99,8 @@ unlock.title=Desbloquear bóveda
|
||||
unlock.passwordPrompt=Ingresar contraseña para "%s":
|
||||
unlock.savePassword=Recordar contraseña
|
||||
unlock.unlockBtn=Desbloquear
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Seleccionar archivo Masterkey
|
||||
## Success
|
||||
unlock.success.message=¡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
|
||||
@@ -174,11 +178,9 @@ preferences.updates.currentVersion=Versión actual: %s
|
||||
preferences.updates.autoUpdateCheck=Buscar actualizaciones automáticamente
|
||||
preferences.updates.checkNowBtn=Buscar ahora
|
||||
preferences.updates.updateAvailable=Actualización a la versión %s disponible.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donación
|
||||
preferences.donationKey.registeredFor=Registrado para %s
|
||||
preferences.donationKey.noDonationKey=No se encontró una clave de donación válida. Es como una clave de licencia pero para personas geniales que usan software libre.
|
||||
preferences.donationKey.getDonationKey=Obtener clave de donación
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Acerca de
|
||||
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Emplacement personnalisé
|
||||
addvaultwizard.new.directoryPickerButton=Choisir...
|
||||
addvaultwizard.new.directoryPickerTitle=Choisissez le répertoire
|
||||
addvaultwizard.new.fileAlreadyExists=Le coffre ne peut pas être créé sur ce chemin car un objet existe déjà.
|
||||
addvaultwizard.new.locationDoesNotExist=Le coffre ne peut pas être créé à cet endroit car au moins un répertoire du chemin n'existe pas.
|
||||
addvaultwizard.new.fileAlreadyExists=Un fichier ou un répertoire avec ce nom existe déjà
|
||||
addvaultwizard.new.locationDoesNotExist=Un répertoire dans le chemin spécifié n'existe pas ou ne peut pas être accédé
|
||||
addvaultwizard.new.locationIsNotWritable=Pas d'accès en écriture au chemin spécifié
|
||||
addvaultwizard.new.locationIsOk=Lieu approprié pour votre coffre
|
||||
addvaultwizard.new.invalidName=Nom de coffre invalide. Préférez un nom de répertoire habituel.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Créer un coffre
|
||||
@@ -97,6 +99,8 @@ unlock.title=Déverrouiller le coffre
|
||||
unlock.passwordPrompt=Entrez le mot de passe pour “%s” :
|
||||
unlock.savePassword=Mémoriser le mot de passe
|
||||
unlock.unlockBtn=Déverrouiller
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Sélectionner le fichier clef
|
||||
## Success
|
||||
unlock.success.message=“%s” déverrouillé ! Le contenu de votre coffre est maintenant accessible par son lecteur virtuel.
|
||||
unlock.success.rememberChoice=Se souvenir de mon choix et ne plus me demander
|
||||
@@ -174,11 +178,14 @@ preferences.updates.currentVersion=Version actuelle : “%s”
|
||||
preferences.updates.autoUpdateCheck=Vérifier automatiquement l’existence de mise à jour
|
||||
preferences.updates.checkNowBtn=Vérifier maintenant
|
||||
preferences.updates.updateAvailable=Mise à jour “%s” disponible.
|
||||
## Donation Key
|
||||
preferences.donationKey=Faire un Don
|
||||
preferences.donationKey.registeredFor=Licence enregistrée à %s
|
||||
preferences.donationKey.noDonationKey=Aucune clé de don valide trouvée. C'est comme une clé de licence mais pour des personnes géniales utilisant un logiciel libre ;-)
|
||||
preferences.donationKey.getDonationKey=Obtenir une clé de don
|
||||
## Contribution
|
||||
preferences.contribute=Nous soutenir
|
||||
preferences.contribute.registeredFor=Certificat de soutien enregistré pour %s
|
||||
preferences.contribute.noCertificate=Soutenez Cryptomator et recevez un certificat de soutien. C'est comme une clé de licence, mais pour des gens géniaux qui utilisent des logiciels libres. ;-)
|
||||
preferences.contribute.getCertificate=Vous n'en avez pas encore? Découvrez comment vous pouvez l'obtenir.
|
||||
preferences.contribute.promptText=Collez le code du certificat du supporter ici
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=A propos
|
||||
|
||||
|
||||
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=अपने पसंद की जगह डालें
|
||||
addvaultwizard.new.directoryPickerButton=चुनें…
|
||||
addvaultwizard.new.directoryPickerTitle=निर्देशिका चुनें
|
||||
addvaultwizard.new.fileAlreadyExists=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कुछ वस्तु पहले से मौजूद है।
|
||||
addvaultwizard.new.locationDoesNotExist=तिजोरी इस रास्ते पर नहीं बनाई जा सकती क्योंकि कम से कम एक पथ घटक मौजूद नहीं है।
|
||||
addvaultwizard.new.invalidName=अमान्य वॉल्ट नाम। कृपया एक नियमित निर्देशिका नाम पर विचार करें।
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=वॉल्ट बनाएं
|
||||
@@ -72,6 +70,7 @@ changepassword.title=पासवर्ड बदलें
|
||||
|
||||
# Unlock
|
||||
unlock.unlockBtn=अनलॉक करें
|
||||
##
|
||||
## Success
|
||||
## Failure
|
||||
### Invalid Mount Point
|
||||
@@ -94,7 +93,9 @@ preferences.title=प्राथमिकताएं
|
||||
preferences.general=सामान्य
|
||||
## Volume
|
||||
## Updates
|
||||
## Donation Key
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
|
||||
# Vault Statistics
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
# Forget Password
|
||||
|
||||
# Unlock
|
||||
##
|
||||
## Success
|
||||
## Failure
|
||||
### Invalid Mount Point
|
||||
@@ -44,7 +45,9 @@
|
||||
## General
|
||||
## Volume
|
||||
## Updates
|
||||
## Donation Key
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
|
||||
# Vault Statistics
|
||||
|
||||
@@ -43,8 +43,6 @@ addvaultwizard.new.locationLabel=Tárolási hely
|
||||
addvaultwizard.new.directoryPickerLabel=Egyedi hely
|
||||
addvaultwizard.new.directoryPickerButton=Választás…
|
||||
addvaultwizard.new.directoryPickerTitle=Könyvtár kiválasztása
|
||||
addvaultwizard.new.fileAlreadyExists=Nem lehet a széfet létrehozni ezen a helyen, mert egy fájl már létezik itt.
|
||||
addvaultwizard.new.locationDoesNotExist=Nem lehet a széfet létrehozni ezen a helyen, mert az útvonal legalább egy darabja nem létezik.
|
||||
addvaultwizard.new.invalidName=Érvénytelen széf elnevezés. Kérjük vegye figyelembe a szabályos könyvtárelnevezésre vonatkozó szabályokat.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Új széf létrehozása
|
||||
@@ -95,6 +93,8 @@ forgetPassword.confirmBtn=Jelszó elfelejtése
|
||||
unlock.title=Széf feloldása
|
||||
unlock.passwordPrompt=Írja be a jelszavát a következő széfhez "%s":
|
||||
unlock.unlockBtn=Feloldás
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Mesterkulcs fájl kiválasztása
|
||||
## Success
|
||||
unlock.success.rememberChoice=Jegyezze meg a választást és ne mutassa többet
|
||||
unlock.success.revealBtn=Széf megjelenítése
|
||||
@@ -158,11 +158,9 @@ preferences.updates.currentVersion=Jelenlegi verzió: %s
|
||||
preferences.updates.autoUpdateCheck=Frissítések autómatikus keresése
|
||||
preferences.updates.checkNowBtn=Ellenőrzés most
|
||||
preferences.updates.updateAvailable=Frissítés a %s verzióra elérhető.
|
||||
## Donation Key
|
||||
preferences.donationKey=Adomány
|
||||
preferences.donationKey.registeredFor=Regisztrálva %s számára
|
||||
preferences.donationKey.noDonationKey=Nem található érvényes adománykulcs. Mint egy licenckulcs, csak olyan nagyszerű emberek számára, akik ingyenes szofvereket használnak. ;-)
|
||||
preferences.donationKey.getDonationKey=Adománykulcs beszerzése
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Rólunk
|
||||
|
||||
|
||||
@@ -44,8 +44,9 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Sesuaikan Lokasi
|
||||
addvaultwizard.new.directoryPickerButton=Pilih…
|
||||
addvaultwizard.new.directoryPickerTitle=Pilih Folder
|
||||
addvaultwizard.new.fileAlreadyExists=Brankas tidak dibuat disini karena beberapa item telah ada disini.
|
||||
addvaultwizard.new.locationDoesNotExist=Brankas tidak bisa dibuat disini karena setidaknya satu jalur komponen tidak ada.
|
||||
addvaultwizard.new.fileAlreadyExists=Sudah ada file atau direktori dengan nama yang sama
|
||||
addvaultwizard.new.locationDoesNotExist=Direktori pada path yang dipilih tidak ada atau tidak dapat diakses
|
||||
addvaultwizard.new.locationIsNotWritable=Anda tidak memiliki hak akses untuk menulis pada path yang dipilih
|
||||
addvaultwizard.new.invalidName=Nama brankas salah. Harap pilih nama folder yang umum.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Buat Brankas
|
||||
@@ -57,6 +58,8 @@ addvault.new.readme.storageLocation.fileName=PENTING.rtf
|
||||
addvault.new.readme.storageLocation.1=⚠️ BERKAS BRANKAS ⚠️
|
||||
addvault.new.readme.storageLocation.2=Ini adalah lokasi penyimpanan brankas kamu.
|
||||
addvault.new.readme.storageLocation.3=JANGAN
|
||||
addvault.new.readme.storageLocation.4=• mengubah file dalam direktori ini, atau
|
||||
addvault.new.readme.storageLocation.5=• menempelkan file untuk dienkripsi ke dalam direktori ini.
|
||||
addvault.new.readme.storageLocation.6=Jika kamu ingin mengenkripsi berkas dan melihat isi brankas, lakukan hal berikut:
|
||||
addvault.new.readme.storageLocation.7=1. Tambahkan brankas ini ke Cryptomator.
|
||||
addvault.new.readme.storageLocation.8=2. Buka gembok brankas di Cryptomator.
|
||||
@@ -65,18 +68,34 @@ addvault.new.readme.storageLocation.10=Jika kamu butuh bantuan, kunjungi dokumen
|
||||
addvault.new.readme.accessLocation.fileName=SELAMAT_DATANG.rtf
|
||||
addvault.new.readme.accessLocation.1=🔐️ ISI TERENKRIPSI 🔐️
|
||||
addvault.new.readme.accessLocation.2=Ini adalah lokasi akses brankas kamu.
|
||||
addvault.new.readme.accessLocation.3=File yang ditambahkan ke volume ini akan dienkripsi oleh Cryptomator. Anda dapat mempergunakan isi vault seperti dalam folder lain. Saat ini Anda sedang mengakses tampilan versi dekripsi, file Anda selalu terenkripsi di dalam cakram keras Anda.
|
||||
addvault.new.readme.accessLocation.4=Anda dapat menghapus file ini.
|
||||
## Existing
|
||||
addvaultwizard.existing.instruction=Pilih file "masterkey.cryptomator" dalam vault Anda.
|
||||
addvaultwizard.existing.chooseBtn=Pilih…
|
||||
addvaultwizard.existing.filePickerTitle=Pilih File Kunci Induk
|
||||
## Success
|
||||
addvaultwizard.success.nextStepsInstructions=Vault "%s" telah dibuat.\nAnda harus membuka kunci vault ini untuk mengakses atau menambahkan konten. Anda juga dapat membuka kunci vault ini kapan saja di kemudian hari.
|
||||
addvaultwizard.success.unlockNow=Buka Kunci Sekarang
|
||||
|
||||
# Remove Vault
|
||||
removeVault.title=Hapus Vault
|
||||
removeVault.information=Cryptomator hanya akan melupakan vault ini. Anda dapat menambahkan vault ini lagi nantinya. File yang telah dienkripsi tidak akan dihapus dari cakram keras Anda.
|
||||
removeVault.confirmBtn=Hapus Vault
|
||||
|
||||
# Change Password
|
||||
changepassword.title=Ubah Kata Sandi
|
||||
changepassword.enterOldPassword=Masukkan kata sandi untuk "%s" saat ini
|
||||
changepassword.finalConfirmation=Saya mengerti bahwa saya tidak akan dapat mengakses data saya apabila saya lupa kata sandi saya
|
||||
|
||||
# Forget Password
|
||||
forgetPassword.title=Lupa Kata Sandi
|
||||
forgetPassword.confirmBtn=Lupa Kata Sandi
|
||||
|
||||
# Unlock
|
||||
unlock.unlockBtn=Buka Gembok
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Pilih File Kunci Induk
|
||||
## Success
|
||||
## Failure
|
||||
### Invalid Mount Point
|
||||
@@ -89,6 +108,7 @@ unlock.unlockBtn=Buka Gembok
|
||||
## Start
|
||||
## Run
|
||||
## Sucess
|
||||
migration.success.unlockNow=Buka Kunci Sekarang
|
||||
## Missing file system capabilities
|
||||
## Impossible
|
||||
|
||||
@@ -97,7 +117,9 @@ preferences.title=Preferensi
|
||||
## General
|
||||
## Volume
|
||||
## Updates
|
||||
## Donation Key
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
|
||||
# Vault Statistics
|
||||
@@ -110,10 +132,12 @@ main.preferencesBtn.tooltip=Preferensi
|
||||
## Drag 'n' Drop
|
||||
## Vault List
|
||||
main.vaultlist.contextMenu.lock=Gembok
|
||||
main.vaultlist.contextMenu.unlockNow=Buka Kunci Sekarang
|
||||
main.vaultlist.addVaultBtn=Tambah Brankas
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
### Locked
|
||||
main.vaultDetail.unlockNowBtn=Buka Kunci Sekarang
|
||||
### Unlocked
|
||||
main.vaultDetail.lockBtn=Gembok
|
||||
### Missing
|
||||
@@ -127,6 +151,7 @@ vaultOptions.general.vaultName=Nama Brankas
|
||||
## Mount
|
||||
vaultOptions.mount.mountPoint.directoryPickerButton=Pilih…
|
||||
## Master Key
|
||||
vaultOptions.masterkey.changePasswordBtn=Ubah Kata Sandi
|
||||
|
||||
# Recovery Key
|
||||
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Posizione personalizzata
|
||||
addvaultwizard.new.directoryPickerButton=Scegli…
|
||||
addvaultwizard.new.directoryPickerTitle=Seleziona cartella
|
||||
addvaultwizard.new.fileAlreadyExists=La cassaforte non può essere creata in questo percorso perché un oggetto esiste già.
|
||||
addvaultwizard.new.locationDoesNotExist=La cassaforte non può essere creata in questo percorso perché almeno un componente di percorso non esiste.
|
||||
addvaultwizard.new.fileAlreadyExists=Un file o una cartella con il nome del vault esiste già
|
||||
addvaultwizard.new.locationDoesNotExist=Una cartella nel percorso specificato non esiste o non è accessibile
|
||||
addvaultwizard.new.locationIsNotWritable=Non c'è accesso in scrittura nel percorso specificato
|
||||
addvaultwizard.new.locationIsOk=Posizione adatta per il tuo vault
|
||||
addvaultwizard.new.invalidName=Nome della cassaforte non valido. Si prega di considerare un nome di directory regolare.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Crea Cassaforte
|
||||
@@ -97,9 +99,12 @@ unlock.title=Sblocca Cassaforte
|
||||
unlock.passwordPrompt=Inserisci la password per "%s":
|
||||
unlock.savePassword=Ricorda la Password
|
||||
unlock.unlockBtn=Sblocca
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Seleziona file Masterkey
|
||||
## Success
|
||||
unlock.success.message="%s" sbloccato con successo! La tua cassaforte è ora accessibile tramite il suo drive virtuale.
|
||||
unlock.success.rememberChoice=Ricorda la scelta, non mostrare ancora
|
||||
unlock.success.revealBtn=Visualizza disco
|
||||
unlock.success.revealBtn=Rivela Drive
|
||||
## Failure
|
||||
unlock.error.heading=Impossibile sbloccare la cassaforte
|
||||
### Invalid Mount Point
|
||||
@@ -149,6 +154,7 @@ preferences.general.theme.light=Chiaro
|
||||
preferences.general.theme.dark=Scuro
|
||||
preferences.general.unlockThemes=Sblocca modalità scura
|
||||
preferences.general.showMinimizeButton=Mostra pulsante riduci a icona
|
||||
preferences.general.showTrayIcon=Mostra icona nel tray (richiede riavvio)
|
||||
preferences.general.startHidden=Nascondi la finestra all'avvio di Cryptomator
|
||||
preferences.general.debugLogging=Abilita i registri di debug
|
||||
preferences.general.debugDirectory=Mostra file log
|
||||
@@ -172,11 +178,14 @@ preferences.updates.currentVersion=Versione attuale %s
|
||||
preferences.updates.autoUpdateCheck=Controlla automaticamente la presenza di aggiornamenti
|
||||
preferences.updates.checkNowBtn=Controlla adesso
|
||||
preferences.updates.updateAvailable=Disponibile aggiornamento alla versione %s.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donazione
|
||||
preferences.donationKey.registeredFor=Registrato per %s
|
||||
preferences.donationKey.noDonationKey=Nessuna chiave di donazione valida. È come una chiave di licenza ma per persone fantastiche che usano software gratuito. ;-)
|
||||
preferences.donationKey.getDonationKey=Ottieni una chiave di donazione
|
||||
## Contribution
|
||||
preferences.contribute=Supportaci
|
||||
preferences.contribute.registeredFor=Certificato del supporto registrato per %s
|
||||
preferences.contribute.noCertificate=Supporta Cryptomator e ricevi un certificato di supporter. È come una chiave di licenza, ma per persone fantastiche che utilizzano software libero. ;-)
|
||||
preferences.contribute.getCertificate=Non ne hai già uno? Impara come puoi ottenerlo.
|
||||
preferences.contribute.promptText=Incolla qui il codice del certificato di supporter
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Informazioni
|
||||
|
||||
@@ -220,10 +229,12 @@ main.dropZone.dropVault=Aggiungi questa cassaforte
|
||||
main.dropZone.unknownDragboardContent=Se vuoi aggiungere una cassaforte, trascinala in questa finestra
|
||||
## Vault List
|
||||
main.vaultlist.emptyList.onboardingInstruction=Clicca qui per aggiungere una cassaforte
|
||||
main.vaultlist.contextMenu.remove=Rimuovi…
|
||||
main.vaultlist.contextMenu.lock=Blocca
|
||||
main.vaultlist.contextMenu.unlock=Sblocca…
|
||||
main.vaultlist.contextMenu.unlockNow=Sblocca adesso
|
||||
main.vaultlist.contextMenu.reveal=Visualizza disco
|
||||
main.vaultlist.contextMenu.unlockNow=Sblocca Ora
|
||||
main.vaultlist.contextMenu.vaultoptions=Mostra Opzioni Cassaforte
|
||||
main.vaultlist.contextMenu.reveal=Rivela Drive
|
||||
main.vaultlist.addVaultBtn=Aggiungi Cassaforte
|
||||
## Vault Detail
|
||||
### Welcome
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=ユーザー設定
|
||||
addvaultwizard.new.directoryPickerButton=選択...
|
||||
addvaultwizard.new.directoryPickerTitle=ディレクトリを選択
|
||||
addvaultwizard.new.fileAlreadyExists=いくつかのオブジェクトが既に存在しているため、このパスに金庫を作成することはできません。
|
||||
addvaultwizard.new.locationDoesNotExist=このパスには階層がないため金庫を作成することができません。
|
||||
addvaultwizard.new.fileAlreadyExists=金庫名と同じ名前のファイルまたはディレクトリがすでに存在しています
|
||||
addvaultwizard.new.locationDoesNotExist=指定されたパスのディレクトリが存在しないかアクセスできません
|
||||
addvaultwizard.new.locationIsNotWritable=指定されたパスに書き込み権限がありません
|
||||
addvaultwizard.new.locationIsOk=金庫に適した場所
|
||||
addvaultwizard.new.invalidName=無効な金庫の名前です。一般的なディレクトリの名前を検討してください。
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=金庫を作成
|
||||
@@ -97,7 +99,10 @@ unlock.title=金庫を解錠
|
||||
unlock.passwordPrompt="%s" のパスワードを入力してください:
|
||||
unlock.savePassword=パスワードを記憶させる
|
||||
unlock.unlockBtn=解錠
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Masterkey ファイルを選択
|
||||
## Success
|
||||
unlock.success.message="%s" の解錠に成功しました! 仮想ドライブから金庫にアクセス可能です。
|
||||
unlock.success.rememberChoice=選択を記憶させて、再度表示しない
|
||||
unlock.success.revealBtn=ドライブを表示
|
||||
## Failure
|
||||
@@ -173,11 +178,14 @@ preferences.updates.currentVersion=現在のバージョン: %s
|
||||
preferences.updates.autoUpdateCheck=自動的に更新を確認する
|
||||
preferences.updates.checkNowBtn=今すぐ確認
|
||||
preferences.updates.updateAvailable=利用可能なバージョン %s に更新します。
|
||||
## Donation Key
|
||||
preferences.donationKey=寄付
|
||||
preferences.donationKey.registeredFor=%s で登録されています
|
||||
preferences.donationKey.noDonationKey=有効な Donation Key が見つかりませんでした。ライセンスキーに似ていますがフリーソフトを使う寄付者向けのキーです。 ;-)
|
||||
preferences.donationKey.getDonationKey=Donation Key を入手する
|
||||
## Contribution
|
||||
preferences.contribute=サポートする
|
||||
preferences.contribute.registeredFor=サポーター証明書が %s に登録されました
|
||||
preferences.contribute.noCertificate=Cryptomator を支援し、サポーター証明書を受け取りましょう。ライセンスキーに似ていますがフリーソフトを使う寄付者向けのキーです。 ;-)
|
||||
preferences.contribute.getCertificate=まだ証明書を手に入れていませんか? 詳細を確認しましょう。
|
||||
preferences.contribute.promptText=サポーター証明書をここに張り付けてください
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=詳細情報
|
||||
|
||||
@@ -222,9 +230,11 @@ main.dropZone.dropVault=この金庫を追加
|
||||
main.dropZone.unknownDragboardContent=このウィンドウにドラッグして、金庫を追加
|
||||
## 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
|
||||
|
||||
@@ -44,8 +44,10 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=사용자 지정 위치
|
||||
addvaultwizard.new.directoryPickerButton=선택
|
||||
addvaultwizard.new.directoryPickerTitle=디렉터리 선택
|
||||
addvaultwizard.new.fileAlreadyExists=이미 다른 객체가 존재하고 있어 해당 경로에 Vault를 생성할 수 없습니다.
|
||||
addvaultwizard.new.locationDoesNotExist=하나 이상의 경로 구성 요소가 없기 때문에 이 경로에 Vault가 생성 될 수 없습니다.
|
||||
addvaultwizard.new.fileAlreadyExists=Vault 내에 이미 존재하는 파일 또는 디렉터리 이름입니다.
|
||||
addvaultwizard.new.locationDoesNotExist=지정된 디렉터리가 존재하지 않거나 접근 할 수 없습니다.
|
||||
addvaultwizard.new.locationIsNotWritable=지정된 경로에 쓰기 권한이 없습니다.
|
||||
addvaultwizard.new.locationIsOk=Vault에 적당한 위치
|
||||
addvaultwizard.new.invalidName=유효하지 않은 Vault 이름입니다. 일반적인 디렉터리 이름으로 지정해주십시요.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Vault 생성
|
||||
@@ -97,7 +99,10 @@ unlock.title=Vault 잠금해제
|
||||
unlock.passwordPrompt="%s"의 비밀번호를 입력하십시요.
|
||||
unlock.savePassword=비밀번호 기억
|
||||
unlock.unlockBtn=잠금해제
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=마스터키 파일 선택
|
||||
## Success
|
||||
unlock.success.message="%s"이(가) 성공적으로 잠금해제되었습니다. 이제 이 Vault를 가상드라이브로 접근할 수 있습니다.
|
||||
unlock.success.rememberChoice=선택 기억함, 다시 묻지 않음
|
||||
unlock.success.revealBtn=드라이브 표시
|
||||
## Failure
|
||||
@@ -173,11 +178,9 @@ preferences.updates.currentVersion=현재 버전: %s
|
||||
preferences.updates.autoUpdateCheck=자동으로 업데이트 확인
|
||||
preferences.updates.checkNowBtn=지금 확인
|
||||
preferences.updates.updateAvailable=버전 %s의 업데이트가 가능합니다.
|
||||
## Donation Key
|
||||
preferences.donationKey=기부하기
|
||||
preferences.donationKey.registeredFor=%s (으)로 등록되어 있습니다.
|
||||
preferences.donationKey.noDonationKey=유효한 도네이션(기부) 키를 찾지 못했습니다. 라이센스 키와 비슷하나, 무료 소프트웨어를 사용하는 멋진 사람들을 위한 것입니다. ;-)
|
||||
preferences.donationKey.getDonationKey=도네이션(기부) 키 얻기
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=제품 정보
|
||||
|
||||
@@ -222,9 +225,11 @@ main.dropZone.dropVault=이 Vault를 추가
|
||||
main.dropZone.unknownDragboardContent=Vault를 추가하려면, 이 창에 드래그 하십시요.
|
||||
## Vault List
|
||||
main.vaultlist.emptyList.onboardingInstruction=Vault를 추가하기 위해 이곳을 클릭합니다.
|
||||
main.vaultlist.contextMenu.remove=제거...
|
||||
main.vaultlist.contextMenu.lock=잠금
|
||||
main.vaultlist.contextMenu.unlock=잠금해제...
|
||||
main.vaultlist.contextMenu.unlockNow=지금 잠금해제
|
||||
main.vaultlist.contextMenu.vaultoptions=Vault 옵션 보기
|
||||
main.vaultlist.contextMenu.reveal=드라이브 표시
|
||||
main.vaultlist.addVaultBtn=Vault 추가
|
||||
## Vault Detail
|
||||
|
||||
@@ -43,7 +43,6 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Pielāgota atrašanās vieta
|
||||
addvaultwizard.new.directoryPickerButton=Izvēlies...
|
||||
addvaultwizard.new.directoryPickerTitle=Izvēlēties mapi
|
||||
addvaultwizard.new.fileAlreadyExists=Glabātuvi nevar izveidot šajā ceļā, jo kāds objekts jau pastāv.
|
||||
addvaultwizard.new.invalidName=Nederīgs glabātuves nosaukums. Lūdzu izvēlieties vienkāršu mapes nosaukumu.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Izveidot glabātuvi
|
||||
@@ -94,6 +93,8 @@ forgetPassword.confirmBtn=Aizmirst paroli
|
||||
unlock.title=Atslēgt glabātuvi
|
||||
unlock.passwordPrompt=Ievadiet "%s" paroli:
|
||||
unlock.unlockBtn=Atslēgt
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Atlasīt galveno atslēgas datni
|
||||
## Success
|
||||
unlock.success.revealBtn=Atklāt disku
|
||||
## Failure
|
||||
@@ -148,11 +149,9 @@ preferences.updates.currentVersion=Pašreizējā versija: %s
|
||||
preferences.updates.autoUpdateCheck=Automātiski pārbaudīt atjauninājumus
|
||||
preferences.updates.checkNowBtn=Pārbaudīt tagad
|
||||
preferences.updates.updateAvailable=Pieejams atjauninājums uz versiju %s.
|
||||
## Donation Key
|
||||
preferences.donationKey=Ziedojums
|
||||
preferences.donationKey.registeredFor=Reģistrēts uz %s
|
||||
preferences.donationKey.noDonationKey=Nav atrasta derīga ziedojuma atslēga. Tā ir kā licences atslēga , bet priekš satriecošiem cilvēkiem, kas izmanto bezmaksas programmatūru. ;-)
|
||||
preferences.donationKey.getDonationKey=Iegūt ziedojuma atslēgu
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Par lietotni
|
||||
|
||||
|
||||
@@ -44,8 +44,6 @@ addvaultwizard.new.locationPrompt=…
|
||||
addvaultwizard.new.directoryPickerLabel=Tilpasset lagringssted
|
||||
addvaultwizard.new.directoryPickerButton=Velg…
|
||||
addvaultwizard.new.directoryPickerTitle=Velg mappe
|
||||
addvaultwizard.new.fileAlreadyExists=Hvelvet kan ikke opprettes på denne søkestien fordi et objekt allerede eksisterer.
|
||||
addvaultwizard.new.locationDoesNotExist=Hvelvet kan ikke opprettes på denne søkestien fordi minst én søkestikomponent ikke finnes.
|
||||
addvaultwizard.new.invalidName=Ugyldig navn på hvelvet. Vennligst vurder et vanlig mappenavn.
|
||||
### Password
|
||||
addvaultwizard.new.createVaultBtn=Opprett hvelv
|
||||
@@ -97,6 +95,8 @@ unlock.title=Lås opp hvelvet
|
||||
unlock.passwordPrompt=Skriv inn passordet for "%s":
|
||||
unlock.savePassword=Husk passord
|
||||
unlock.unlockBtn=Lås opp
|
||||
##
|
||||
unlock.chooseMasterkey.filePickerTitle=Velg hovednøkkelfil
|
||||
## Success
|
||||
unlock.success.rememberChoice=Husk valget - ikke vis dette igjen
|
||||
unlock.success.revealBtn=Gjør enheten synlig
|
||||
@@ -173,11 +173,9 @@ preferences.updates.currentVersion=Gjeldende versjon: %s
|
||||
preferences.updates.autoUpdateCheck=Se etter oppdateringer automatisk
|
||||
preferences.updates.checkNowBtn=Sjekk nå
|
||||
preferences.updates.updateAvailable=Oppdatering til versjon %s er tilgjengelig.
|
||||
## Donation Key
|
||||
preferences.donationKey=Donasjon
|
||||
preferences.donationKey.registeredFor=Registrert for %s
|
||||
preferences.donationKey.noDonationKey=Ingen gyldig donasjonsnøkkel funnet. Det er som en lisensnøkkel, men for fantastiske mennesker som bruker gratis programvare. ;-)
|
||||
preferences.donationKey.getDonationKey=Få en donasjonsnøkkel
|
||||
## Contribution
|
||||
#<-- Add entries for donations and code/translation/documentation contribution -->
|
||||
|
||||
## About
|
||||
preferences.about=Om
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user