From a86c3bafaf7e0082deb93a3355dfc16248bdd077 Mon Sep 17 00:00:00 2001 From: crschnick Date: Thu, 6 Feb 2025 15:45:17 +0000 Subject: [PATCH] Vault fixes --- .../io/xpipe/app/comp/base/ModalButton.java | 6 +- .../io/xpipe/app/core/mode/OperationMode.java | 3 +- .../io/xpipe/app/prefs/VaultCategory.java | 44 ++++++++------ .../io/xpipe/app/storage/DataStorage.java | 12 +++- .../io/xpipe/app/storage/DataStorageNode.java | 59 +++++++++++-------- .../xpipe/app/storage/DataStorageSecret.java | 35 ++++++----- .../io/xpipe/app/storage/DataStoreEntry.java | 4 +- .../app/storage/ImpersistentStorage.java | 6 ++ .../io/xpipe/app/storage/StandardStorage.java | 13 ++-- .../io/xpipe/app/util/EncryptionToken.java | 22 +++---- lang/strings/translations_en.properties | 15 +++-- 11 files changed, 129 insertions(+), 90 deletions(-) diff --git a/app/src/main/java/io/xpipe/app/comp/base/ModalButton.java b/app/src/main/java/io/xpipe/app/comp/base/ModalButton.java index 9a8b35b67..073d088ad 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ModalButton.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ModalButton.java @@ -40,7 +40,11 @@ public class ModalButton { } public static ModalButton cancel() { - return new ModalButton("cancel", null, true, false); + return cancel(null); + } + + public static ModalButton cancel(Runnable action) { + return new ModalButton("cancel", action, true, false); } public static ModalButton skip() { diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java index 7d5a7c72d..0ed719bad 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java @@ -250,7 +250,8 @@ public abstract class OperationMode { var loc = AppProperties.get().isDevelopmentEnvironment() ? XPipeInstallation.getLocalDefaultInstallationBasePath() : XPipeInstallation.getCurrentInstallationBasePath().toString(); - var exec = XPipeInstallation.createExternalAsyncLaunchCommand(loc, XPipeDaemonMode.GUI, "", true); + var dataDir = AppProperties.get().getDataDir(); + var exec = XPipeInstallation.createExternalAsyncLaunchCommand(loc, XPipeDaemonMode.GUI, "-Dio.xpipe.app.acceptEula=true -Dio.xpipe.app.dataDir=\"" + dataDir + "\"", true); LocalShell.getShell().executeSimpleCommand(exec); } diff --git a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java index cbe017c5b..788ab79a4 100644 --- a/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/VaultCategory.java @@ -1,8 +1,12 @@ package io.xpipe.app.prefs; +import atlantafx.base.layout.ModalBox; import io.xpipe.app.comp.Comp; import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.comp.base.ModalButton; +import io.xpipe.app.comp.base.ModalOverlay; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.window.AppDialog; import io.xpipe.app.core.window.AppWindowHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorageSyncHandler; @@ -30,36 +34,43 @@ public class VaultCategory extends AppPrefsCategory { var encryptVault = new SimpleBooleanProperty(prefs.encryptAllVaultData().get()); encryptVault.addListener((observable, oldValue, newValue) -> { - if (!newValue - && !AppWindowHelper.showConfirmationAlert( - "confirmVaultUnencryptTitle", - "confirmVaultUnencryptHeader", - "confirmVaultUnencryptContent")) { - Platform.runLater(() -> { - encryptVault.set(true); - }); - return; + if (!newValue) { + var modal = ModalOverlay.of("confirmVaultUnencryptTitle", AppDialog.dialogTextKey("confirmVaultUnencryptContent")); + modal.addButton(ModalButton.cancel(() -> { + Platform.runLater(() -> { + encryptVault.set(true); + }); + })); + modal.addButton(ModalButton.ok(() -> { + prefs.encryptAllVaultData.setValue(false); + })); + modal.showAndWait(); + } else { + prefs.encryptAllVaultData.setValue(true); } - - prefs.encryptAllVaultData.setValue(newValue); }); + var uh = DataStorageUserHandler.getInstance(); + var vaultTypeKey = uh.getUserCount() == 0 ? "vaultTypeDefault" : uh.getUserCount() == 1 ? (uh.getActiveUser() != null && uh.getActiveUser().equals("legacy") ? + "vaultTypeLegacy" : "vaultTypePersonal") : "vaultTypeTeam"; + builder.addTitle("vaultUsers") .sub(new OptionsBuilder() - .nameAndDescription("personalVault") + .name("vaultType") + .description(vaultTypeKey) .addComp(Comp.empty()) - .hide(new SimpleBooleanProperty(DataStorageUserHandler.getInstance().getUserCount() > 1)) + .hide(new SimpleBooleanProperty(uh.getUserCount() > 1)) .name("userManagement") .description( - DataStorageUserHandler.getInstance().getActiveUser() != null + uh.getActiveUser() != null ? "userManagementDescription" : "userManagementDescriptionEmpty") - .addComp(DataStorageUserHandler.getInstance().createOverview()) + .addComp(uh.createOverview()) .nameAndDescription("teamVaults") .addComp(Comp.empty()) .licenseRequirement("team") .disable(!LicenseProvider.get().getFeature("team").isSupported()) - .hide(new SimpleBooleanProperty(DataStorageUserHandler.getInstance().getUserCount() > 1)) + .hide(new SimpleBooleanProperty(uh.getUserCount() > 1)) .nameAndDescription("syncTeamVaults") .addComp(new ButtonComp(AppI18n.observable("enableGitSync"), () -> AppPrefs.get() .selectCategory("sync"))) @@ -73,7 +84,6 @@ public class VaultCategory extends AppPrefsCategory { .addToggle(prefs.lockVaultOnHibernation) .pref(prefs.encryptAllVaultData) .addToggle(encryptVault) - .disable(DataStorageUserHandler.getInstance().getUserCount() > 1) ); builder.addTitle("vault") .sub(new OptionsBuilder() diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index 2d53380c5..ededde3a7 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -307,8 +307,8 @@ public abstract class DataStorage { DataStoreEntry c = entry; do { - // We can't check for sharing of invalid entries - if (!c.getValidity().isUsable()) { + // We can't check for sharing of failed entries + if (c.getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { return false; } @@ -316,7 +316,11 @@ public abstract class DataStorage { return true; } - if (!c.getProvider().isShareable(c)) { + try { + if (!c.getProvider().isShareable(c)) { + return false; + } + } catch (Exception e) { return false; } } while ((c = DataStorage.get().getDefaultDisplayParent(c).orElse(null)) != null); @@ -398,6 +402,8 @@ public abstract class DataStorage { DataStorage.get().saveAsync(); } + public abstract boolean isOtherUserEntry(UUID uuid); + public void moveEntryToCategory(DataStoreEntry entry, DataStoreCategory newCategory) { if (newCategory.getUuid().equals(entry.getCategoryUuid())) { return; diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageNode.java b/app/src/main/java/io/xpipe/app/storage/DataStorageNode.java index 278e01d3f..6b533657c 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageNode.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageNode.java @@ -23,16 +23,45 @@ import java.io.IOException; @Value public class DataStorageNode { - public static DataStorageNode ofNewStore(DataStore store) { + private static boolean encryptPerUser(DataStore store) { + if (DataStorageUserHandler.getInstance().getActiveUser() == null) { + return false; + } + var perUser = false; try { perUser = store instanceof UserScopeStore s && s.isPerUser(); } catch (Exception ignored) { } - var encrypted = perUser - || (AppPrefs.get() != null - && AppPrefs.get().encryptAllVaultData().get()); - return new DataStorageNode(JacksonMapper.getDefault().valueToTree(store), perUser, true, encrypted); + + if (perUser) { + return true; + } + + var all = AppPrefs.get() != null && AppPrefs.get().encryptAllVaultData().get(); + var useUserKey = DataStorageUserHandler.getInstance().getUserCount() == 1 && DataStorageUserHandler.getInstance().getActiveUser() != null; + return all && useUserKey; + } + + private static boolean encrypt(DataStore store) { + if (AppPrefs.get() != null && AppPrefs.get().encryptAllVaultData().get()) { + return true; + } + + if (DataStorageUserHandler.getInstance().getActiveUser() == null) { + return false; + } + + var perUser = false; + try { + perUser = store instanceof UserScopeStore s && s.isPerUser(); + } catch (Exception ignored) { + } + return perUser; + } + + public static DataStorageNode ofNewStore(DataStore store) { + return new DataStorageNode(JacksonMapper.getDefault().valueToTree(store), encryptPerUser(store), true, encrypt(store)); } public static DataStorageNode fail() { @@ -74,10 +103,7 @@ public class DataStorageNode { } public static JsonNode encryptNodeIfNeeded(DataStorageNode node) { - var encrypt = - (AppPrefs.get() != null && AppPrefs.get().encryptAllVaultData().get()) - || (node.isPerUser() && node.hasAccess()); - if (!encrypt) { + if (!node.isEncrypted()) { return node.getContentNode(); } @@ -108,21 +134,6 @@ public class DataStorageNode { return !perUser || availableForUser; } - public DataStorageNode withStore(DataStore store) { - if (store == null) { - return fail(); - } - - try { - var perUser = store instanceof UserScopeStore s && s.isPerUser(); - return new DataStorageNode( - JacksonMapper.getDefault().valueToTree(store), perUser, availableForUser, encrypted); - } catch (Exception e) { - // The per user check might fail for incomplete stores - return new DataStorageNode(JacksonMapper.getDefault().valueToTree(store), false, true, encrypted); - } - } - JsonNode contentNode; boolean perUser; boolean availableForUser; diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorageSecret.java b/app/src/main/java/io/xpipe/app/storage/DataStorageSecret.java index 4a8c6fbf1..10fa3514a 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorageSecret.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorageSecret.java @@ -5,6 +5,7 @@ import io.xpipe.app.util.EncryptionToken; import io.xpipe.app.util.PasswordLockSecretValue; import io.xpipe.app.util.VaultKeySecretValue; import io.xpipe.core.util.EncryptedSecretValue; +import io.xpipe.core.util.InPlaceSecretValue; import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.SecretValue; @@ -53,7 +54,7 @@ public class DataStorageSecret { token = userToken ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey(); } - return new DataStorageSecret(token, secretTree, secret); + return new DataStorageSecret(token, secretTree, secret.inPlace()); } public static DataStorageSecret ofCurrentSecret(SecretValue internalSecret) { @@ -61,22 +62,22 @@ public class DataStorageSecret { return new DataStorageSecret( handler.getActiveUser() != null ? EncryptionToken.ofUser() : EncryptionToken.ofVaultKey(), null, - internalSecret); + internalSecret.inPlace()); } public static DataStorageSecret ofSecret(SecretValue internalSecret, EncryptionToken token) { - return new DataStorageSecret(token, null, internalSecret); + return new DataStorageSecret(token, null, internalSecret.inPlace()); } @NonFinal JsonNode originalNode; - SecretValue internalSecret; + InPlaceSecretValue internalSecret; @NonFinal EncryptionToken encryptedToken; - public DataStorageSecret(EncryptionToken encryptedToken, JsonNode originalNode, SecretValue internalSecret) { + public DataStorageSecret(EncryptionToken encryptedToken, JsonNode originalNode, InPlaceSecretValue internalSecret) { this.encryptedToken = encryptedToken; this.originalNode = originalNode; this.internalSecret = internalSecret; @@ -89,16 +90,14 @@ public class DataStorageSecret { // User key must have changed if (!isUser && !isVault) { - // There must be a key mismatch + // We have loaded a secret with a user key that does no longer exist + // This means that the user was deleted in this session + // Replace it with a vault key if (userHandler.getActiveUser() == null) { - return false; - } - - // We don't want to use the new user key - if (!allowUserSecretKey) { - return false; + return true; } + // Password was changed return true; } @@ -115,19 +114,18 @@ public class DataStorageSecret { return false; } - public JsonNode rewrite(boolean allowUserSecretKey) { + private void rewrite(boolean allowUserSecretKey) { var handler = DataStorageUserHandler.getInstance(); if (handler != null && handler.getActiveUser() != null && allowUserSecretKey) { var val = new PasswordLockSecretValue(getSecret()); originalNode = JacksonMapper.getDefault().valueToTree(val); encryptedToken = EncryptionToken.ofUser(); - return originalNode; + return; } var val = new VaultKeySecretValue(getSecret()); originalNode = JacksonMapper.getDefault().valueToTree(val); encryptedToken = EncryptionToken.ofVaultKey(); - return originalNode; } public JsonNode serialize(boolean allowUserSecretKey) { @@ -137,17 +135,18 @@ public class DataStorageSecret { var mapper = JacksonMapper.getDefault(); var tree = JsonNodeFactory.instance.objectNode(); - tree.set("encryptedToken", mapper.valueToTree(getEncryptedToken())); // Preserve same output if not changed if (getOriginalNode() != null && !requiresRewrite(allowUserSecretKey)) { tree.set("secret", getOriginalNode()); + tree.set("encryptedToken", mapper.valueToTree(getEncryptedToken())); return tree; } // Reencrypt - var val = rewrite(allowUserSecretKey); - tree.set("secret", val); + rewrite(allowUserSecretKey); + tree.set("secret", getOriginalNode()); + tree.set("encryptedToken", mapper.valueToTree(getEncryptedToken())); return tree; } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 3efc4eb15..0a3f08734 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -523,7 +523,7 @@ public class DataStoreEntry extends StorageElement { } this.store = store; - this.storeNode = this.storeNode.withStore(store); + this.storeNode = DataStorageNode.ofNewStore(store); this.provider = DataStoreProviders.byStore(store); if (updateTime) { lastModified = Instant.now(); @@ -533,7 +533,7 @@ public class DataStoreEntry extends StorageElement { } public void reassignStoreNode() { - this.storeNode = this.storeNode.withStore(store); + this.storeNode = DataStorageNode.ofNewStore(store); dirty = true; } diff --git a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java index 55ba1f608..a0e53d496 100644 --- a/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/ImpersistentStorage.java @@ -5,6 +5,7 @@ import io.xpipe.app.ext.LocalStore; import io.xpipe.app.util.EncryptionKey; import java.time.Instant; +import java.util.UUID; import javax.crypto.SecretKey; public class ImpersistentStorage extends DataStorage { @@ -63,4 +64,9 @@ public class ImpersistentStorage extends DataStorage { public boolean supportsSharing() { return false; } + + @Override + public boolean isOtherUserEntry(UUID uuid) { + return false; + } } diff --git a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java index ca735bb2b..c62ea3d45 100644 --- a/app/src/main/java/io/xpipe/app/storage/StandardStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/StandardStorage.java @@ -17,13 +17,11 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.UUID; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.crypto.SecretKey; @@ -43,6 +41,7 @@ public class StandardStorage extends DataStorage { private boolean disposed; private boolean saveQueued; private final ReentrantLock busyIo = new ReentrantLock(); + private final Set inaccessibleEntries = new HashSet<>(); StandardStorage() { @@ -294,6 +293,7 @@ public class StandardStorage extends DataStorage { .map(dataStoreEntry -> dataStoreEntry.getDirectory()) .toList()); toRemove.forEach(storeEntries::remove); + inaccessibleEntries.addAll(toRemove.stream().map(dataStoreEntry -> dataStoreEntry.getUuid()).collect(Collectors.toSet())); } private boolean shouldRemoveOtherUserEntry(DataStoreEntry entry) { @@ -435,6 +435,11 @@ public class StandardStorage extends DataStorage { return dataStorageSyncHandler.supportsSync(); } + @Override + public boolean isOtherUserEntry(UUID uuid) { + return inaccessibleEntries.contains(uuid); + } + private void deleteLeftovers() { var storesDir = getStoresDir(); var categoriesDir = getCategoriesDir(); diff --git a/app/src/main/java/io/xpipe/app/util/EncryptionToken.java b/app/src/main/java/io/xpipe/app/util/EncryptionToken.java index da76213f9..c75d2fd84 100644 --- a/app/src/main/java/io/xpipe/app/util/EncryptionToken.java +++ b/app/src/main/java/io/xpipe/app/util/EncryptionToken.java @@ -22,10 +22,6 @@ public class EncryptionToken { private static EncryptionToken createUserToken() { var userHandler = DataStorageUserHandler.getInstance(); - if (userHandler.getActiveUser() == null) { - throw new IllegalStateException("No active user available"); - } - var userSecretValue = new PasswordLockSecretValue(userHandler.getActiveUser().toCharArray()) { @Override @@ -44,10 +40,15 @@ public class EncryptionToken { } public static EncryptionToken ofInvalid() { - return EncryptionToken.builder().token("").isUser(false).isVault(false).build(); + return EncryptionToken.builder().token("").isVault(false).build(); } public static EncryptionToken ofUser() { + var userHandler = DataStorageUserHandler.getInstance(); + if (userHandler.getActiveUser() == null) { + throw new IllegalStateException("No active user available"); + } + if (userToken == null) { userToken = createUserToken(); } @@ -63,9 +64,6 @@ public class EncryptionToken { private final String token; - @JsonIgnore - private Boolean isUser; - @JsonIgnore private Boolean isVault; @@ -84,16 +82,12 @@ public class EncryptionToken { } public boolean isUser() { - if (isUser != null) { - return isUser; - } - var userHandler = DataStorageUserHandler.getInstance(); if (userHandler.getActiveUser() == null) { - return (isUser = false); + return false; } - return (isUser = userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey()))); + return userHandler.getActiveUser().equals(decode(userHandler.getEncryptionKey())); } public boolean isVault() { diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 519f366d4..70c1570b1 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -487,8 +487,7 @@ xshellSetup=Xshell setup termiusSetup=Termius setup tryPtbDescription=Try out new features early in XPipe developer builds confirmVaultUnencryptTitle=Confirm vault unencryption -confirmVaultUnencryptHeader=Do you really want to disable advanced vault encryption? -confirmVaultUnencryptContent=This will remove the additional encryption for stored data and will overwrite existing data. +confirmVaultUnencryptContent=Do you really want to disable advanced vault encryption? This will remove the additional encryption for stored data and will overwrite existing data. enableHttpApi=Enable HTTP API enableHttpApiDescription=Enables the API, allowing external programs to call the XPipe daemon to perform actions with your managed connections. chooseCustomIcon=Choose custom icon @@ -559,12 +558,16 @@ userName=Username team=Team teamSettings=Team settings teamVaults=Team vaults -personalVault=Personal vault -personalVaultDescription=You are currently using a personal vault, which can have up to one user. Creating a vault user for yourself allows you to encrypt vault secrets with your own personal passphrase that you have to input on each login to unlock the vault. +vaultType=Vault type teamVaultsDescription=Team vaults allow multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user by encrypting them with your personal key. Other vault users can't access your personal connections and identities. +vaultTypeDefault=You are currently using a default vault with no user and custom passphrase set. Secrets are encrypted with the local vault key. You can upgrade to a personal vault by creating a vault user account. This allows you to encrypt vault secrets with your own personal passphrase that you have to input on each login to unlock the vault. +vaultTypeLegacy=You are currently using a legacy personal vault for your user. Secrets are encrypted with your personal passphrase. This legacy compatibility has limited features and can't be upgraded to a team vault in-place. +vaultTypePersonal=You are currently using a personal vault for your user. Secrets are encrypted with your personal passphrase. You can upgrade to a team vault by adding additional vault users. +vaultTypeTeam=You are currently using a team vault, which allows multiple users to have secure access to a shared vault. You can configure connections and identities to either be shared for all users or only have them available for your personal user by encrypting them with your personal key. Other vault users can't access your personal connections and identities. userManagement=User management -userManagementDescription=Manage existing vault team members or create new ones. -userManagementDescriptionEmpty=Manage existing vault team members or create new ones. Create a user for yourself to be able to encrypt connections and identities with your personal key.\n\nA single user account is supported in the community edition. Multiple user accounts for a team are supported in the professional plan. +#force +userManagementDescription=Manage existing vault users or create new ones. +userManagementDescriptionEmpty=Manage existing vault users or create new ones. Create a user for yourself to be able to encrypt connections and identities with your personal key.\n\nA single user account is supported in the community edition. Multiple user accounts for a team are supported in the professional plan. userIntroHeader=User management userIntroContent=Create the first user account for yourself to get started. This allows you to lock this workspace with a password. addReusableIdentity=Add reusable identity