From ccb807262565674a860c0cd041ec7f64fefcfe03 Mon Sep 17 00:00:00 2001 From: crschnick Date: Wed, 19 Apr 2023 01:44:33 +0000 Subject: [PATCH] Small fixes --- .../java/io/xpipe/app/core/mode/BaseMode.java | 5 ++ .../java/io/xpipe/app/core/mode/GuiMode.java | 4 +- .../app/fxcomps/impl/SecretFieldComp.java | 3 +- .../io/xpipe/app/issue/ErrorHandlerComp.java | 12 ++- .../xpipe/app/issue/SentryErrorHandler.java | 2 + .../xpipe/app/issue/TerminalErrorHandler.java | 11 +-- .../java/io/xpipe/app/prefs/AppPrefs.java | 89 ++++++++++++++++--- .../java/io/xpipe/app/prefs/SecretField.java | 24 +++++ .../io/xpipe/app/util/DefaultSecretValue.java | 35 ++++++++ .../io/xpipe/app/util/LockChangeAlert.java | 41 +++++++++ .../io/xpipe/app/util/LockedSecretValue.java | 41 +++++++++ .../java/io/xpipe/app/util/ScriptHelper.java | 4 +- .../java/io/xpipe/app/util/SecretHelper.java | 43 +++++++++ .../java/io/xpipe/app/util/UnlockAlert.java | 50 +++++++++++ .../resources/lang/preferences_en.properties | 6 +- .../resources/lang/translations_en.properties | 9 ++ .../io/xpipe/core/dialog/QueryConverter.java | 5 +- .../io/xpipe/core/util/AesSecretValue.java | 69 ++++++++++++++ .../io/xpipe/core/util/CoreJacksonModule.java | 19 ---- .../xpipe/core/util/EncryptedSecretValue.java | 52 +++++++++++ .../java/io/xpipe/core/util/SecretValue.java | 75 +++------------- dist/debug/windows/xpiped_debug.bat | 1 + .../ext/base/actions/AddStoreAction.java | 4 +- .../ext/base/actions/ShareStoreAction.java | 4 +- 24 files changed, 489 insertions(+), 119 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/prefs/SecretField.java create mode 100644 app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java create mode 100644 app/src/main/java/io/xpipe/app/util/LockChangeAlert.java create mode 100644 app/src/main/java/io/xpipe/app/util/LockedSecretValue.java create mode 100644 app/src/main/java/io/xpipe/app/util/SecretHelper.java create mode 100644 app/src/main/java/io/xpipe/app/util/UnlockAlert.java create mode 100644 core/src/main/java/io/xpipe/core/util/AesSecretValue.java create mode 100644 core/src/main/java/io/xpipe/core/util/EncryptedSecretValue.java diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 7906fca7f..66954cff4 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -6,7 +6,9 @@ import io.xpipe.app.core.*; import io.xpipe.app.issue.*; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; +import io.xpipe.app.util.DefaultSecretValue; import io.xpipe.app.util.FileBridge; +import io.xpipe.app.util.LockedSecretValue; import io.xpipe.core.util.JacksonMapper; public class BaseMode extends OperationMode { @@ -32,6 +34,9 @@ public class BaseMode extends OperationMode { TrackEvent.info("mode", "Initializing base mode components ..."); AppExtensionManager.init(true); JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer()); + JacksonMapper.configure(objectMapper -> { + objectMapper.registerSubtypes(LockedSecretValue.class, DefaultSecretValue.class); + }); AppPrefs.init(); AppCharsets.init(); AppCharsetter.init(); diff --git a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java index 8b8ac0931..32ce9e4d5 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/GuiMode.java @@ -5,6 +5,7 @@ import io.xpipe.app.core.AppGreetings; import io.xpipe.app.issue.*; import io.xpipe.app.update.UpdateChangelogAlert; import io.xpipe.app.util.PlatformState; +import io.xpipe.app.util.UnlockAlert; import javafx.application.Platform; import java.util.concurrent.CountDownLatch; @@ -26,6 +27,7 @@ public class GuiMode extends PlatformMode { Platform.runLater(() -> { try { TrackEvent.info("mode", "Setting up window ..."); + UnlockAlert.showIfNeeded(); App.getApp().setupWindow(); AppGreetings.showIfNeeded(); UpdateChangelogAlert.showIfNeeded(); @@ -57,7 +59,7 @@ public class GuiMode extends PlatformMode { var log = new LogErrorHandler(); return new SyncErrorHandler(event -> { log.handle(event); - ErrorHandlerComp.showAndWait(event); + ErrorHandlerComp.showAndTryWait(event, false); }); } } diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java index b31aa41e5..a76f8bb54 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/SecretFieldComp.java @@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.util.SecretHelper; import io.xpipe.core.util.SecretValue; import javafx.beans.property.Property; import javafx.scene.control.PasswordField; @@ -22,7 +23,7 @@ public class SecretFieldComp extends Comp> { var text = new PasswordField(); text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); text.textProperty().addListener((c, o, n) -> { - value.setValue(n != null && n.length() > 0 ? SecretValue.encrypt(n) : null); + value.setValue(n != null && n.length() > 0 ? SecretHelper.encrypt(n) : null); }); value.addListener((c, o, n) -> { PlatformThread.runLaterIfNeeded(() -> { diff --git a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java index 74b8aa87e..d3a1c5ba3 100644 --- a/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java +++ b/app/src/main/java/io/xpipe/app/issue/ErrorHandlerComp.java @@ -43,14 +43,14 @@ public class ErrorHandlerComp extends SimpleComp { this.stage = stage; } - public static void showAndWait(ErrorEvent event) { + public static void showAndTryWait(ErrorEvent event, boolean forceWait) { if (PlatformState.getCurrent() != PlatformState.RUNNING || event.isOmitted()) { ErrorAction.ignore().handle(event); return; } if (Platform.isFxApplicationThread()) { - showAndWaitWithPlatformThread(event); + showAndWaitWithPlatformThread(event, forceWait); } else { showAndWaitWithOtherThread(event); } @@ -70,7 +70,7 @@ public class ErrorHandlerComp extends SimpleComp { return c; } - public static void showAndWaitWithPlatformThread(ErrorEvent event) { + public static void showAndWaitWithPlatformThread(ErrorEvent event, boolean forceWait) { var finishLatch = new CountDownLatch(1); if (!showing.get()) { showing.set(true); @@ -82,7 +82,11 @@ public class ErrorHandlerComp extends SimpleComp { // An exception is thrown when show and wait is called // within an animation or layout processing task, so use show try { - window.show(); + if (forceWait) { + window.showAndWait(); + } else { + window.show(); + } } catch (Throwable t) { t.printStackTrace(); } diff --git a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java index dc120e108..606121507 100644 --- a/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/SentryErrorHandler.java @@ -5,6 +5,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppProperties; +import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.update.XPipeDistributionType; import org.apache.commons.io.FileUtils; @@ -94,6 +95,7 @@ public class SentryErrorHandler implements ErrorHandler { .toList(); atts.forEach(attachment -> s.addAttachment(attachment)); + s.setTag("initError", String.valueOf(OperationMode.isInStartup())); s.setTag("developerMode", AppPrefs.get() != null ? AppPrefs.get().developerMode().getValue().toString() : "false"); s.setTag("terminal", Boolean.toString(ee.isTerminal())); s.setTag("omitted", Boolean.toString(ee.isOmitted())); diff --git a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java index 1edd2eb8c..496837cc5 100644 --- a/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java +++ b/app/src/main/java/io/xpipe/app/issue/TerminalErrorHandler.java @@ -20,9 +20,8 @@ public class TerminalErrorHandler implements ErrorHandler { @Override public void handle(ErrorEvent event) { basic.handle(event); - handleSentry(event); - if (!OperationMode.GUI.isSupported()) { + if (!OperationMode.GUI.isSupported() || event.isOmitted()) { event.clearAttachments(); SentryErrorHandler.getInstance().handle(event); OperationMode.halt(1); @@ -31,12 +30,6 @@ public class TerminalErrorHandler implements ErrorHandler { handleGui(event); } - private void handleSentry(ErrorEvent event) { - if (OperationMode.isInStartup()) { - Sentry.setExtra("initError", "true"); - } - } - private void handleGui(ErrorEvent event) { if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) { try { @@ -63,7 +56,7 @@ public class TerminalErrorHandler implements ErrorHandler { AppExtensionManager.init(false); AppI18n.init(); AppStyle.init(); - ErrorHandlerComp.showAndWait(event); + ErrorHandlerComp.showAndTryWait(event, true); Sentry.flush(5000); } catch (Throwable r) { event.clearAttachments(); diff --git a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java index 5ef078d87..d852a4cc9 100644 --- a/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java +++ b/app/src/main/java/io/xpipe/app/prefs/AppPrefs.java @@ -2,11 +2,14 @@ package io.xpipe.app.prefs; import com.dlsc.formsfx.model.structure.*; import com.dlsc.preferencesfx.formsfx.view.controls.SimpleComboBoxControl; +import com.dlsc.preferencesfx.formsfx.view.controls.SimpleControl; import com.dlsc.preferencesfx.formsfx.view.controls.SimpleTextControl; import com.dlsc.preferencesfx.model.Category; import com.dlsc.preferencesfx.model.Group; import com.dlsc.preferencesfx.model.Setting; import com.dlsc.preferencesfx.util.VisibilityProperty; +import io.xpipe.app.comp.base.ButtonComp; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppStyle; import io.xpipe.app.ext.PrefsChoiceValue; @@ -14,11 +17,17 @@ import io.xpipe.app.ext.PrefsHandler; import io.xpipe.app.ext.PrefsProvider; import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.LockChangeAlert; +import io.xpipe.app.util.LockedSecretValue; +import io.xpipe.core.util.SecretValue; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.geometry.Pos; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; import java.nio.file.Path; import java.util.*; @@ -98,13 +107,39 @@ public class AppPrefs { // External terminal // ================= - private final ObjectProperty terminalType = typed(new SimpleObjectProperty<>(), ExternalTerminalType.class); + private final ObjectProperty terminalType = + typed(new SimpleObjectProperty<>(), ExternalTerminalType.class); private final SimpleListProperty terminalTypeList = new SimpleListProperty<>( FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class))); private final SingleSelectionField terminalTypeControl = Field.ofSingleSelectionType( terminalTypeList, terminalType) .render(() -> new TranslatableComboBoxControl<>()); + // Lock + // ==== + + private final Property lockPassword = new SimpleObjectProperty(); + private final StringProperty lockCrypt = typed(new SimpleStringProperty(null), String.class); + private final StringField lockCryptControl = StringField.ofStringType(lockCrypt).render(() -> new SimpleControl() { + + private Region button; + + @Override + public void initializeParts() { + super.initializeParts(); + this.node = new StackPane(); + button = new ButtonComp(Bindings.createStringBinding(() -> { + return lockCrypt.getValue() != null? AppI18n.get("changeLock"):AppI18n.get("createLock"); + }), () -> LockChangeAlert.show()).createRegion(); + } + + @Override + public void layoutParts() { + ((StackPane)this.node).getChildren().addAll(this.button); + ((StackPane)this.node).setAlignment(Pos.CENTER_LEFT); + } + }); + // Custom terminal // =============== private final StringProperty customTerminalCommand = typed(new SimpleStringProperty(""), String.class); @@ -112,10 +147,8 @@ public class AppPrefs { StringField.ofStringType(customTerminalCommand).render(() -> new SimpleTextControl()), terminalType.isEqualTo(ExternalTerminalType.CUSTOM)); - // Close behaviour // =============== - private final ObjectProperty closeBehaviour = typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class); private final SingleSelectionField closeBehaviourControl = Field.ofSingleSelectionType( @@ -135,9 +168,8 @@ public class AppPrefs { StringField.ofStringType(customEditorCommand).render(() -> new SimpleTextControl()), externalEditor.isEqualTo(ExternalEditorType.CUSTOM)); private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class); - private final ObjectProperty externalStartupBehaviour = typed( - new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY), - ExternalStartupBehaviour.class); + private final ObjectProperty externalStartupBehaviour = + typed(new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY), ExternalStartupBehaviour.class); private final SingleSelectionField externalStartupBehaviourControl = Field.ofSingleSelectionType(externalStartupBehaviourList, externalStartupBehaviour) @@ -227,6 +259,36 @@ public class AppPrefs { return customEditorCommand; } + public void changeLock(SecretValue newLockPw) { + if (newLockPw == null) { + lockCrypt.setValue(""); + lockPassword.setValue(null); + return; + } + + lockPassword.setValue(newLockPw); + lockCrypt.setValue(new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue()); + } + + public boolean checkLock(SecretValue lockPw) { + lockPassword.setValue(lockPw); + var check = new LockedSecretValue("xpipe".toCharArray()).getEncryptedValue(); + lockPassword.setValue(null); + return check.equals(lockCrypt.get()); + } + + public StringProperty getLockCrypt() { + return lockCrypt; + } + + public Property getLockPassword() { + return lockPassword; + } + + public StringProperty lockCryptProperty() { + return lockCrypt; + } + public final ReadOnlyIntegerProperty editorReloadTimeout() { return editorReloadTimeout; } @@ -429,6 +491,9 @@ public class AppPrefs { externalStartupBehaviourControl, externalStartupBehaviour), Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)), + Group.of( + "security", + Setting.of("workspaceLock", lockCryptControl, lockCrypt)), Group.of( "updates", Setting.of("automaticallyUpdate", automaticallyCheckForUpdatesField, automaticallyCheckForUpdates), @@ -460,12 +525,12 @@ public class AppPrefs { editorReloadTimeout, editorReloadTimeoutMin, editorReloadTimeoutMax)), - Group.of( - "terminal", - Setting.of("terminalProgram", terminalTypeControl, terminalType), - Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand) - .applyVisibility(VisibilityProperty.of( - terminalType.isEqualTo(ExternalTerminalType.CUSTOM))))), + Group.of( + "terminal", + Setting.of("terminalProgram", terminalTypeControl, terminalType), + Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand) + .applyVisibility(VisibilityProperty.of( + terminalType.isEqualTo(ExternalTerminalType.CUSTOM))))), Category.of( "developer", Setting.of( diff --git a/app/src/main/java/io/xpipe/app/prefs/SecretField.java b/app/src/main/java/io/xpipe/app/prefs/SecretField.java new file mode 100644 index 000000000..2e64d987a --- /dev/null +++ b/app/src/main/java/io/xpipe/app/prefs/SecretField.java @@ -0,0 +1,24 @@ +package io.xpipe.app.prefs; + +import com.dlsc.formsfx.model.structure.DataField; +import com.dlsc.formsfx.view.controls.SimplePasswordControl; +import io.xpipe.app.util.SecretHelper; +import io.xpipe.core.util.SecretValue; +import javafx.beans.property.Property; + +public class SecretField extends DataField, SecretValue, com.dlsc.formsfx.model.structure.PasswordField> { + + protected SecretField(Property valueProperty, Property persistentValueProperty) { + super(valueProperty, persistentValueProperty); + + stringConverter = new AbstractStringConverter() { + @Override + public SecretValue fromString(String string) { + return SecretHelper.encrypt(string); + } + }; + rendererSupplier = () -> new SimplePasswordControl(); + + userInput.set(stringConverter.toString(value.getValue())); + } +} \ No newline at end of file diff --git a/app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java b/app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java new file mode 100644 index 000000000..87f64bc31 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java @@ -0,0 +1,35 @@ +package io.xpipe.app.util; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.util.AesSecretValue; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Random; + + +@JsonTypeName("default") +@SuperBuilder +@Jacksonized +public class DefaultSecretValue extends AesSecretValue { + + public DefaultSecretValue(char[] secret) { + super(secret); + } + + protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + var salt = new byte[16]; + new Random(keysize).nextBytes(salt); + KeySpec spec = new PBEKeySpec(new char[] {'X', 'P', 'E' << 1}, salt, 2048, keysize); + SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + return secret; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java b/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java new file mode 100644 index 000000000..9cd1797f8 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/LockChangeAlert.java @@ -0,0 +1,41 @@ +package io.xpipe.app.util; + +import atlantafx.base.controls.Spacer; +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppWindowHelper; +import io.xpipe.app.fxcomps.impl.LabelComp; +import io.xpipe.app.fxcomps.impl.SecretFieldComp; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.SecretValue; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Alert; +import javafx.scene.layout.VBox; + +public class LockChangeAlert { + + public static void show() { + var prop1 = new SimpleObjectProperty(); + var prop2 = new SimpleObjectProperty(); + AppWindowHelper.showBlockingAlert(alert -> { + alert.setTitle(AppI18n.get("lockCreationAlertTitle")); + alert.setHeaderText(AppI18n.get("lockCreationAlertHeader")); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + + var label1 = new LabelComp(AppI18n.observable("password")).createRegion(); + var p1 = new SecretFieldComp(prop1).createRegion(); + p1.setStyle("-fx-border-width: 1px"); + + var label2 = new LabelComp(AppI18n.observable("repeatPassword")).createRegion(); + var p2 = new SecretFieldComp(prop2).createRegion(); + p1.setStyle("-fx-border-width: 1px"); + + var content = new VBox(label1, p1, new Spacer(15), label2, p2); + content.setSpacing(5); + alert.getDialogPane().setContent(content); + }) + .filter(b -> b.getButtonData().isDefaultButton() && (prop1.getValue() != null && prop2.getValue() != null && prop1.getValue().equals(prop2.getValue())) || (prop1.getValue() == null && prop2.getValue() == null)) + .ifPresent(t -> { + AppPrefs.get().changeLock(prop1.getValue()); + }); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java b/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java new file mode 100644 index 000000000..f1381a342 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/LockedSecretValue.java @@ -0,0 +1,41 @@ +package io.xpipe.app.util; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.AesSecretValue; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.util.Random; + +@JsonTypeName("locked") +@SuperBuilder +@Jacksonized +public class LockedSecretValue extends AesSecretValue { + + public LockedSecretValue(char[] secret) { + super(secret); + } + + @Override + public String toString() { + return ""; + } + + protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { + var chars = AppPrefs.get().getLockPassword().getValue() != null ? AppPrefs.get().getLockPassword().getValue().getSecret() : new char[0]; + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + var salt = new byte[16]; + new Random(keysize).nextBytes(salt); + KeySpec spec = new PBEKeySpec(chars, salt, 8192, keysize); + SecretKey secret = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES"); + return secret; + } +} diff --git a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java index 69de6cde6..f5f6d3a1b 100644 --- a/app/src/main/java/io/xpipe/app/util/ScriptHelper.java +++ b/app/src/main/java/io/xpipe/app/util/ScriptHelper.java @@ -124,12 +124,12 @@ public class ScriptHelper { var file = FileNames.join(temp, fileName); if (type != parent.getShellDialect()) { try (var sub = parent.subShell(type)) { - var content = sub.getShellDialect().prepareAskpassContent(sub, file, Collections.singletonList(pass.getSecretValue())); + var content = sub.getShellDialect().prepareAskpassContent(sub, file,pass != null? Collections.singletonList(pass.getSecretValue()) : List.of()); var exec = createExecScript(sub, file, content); return exec; } } else { - var content = parent.getShellDialect().prepareAskpassContent(parent, file, Collections.singletonList(pass.getSecretValue())); + var content = parent.getShellDialect().prepareAskpassContent(parent, file, pass != null?Collections.singletonList(pass.getSecretValue()) : List.of()); var exec = createExecScript(parent, file, content); return exec; } diff --git a/app/src/main/java/io/xpipe/app/util/SecretHelper.java b/app/src/main/java/io/xpipe/app/util/SecretHelper.java new file mode 100644 index 000000000..f2851e839 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/SecretHelper.java @@ -0,0 +1,43 @@ +package io.xpipe.app.util; + +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.EncryptedSecretValue; + +public class SecretHelper { + + public static EncryptedSecretValue encryptInPlace(char[] c) { + if (c == null) { + return null; + } + + return new DefaultSecretValue(c); + } + + public static EncryptedSecretValue encryptInPlace(String s) { + if (s == null) { + return null; + } + + return encryptInPlace(s.toCharArray()); + } + + public static EncryptedSecretValue encrypt(char[] c) { + if (c == null) { + return null; + } + + if (AppPrefs.get() != null && AppPrefs.get().getLockPassword().getValue() != null) { + return new LockedSecretValue(c); + } + + return new DefaultSecretValue(c); + } + + public static EncryptedSecretValue encrypt(String s) { + if (s == null) { + return null; + } + + return encrypt(s.toCharArray()); + } +} diff --git a/app/src/main/java/io/xpipe/app/util/UnlockAlert.java b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java new file mode 100644 index 000000000..c513c33da --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/UnlockAlert.java @@ -0,0 +1,50 @@ +package io.xpipe.app.util; + +import io.xpipe.app.core.AppI18n; +import io.xpipe.app.core.AppWindowHelper; +import io.xpipe.app.fxcomps.impl.SecretFieldComp; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.prefs.AppPrefs; +import io.xpipe.core.util.SecretValue; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Alert; +import javafx.scene.layout.VBox; + +public class UnlockAlert { + + public static void showIfNeeded() { + if (AppPrefs.get().getLockCrypt().getValue() == null || AppPrefs.get().getLockCrypt().getValue().isEmpty()) { + return; + } + + while (true) { + var pw = new SimpleObjectProperty(); + var canceled = new SimpleBooleanProperty(); + AppWindowHelper.showBlockingAlert(alert -> { + alert.setTitle(AppI18n.get("unlockAlertTitle")); + alert.setHeaderText(AppI18n.get("unlockAlertHeader")); + alert.setAlertType(Alert.AlertType.CONFIRMATION); + + var p1 = new SecretFieldComp(pw).createRegion(); + p1.setStyle("-fx-border-width: 1px"); + + var content = new VBox(p1); + content.setSpacing(5); + alert.getDialogPane().setContent(content); + }) + .filter(b -> b.getButtonData().isDefaultButton()) + .ifPresentOrElse(t -> { + }, () -> canceled.set(true)); + + if (canceled.get()) { + ErrorEvent.fromMessage("Unlock cancelled").term().omit().handle(); + return; + } + + if (AppPrefs.get().checkLock(pw.get())) { + return; + } + } + } +} diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties index 36765d2fa..2d4f4ccec 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/preferences_en.properties @@ -7,6 +7,8 @@ editorProgramDescription=The default text editor to use when editing any kind of useSystemFont=Use system font updates=Updates advanced=Advanced +workspaceLock=Workspace lock +workspaceLockDescription=Sets a custom password to encrypt your stored information in X-Pipe. This results in increased security as it provides an additional layer of encryption for your stored sensitive information. You will then be prompted to enter the password when X-Pipe starts. useSystemFontDescription=Controls whether to use your system font or the default font used by X-Pipe (Roboto). tooltipDelay=Tooltip delay tooltipDelayDescription=The amount of milliseconds to wait until a tooltip is displayed. @@ -66,6 +68,7 @@ notepad++=Notepad++ notepad++Windows=Notepad++ notepad++Linux=Notepad++ notepad=Notepad +security=Security developer=Developer developerDisableUpdateVersionCheck=Disable Update Version Check developerDisableUpdateVersionCheckDescription=Controls whether the update checker will ignore the version number when looking for an update. @@ -92,4 +95,5 @@ cmd=cmd.exe powershell=Powershell pwsh=Powershell Core windowsTerminal=Windows Terminal -gnomeTerminal=Gnome Terminal \ No newline at end of file +gnomeTerminal=Gnome Terminal +createLock=Create lock diff --git a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties index db3eedcac..ec5f851be 100644 --- a/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties +++ b/app/src/main/resources/io/xpipe/app/resources/lang/translations_en.properties @@ -5,6 +5,15 @@ lf=LF (Linux) none=None common=Common other=Other +setLock=Set lock +changeLock=Change lock +lockCreationAlertTitle=Create Lock +lockCreationAlertHeader=Set your new lock password +password=Password +unlockAlertTitle=Unlock workspace +unlockAlertHeader=Enter your lock password to continue +enterLockPassword=Enter lock password +repeatPassword=Repeat password askpassAlertTitle=Askpass nullPointer=Null Pointer moveAlertTitle=Confirm move diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java index 3aba5104a..727acd800 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java @@ -50,12 +50,13 @@ public abstract class QueryConverter { public static final QueryConverter SECRET = new QueryConverter() { @Override protected SecretValue fromString(String s) { - return new SecretValue(s); + //TODO + return null; } @Override protected String toString(SecretValue value) { - return value.getEncryptedValue(); + return value.getSecretValue(); } }; diff --git a/core/src/main/java/io/xpipe/core/util/AesSecretValue.java b/core/src/main/java/io/xpipe/core/util/AesSecretValue.java new file mode 100644 index 000000000..f171c7a82 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/AesSecretValue.java @@ -0,0 +1,69 @@ +package io.xpipe.core.util; + +import lombok.SneakyThrows; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; + +@SuperBuilder +@Jacksonized +public class AesSecretValue extends EncryptedSecretValue { + + private static final String ENCRYPT_ALGO = "AES/GCM/NoPadding"; + private static final int TAG_LENGTH_BIT = 128; + private static final int IV_LENGTH_BYTE = 12; + private static final int AES_KEY_BIT = 128; + private static final byte[] IV = getFixedNonce(IV_LENGTH_BYTE); + + public AesSecretValue(char[] secret) { + super(secret); + } + + private static byte[] getFixedNonce(int numBytes) { + byte[] nonce = new byte[numBytes]; + new SecureRandom(new byte[] {1, -28, 123}).nextBytes(nonce); + return nonce; + } + + protected SecretKey getAESKey(int keysize) throws NoSuchAlgorithmException, InvalidKeySpecException { + throw new UnsupportedOperationException(); + } + + @Override + @SneakyThrows + public byte[] encrypt(byte[] c) { + SecretKey secretKey = getAESKey(AES_KEY_BIT); + Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, IV)); + var bytes = cipher.doFinal(c); + bytes = ByteBuffer.allocate(IV.length + bytes.length) + .order(ByteOrder.LITTLE_ENDIAN) + .put(IV) + .put(bytes) + .array(); + return bytes; + } + + @Override + @SneakyThrows + public byte[] decrypt(byte[] c) { + ByteBuffer bb = ByteBuffer.wrap(c).order(ByteOrder.LITTLE_ENDIAN); + byte[] iv = new byte[IV_LENGTH_BYTE]; + bb.get(iv); + byte[] cipherText = new byte[bb.remaining()]; + bb.get(cipherText); + + SecretKey secretKey = getAESKey(AES_KEY_BIT); + Cipher cipher = Cipher.getInstance(ENCRYPT_ALGO); + cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(TAG_LENGTH_BIT, iv)); + return cipher.doFinal(cipherText); + } +} diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index ce8bab368..ba477bdaf 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -67,9 +67,6 @@ public class CoreJacksonModule extends SimpleModule { addSerializer(Path.class, new LocalPathSerializer()); addDeserializer(Path.class, new LocalPathDeserializer()); - addSerializer(SecretValue.class, new SecretSerializer()); - addDeserializer(SecretValue.class, new SecretDeserializer()); - addSerializer(DataSourceReference.class, new DataSourceReferenceSerializer()); addDeserializer(DataSourceReference.class, new DataSourceReferenceDeserializer()); @@ -179,22 +176,6 @@ public class CoreJacksonModule extends SimpleModule { } } - public static class SecretSerializer extends JsonSerializer { - - @Override - public void serialize(SecretValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeString(value.getEncryptedValue()); - } - } - - public static class SecretDeserializer extends JsonDeserializer { - - @Override - public SecretValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return new SecretValue(p.getValueAsString()); - } - } - @JsonSerialize(as = Throwable.class) public abstract static class ThrowableTypeMixIn { diff --git a/core/src/main/java/io/xpipe/core/util/EncryptedSecretValue.java b/core/src/main/java/io/xpipe/core/util/EncryptedSecretValue.java new file mode 100644 index 000000000..1129a17df --- /dev/null +++ b/core/src/main/java/io/xpipe/core/util/EncryptedSecretValue.java @@ -0,0 +1,52 @@ +package io.xpipe.core.util; + +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@SuperBuilder +@Jacksonized +public class EncryptedSecretValue implements SecretValue { + + @Getter + String encryptedValue; + + public EncryptedSecretValue(char[] c) { + var utf8 = StandardCharsets.UTF_8.encode(CharBuffer.wrap(c)); + var bytes = new byte[utf8.limit()]; + utf8.get(bytes); + encryptedValue = SecretValue.base64e(encrypt(bytes)); + } + + @Override + public String toString() { + return ""; + } + + @Override + public char[] getSecret() { + try { + var bytes = Base64.getDecoder().decode(encryptedValue.replace("-", "/")); + bytes = decrypt(bytes); + var charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes)); + var chars = new char[charBuffer.limit()]; + charBuffer.get(chars); + return chars; + } catch (Exception ex) { + return new char[0]; + } + } + + public byte[] encrypt(byte[] c) { + throw new UnsupportedOperationException(); + } + + public byte[] decrypt(byte[] c) { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/io/xpipe/core/util/SecretValue.java b/core/src/main/java/io/xpipe/core/util/SecretValue.java index 0d2d5db87..4824c56e1 100644 --- a/core/src/main/java/io/xpipe/core/util/SecretValue.java +++ b/core/src/main/java/io/xpipe/core/util/SecretValue.java @@ -1,81 +1,28 @@ package io.xpipe.core.util; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; +import com.fasterxml.jackson.annotation.JsonTypeInfo; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; import java.util.function.Consumer; -@AllArgsConstructor(access = AccessLevel.PUBLIC) -@EqualsAndHashCode -public class SecretValue { +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface SecretValue { - String value; - - public static SecretValue encrypt(char[] c) { - if (c == null) { - return null; - } - - var utf8 = StandardCharsets.UTF_8.encode(CharBuffer.wrap(c)); - var bytes = new byte[utf8.limit()]; - utf8.get(bytes); - Arrays.fill(c, (char) 0); - bytes = SecretProvider.get().encrypt(bytes); - var base64 = Base64.getEncoder().encodeToString(bytes); - return new SecretValue(base64.replace("/", "-")); + public static String base64e(byte[] b) { + var base64 = Base64.getEncoder().encodeToString(b); + return base64.replace("/", "-"); } - public static SecretValue encrypt(String s) { - if (s == null) { - return null; - } - - return encrypt(s.toCharArray()); - } - - public void withSecretValue(Consumer con) { - var chars = decryptChars(); + public default void withSecretValue(Consumer con) { + var chars = getSecret(); con.accept(chars); Arrays.fill(chars, (char) 0); } - @Override - public String toString() { - return ""; - } + public abstract char[] getSecret(); - public String getEncryptedValue() { - return value; - } - - public char[] decryptChars() { - try { - var bytes = Base64.getDecoder().decode(value.replace("-", "/")); - bytes = SecretProvider.get().decrypt(bytes); - var charBuffer = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes)); - var chars = new char[charBuffer.limit()]; - charBuffer.get(chars); - return chars; - } catch (Exception ex) { - return new char[0]; - } - } - - public String decrypt() { - return new String(decryptChars()); - } - - public static SecretValue ofSecret(String s) { - return new SecretValue(s); - } - - public String getSecretValue() { - return decrypt(); + public default String getSecretValue() { + return new String(getSecret()); } } diff --git a/dist/debug/windows/xpiped_debug.bat b/dist/debug/windows/xpiped_debug.bat index 3feb3321d..986aea18c 100644 --- a/dist/debug/windows/xpiped_debug.bat +++ b/dist/debug/windows/xpiped_debug.bat @@ -1,3 +1,4 @@ @echo off set CDS_JVM_OPTS=JVM-ARGS +chcp 65001 > NUL CALL "%~dp0\..\runtime\bin\xpiped.bat" %* diff --git a/ext/base/src/main/java/io/xpipe/ext/base/actions/AddStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/actions/AddStoreAction.java index 4441d8371..493bf3b58 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/actions/AddStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/actions/AddStoreAction.java @@ -3,9 +3,9 @@ package io.xpipe.ext.base.actions; import io.xpipe.app.comp.source.store.GuiDsStoreCreator; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.storage.DataStoreEntry; +import io.xpipe.app.util.SecretHelper; import io.xpipe.core.store.DataStore; import io.xpipe.core.util.JacksonMapper; -import io.xpipe.core.util.SecretValue; import lombok.Value; import java.util.List; @@ -43,7 +43,7 @@ public class AddStoreAction implements ActionProvider { @Override public Action createAction(List args) throws Exception { - var storeString = SecretValue.ofSecret(args.get(0)); + var storeString = SecretHelper.encryptInPlace(args.get(0)); var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class); return new Action(store); } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/actions/ShareStoreAction.java b/ext/base/src/main/java/io/xpipe/ext/base/actions/ShareStoreAction.java index cf220acbd..143e8f471 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/actions/ShareStoreAction.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/actions/ShareStoreAction.java @@ -4,8 +4,8 @@ import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppI18n; import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.DataStoreProviders; +import io.xpipe.app.util.SecretHelper; import io.xpipe.core.store.DataStore; -import io.xpipe.core.util.SecretValue; import javafx.beans.value.ObservableValue; import lombok.Value; @@ -26,7 +26,7 @@ public class ShareStoreAction implements ActionProvider { } public static String create(DataStore store) { - return "xpipe://addStore/" + SecretValue.encrypt(store.toString()).getEncryptedValue(); + return "xpipe://addStore/" + SecretHelper.encryptInPlace(store.toString()).getEncryptedValue(); } @Override