mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-09-22 07:54:19 +00:00
Refactor Keyloading to use only the required info
* new record KeyLoadingRef(config, name) * masterkey vaults still need Vault.java * hub relies on KeyLoadingRef Signed-off-by: Armin Schrenk <armin.schrenk@skymatic.de>
This commit is contained in:
@@ -33,7 +33,7 @@
|
||||
<nonModularGroupIds>org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents</nonModularGroupIds>
|
||||
|
||||
<!-- cryptomator dependencies -->
|
||||
<cryptomator.cryptofs.version>2.10.0</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptofs.version>2.11.0-SNAPSHOT</cryptomator.cryptofs.version>
|
||||
<cryptomator.cryptolib.version>2.2.2</cryptomator.cryptolib.version>
|
||||
<cryptomator.integrations.version>1.9.0</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.6.1</cryptomator.integrations.win.version>
|
||||
|
||||
@@ -18,15 +18,16 @@ import java.util.UUID;
|
||||
/**
|
||||
* Requests opening a Hub vault from an {@code org.cryptomator://vault/open#vaultConfig=…} deeplink.
|
||||
* <p>
|
||||
* The single parameter is the vault's {@code vault.cryptomator}, a compact JWS embedded verbatim - a compact JWS
|
||||
* consists only of characters that are unreserved in a URI fragment, so it needs no further encoding. It is carried in
|
||||
* the fragment part so that it stays out of the logs of any web page that mints such a link.
|
||||
* The single parameter is the vault's {@code vault.cryptomator}, a compact JWS embedded verbatim, carried in the fragment part.
|
||||
* <p>
|
||||
* The config is read <em>unverified</em>: its signature is keyed on the masterkey, which is only obtainable from Hub
|
||||
* later on. Everything consumed before that point is therefore validated here rather than trusted.
|
||||
* Notes:
|
||||
* <ul>
|
||||
* <li> The config is read <em>unverified</em>, since its signature is keyed on the masterkey, which is only obtainable from Hu later on. </li>
|
||||
* <li> The deeplink parsing makes a strict validation due to untrusted input</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param vaultConfig the decoded, unverified vault config
|
||||
* @param vaultId the vault's id within its Hub instance, taken from the config's key id
|
||||
* @param vaultId the vault's id within its Hub instance, taken from the config's {@code jti} claim
|
||||
*/
|
||||
public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, UUID vaultId) implements AppLaunchEvent {
|
||||
|
||||
@@ -59,7 +60,7 @@ public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, U
|
||||
}
|
||||
var vaultConfig = decode(token);
|
||||
requireHubVault(vaultConfig);
|
||||
var vaultId = extractVaultId(vaultConfig.getKeyId());
|
||||
var vaultId = extractVaultId(vaultConfig);
|
||||
|
||||
var leftoverParams = params.keySet().stream().filter(k -> !k.equals(PARAM_VAULT_CONFIG)).toList();
|
||||
if (!leftoverParams.isEmpty()) {
|
||||
@@ -123,8 +124,8 @@ public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, U
|
||||
if (!uri.isAbsolute() || uri.getHost() == null) {
|
||||
throw new IllegalArgumentException("Vault config's hub " + field + " is not an absolute url with a host, but was '" + uri + "'.");
|
||||
}
|
||||
// Whether an http host is acceptable (it is, for local development) is decided by CheckHostTrustController, the
|
||||
// authority on host trust. Here we only ensure the endpoint is shaped like something that decision can be made on.
|
||||
// Whether an http host is acceptable (it is, for local development) is decided by CheckHostTrustController
|
||||
// Here we only ensure the endpoint is shaped like something that decision can be made on.
|
||||
var scheme = uri.getScheme();
|
||||
if (!"https".equalsIgnoreCase(scheme) && !"http".equalsIgnoreCase(scheme)) {
|
||||
throw new IllegalArgumentException("Vault config's hub " + field + " is neither http nor https, but was '" + uri + "'.");
|
||||
@@ -132,24 +133,33 @@ public record OpenHubVaultEvent(VaultConfig.UnverifiedVaultConfig vaultConfig, U
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the vault id from the trailing path segment of a key id such as
|
||||
* {@code hub+https://hub.example.com/api/vaults/<vaultId>}.
|
||||
* Reads the vault id from the config's {@code jti} claim.
|
||||
* <p>
|
||||
* Requiring a UUID matters beyond well-formedness: the id is interpolated into the {@code api/vaults/{vaultId}/…}
|
||||
* request path, so it must not be able to introduce a path segment. Re-serializing the parsed {@link UUID} rather
|
||||
* than passing the raw segment on keeps that guarantee.
|
||||
* request path, so it must not be able to introduce a path segment. A {@code jti} is an arbitrary string, so parsing
|
||||
* it as a {@link UUID} and passing that on - rather than the raw claim - is what keeps that guarantee.
|
||||
* <p>
|
||||
* Hub writes the same id into the key id's trailing path segment, and the two have always agreed, so a config where
|
||||
* they differ is forged or broken and is rejected.
|
||||
*/
|
||||
private static UUID extractVaultId(URI keyId) {
|
||||
var path = keyId.getPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
throw new IllegalArgumentException("Vault config's key id contains no vault id, but was '" + keyId + "'.");
|
||||
private static UUID extractVaultId(VaultConfig.UnverifiedVaultConfig vaultConfig) {
|
||||
var allegedVaultId = vaultConfig.allegedVaultId();
|
||||
if (allegedVaultId == null || allegedVaultId.isBlank()) {
|
||||
throw new IllegalArgumentException("Vault config declares no vault id.");
|
||||
}
|
||||
var lastSegment = path.substring(path.lastIndexOf('/') + 1);
|
||||
UUID vaultId;
|
||||
try {
|
||||
return UUID.fromString(lastSegment);
|
||||
vaultId = UUID.fromString(allegedVaultId);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Vault config's key id does not end in a vault id, but was '" + keyId + "'.", e);
|
||||
throw new IllegalArgumentException("Vault config's vault id is not a uuid, but was '" + allegedVaultId + "'.", e);
|
||||
}
|
||||
var keyId = vaultConfig.getKeyId();
|
||||
var path = keyId.getPath();
|
||||
var lastSegment = path == null ? "" : path.substring(path.lastIndexOf('/') + 1);
|
||||
if (!vaultId.toString().equalsIgnoreCase(lastSegment)) {
|
||||
throw new IllegalArgumentException("Vault config's vault id '" + vaultId + "' does not match its key id '" + keyId + "'.");
|
||||
}
|
||||
return vaultId;
|
||||
}
|
||||
|
||||
private static Map<String, String> parseParams(String rawParams) {
|
||||
|
||||
@@ -62,9 +62,9 @@ public class Dialogs {
|
||||
.setOkButtonKey(BUTTON_KEY_CLOSE);
|
||||
}
|
||||
|
||||
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, Vault vault) {
|
||||
public SimpleDialog.Builder prepareHubVaultArchived(Stage window, String vaultDisplayName) {
|
||||
return createDialogBuilder().setOwner(window) //
|
||||
.setTitleKey("unlock.title", vault.getDisplayName()) //
|
||||
.setTitleKey("unlock.title", vaultDisplayName) //
|
||||
.setMessageKey("hub.archived.message") //
|
||||
.setDescriptionKey("hub.archived.description") //
|
||||
.setIcon(FontAwesome5Icon.BAN)//
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingRef;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
|
||||
import javax.inject.Named;
|
||||
@@ -64,7 +65,11 @@ abstract class HealthCheckModule {
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
|
||||
return compFactory.create(vault, window).keyloadingStrategy();
|
||||
try {
|
||||
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
|
||||
} catch (IOException e) {
|
||||
return KeyLoadingStrategy.failed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading;
|
||||
|
||||
import dagger.BindsInstance;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
|
||||
import javafx.stage.Stage;
|
||||
@@ -16,7 +17,14 @@ public interface KeyLoadingComponent {
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
KeyLoadingComponent create(@BindsInstance @KeyLoading Vault vault, @KeyLoading @BindsInstance Stage window);
|
||||
/**
|
||||
* @param vaultRef the {@link KeyLoadingRef} containing the info to load the key
|
||||
* @param vault the local vault, or {@code null} if it is not set up on this machine.
|
||||
* @param window the window to show the key loading scenes in
|
||||
*/
|
||||
KeyLoadingComponent create(@BindsInstance @KeyLoading KeyLoadingRef vaultRef, //
|
||||
@BindsInstance @KeyLoading @Nullable Vault vault, //
|
||||
@BindsInstance @KeyLoading Stage window);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.cryptomator.ui.keyloading;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.DefaultSceneFactory;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
@@ -10,7 +9,6 @@ import org.cryptomator.ui.keyloading.hub.HubKeyLoadingModule;
|
||||
import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule;
|
||||
|
||||
import javax.inject.Provider;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@@ -27,14 +25,10 @@ abstract class KeyLoadingModule {
|
||||
@Provides
|
||||
@KeyLoading
|
||||
@KeyLoadingScoped
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading Vault vault, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||
try {
|
||||
String scheme = vault.getVaultConfigCache().get().getKeyId().getScheme();
|
||||
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||
} catch (IOException e) {
|
||||
return KeyLoadingStrategy.failed(e);
|
||||
}
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(@KeyLoading KeyLoadingRef vaultRef, Map<String, Provider<KeyLoadingStrategy>> strategies) {
|
||||
String scheme = vaultRef.keyId().getScheme();
|
||||
var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme));
|
||||
return strategies.getOrDefault(scheme, () -> fallback).get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package org.cryptomator.ui.keyloading;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Identifies the vault a key is being loaded for, independently of whether that vault exists on this machine.
|
||||
* <p>
|
||||
* Key loading needs the (unverified) vault config selecting the strategy and addresses
|
||||
* the vault within the strategy, and the display name titles the windows.
|
||||
* <p>
|
||||
* The config is <em>unverified</em>: its signature is keyed on the masterkey, which is exactly what key loading is
|
||||
* about to obtain.
|
||||
*
|
||||
* @param vaultConfig the vault's unverified config
|
||||
* @param displayName the vault's name, as shown to the user
|
||||
*/
|
||||
public record KeyLoadingRef(VaultConfig.UnverifiedVaultConfig vaultConfig, String displayName) {
|
||||
|
||||
/**
|
||||
* Describes a vault that is already set up on this machine.
|
||||
*
|
||||
* @param vault the vault to load a key for
|
||||
* @throws IOException if the vault's config cannot be read
|
||||
*/
|
||||
public static KeyLoadingRef forVault(Vault vault) throws IOException {
|
||||
return new KeyLoadingRef(vault.getVaultConfigCache().get(), vault.getDisplayName());
|
||||
}
|
||||
|
||||
/**
|
||||
* The key id, whose scheme selects the key loading strategy.
|
||||
*/
|
||||
public URI keyId() {
|
||||
return vaultConfig.getKeyId();
|
||||
}
|
||||
|
||||
/**
|
||||
* The vault's id, i.e. how the vault is addressed within its Hub instance.
|
||||
* <p>
|
||||
* Read from the config's {@code jti} claim, which is the authoritative source: the key id carries the same id in its
|
||||
* trailing path segment, but only its <em>scheme</em> is a source of truth here.
|
||||
*/
|
||||
public String vaultId() {
|
||||
return vaultConfig.allegedVaultId();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import dagger.multibindings.StringKey;
|
||||
import org.cryptomator.common.settings.DeviceKey;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptolib.common.MessageDigestSupplier;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxControllerKey;
|
||||
@@ -15,13 +14,12 @@ import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingRef;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javafx.scene.Scene;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -32,19 +30,15 @@ public abstract class HubKeyLoadingModule {
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
static HubConfig provideHubConfig(@KeyLoading Vault vault) {
|
||||
try {
|
||||
return vault.getVaultConfigCache().get().getHeader("hub", HubConfig.class);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
static HubConfig provideHubConfig(@KeyLoading KeyLoadingRef vaultRef) {
|
||||
return vaultRef.vaultConfig().getHeader("hub", HubConfig.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@KeyLoadingScoped
|
||||
@Named("windowTitle")
|
||||
static String provideWindowTitle(@KeyLoading Vault vault, ResourceBundle resourceBundle) {
|
||||
return String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName());
|
||||
static String provideWindowTitle(@KeyLoading KeyLoadingRef vaultRef, ResourceBundle resourceBundle) {
|
||||
return String.format(resourceBundle.getString("unlock.title"), vaultRef.displayName());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import com.google.common.base.Preconditions;
|
||||
import com.nimbusds.jose.JWEObject;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.Constants;
|
||||
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 org.cryptomator.ui.dialogs.Dialogs;
|
||||
import org.cryptomator.ui.keyloading.KeyLoading;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingRef;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingScoped;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -60,10 +60,10 @@ public class ReceiveKeyController implements FxController {
|
||||
private final Lazy<Scene> invalidLicenseScene;
|
||||
private final HttpClient httpClient;
|
||||
private final Dialogs dialogs;
|
||||
private final Vault vault;
|
||||
private final KeyLoadingRef vaultRef;
|
||||
|
||||
@Inject
|
||||
public ReceiveKeyController(@KeyLoading Vault vault, //
|
||||
public ReceiveKeyController(@KeyLoading KeyLoadingRef vaultRef, //
|
||||
ExecutorService executor, //
|
||||
@KeyLoading Stage window, //
|
||||
HubConfig hubConfig, //
|
||||
@@ -79,7 +79,7 @@ public class ReceiveKeyController implements FxController {
|
||||
Dialogs dialogs) {
|
||||
this.window = window;
|
||||
this.hubConfig = hubConfig;
|
||||
this.vaultId = extractVaultId(vault.getVaultConfigCache().getUnchecked().getKeyId()); // TODO: access vault config's JTI directly (requires changes in cryptofs)
|
||||
this.vaultId = vaultRef.vaultId();
|
||||
this.deviceId = deviceId;
|
||||
this.bearerToken = Objects.requireNonNull(tokenRef.get());
|
||||
this.fsOwnerId = fsOwnerId;
|
||||
@@ -92,7 +92,7 @@ public class ReceiveKeyController implements FxController {
|
||||
this.window.addEventHandler(WindowEvent.WINDOW_HIDING, this::windowClosed);
|
||||
this.httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).executor(executor).build();
|
||||
this.dialogs = dialogs;
|
||||
this.vault = vault;
|
||||
this.vaultRef = vaultRef;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -313,7 +313,7 @@ public class ReceiveKeyController implements FxController {
|
||||
|
||||
private void accessGoneVaultArchived() {
|
||||
window.close();
|
||||
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vault).build().showAndWait();
|
||||
dialogs.prepareHubVaultArchived((Stage)window.getOwner(), vaultRef.displayName()).build().showAndWait();
|
||||
}
|
||||
|
||||
private void accountInitializationRequired() {
|
||||
@@ -343,12 +343,6 @@ public class ReceiveKeyController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
private static String extractVaultId(URI vaultKeyUri) {
|
||||
assert vaultKeyUri.getScheme().startsWith(HubKeyLoadingStrategy.SCHEME_PREFIX);
|
||||
var path = vaultKeyUri.getPath();
|
||||
return path.substring(path.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
private record UserDto(@JsonProperty(value = "name", required = true) String name) {}
|
||||
|
||||
|
||||
+4
-2
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.recovery.RecoveryActionType;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
@@ -18,6 +19,7 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@@ -41,12 +43,12 @@ public class ChooseMasterkeyFileController implements FxController {
|
||||
|
||||
@Inject
|
||||
public ChooseMasterkeyFileController(@KeyLoading Stage window, //
|
||||
@KeyLoading Vault vault, //
|
||||
@KeyLoading @Nullable Vault vault, //
|
||||
CompletableFuture<Path> result, //
|
||||
RecoveryKeyComponent.Factory recoveryKeyWindow, //
|
||||
ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
|
||||
this.result = result;
|
||||
this.recoveryKeyWindow = recoveryKeyWindow;
|
||||
this.resourceBundle = resourceBundle;
|
||||
|
||||
+10
-2
@@ -5,6 +5,7 @@ import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import dagger.multibindings.IntoMap;
|
||||
import dagger.multibindings.StringKey;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.integrations.keychain.KeychainAccessException;
|
||||
@@ -15,20 +16,27 @@ import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Named;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Module(subcomponents = {ForgetPasswordComponent.class, PassphraseEntryComponent.class, ChooseMasterkeyFileComponent.class})
|
||||
public interface MasterkeyFileLoadingModule {
|
||||
|
||||
/**
|
||||
* Key loading may run for a yet-to-setup vault (i.e. deeplink with only a config) - Masterkey loading requires in the current implementation
|
||||
* an already setup vault.
|
||||
*/
|
||||
String NO_LOCAL_VAULT = "masterkey file loading requires a local vault";
|
||||
|
||||
@Provides
|
||||
@Named("savedPassword")
|
||||
@KeyLoadingScoped
|
||||
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading Vault vault) {
|
||||
static Optional<char[]> provideStoredPassword(KeychainManager keychain, @KeyLoading @Nullable Vault vault) {
|
||||
if (!keychain.isSupported() || keychain.isLocked()) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
try {
|
||||
return Optional.ofNullable(keychain.loadPassphrase(vault.getId()));
|
||||
return Optional.ofNullable(keychain.loadPassphrase(Objects.requireNonNull(vault, NO_LOCAL_VAULT).getId()));
|
||||
} catch (KeychainAccessException e) {
|
||||
LoggerFactory.getLogger(MasterkeyFileLoadingModule.class).error("Failed to load entry from system keychain.", e);
|
||||
return Optional.empty();
|
||||
|
||||
+4
-2
@@ -2,6 +2,7 @@ package org.cryptomator.ui.keyloading.masterkeyfile;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.common.Constants;
|
||||
import org.cryptomator.common.Nullable;
|
||||
import org.cryptomator.common.Passphrase;
|
||||
import org.cryptomator.common.keychain.KeychainManager;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
@@ -25,6 +26,7 @@ import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CancellationException;
|
||||
@@ -48,8 +50,8 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
|
||||
private boolean wrongPassphrase;
|
||||
|
||||
@Inject
|
||||
public MasterkeyFileLoadingStrategy(@KeyLoading Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
|
||||
this.vault = vault;
|
||||
public MasterkeyFileLoadingStrategy(@KeyLoading @Nullable Vault vault, MasterkeyFileAccess masterkeyFileAccess, @KeyLoading Stage window, @Named("savedPassword") Optional<char[]> savedPassphrase, PassphraseEntryComponent.Builder passphraseEntry, ChooseMasterkeyFileComponent.Builder masterkeyFileChoice, KeychainManager keychain, ResourceBundle resourceBundle) {
|
||||
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
|
||||
this.masterkeyFileAccess = masterkeyFileAccess;
|
||||
this.window = window;
|
||||
this.passphraseEntry = passphraseEntry;
|
||||
|
||||
+3
-2
@@ -35,6 +35,7 @@ import javafx.scene.transform.Translate;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
import javafx.util.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@@ -66,9 +67,9 @@ public class PassphraseEntryController implements FxController {
|
||||
public Animation unlockAnimation;
|
||||
|
||||
@Inject
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
|
||||
public PassphraseEntryController(@KeyLoading Stage window, @KeyLoading @Nullable Vault vault, CompletableFuture<PassphraseEntryResult> result, @Nullable @Named("savedPassword") Passphrase savedPassword, ForgetPasswordComponent.Builder forgetPassword, KeychainManager keychain, ExecutorService backgroundExecutorService) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.vault = Objects.requireNonNull(vault, MasterkeyFileLoadingModule.NO_LOCAL_VAULT);
|
||||
this.result = result;
|
||||
this.savedPassword = savedPassword;
|
||||
this.forgetPassword = forgetPassword;
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingRef;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -25,6 +26,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@@ -58,7 +60,11 @@ abstract class UnlockModule {
|
||||
@UnlockWindow
|
||||
@UnlockScoped
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Factory compFactory, @UnlockWindow Vault vault, @UnlockWindow Stage window) {
|
||||
return compFactory.create(vault, window).keyloadingStrategy();
|
||||
try {
|
||||
return compFactory.create(KeyLoadingRef.forVault(vault), vault, window).keyloadingStrategy();
|
||||
} catch (IOException e) {
|
||||
return KeyLoadingStrategy.failed(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -73,21 +73,32 @@ public class OpenHubVaultEventTest {
|
||||
@Test
|
||||
@DisplayName("a config without a hub header is rejected")
|
||||
public void testNoHubHeader() {
|
||||
var token = vaultConfig(KEY_ID, null);
|
||||
var token = vaultConfig(KEY_ID, VAULT_ID, null);
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, //
|
||||
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("a key id whose last segment is not a vault id is rejected")
|
||||
@DisplayName("a vault id that is not a uuid is rejected")
|
||||
@ValueSource(strings = { //
|
||||
"hub+https://hub.example.com/api/vaults/not-a-uuid", //
|
||||
"hub+https://hub.example.com/api/vaults/", // empty segment
|
||||
"hub+https://hub.example.com" // no path at all
|
||||
"not-a-uuid", //
|
||||
"", // absent
|
||||
"../../evil" // must never reach the api/vaults/{vaultId}/... request path
|
||||
})
|
||||
public void testInvalidVaultId(String keyId) {
|
||||
var token = hubVaultConfig(keyId);
|
||||
public void testInvalidVaultId(String vaultId) {
|
||||
var token = vaultConfig(KEY_ID, vaultId, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, //
|
||||
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("a vault id disagreeing with the key id is rejected")
|
||||
public void testVaultIdMismatch() {
|
||||
// Hub writes the same id into both, so a config where they differ is forged or broken
|
||||
var token = vaultConfig(KEY_ID, "11111111-2222-3333-4444-555555555555", //
|
||||
hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, //
|
||||
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
|
||||
@@ -101,7 +112,7 @@ public class OpenHubVaultEventTest {
|
||||
"https:///api" // no host
|
||||
})
|
||||
public void testUnusableApiBaseUrl(String apiBaseUrl) {
|
||||
var token = vaultConfig(KEY_ID, hubHeader(apiBaseUrl, "https://login.example.com/auth"));
|
||||
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader(apiBaseUrl, "https://login.example.com/auth"));
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, //
|
||||
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
|
||||
@@ -110,7 +121,7 @@ public class OpenHubVaultEventTest {
|
||||
@Test
|
||||
@DisplayName("an unusable auth endpoint is rejected")
|
||||
public void testUnusableAuthEndpoint() {
|
||||
var token = vaultConfig(KEY_ID, hubHeader("https://hub.example.com/api", "not a url"));
|
||||
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("https://hub.example.com/api", "not a url"));
|
||||
|
||||
Assertions.assertThrows(IllegalArgumentException.class, //
|
||||
() -> OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)));
|
||||
@@ -119,7 +130,7 @@ public class OpenHubVaultEventTest {
|
||||
@Test
|
||||
@DisplayName("an http endpoint is accepted here, host trust decides later")
|
||||
public void testHttpEndpointAccepted() {
|
||||
var token = vaultConfig(KEY_ID, hubHeader("http://localhost:8080/api", "http://localhost:8080/auth"));
|
||||
var token = vaultConfig(KEY_ID, VAULT_ID, hubHeader("http://localhost:8080/api", "http://localhost:8080/auth"));
|
||||
|
||||
Assertions.assertTrue(OpenHubVaultEvent.tryParse(URI.create("org.cryptomator://vault/open#vaultConfig=" + token)).isPresent());
|
||||
}
|
||||
@@ -180,14 +191,14 @@ public class OpenHubVaultEventTest {
|
||||
}
|
||||
|
||||
private static String hubVaultConfig(String keyId) {
|
||||
return vaultConfig(keyId, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
|
||||
return vaultConfig(keyId, VAULT_ID, hubHeader("https://hub.example.com/api", "https://login.example.com/auth"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a vault config token. Its signature is keyed on the masterkey, which the deeplink never carries, so a dummy
|
||||
* signature is exactly what the parser operates on.
|
||||
*/
|
||||
private static String vaultConfig(String keyId, String extraHeaderFields) {
|
||||
private static String vaultConfig(String keyId, String vaultId, String extraHeaderFields) {
|
||||
var header = """
|
||||
{ "kid":"%s",\
|
||||
"typ":"JWT",\
|
||||
@@ -195,10 +206,11 @@ public class OpenHubVaultEventTest {
|
||||
%s
|
||||
}""".formatted(keyId, extraHeaderFields == null ? "" : extraHeaderFields);
|
||||
var payload = """
|
||||
{ "format":8,\
|
||||
{ "jti":"%s",\
|
||||
"format":8,\
|
||||
"cipherCombo":"SIV_GCM",\
|
||||
"shorteningThreshold":220\
|
||||
}""";
|
||||
}""".formatted(vaultId);
|
||||
var encoder = Base64.getUrlEncoder().withoutPadding();
|
||||
return encoder.encodeToString(header.getBytes(StandardCharsets.UTF_8)) //
|
||||
+ "." + encoder.encodeToString(payload.getBytes(StandardCharsets.UTF_8)) //
|
||||
|
||||
Reference in New Issue
Block a user