diff --git a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java index 2a0a42ecb..f8e0e8078 100644 --- a/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/CommonsModule.java @@ -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") diff --git a/main/commons/src/main/java/org/cryptomator/common/Constants.java b/main/commons/src/main/java/org/cryptomator/common/Constants.java index 432cd08e5..06dedfc2d 100644 --- a/main/commons/src/main/java/org/cryptomator/common/Constants.java +++ b/main/commons/src/main/java/org/cryptomator/common/Constants.java @@ -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]; } diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index b273bb642..03aef5629 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -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 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 actionAfterUnlock() { diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 04a352a49..d68a67e0b 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -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; } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index 319272687..e36369c35 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -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,7 +37,8 @@ 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; @@ -45,13 +48,12 @@ 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 volumeProvider; @@ -100,32 +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 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); + } } - int usedFilenameLengthLimit; - var fileSystemCapabilityChecker = new FileSystemCapabilityChecker(); - if (flags.contains(FileSystemFlags.READONLY)) { - usedFilenameLengthLimit = Constants.MAX_CIPHERTEXT_NAME_LENGTH; - } else if (vaultSettings.filenameLengthLimit().get() == -1) { - LOG.debug("Determining file name length limitations..."); - usedFilenameLengthLimit = fileSystemCapabilityChecker.determineSupportedFileNameLength(getPath()); - vaultSettings.filenameLengthLimit().set(usedFilenameLengthLimit); - LOG.info("Storing file name length limit of {}", usedFilenameLengthLimit); - } else { - usedFilenameLengthLimit = vaultSettings.filenameLengthLimit().get(); + if (vaultSettings.maxCleartextFilenameLength().get() < UNLIMITED_FILENAME_LENGTH) { + LOG.warn("Limiting cleartext filename length on this device to {}.", vaultSettings.maxCleartextFilenameLength().get()); } - assert usedFilenameLengthLimit > 0; CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // - .withPassphrase(passphrase) // + .withKeyLoader(keyLoader) // .withFlags(flags) // - .withMasterkeyFilename(MASTERKEY_FILENAME) // - .withMaxPathLength(vaultSettings.filenameLengthLimit().get() + Constants.MAX_ADDITIONAL_PATH_LENGTH) // - .withMaxNameLength(usedFilenameLengthLimit) // + .withMaxCleartextNameLength(vaultSettings.maxCleartextFilenameLength().get()) // .build(); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); } @@ -142,20 +143,22 @@ 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(), this::lockOnVolumeExit); - } 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(); + } + } } private void lockOnVolumeExit(Throwable t) { @@ -324,6 +327,16 @@ public class Vault { return stats; } + public Optional 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}; } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java index 4b8ca71e8..d5038630a 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultListManager.java @@ -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,6 +21,7 @@ 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; @@ -27,6 +29,8 @@ import java.util.Optional; import java.util.ResourceBundle; 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 { @@ -51,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 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) { @@ -97,7 +100,7 @@ public class VaultListManager { compBuilder.initialVaultState(vaultState); } catch (IOException e) { LOG.warn("Failed to determine vault state for " + vaultSettings.path().get(), e); - compBuilder.initialVaultState(VaultState.Value.ERROR); + compBuilder.initialVaultState(ERROR); compBuilder.initialErrorCause(e); } return compBuilder.build().vault(); @@ -109,14 +112,14 @@ public class VaultListManager { return switch (previousState) { case LOCKED, NEEDS_MIGRATION, MISSING -> { try { - VaultState.Value determinedState = determineVaultState(vault.getPath()); + var determinedState = determineVaultState(vault.getPath()); state.set(determinedState); yield determinedState; } catch (IOException e) { LOG.warn("Failed to determine vault state for " + vault.getPath(), e); - state.set(VaultState.Value.ERROR); + state.set(ERROR); vault.setLastKnownException(e); - yield VaultState.Value.ERROR; + yield ERROR; } } case ERROR, UNLOCKED, PROCESSING -> previousState; @@ -124,13 +127,14 @@ public class VaultListManager { } private static VaultState.Value determineVaultState(Path pathToVault) throws IOException { - if (!CryptoFileSystemProvider.containsVault(pathToVault, MASTERKEY_FILENAME)) { + if (!Files.exists(pathToVault)) { return VaultState.Value.MISSING; - } else if (Migrators.get().needsMigration(pathToVault, MASTERKEY_FILENAME)) { - return VaultState.Value.NEEDS_MIGRATION; - } else { - return VaultState.Value.LOCKED; } + return switch (CryptoFileSystemProvider.checkDirStructureForVault(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME)) { + case VAULT -> VaultState.Value.LOCKED; + case UNRELATED -> VaultState.Value.MISSING; + case MAYBE_LEGACY -> Migrators.get().needsMigration(pathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) ? VaultState.Value.NEEDS_MIGRATION : VaultState.Value.MISSING; + }; } } diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java index b6e070310..19d577975 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java @@ -47,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(); diff --git a/main/pom.xml b/main/pom.xml index 7e720ee4b..64222a7a0 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -25,7 +25,7 @@ 16 - 1.9.14 + 2.0.0-rc2 1.0.0-beta2 1.0.0-beta2 1.0.0-beta2 @@ -37,17 +37,17 @@ 16 3.11 - 3.13.0 + 3.15.0 2.1.0 - 30.0-jre - 2.32 + 30.1.1-jre + 2.35.1 2.8.6 1.7.30 1.2.3 - 5.7.0 - 3.6.0 + 5.7.1 + 3.9.0 2.2 @@ -76,6 +76,12 @@ cryptofs ${cryptomator.cryptofs.version} + + + org.cryptomator + cryptolib + 2.0.0-rc1 + org.cryptomator fuse-nio-adapter diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java index a5398d51b..214ed19b0 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java @@ -81,7 +81,7 @@ public class ChooseExistingVaultController implements FxController { Vault newVault = vaultListManager.add(vaultPath.get()); vault.set(newVault); window.setScene(successScene.get()); - } catch (NoSuchFileException e) { + } catch (IOException e) { LOG.error("Failed to open existing vault.", e); errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index c4e226359..4be978956 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -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.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,6 +55,7 @@ import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; public class CreateNewVaultPasswordController implements FxController { private static final Logger LOG = LoggerFactory.getLogger(CreateNewVaultPasswordController.class); + private static final URI DEFAULT_KEY_ID = URI.create(MasterkeyFileLoadingStrategy.SCHEME + ":" + MASTERKEY_FILENAME); // TODO better place? private final Stage window; private final Lazy chooseLocationScene; @@ -64,6 +72,8 @@ public class CreateNewVaultPasswordController implements FxController { private final ResourceBundle resourceBundle; private final ObjectProperty password; private final ReadmeGenerator readmeGenerator; + private final SecureRandom csprng; + private final MasterkeyFileAccess masterkeyFileAccess; private final BooleanProperty processing; private final BooleanProperty readyToCreateVault; private final ObjectBinding createVaultButtonState; @@ -73,7 +83,7 @@ public class CreateNewVaultPasswordController implements FxController { public Toggle skipRecoveryKey; @Inject - CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator) { + CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { this.window = window; this.chooseLocationScene = chooseLocationScene; this.recoveryKeyScene = recoveryKeyScene; @@ -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); @@ -161,23 +173,34 @@ public class CreateNewVaultPasswordController implements FxController { } 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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index 52cfe2b81..b0ad164e2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -2,8 +2,10 @@ package org.cryptomator.ui.changepassword; import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; -import org.cryptomator.cryptofs.CryptoFileSystemProvider; +import org.cryptomator.cryptofs.common.MasterkeyBackupHelper; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.common.MasterkeyFileAccess; import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; @@ -23,8 +25,13 @@ import javafx.scene.control.CheckBox; import javafx.stage.Stage; import java.io.IOException; import java.nio.CharBuffer; -import java.util.Optional; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.security.SecureRandom; +import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX; import static org.cryptomator.common.Constants.MASTERKEY_FILENAME; @ChangePasswordScoped @@ -37,18 +44,22 @@ public class ChangePasswordController implements FxController { private final ObjectProperty newPassword; private final ErrorComponent.Builder errorComponent; private final KeychainManager keychain; + private final SecureRandom csprng; + private final MasterkeyFileAccess masterkeyFileAccess; public NiceSecurePasswordField oldPasswordField; public CheckBox finalConfirmationCheckbox; public Button finishButton; @Inject - public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain) { + public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { this.window = window; this.vault = vault; this.newPassword = newPassword; this.errorComponent = errorComponent; this.keychain = keychain; + this.csprng = csprng; + this.masterkeyFileAccess = masterkeyFileAccess; } @FXML @@ -67,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); - errorComponent.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(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 4d429ab7d..32dc94f14 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -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"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java new file mode 100644 index 000000000..51f9302c8 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoading.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.keyloading; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +public @interface KeyLoading { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java new file mode 100644 index 000000000..190bf0e1f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingComponent.java @@ -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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java new file mode 100644 index 000000000..15a5d27b8 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -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, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @KeyLoading + @KeyLoadingScoped + static Optional provideKeyId(@KeyLoading Vault vault) { + return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId); + } + + @Provides + @KeyLoading + @KeyLoadingScoped + static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional keyId, Map> 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(); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java new file mode 100644 index 000000000..5f4355f96 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingScoped.java @@ -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 { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java new file mode 100644 index 000000000..ed8ca0540 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingStrategy.java @@ -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. + *

+ * 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 true 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 true 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); + } + }; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java new file mode 100644 index 000000000..8eda41cd0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingFinisher.java @@ -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 storedPassword; + private final AtomicReference enteredPassword; + private final AtomicBoolean shouldSavePassword; + private final KeychainManager keychain; + + @Inject + MasterkeyFileLoadingFinisher(@KeyLoading Vault vault, @Named("savedPassword") Optional storedPassword, AtomicReference 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, ' '); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java new file mode 100644 index 000000000..d9413121c --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingModule.java @@ -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 providePasswordEntryLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @KeyLoadingScoped + static UserInteractionLock provideMasterkeyFileProvisionLock() { + return new UserInteractionLock<>(null); + } + + @Provides + @Named("savedPassword") + @KeyLoadingScoped + static Optional 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 provideUserProvidedMasterkeyPath() { + return new AtomicReference<>(); + } + + @Provides + @KeyLoadingScoped + static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { + return new AtomicReference<>(storedPassword.orElse(null)); + } + + @Provides + @Named("savePassword") + @KeyLoadingScoped + static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional 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); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java new file mode 100644 index 000000000..464671929 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -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 passphraseEntryScene; + private final Lazy selectMasterkeyFileScene; + private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock masterkeyFileProvisionLock; + private final AtomicReference password; + private final AtomicReference 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 passphraseEntryScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, AtomicReference password, AtomicReference 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(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java similarity index 88% rename from main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java rename to main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java index 674b16142..d4356be85 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/PassphraseEntryController.java @@ -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 password; private final AtomicBoolean savePassword; private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock passwordEntryLock; private final ForgetPasswordComponent.Builder forgetPassword; private final KeychainManager keychain; private final ObjectBinding unlockButtonContentDisplay; @@ -66,7 +69,7 @@ public class UnlockController implements FxController { public Animation unlockAnimation; @Inject - public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain) { + public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock 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(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java new file mode 100644 index 000000000..39be2b36e --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/SelectMasterkeyFileController.java @@ -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 masterkeyPath; + private final UserInteractionLock masterkeyFileProvisionLock; + private final ResourceBundle resourceBundle; + + @Inject + public SelectMasterkeyFileController(@KeyLoading Stage window, AtomicReference masterkeyPath, UserInteractionLock 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); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java index 0ab11d374..49ef79e83 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java +++ b/main/ui/src/main/java/org/cryptomator/ui/launcher/AppLaunchEventHandler.java @@ -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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java index 65c3a8fcf..392671246 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java @@ -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); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java index 2914bc4e1..a9b0085d8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -44,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 { @@ -116,8 +117,8 @@ 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()); diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 902f2cd50..104794604 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -2,6 +2,7 @@ package org.cryptomator.ui.recoverykey; import dagger.Lazy; import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; @@ -80,7 +81,7 @@ public class RecoveryKeyCreationController implements FxController { } @Override - protected String call() throws IOException { + protected String call() throws IOException, CryptoException { return recoveryKeyFactory.createRecoveryKey(vault.getPath(), passwordField.getCharacters()); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java index 15d4e651e..c078c718b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java @@ -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 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); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java new file mode 100644 index 000000000..c795eb5ea --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockCancelledException.java @@ -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); + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java index 1850953c9..fd85db988 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockInvalidMountPointController.java @@ -1,15 +1,11 @@ package org.cryptomator.ui.unlock; -import dagger.Lazy; import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; -import org.cryptomator.ui.common.FxmlFile; -import org.cryptomator.ui.common.FxmlScene; import javax.inject.Inject; import javafx.fxml.FXML; -import javafx.scene.Scene; import javafx.stage.Stage; //At the current point in time only the CustomMountPointChooser may cause this window to be shown. @@ -17,19 +13,17 @@ import javafx.stage.Stage; public class UnlockInvalidMountPointController implements FxController { private final Stage window; - private final Lazy unlockScene; private final Vault vault; @Inject - UnlockInvalidMountPointController(@UnlockWindow Stage window, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @UnlockWindow Vault vault) { + UnlockInvalidMountPointController(@UnlockWindow Stage window, @UnlockWindow Vault vault) { this.window = window; - this.unlockScene = unlockScene; this.vault = vault; } @FXML - public void back() { - window.setScene(unlockScene.get()); + public void close() { + window.close(); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index 2ed17fdff..3c1267e65 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -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 providePasswordEntryLock() { - return new UserInteractionLock<>(null); - } - - @Provides - @Named("savedPassword") - @UnlockScoped - static Optional 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 providePassword(@Named("savedPassword") Optional storedPassword) { - return new AtomicReference(storedPassword.orElse(null)); - } - - @Provides - @Named("savePassword") - @UnlockScoped - static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional storedPassword) { - return new AtomicBoolean(storedPassword.isPresent()); - } - @Provides @UnlockWindow @UnlockScoped @@ -99,10 +51,10 @@ abstract class UnlockModule { } @Provides - @FxmlScene(FxmlFile.UNLOCK) + @UnlockWindow @UnlockScoped - static Scene provideUnlockScene(@UnlockWindow FxmlLoaderFactory fxmlLoaders) { - return fxmlLoaders.createScene(FxmlFile.UNLOCK); + static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @UnlockWindow Vault vault, @UnlockWindow Stage window) { + return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); } @Provides @@ -121,11 +73,6 @@ abstract class UnlockModule { // ------------------ - @Binds - @IntoMap - @FxControllerKey(UnlockController.class) - abstract FxController bindUnlockController(UnlockController controller); - @Binds @IntoMap @FxControllerKey(UnlockSuccessController.class) diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java index 2815ad3c4..36c3eacf9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -1,40 +1,31 @@ package org.cryptomator.ui.unlock; import dagger.Lazy; -import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.mountpoint.InvalidMountPointException; import org.cryptomator.common.vaults.MountPointRequirement; import org.cryptomator.common.vaults.Vault; import org.cryptomator.common.vaults.VaultState; import org.cryptomator.common.vaults.Volume.VolumeException; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.integrations.keychain.KeychainAccessException; -import org.cryptomator.ui.common.Animations; +import org.cryptomator.cryptolib.api.CryptoException; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; -import org.cryptomator.ui.common.UserInteractionLock; import org.cryptomator.ui.common.VaultService; -import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry; +import org.cryptomator.ui.keyloading.KeyLoadingComponent; +import org.cryptomator.ui.keyloading.KeyLoadingStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.scene.Scene; import javafx.stage.Stage; -import javafx.stage.Window; import java.io.IOException; -import java.nio.CharBuffer; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.NotDirectoryException; -import java.util.Arrays; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; /** * A multi-step task that consists of background activities as well as user interaction. @@ -49,88 +40,47 @@ public class UnlockWorkflow extends Task { private final Stage window; private final Vault vault; private final VaultService vaultService; - private final AtomicReference password; - private final AtomicBoolean savePassword; - private final Optional savedPassword; - private final UserInteractionLock passwordEntryLock; - private final KeychainManager keychain; - private final Lazy unlockScene; private final Lazy successScene; private final Lazy invalidMountPointScene; private final ErrorComponent.Builder errorComponent; + private final KeyLoadingStrategy keyLoadingStrategy; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) { + UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent, @UnlockWindow KeyLoadingStrategy keyLoadingStrategy) { this.window = window; this.vault = vault; this.vaultService = vaultService; - this.password = password; - this.savePassword = savePassword; - this.savedPassword = savedPassword; - this.passwordEntryLock = passwordEntryLock; - this.keychain = keychain; - this.unlockScene = unlockScene; this.successScene = successScene; this.invalidMountPointScene = invalidMountPointScene; this.errorComponent = errorComponent; + this.keyLoadingStrategy = keyLoadingStrategy; } @Override - protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException { + protected Boolean call() throws InterruptedException, IOException, VolumeException, InvalidMountPointException, CryptoException { try { - if (attemptUnlock()) { - if (savePassword.get()) { - savePasswordToSystemkeychain(); //savePassword will be wiped on method return, so it must be set here - } - 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 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); } } @@ -171,12 +121,6 @@ public class UnlockWorkflow extends Task { errorComponent.cause(e).window(window).build().showErrorScene(); } - private void wipePassword(char[] pw) { - if (pw != null) { - Arrays.fill(pw, ' '); - } - } - @Override protected void succeeded() { LOG.info("Unlock of '{}' succeeded.", vault.getDisplayName()); diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock_enter_password.fxml similarity index 97% rename from main/ui/src/main/resources/fxml/unlock.fxml rename to main/ui/src/main/resources/fxml/unlock_enter_password.fxml index 54adc5279..5fb55dd01 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock_enter_password.fxml @@ -14,7 +14,7 @@ - + -