This commit is contained in:
crschnick
2026-02-27 14:23:55 +00:00
parent 72e4c894b5
commit 82a00d96b0
23 changed files with 543 additions and 186 deletions
@@ -75,16 +75,12 @@ public class CustomAgentStrategy implements SshIdentityStrategy {
i -> {
return i != null;
}))
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.bind(
() -> {
return new CustomAgentStrategy(forward.get(), publicKey.get());
@@ -36,11 +36,11 @@ public class GpgAgentStrategy implements SshIdentityStrategy {
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
@@ -1,86 +0,0 @@
package io.xpipe.app.cred;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.List;
@JsonTypeName("customAgent")
@Value
@Jacksonized
@Builder
public class InternalCustomAgentStrategy implements SshIdentityStrategy {
boolean forwardAgent;
String publicKey;
@Override
public void prepareParent(ShellControl parent) throws Exception {
if (parent.isLocal()) {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
SshIdentityStateManager.prepareLocalCustomAgent(
parent, agent);
}
}
@Override
public void buildCommand(CommandBuilder builder) {}
private String getIdentityAgent(ShellControl sc) throws Exception {
if (!sc.isLocal() || sc.getOsType() == OsType.WINDOWS) {
return null;
}
if (AppPrefs.get() != null) {
var agent = AppPrefs.get().sshAgentSocket().getValue();
if (agent == null) {
agent = AppPrefs.get().defaultSshAgentSocket().getValue();
}
if (agent != null) {
return agent.resolveTildeHome(sc.view().userHome()).toString();
}
}
return null;
}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
var l = new ArrayList<>(List.of(
new KeyValue("IdentitiesOnly", file.isPresent() ? "yes" : "no"),
new KeyValue("ForwardAgent", forwardAgent ? "yes" : "no"),
new KeyValue("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
new KeyValue("PKCS11Provider", "none")));
var agent = getIdentityAgent(sc);
if (agent != null) {
l.add(new KeyValue("IdentityAgent", "\"" + agent + "\""));
}
return l;
}
}
@@ -39,16 +39,12 @@ public class OpenSshAgentStrategy implements SshIdentityStrategy {
.nameAndDescription("agentSocket")
.addStaticString(socket != null ? socket : AppI18n.get("agentSocketNotFound"))
.hide(OsType.ofLocal() == OsType.WINDOWS)
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.bind(
() -> {
return new OpenSshAgentStrategy(forward.get(), publicKey.get());
@@ -31,16 +31,13 @@ public class OtherExternalAgentStrategy implements SshIdentityStrategy {
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.bind(
() -> {
return new OtherExternalAgentStrategy(forward.get(), publicKey.get());
@@ -39,16 +39,12 @@ public class PageantStrategy implements SshIdentityStrategy {
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
return new OptionsBuilder()
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.nameAndDescription("publicKey")
.addComp(
new TextFieldComp(publicKey)
.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== Your Comment")),
publicKey)
.bind(
() -> {
return new PageantStrategy(forward.get(), publicKey.get());
@@ -92,6 +88,9 @@ public class PageantStrategy implements SshIdentityStrategy {
throw ErrorEventFactory.expected(new IllegalStateException(
"Pageant is not running as the primary agent via the $SSH_AUTH_SOCK variable."));
}
} else if (parent.isLocal()) {
// Check if it exists
getPageantWindowsPipe();
}
}
@@ -0,0 +1,105 @@
package io.xpipe.app.cred;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.List;
@JsonTypeName("passwordManager")
@Value
@Jacksonized
@Builder
public class PasswordManagerAgentStrategy implements SshIdentityStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<PasswordManagerAgentStrategy> p, SshIdentityStrategyChoiceConfig config) {
var forward =
new SimpleBooleanProperty(p.getValue() != null && p.getValue().isForwardAgent());
var publicKey =
new SimpleStringProperty(p.getValue() != null ? p.getValue().getPublicKey() : null);
var pwmanBinding = Bindings.createObjectBinding(() -> {
var pwman = AppPrefs.get().passwordManager().getValue();
if (pwman == null) {
return AppI18n.get("passwordManagerEmpty");
}
if (!pwman.getKeyStrategy().supportsAgent()) {
return AppI18n.get("passwordManagerNoAgentSupport");
}
return null;
}, AppPrefs.get().passwordManager(), AppI18n.activeLanguage());
var pwmanProp = new SimpleStringProperty();
pwmanProp.bind(pwmanBinding);
var pwmanDisplay = new HorizontalComp(List.of(
new TextFieldComp(pwmanProp)
.apply(struc -> struc.setEditable(false))
.hgrow(),
new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory("passwordManager");
})
.padding(new Insets(7))))
.spacing(9);
return new OptionsBuilder()
.nameAndDescription("passwordManagerSshKeyConfig")
.addComp(pwmanDisplay)
.nameAndDescription("publicKey")
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey), publicKey)
.nameAndDescription("forwardAgent")
.addToggle(forward)
.nonNull()
.hide(!config.isAllowAgentForward())
.bind(
() -> {
return new PasswordManagerAgentStrategy(forward.get(), publicKey.get());
},
p);
}
boolean forwardAgent;
String publicKey;
@Override
public void prepareParent(ShellControl parent) throws Exception {
var pwman = AppPrefs.get().passwordManager().getValue();
var strat = pwman.getKeyStrategy().getSshIdentityStrategy();
strat.prepareParent(parent);
}
@Override
public void buildCommand(CommandBuilder builder) {
var pwman = AppPrefs.get().passwordManager().getValue();
var strat = pwman.getKeyStrategy().getSshIdentityStrategy();
strat.buildCommand(builder);
}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var pwman = AppPrefs.get().passwordManager().getValue();
var strat = pwman.getKeyStrategy().getSshIdentityStrategy();
return strat.configOptions(sc);
}
}
@@ -0,0 +1,43 @@
package io.xpipe.app.cred;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.storage.DataStoreEntryRef;
import lombok.Value;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class SshAgentKeyList {
@Value
public static class Entry {
String type;
String publicKey;
String name;
}
public static List<Entry> listAgentIdentities(DataStoreEntryRef<ShellStore> ref, SshIdentityStrategy strategy) throws Exception {
var session = ref.getStore().getOrStartSession();
strategy.prepareParent(session);
var out = session.command(CommandBuilder.of().add("ssh-add", "-L")).readStdoutOrThrow();
var pattern = Pattern.compile("([^ ]+) ([^ ]+) (.+)");
var lines = out.lines().toList();
var list = new ArrayList<Entry>();
for (String line : lines) {
var matcher = pattern.matcher(line);
if (!matcher.matches()) {
continue;
}
var type = matcher.group(1);
var publicKey = matcher.group(2);
var name = matcher.group(3);
list.add(new Entry(type, publicKey, name));
}
return list;
}
}
@@ -0,0 +1,86 @@
package io.xpipe.app.cred;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
public class SshAgentKeyListComp extends SimpleRegionBuilder {
private final ObservableValue<DataStoreEntryRef<ShellStore>> ref;
private final ObservableValue<? extends SshIdentityStrategy> sshIdentityStrategy;
private final StringProperty value;
public SshAgentKeyListComp(ObservableValue<DataStoreEntryRef<ShellStore>> ref, ObservableValue<? extends SshIdentityStrategy> sshIdentityStrategy, StringProperty value) {
this.ref = ref;
this.sshIdentityStrategy = sshIdentityStrategy;
this.value = value;}
@Override
protected Region createSimple() {
var field = new TextFieldComp(value);
field.apply(struc -> struc.setPromptText(
"ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== <key comment>"));
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2m-magnify-scan"), null);
button.apply(struc -> {
struc.setOnAction(event -> {
DataStoreEntryRef<ShellStore> refToUse = ref.getValue() != null ? ref.getValue() : DataStorage.get().local().ref();
ThreadHelper.runFailableAsync(() -> {
var list = SshAgentKeyList.listAgentIdentities(refToUse, sshIdentityStrategy.getValue());
Platform.runLater(() -> {
var popover = new Popover();
popover.setArrowLocation(Popover.ArrowLocation.TOP_CENTER);
if (list.size() > 0) {
var content = new VBox();
content.setPadding(new Insets(10));
content.setFillWidth(true);
content.getChildren().add(new Label(AppI18n.get("sshAgentHasKeys")));
for (SshAgentKeyList.Entry entry : list) {
var entryButton = new Button(entry.getName() + " (" + entry.getType() + ")");
entryButton.maxWidth(10000);
entryButton.getStyleClass().add(Styles.FLAT);
entryButton.setOnAction(e -> {
value.setValue(entry.getPublicKey());
popover.hide();
e.consume();
});
content.getChildren().add(entryButton);
}
popover.setContentNode(content);
} else {
popover.setContentNode(new Label(AppI18n.get("sshAgentNoKeys")));
}
var target = struc.getParent().getChildrenUnmodifiable().getFirst();
popover.show(target);
});
});
event.consume();
});
});
var inputGroup = new InputGroupComp(List.of(field, button));
inputGroup.setMainReference(field);
return inputGroup.build();
}
}
@@ -0,0 +1,70 @@
package io.xpipe.app.cred;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.SimpleRegionBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class SshAgentTestComp extends SimpleRegionBuilder {
private final ObservableValue<? extends SshIdentityStrategy> sshIdentityStrategy;
public SshAgentTestComp(ObservableValue<? extends SshIdentityStrategy> sshIdentityStrategy) {this.sshIdentityStrategy = sshIdentityStrategy;}
@Override
protected Region createSimple() {
var button = new ButtonComp(AppI18n.observable("test"), new LabelGraphic.IconGraphic("mdi2p-play"), null);
button.apply(struc -> {
struc.setOnAction(event -> {
DataStoreEntryRef<ShellStore> refToUse = DataStorage.get().local().ref();
ThreadHelper.runFailableAsync(() -> {
var list = SshAgentKeyList.listAgentIdentities(refToUse, sshIdentityStrategy.getValue());
Platform.runLater(() -> {
var popover = new Popover();
popover.setArrowLocation(Popover.ArrowLocation.LEFT_CENTER);
if (list.size() > 0) {
var content = new VBox();
content.setPadding(new Insets(10));
content.setFillWidth(true);
content.getChildren().add(new Label(AppI18n.get("sshAgentHasKeys")));
for (SshAgentKeyList.Entry entry : list) {
var entryButton = new Button(entry.getName() + " (" + entry.getType() + ")");
entryButton.maxWidth(10000);
entryButton.getStyleClass().add(Styles.FLAT);
content.getChildren().add(entryButton);
}
popover.setContentNode(content);
} else {
popover.setContentNode(new Label(AppI18n.get("sshAgentNoKeys")));
}
var target = struc;
popover.show(target);
});
});
event.consume();
});
});
return button.build();
}
}
@@ -11,7 +11,6 @@ import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.util.ArrayList;
@@ -19,26 +18,15 @@ import java.util.List;
import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = NoIdentityStrategy.class),
@JsonSubTypes.Type(value = KeyFileStrategy.class),
@JsonSubTypes.Type(value = InPlaceKeyStrategy.class),
@JsonSubTypes.Type(value = OpenSshAgentStrategy.class),
@JsonSubTypes.Type(value = PageantStrategy.class),
@JsonSubTypes.Type(value = CustomAgentStrategy.class),
@JsonSubTypes.Type(value = GpgAgentStrategy.class),
@JsonSubTypes.Type(value = YubikeyPivStrategy.class),
@JsonSubTypes.Type(value = CustomPkcs11LibraryStrategy.class),
@JsonSubTypes.Type(value = OtherExternalAgentStrategy.class)
})
public interface SshIdentityStrategy {
static List<Class<?>> getSubclasses() {
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(NoIdentityStrategy.class);
l.add(InPlaceKeyStrategy.class);
l.add(KeyFileStrategy.class);
l.add(OpenSshAgentStrategy.class);
l.add(PasswordManagerAgentStrategy.class);
if (OsType.ofLocal() != OsType.WINDOWS) {
l.add(CustomAgentStrategy.class);
}
@@ -10,7 +10,6 @@ import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.vnc.VncBaseStore;
import io.xpipe.core.FilePath;
import io.xpipe.core.SecretValue;
import javafx.beans.property.Property;
@@ -80,5 +79,4 @@ public abstract class ProcessControlProvider {
public abstract void pullRepository(Path target) throws Exception;
public abstract void checkSshAgent(ShellControl sc, FilePath agent) throws Exception;
}
@@ -5,6 +5,9 @@ import io.xpipe.app.comp.BaseRegionBuilder;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.cred.CustomAgentStrategy;
import io.xpipe.app.cred.SshAgentTestComp;
import io.xpipe.app.cred.SshIdentityStateManager;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.LabelGraphic;
@@ -18,6 +21,7 @@ import io.xpipe.core.OsType;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.layout.Region;
@@ -46,35 +50,7 @@ public class SshCategory extends AppPrefsCategory {
options.addComp(prefs.getCustomOptions("x11WslInstance").buildComp());
}
AtomicReference<Region> button = new AtomicReference<>();
var agentTest = new ButtonComp(AppI18n.observable("test"), new FontIcon("mdi2p-play"), () -> {
ThreadHelper.runFailableAsync(() -> {
var agent = prefs.sshAgentSocket().getValue();
if (agent == null) {
agent = prefs.defaultSshAgentSocket().getValue();
}
if (agent == null) {
return;
}
try {
ProcessControlProvider.get().checkSshAgent(LocalShell.getShell(), agent);
} catch (Exception e) {
ErrorEventFactory.fromThrowable(e).expected().handle();
return;
}
Platform.runLater(() -> {
button.get().getStyleClass().add(Styles.SUCCESS);
});
});
})
.padding(new Insets(6, 11, 6, 5))
.apply(struc -> struc.setAlignment(Pos.CENTER_LEFT));
agentTest.apply(struc -> button.set(struc));
var agentTest = new SshAgentTestComp(new SimpleObjectProperty<>(CustomAgentStrategy.builder().build()));
if (OsType.ofLocal() != OsType.WINDOWS) {
var choice = new ContextualFileReferenceChoiceComp(
new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),
@@ -22,22 +22,28 @@ import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
@JsonTypeName("bitwarden")
@Builder
@Jacksonized
public class BitwardenPasswordManager implements PasswordManager {
private static ShellControl SHELL;
private static boolean copied;
private PasswordManagerAgentStrategy agentStrategy;
private final PasswordManagerAgentStrategy agentStrategy;
private static synchronized ShellControl getOrStartShell() throws Exception {
if (SHELL == null) {
@@ -75,7 +81,7 @@ public class BitwardenPasswordManager implements PasswordManager {
var agentStrategyChoice = OptionsChoiceBuilder.builder()
.allowNull(true)
.available(PasswordManagerAgentStrategy.getClasses())
.available(List.of(PasswordManagerAgentStrategy.Agent.class))
.property(agentStrategy)
.build();
@@ -83,7 +89,10 @@ public class BitwardenPasswordManager implements PasswordManager {
.addComp(testButton)
.nameAndDescription("passwordManagerAgentStrategy")
.sub(agentStrategyChoice.build(), agentStrategy)
.nonNull();
.nonNull()
.bind(() -> {
return BitwardenPasswordManager.builder().agentStrategy(agentStrategy.getValue()).build();
}, p);
}
@@ -258,12 +267,12 @@ public class BitwardenPasswordManager implements PasswordManager {
return new PasswordManagerKeyStrategy() {
@Override
public boolean supportsInlineSshKeys() {
return agentStrategy != null && agentStrategy.getSshIdentityStrategy() == null;
return true;
}
@Override
public boolean supportsAgent() {
return agentStrategy != null && agentStrategy.getSshIdentityStrategy() != null;
return agentStrategy != null;
}
@Override
@@ -273,7 +282,7 @@ public class BitwardenPasswordManager implements PasswordManager {
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return agentStrategy != null ? agentStrategy.getSshIdentityStrategy() : null;
return agentStrategy.getSshIdentityStrategy();
}
};
}
@@ -3,9 +3,11 @@ package io.xpipe.app.pwman;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.cred.SshIdentityStrategy;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.DerivedObservableList;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.LocalShell;
import io.xpipe.app.util.*;
@@ -14,6 +16,7 @@ import io.xpipe.core.OsType;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import com.fasterxml.jackson.annotation.JsonTypeName;
@@ -38,14 +41,42 @@ public class KeePassXcPasswordManager implements PasswordManager {
private static KeePassXcProxyClient client;
private final List<KeePassXcAssociationKey> associationKeys;
private final PasswordManagerAgentStrategy agentStrategy;
@Override
public PasswordManagerKeyStrategy getKeyStrategy() {
return PasswordManagerKeyStrategy.none();
return new PasswordManagerKeyStrategy() {
@Override
public boolean supportsInlineSshKeys() {
return false;
}
@Override
public boolean supportsAgent() {
return agentStrategy != null;
}
@Override
public boolean supportsJoinedEntries() {
return false;
}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return agentStrategy.getSshIdentityStrategy();
}
};
}
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<KeePassXcPasswordManager> p) {
var agentStrategy = new SimpleObjectProperty<>(p.getValue().getAgentStrategy());
var agentStrategyChoice = OptionsChoiceBuilder.builder()
.allowNull(true)
.available(List.of(PasswordManagerAgentStrategy.KeePassXcOpenSshAgent.class, PasswordManagerAgentStrategy.KeePassXcPageant.class))
.property(agentStrategy)
.build();
var prop = FXCollections.<KeePassXcAssociationKey>observableArrayList();
p.subscribe(keePassXcManager -> {
DerivedObservableList.wrap(prop, true)
@@ -83,9 +114,11 @@ public class KeePassXcPasswordManager implements PasswordManager {
}))
.hide(Bindings.isEmpty(prop))
.addProperty(prop)
.nameAndDescription("passwordManagerAgentStrategy")
.sub(agentStrategyChoice.build(), agentStrategy)
.bind(
() -> {
return new KeePassXcPasswordManager(prop);
return new KeePassXcPasswordManager(prop, agentStrategy.getValue());
},
p);
}
@@ -41,9 +41,11 @@ public class KeeperPasswordManager implements PasswordManager {
@Override
public PasswordManagerKeyStrategy getKeyStrategy() {
return PasswordManagerKeyStrategy.none();
return PasswordManagerKeyStrategy.of(true, true, agentStrategy);
}
private final PasswordManagerAgentStrategy agentStrategy;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface KeeperAuth {
@@ -402,6 +404,7 @@ public class KeeperPasswordManager implements PasswordManager {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<KeeperPasswordManager> p) {
var agentStrategy = new SimpleObjectProperty<>(p.getValue().getAgentStrategy());
var mfa = new SimpleObjectProperty<>(p.getValue().getTwoFactorAuth() != null ? p.getValue().getTwoFactorAuth() : new KeeperAuth.None());
var choice = OptionsChoiceBuilder.builder()
@@ -409,10 +412,19 @@ public class KeeperPasswordManager implements PasswordManager {
.available(KeeperAuth.getClasses())
.property(mfa)
.build();
var agentStrategyChoice = OptionsChoiceBuilder.builder()
.allowNull(true)
.available(List.of(PasswordManagerAgentStrategy.Agent.class))
.property(agentStrategy)
.build();
return new OptionsBuilder()
.nameAndDescription("keeper2fa")
.sub(choice.build(), mfa)
.nameAndDescription("passwordManagerAgentStrategy")
.sub(agentStrategyChoice.build(), agentStrategy)
.nonNull()
.bind(
() -> {
return KeeperPasswordManager.builder()
@@ -1,29 +1,68 @@
package io.xpipe.app.pwman;
import com.fasterxml.jackson.databind.JsonNode;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.CommandSupport;
import io.xpipe.app.process.ProcessOutputException;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.JacksonMapper;
import com.fasterxml.jackson.annotation.JsonTypeName;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;
@JsonTypeName("onePassword")
@Builder
@Jacksonized
@Getter
public class OnePasswordManager implements PasswordManager {
@Override
public PasswordManagerKeyStrategy getKeyStrategy() {
return PasswordManagerKeyStrategy.none();
return PasswordManagerKeyStrategy.of(true, false, agentStrategy);
}
private static ShellControl SHELL;
private final PasswordManagerAgentStrategy agentStrategy;
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<OnePasswordManager> p) {
var agentStrategy = new SimpleObjectProperty<>(p.getValue().getAgentStrategy());
var agentStrategyChoice = OptionsChoiceBuilder.builder()
.allowNull(true)
.available(List.of(PasswordManagerAgentStrategy.Agent.class))
.property(agentStrategy)
.build();
return new OptionsBuilder()
.nameAndDescription("passwordManagerAgentStrategy")
.sub(agentStrategyChoice.build(), agentStrategy)
.nonNull()
.bind(() -> {
return OnePasswordManager.builder().agentStrategy(agentStrategy.getValue()).build();
}, p);
}
private static synchronized ShellControl getOrStartShell() throws Exception {
if (SHELL == null) {
@@ -23,12 +23,12 @@ public interface PasswordManager {
l.add(OnePasswordManager.class);
l.add(KeePassXcPasswordManager.class);
l.add(BitwardenPasswordManager.class);
l.add(DashlanePasswordManager.class);
l.add(KeeperPasswordManager.class);
if (OsType.ofLocal() != OsType.WINDOWS) {
l.add(LastpassPasswordManager.class);
l.add(EnpassPasswordManager.class);
}
l.add(KeeperPasswordManager.class);
l.add(DashlanePasswordManager.class);
l.add(PsonoPasswordManager.class);
l.add(PassboltPasswordManager.class);
if (OsType.ofLocal() == OsType.WINDOWS) {
@@ -1,12 +1,15 @@
package io.xpipe.app.pwman;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.IntegratedTextAreaComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.cred.OpenSshAgentStrategy;
import io.xpipe.app.cred.PageantStrategy;
import io.xpipe.app.cred.SshAgentTestComp;
import io.xpipe.app.cred.SshIdentityStrategy;
import io.xpipe.app.ext.ShellDialectChoiceComp;
import io.xpipe.app.ext.ValidationException;
@@ -17,15 +20,18 @@ import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.HttpHelper;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import io.xpipe.core.UuidHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -41,10 +47,6 @@ import java.util.ArrayList;
import java.util.List;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = PasswordManagerAgentStrategy.Inline.class),
@JsonSubTypes.Type(value = PasswordManagerAgentStrategy.Agent.class)
})
public interface PasswordManagerAgentStrategy {
@JsonTypeName("inline")
@@ -58,9 +60,6 @@ public interface PasswordManagerAgentStrategy {
return "inlineKey";
}
@Override
public void checkComplete() {}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return null;
@@ -73,6 +72,8 @@ public interface PasswordManagerAgentStrategy {
@Builder
class Agent implements PasswordManagerAgentStrategy {
FilePath customSocket;
@SuppressWarnings("unused")
public static String getOptionsNameKey() {
return "keyAgent";
@@ -80,24 +81,82 @@ public interface PasswordManagerAgentStrategy {
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<Agent> property) {
var customSocket = new SimpleObjectProperty<>(property.getValue().getCustomSocket());
var choice = new ContextualFileReferenceChoiceComp(
new ReadOnlyObjectWrapper<>(DataStorage.get().local().ref()),
customSocket,
null,
List.of(),
e -> e.equals(DataStorage.get().local()),
false);
return new OptionsBuilder()
.addComp(new SshAgentTestComp(Bindings.createObjectBinding(() -> {
return property.getValue().getSshIdentityStrategy();
}, property)))
.nameAndDescription("passwordManagerSshAgentSocket")
.addComp(choice, customSocket)
.hide(OsType.ofLocal() == OsType.WINDOWS)
.bind(
() -> Agent.builder()
.customSocket(customSocket.get())
.build(),
property);
}
@Override
public void checkComplete() throws ValidationException {
}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return OpenSshAgentStrategy.builder().build();
}
}
void checkComplete() throws ValidationException;
@JsonTypeName("keePassXcOpenSshAgent")
@Value
@Jacksonized
@Builder
class KeePassXcOpenSshAgent implements PasswordManagerAgentStrategy {
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<KeePassXcOpenSshAgent> property) {
return new OptionsBuilder()
.addComp(new SshAgentTestComp(Bindings.createObjectBinding(() -> {
return property.getValue().getSshIdentityStrategy();
}, property)))
.bind(
() -> KeePassXcOpenSshAgent.builder().build(),
property);
}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return OpenSshAgentStrategy.builder().build();
}
}
@JsonTypeName("keePassXcPageant")
@Value
@Jacksonized
@Builder
class KeePassXcPageant implements PasswordManagerAgentStrategy {
@SuppressWarnings("unused")
static OptionsBuilder createOptions(Property<KeePassXcPageant> property) {
return new OptionsBuilder()
.addComp(new SshAgentTestComp(Bindings.createObjectBinding(() -> {
return property.getValue().getSshIdentityStrategy();
}, property)))
.bind(
() -> KeePassXcPageant.builder().build(),
property);
}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return PageantStrategy.builder().build();
}
}
SshIdentityStrategy getSshIdentityStrategy();
@@ -105,6 +164,8 @@ public interface PasswordManagerAgentStrategy {
var l = new ArrayList<Class<?>>();
l.add(Inline.class);
l.add(Agent.class);
l.add(KeePassXcOpenSshAgent.class);
l.add(KeePassXcPageant.class);
return l;
}
}
@@ -4,6 +4,30 @@ import io.xpipe.app.cred.SshIdentityStrategy;
public interface PasswordManagerKeyStrategy {
static PasswordManagerKeyStrategy of(boolean inline, boolean joined, PasswordManagerAgentStrategy strategy) {
return new PasswordManagerKeyStrategy() {
@Override
public boolean supportsInlineSshKeys() {
return inline;
}
@Override
public boolean supportsAgent() {
return strategy != null;
}
@Override
public boolean supportsJoinedEntries() {
return joined;
}
@Override
public SshIdentityStrategy getSshIdentityStrategy() {
return strategy.getSshIdentityStrategy();
}
};
}
static PasswordManagerKeyStrategy none() {
return new PasswordManagerKeyStrategy() {
@Override
@@ -1,13 +1,11 @@
package io.xpipe.app.util;
import io.xpipe.app.cred.SshIdentityStrategy;
import io.xpipe.app.ext.HostAddress;
import io.xpipe.app.process.ShellDialect;
import io.xpipe.app.process.ShellDialects;
import io.xpipe.app.process.ShellScript;
import io.xpipe.app.pwman.KeePassXcAssociationKey;
import io.xpipe.app.pwman.KeePassXcPasswordManager;
import io.xpipe.app.pwman.KeeperPasswordManager;
import io.xpipe.app.pwman.PasswordManager;
import io.xpipe.app.pwman.*;
import io.xpipe.app.rdp.ExternalRdpClient;
import io.xpipe.app.secret.*;
import io.xpipe.app.spice.ExternalSpiceClient;
@@ -80,6 +78,8 @@ public class AppJacksonModule extends SimpleModule {
context.registerSubtypes(new NamedType(t.getClass()));
}
context.registerSubtypes(SshIdentityStrategy.getClasses());
context.registerSubtypes(PasswordManagerAgentStrategy.getClasses());
context.registerSubtypes(PasswordManager.getClasses());
context.registerSubtypes(TerminalMultiplexer.getClasses());
context.registerSubtypes(TerminalPrompt.getClasses());
@@ -85,7 +85,7 @@ public class IdentityChoiceBuilder {
.allowNull(false)
.property(identity)
.customConfiguration(config)
.available(SshIdentityStrategy.getSubclasses())
.available(SshIdentityStrategy.getClasses())
.transformer(entryComboBox -> {
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2k-key-plus"), () -> {
ProcessControlProvider.get().showSshKeygenDialog(null, identity);
+12 -1
View File
@@ -1627,7 +1627,8 @@ pkcs11Library=PKCS#11 library
pkcs11LibraryDescription=The path of the dynamically linked library file
sshAgentSocket=Custom SSH agent socket
sshAgentSocketDescription=The custom socket to use to communicate with the SSH agent. This custom agent can be used for a connection by selecting the custom agent option for it.
publicKey=Public key identifier
#force
publicKey=Key selector
publicKeyDescription=The optional public key to force the agent to only offer the matching private key
actions=Actions
hcloudServer.displayName=Hetzner cloud server
@@ -1998,3 +1999,13 @@ inlineKey=Retrieve directly from CLI
keyAgent=Use agent integration
passwordManagerAgentStrategy=SSH key retrieval method
passwordManagerAgentStrategyDescription=How and where to retrieve any stored SSH keys from.
sshAgentNoKeys=The agent does not have any keys at the moment
sshAgentHasKeys=The agent currently offers the following keys:
passwordManagerNoAgentSupport=Password manager does not support retrieving SSH keys
passwordManagerEmpty=No password manager is configured
passwordManagerSshKeyConfig=Password manager configuration
passwordManagerSshKeyConfigDescription=Check the status of the password manager
keePassXcOpenSshAgent=Use OpenSSH agent
keePassXcPageant=Use Pageant
passwordManagerSshAgentSocket=SSH agent socket
passwordManagerSshAgentSocketDescription=Override the default agent socket location that should be used.