mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-25 09:00:26 +00:00
Small fixes
This commit is contained in:
parent
aaf4ab9560
commit
ccb8072625
24 changed files with 489 additions and 119 deletions
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<CompStructure<TextField>> {
|
|||
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(() -> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<ExternalTerminalType> terminalType = typed(new SimpleObjectProperty<>(), ExternalTerminalType.class);
|
||||
private final ObjectProperty<ExternalTerminalType> terminalType =
|
||||
typed(new SimpleObjectProperty<>(), ExternalTerminalType.class);
|
||||
private final SimpleListProperty<ExternalTerminalType> terminalTypeList = new SimpleListProperty<>(
|
||||
FXCollections.observableArrayList(PrefsChoiceValue.getSupported(ExternalTerminalType.class)));
|
||||
private final SingleSelectionField<ExternalTerminalType> terminalTypeControl = Field.ofSingleSelectionType(
|
||||
terminalTypeList, terminalType)
|
||||
.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
|
||||
// ===============
|
||||
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> closeBehaviour =
|
||||
typed(new SimpleObjectProperty<>(CloseBehaviour.QUIT), CloseBehaviour.class);
|
||||
private final SingleSelectionField<CloseBehaviour> 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> externalStartupBehaviour = typed(
|
||||
new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY),
|
||||
ExternalStartupBehaviour.class);
|
||||
private final ObjectProperty<ExternalStartupBehaviour> externalStartupBehaviour =
|
||||
typed(new SimpleObjectProperty<>(ExternalStartupBehaviour.TRAY), ExternalStartupBehaviour.class);
|
||||
|
||||
private final SingleSelectionField<ExternalStartupBehaviour> 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<SecretValue> 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(
|
||||
|
|
24
app/src/main/java/io/xpipe/app/prefs/SecretField.java
Normal file
24
app/src/main/java/io/xpipe/app/prefs/SecretField.java
Normal 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()));
|
||||
}
|
||||
}
|
35
app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java
Normal file
35
app/src/main/java/io/xpipe/app/util/DefaultSecretValue.java
Normal 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;
|
||||
}
|
||||
}
|
41
app/src/main/java/io/xpipe/app/util/LockChangeAlert.java
Normal file
41
app/src/main/java/io/xpipe/app/util/LockChangeAlert.java
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
41
app/src/main/java/io/xpipe/app/util/LockedSecretValue.java
Normal file
41
app/src/main/java/io/xpipe/app/util/LockedSecretValue.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
43
app/src/main/java/io/xpipe/app/util/SecretHelper.java
Normal file
43
app/src/main/java/io/xpipe/app/util/SecretHelper.java
Normal 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());
|
||||
}
|
||||
}
|
50
app/src/main/java/io/xpipe/app/util/UnlockAlert.java
Normal file
50
app/src/main/java/io/xpipe/app/util/UnlockAlert.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
@ -93,3 +96,4 @@ powershell=Powershell
|
|||
pwsh=Powershell Core
|
||||
windowsTerminal=Windows Terminal
|
||||
gnomeTerminal=Gnome Terminal
|
||||
createLock=Create lock
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -50,12 +50,13 @@ public abstract class QueryConverter<T> {
|
|||
public static final QueryConverter<SecretValue> SECRET = new QueryConverter<SecretValue>() {
|
||||
@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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
69
core/src/main/java/io/xpipe/core/util/AesSecretValue.java
Normal file
69
core/src/main/java/io/xpipe/core/util/AesSecretValue.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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<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)
|
||||
public abstract static class ThrowableTypeMixIn {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<char[]> con) {
|
||||
var chars = decryptChars();
|
||||
public default void withSecretValue(Consumer<char[]> con) {
|
||||
var chars = getSecret();
|
||||
con.accept(chars);
|
||||
Arrays.fill(chars, (char) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<secret>";
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
1
dist/debug/windows/xpiped_debug.bat
vendored
1
dist/debug/windows/xpiped_debug.bat
vendored
|
@ -1,3 +1,4 @@
|
|||
@echo off
|
||||
set CDS_JVM_OPTS=JVM-ARGS
|
||||
chcp 65001 > NUL
|
||||
CALL "%~dp0\..\runtime\bin\xpiped.bat" %*
|
||||
|
|
|
@ -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<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);
|
||||
return new Action(store);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue