Small fixes

This commit is contained in:
crschnick 2023-04-19 01:44:33 +00:00
parent aaf4ab9560
commit ccb8072625
24 changed files with 489 additions and 119 deletions

View file

@ -6,7 +6,9 @@ import io.xpipe.app.core.*;
import io.xpipe.app.issue.*; import io.xpipe.app.issue.*;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DefaultSecretValue;
import io.xpipe.app.util.FileBridge; import io.xpipe.app.util.FileBridge;
import io.xpipe.app.util.LockedSecretValue;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
public class BaseMode extends OperationMode { public class BaseMode extends OperationMode {
@ -32,6 +34,9 @@ public class BaseMode extends OperationMode {
TrackEvent.info("mode", "Initializing base mode components ..."); TrackEvent.info("mode", "Initializing base mode components ...");
AppExtensionManager.init(true); AppExtensionManager.init(true);
JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer()); JacksonMapper.initModularized(AppExtensionManager.getInstance().getExtendedLayer());
JacksonMapper.configure(objectMapper -> {
objectMapper.registerSubtypes(LockedSecretValue.class, DefaultSecretValue.class);
});
AppPrefs.init(); AppPrefs.init();
AppCharsets.init(); AppCharsets.init();
AppCharsetter.init(); AppCharsetter.init();

View file

@ -5,6 +5,7 @@ import io.xpipe.app.core.AppGreetings;
import io.xpipe.app.issue.*; import io.xpipe.app.issue.*;
import io.xpipe.app.update.UpdateChangelogAlert; import io.xpipe.app.update.UpdateChangelogAlert;
import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.UnlockAlert;
import javafx.application.Platform; import javafx.application.Platform;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -26,6 +27,7 @@ public class GuiMode extends PlatformMode {
Platform.runLater(() -> { Platform.runLater(() -> {
try { try {
TrackEvent.info("mode", "Setting up window ..."); TrackEvent.info("mode", "Setting up window ...");
UnlockAlert.showIfNeeded();
App.getApp().setupWindow(); App.getApp().setupWindow();
AppGreetings.showIfNeeded(); AppGreetings.showIfNeeded();
UpdateChangelogAlert.showIfNeeded(); UpdateChangelogAlert.showIfNeeded();
@ -57,7 +59,7 @@ public class GuiMode extends PlatformMode {
var log = new LogErrorHandler(); var log = new LogErrorHandler();
return new SyncErrorHandler(event -> { return new SyncErrorHandler(event -> {
log.handle(event); log.handle(event);
ErrorHandlerComp.showAndWait(event); ErrorHandlerComp.showAndTryWait(event, false);
}); });
} }
} }

View file

@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.SecretHelper;
import io.xpipe.core.util.SecretValue; import io.xpipe.core.util.SecretValue;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.scene.control.PasswordField; import javafx.scene.control.PasswordField;
@ -22,7 +23,7 @@ public class SecretFieldComp extends Comp<CompStructure<TextField>> {
var text = new PasswordField(); var text = new PasswordField();
text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null);
text.textProperty().addListener((c, o, n) -> { 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) -> { value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {

View file

@ -43,14 +43,14 @@ public class ErrorHandlerComp extends SimpleComp {
this.stage = stage; this.stage = stage;
} }
public static void showAndWait(ErrorEvent event) { public static void showAndTryWait(ErrorEvent event, boolean forceWait) {
if (PlatformState.getCurrent() != PlatformState.RUNNING || event.isOmitted()) { if (PlatformState.getCurrent() != PlatformState.RUNNING || event.isOmitted()) {
ErrorAction.ignore().handle(event); ErrorAction.ignore().handle(event);
return; return;
} }
if (Platform.isFxApplicationThread()) { if (Platform.isFxApplicationThread()) {
showAndWaitWithPlatformThread(event); showAndWaitWithPlatformThread(event, forceWait);
} else { } else {
showAndWaitWithOtherThread(event); showAndWaitWithOtherThread(event);
} }
@ -70,7 +70,7 @@ public class ErrorHandlerComp extends SimpleComp {
return c; return c;
} }
public static void showAndWaitWithPlatformThread(ErrorEvent event) { public static void showAndWaitWithPlatformThread(ErrorEvent event, boolean forceWait) {
var finishLatch = new CountDownLatch(1); var finishLatch = new CountDownLatch(1);
if (!showing.get()) { if (!showing.get()) {
showing.set(true); showing.set(true);
@ -82,7 +82,11 @@ public class ErrorHandlerComp extends SimpleComp {
// An exception is thrown when show and wait is called // An exception is thrown when show and wait is called
// within an animation or layout processing task, so use show // within an animation or layout processing task, so use show
try { try {
window.show(); if (forceWait) {
window.showAndWait();
} else {
window.show();
}
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
} }

View file

@ -5,6 +5,7 @@ import io.sentry.protocol.SentryId;
import io.sentry.protocol.User; import io.sentry.protocol.User;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
@ -94,6 +95,7 @@ public class SentryErrorHandler implements ErrorHandler {
.toList(); .toList();
atts.forEach(attachment -> s.addAttachment(attachment)); 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("developerMode", AppPrefs.get() != null ? AppPrefs.get().developerMode().getValue().toString() : "false");
s.setTag("terminal", Boolean.toString(ee.isTerminal())); s.setTag("terminal", Boolean.toString(ee.isTerminal()));
s.setTag("omitted", Boolean.toString(ee.isOmitted())); s.setTag("omitted", Boolean.toString(ee.isOmitted()));

View file

@ -20,9 +20,8 @@ public class TerminalErrorHandler implements ErrorHandler {
@Override @Override
public void handle(ErrorEvent event) { public void handle(ErrorEvent event) {
basic.handle(event); basic.handle(event);
handleSentry(event);
if (!OperationMode.GUI.isSupported()) { if (!OperationMode.GUI.isSupported() || event.isOmitted()) {
event.clearAttachments(); event.clearAttachments();
SentryErrorHandler.getInstance().handle(event); SentryErrorHandler.getInstance().handle(event);
OperationMode.halt(1); OperationMode.halt(1);
@ -31,12 +30,6 @@ public class TerminalErrorHandler implements ErrorHandler {
handleGui(event); handleGui(event);
} }
private void handleSentry(ErrorEvent event) {
if (OperationMode.isInStartup()) {
Sentry.setExtra("initError", "true");
}
}
private void handleGui(ErrorEvent event) { private void handleGui(ErrorEvent event) {
if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) { if (PlatformState.getCurrent() == PlatformState.NOT_INITIALIZED) {
try { try {
@ -63,7 +56,7 @@ public class TerminalErrorHandler implements ErrorHandler {
AppExtensionManager.init(false); AppExtensionManager.init(false);
AppI18n.init(); AppI18n.init();
AppStyle.init(); AppStyle.init();
ErrorHandlerComp.showAndWait(event); ErrorHandlerComp.showAndTryWait(event, true);
Sentry.flush(5000); Sentry.flush(5000);
} catch (Throwable r) { } catch (Throwable r) {
event.clearAttachments(); event.clearAttachments();

View file

@ -2,11 +2,14 @@ package io.xpipe.app.prefs;
import com.dlsc.formsfx.model.structure.*; import com.dlsc.formsfx.model.structure.*;
import com.dlsc.preferencesfx.formsfx.view.controls.SimpleComboBoxControl; 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.formsfx.view.controls.SimpleTextControl;
import com.dlsc.preferencesfx.model.Category; import com.dlsc.preferencesfx.model.Category;
import com.dlsc.preferencesfx.model.Group; import com.dlsc.preferencesfx.model.Group;
import com.dlsc.preferencesfx.model.Setting; import com.dlsc.preferencesfx.model.Setting;
import com.dlsc.preferencesfx.util.VisibilityProperty; 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.AppProperties;
import io.xpipe.app.core.AppStyle; import io.xpipe.app.core.AppStyle;
import io.xpipe.app.ext.PrefsChoiceValue; 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.ext.PrefsProvider;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; 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.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; 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.nio.file.Path;
import java.util.*; import java.util.*;
@ -98,13 +107,39 @@ public class AppPrefs {
// External terminal // External terminal
// ================= // =================
private final ObjectProperty<ExternalTerminalType> terminalType = typed(new SimpleObjectProperty<>(), ExternalTerminalType.class); private final ObjectProperty<ExternalTerminalType> terminalType =
typed(new SimpleObjectProperty<>(), ExternalTerminalType.class);
private final SimpleListProperty<ExternalTerminalType> terminalTypeList = new SimpleListProperty<>( private final SimpleListProperty<ExternalTerminalType> terminalTypeList = new SimpleListProperty<>(
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class))); FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class)));
private final SingleSelectionField<ExternalTerminalType> terminalTypeControl = Field.ofSingleSelectionType( private final SingleSelectionField<ExternalTerminalType> terminalTypeControl = Field.ofSingleSelectionType(
terminalTypeList, terminalType) terminalTypeList, terminalType)
.render(() -> new TranslatableComboBoxControl<>()); .render(() -> new TranslatableComboBoxControl<>());
// Lock
// ====
private final Property<SecretValue> lockPassword = new SimpleObjectProperty<SecretValue>();
private final StringProperty lockCrypt = typed(new SimpleStringProperty(null), String.class);
private final StringField lockCryptControl = StringField.ofStringType(lockCrypt).render(() -> new SimpleControl<StringField, StackPane>() {
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 // Custom terminal
// =============== // ===============
private final StringProperty customTerminalCommand = typed(new SimpleStringProperty(""), String.class); private final StringProperty customTerminalCommand = typed(new SimpleStringProperty(""), String.class);
@ -112,10 +147,8 @@ public class AppPrefs {
StringField.ofStringType(customTerminalCommand).render(() -> new SimpleTextControl()), StringField.ofStringType(customTerminalCommand).render(() -> new SimpleTextControl()),
terminalType.isEqualTo(ExternalTerminalType.CUSTOM)); terminalType.isEqualTo(ExternalTerminalType.CUSTOM));
// Close behaviour // Close behaviour
// =============== // ===============
private final ObjectProperty<CloseBehaviour> closeBehaviour = private final ObjectProperty<CloseBehaviour> closeBehaviour =
typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class); typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class);
private final SingleSelectionField<CloseBehaviour> closeBehaviourControl = Field.ofSingleSelectionType( private final SingleSelectionField<CloseBehaviour> closeBehaviourControl = Field.ofSingleSelectionType(
@ -135,9 +168,8 @@ public class AppPrefs {
StringField.ofStringType(customEditorCommand).render(() -> new SimpleTextControl()), StringField.ofStringType(customEditorCommand).render(() -> new SimpleTextControl()),
externalEditor.isEqualTo(ExternalEditorType.CUSTOM)); externalEditor.isEqualTo(ExternalEditorType.CUSTOM));
private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class); private final IntegerProperty editorReloadTimeout = typed(new SimpleIntegerProperty(1000), Integer.class);
private final ObjectProperty<ExternalStartupBehaviour> externalStartupBehaviour = typed( private final ObjectProperty<ExternalStartupBehaviour> externalStartupBehaviour =
new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY), typed(new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY), ExternalStartupBehaviour.class);
ExternalStartupBehaviour.class);
private final SingleSelectionField<ExternalStartupBehaviour> externalStartupBehaviourControl = private final SingleSelectionField<ExternalStartupBehaviour> externalStartupBehaviourControl =
Field.ofSingleSelectionType(externalStartupBehaviourList, externalStartupBehaviour) Field.ofSingleSelectionType(externalStartupBehaviourList, externalStartupBehaviour)
@ -227,6 +259,36 @@ public class AppPrefs {
return customEditorCommand; 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<SecretValue> getLockPassword() {
return lockPassword;
}
public StringProperty lockCryptProperty() {
return lockCrypt;
}
public final ReadOnlyIntegerProperty editorReloadTimeout() { public final ReadOnlyIntegerProperty editorReloadTimeout() {
return editorReloadTimeout; return editorReloadTimeout;
} }
@ -429,6 +491,9 @@ public class AppPrefs {
externalStartupBehaviourControl, externalStartupBehaviourControl,
externalStartupBehaviour), externalStartupBehaviour),
Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)), Setting.of("closeBehaviour", closeBehaviourControl, closeBehaviour)),
Group.of(
"security",
Setting.of("workspaceLock", lockCryptControl, lockCrypt)),
Group.of( Group.of(
"updates", "updates",
Setting.of("automaticallyUpdate", automaticallyCheckForUpdatesField, automaticallyCheckForUpdates), Setting.of("automaticallyUpdate", automaticallyCheckForUpdatesField, automaticallyCheckForUpdates),
@ -460,12 +525,12 @@ public class AppPrefs {
editorReloadTimeout, editorReloadTimeout,
editorReloadTimeoutMin, editorReloadTimeoutMin,
editorReloadTimeoutMax)), editorReloadTimeoutMax)),
Group.of( Group.of(
"terminal", "terminal",
Setting.of("terminalProgram", terminalTypeControl, terminalType), Setting.of("terminalProgram", terminalTypeControl, terminalType),
Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand) Setting.of("customTerminalCommand", customTerminalCommandControl, customTerminalCommand)
.applyVisibility(VisibilityProperty.of( .applyVisibility(VisibilityProperty.of(
terminalType.isEqualTo(ExternalTerminalType.CUSTOM))))), terminalType.isEqualTo(ExternalTerminalType.CUSTOM))))),
Category.of( Category.of(
"developer", "developer",
Setting.of( Setting.of(

View file

@ -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<Property<SecretValue>, SecretValue, com.dlsc.formsfx.model.structure.PasswordField> {
protected SecretField(Property<SecretValue> valueProperty, Property<SecretValue> persistentValueProperty) {
super(valueProperty, persistentValueProperty);
stringConverter = new AbstractStringConverter<SecretValue>() {
@Override
public SecretValue fromString(String string) {
return SecretHelper.encrypt(string);
}
};
rendererSupplier = () -> new SimplePasswordControl();
userInput.set(stringConverter.toString(value.getValue()));
}
}

View file

@ -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;
}
}

View file

@ -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<SecretValue>();
var prop2 = new SimpleObjectProperty<SecretValue>();
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());
});
}
}

View file

@ -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 "<locked secret>";
}
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;
}
}

View file

@ -124,12 +124,12 @@ public class ScriptHelper {
var file = FileNames.join(temp, fileName); var file = FileNames.join(temp, fileName);
if (type != parent.getShellDialect()) { if (type != parent.getShellDialect()) {
try (var sub = parent.subShell(type)) { 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); var exec = createExecScript(sub, file, content);
return exec; return exec;
} }
} else { } 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); var exec = createExecScript(parent, file, content);
return exec; return exec;
} }

View file

@ -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());
}
}

View file

@ -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<SecretValue>();
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;
}
}
}
}

View file

@ -7,6 +7,8 @@ editorProgramDescription=The default text editor to use when editing any kind of
useSystemFont=Use system font useSystemFont=Use system font
updates=Updates updates=Updates
advanced=Advanced 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). useSystemFontDescription=Controls whether to use your system font or the default font used by X-Pipe (Roboto).
tooltipDelay=Tooltip delay tooltipDelay=Tooltip delay
tooltipDelayDescription=The amount of milliseconds to wait until a tooltip is displayed. tooltipDelayDescription=The amount of milliseconds to wait until a tooltip is displayed.
@ -66,6 +68,7 @@ notepad++=Notepad++
notepad++Windows=Notepad++ notepad++Windows=Notepad++
notepad++Linux=Notepad++ notepad++Linux=Notepad++
notepad=Notepad notepad=Notepad
security=Security
developer=Developer developer=Developer
developerDisableUpdateVersionCheck=Disable Update Version Check developerDisableUpdateVersionCheck=Disable Update Version Check
developerDisableUpdateVersionCheckDescription=Controls whether the update checker will ignore the version number when looking for an update. 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 powershell=Powershell
pwsh=Powershell Core pwsh=Powershell Core
windowsTerminal=Windows Terminal windowsTerminal=Windows Terminal
gnomeTerminal=Gnome Terminal gnomeTerminal=Gnome Terminal
createLock=Create lock

View file

@ -5,6 +5,15 @@ lf=LF (Linux)
none=None none=None
common=Common common=Common
other=Other 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 askpassAlertTitle=Askpass
nullPointer=Null Pointer nullPointer=Null Pointer
moveAlertTitle=Confirm move moveAlertTitle=Confirm move

View file

@ -50,12 +50,13 @@ public abstract class QueryConverter<T> {
public static final QueryConverter<SecretValue> SECRET = new QueryConverter<SecretValue>() { public static final QueryConverter<SecretValue> SECRET = new QueryConverter<SecretValue>() {
@Override @Override
protected SecretValue fromString(String s) { protected SecretValue fromString(String s) {
return new SecretValue(s); //TODO
return null;
} }
@Override @Override
protected String toString(SecretValue value) { protected String toString(SecretValue value) {
return value.getEncryptedValue(); return value.getSecretValue();
} }
}; };

View file

@ -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);
}
}

View file

@ -67,9 +67,6 @@ public class CoreJacksonModule extends SimpleModule {
addSerializer(Path.class, new LocalPathSerializer()); addSerializer(Path.class, new LocalPathSerializer());
addDeserializer(Path.class, new LocalPathDeserializer()); addDeserializer(Path.class, new LocalPathDeserializer());
addSerializer(SecretValue.class, new SecretSerializer());
addDeserializer(SecretValue.class, new SecretDeserializer());
addSerializer(DataSourceReference.class, new DataSourceReferenceSerializer()); addSerializer(DataSourceReference.class, new DataSourceReferenceSerializer());
addDeserializer(DataSourceReference.class, new DataSourceReferenceDeserializer()); addDeserializer(DataSourceReference.class, new DataSourceReferenceDeserializer());
@ -179,22 +176,6 @@ public class CoreJacksonModule extends SimpleModule {
} }
} }
public static class SecretSerializer extends JsonSerializer<SecretValue> {
@Override
public void serialize(SecretValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(value.getEncryptedValue());
}
}
public static class SecretDeserializer extends JsonDeserializer<SecretValue> {
@Override
public SecretValue deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return new SecretValue(p.getValueAsString());
}
}
@JsonSerialize(as = Throwable.class) @JsonSerialize(as = Throwable.class)
public abstract static class ThrowableTypeMixIn { public abstract static class ThrowableTypeMixIn {

View file

@ -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 "<encrypted secret>";
}
@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();
}
}

View file

@ -1,81 +1,28 @@
package io.xpipe.core.util; package io.xpipe.core.util;
import lombok.AccessLevel; import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.function.Consumer; import java.util.function.Consumer;
@AllArgsConstructor(access = AccessLevel.PUBLIC) @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@EqualsAndHashCode public interface SecretValue {
public class SecretValue {
String value; public static String base64e(byte[] b) {
var base64 = Base64.getEncoder().encodeToString(b);
public static SecretValue encrypt(char[] c) { return base64.replace("/", "-");
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 SecretValue encrypt(String s) { public default void withSecretValue(Consumer<char[]> con) {
if (s == null) { var chars = getSecret();
return null;
}
return encrypt(s.toCharArray());
}
public void withSecretValue(Consumer<char[]> con) {
var chars = decryptChars();
con.accept(chars); con.accept(chars);
Arrays.fill(chars, (char) 0); Arrays.fill(chars, (char) 0);
} }
@Override public abstract char[] getSecret();
public String toString() {
return "<secret>";
}
public String getEncryptedValue() { public default String getSecretValue() {
return value; return new String(getSecret());
}
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();
} }
} }

View file

@ -1,3 +1,4 @@
@echo off @echo off
set CDS_JVM_OPTS=JVM-ARGS set CDS_JVM_OPTS=JVM-ARGS
chcp 65001 > NUL
CALL "%~dp0\..\runtime\bin\xpiped.bat" %* CALL "%~dp0\..\runtime\bin\xpiped.bat" %*

View file

@ -3,9 +3,9 @@ package io.xpipe.ext.base.actions;
import io.xpipe.app.comp.source.store.GuiDsStoreCreator; import io.xpipe.app.comp.source.store.GuiDsStoreCreator;
import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.SecretHelper;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.SecretValue;
import lombok.Value; import lombok.Value;
import java.util.List; import java.util.List;
@ -43,7 +43,7 @@ public class AddStoreAction implements ActionProvider {
@Override @Override
public Action createAction(List<String> args) throws Exception { public Action createAction(List<String> 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); var store = JacksonMapper.parse(storeString.getSecretValue(), DataStore.class);
return new Action(store); return new Action(store);
} }

View file

@ -4,8 +4,8 @@ import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.util.SecretHelper;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.SecretValue;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import lombok.Value; import lombok.Value;
@ -26,7 +26,7 @@ public class ShareStoreAction implements ActionProvider {
} }
public static String create(DataStore store) { public static String create(DataStore store) {
return "xpipe://addStore/" + SecretValue.encrypt(store.toString()).getEncryptedValue(); return "xpipe://addStore/" + SecretHelper.encryptInPlace(store.toString()).getEncryptedValue();
} }
@Override @Override