diff --git a/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java index 60d093f2b..ae65e083d 100644 --- a/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java @@ -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()); diff --git a/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java index cdef120d8..d32c08e0e 100644 --- a/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java @@ -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( diff --git a/app/src/main/java/io/xpipe/app/cred/InternalCustomAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/InternalCustomAgentStrategy.java deleted file mode 100644 index 819f2cab5..000000000 --- a/app/src/main/java/io/xpipe/app/cred/InternalCustomAgentStrategy.java +++ /dev/null @@ -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 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; - } -} diff --git a/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java index 18e7bb2c4..98434b9dd 100644 --- a/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java @@ -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()); diff --git a/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java index fb77e9687..0d74daf55 100644 --- a/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java @@ -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()); diff --git a/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java b/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java index 86a5400c3..1b1de5b90 100644 --- a/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java @@ -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(); } } diff --git a/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java new file mode 100644 index 000000000..4afb3aa04 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java @@ -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 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 configOptions(ShellControl sc) throws Exception { + var pwman = AppPrefs.get().passwordManager().getValue(); + var strat = pwman.getKeyStrategy().getSshIdentityStrategy(); + return strat.configOptions(sc); + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java new file mode 100644 index 000000000..fca3a5368 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java @@ -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 listAgentIdentities(DataStoreEntryRef 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(); + 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; + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java new file mode 100644 index 000000000..3871ed557 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java @@ -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> ref; + private final ObservableValue sshIdentityStrategy; + private final StringProperty value; + + public SshAgentKeyListComp(ObservableValue> ref, ObservableValue 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...== ")); + var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2m-magnify-scan"), null); + button.apply(struc -> { + struc.setOnAction(event -> { + DataStoreEntryRef 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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java new file mode 100644 index 000000000..bbceb3626 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java @@ -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 sshIdentityStrategy; + + public SshAgentTestComp(ObservableValue 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 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(); + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java b/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java index 59c3a7447..1aeef16b7 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java @@ -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> getSubclasses() { + static List> getClasses() { var l = new ArrayList>(); 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); } diff --git a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java index d515aab5c..48dda35b1 100644 --- a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/prefs/SshCategory.java b/app/src/main/java/io/xpipe/app/prefs/SshCategory.java index 5788d537e..74eb7dac8 100644 --- a/app/src/main/java/io/xpipe/app/prefs/SshCategory.java +++ b/app/src/main/java/io/xpipe/app/prefs/SshCategory.java @@ -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 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()), diff --git a/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java index 26e0828c1..68d5b9a6c 100644 --- a/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/BitwardenPasswordManager.java @@ -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(); } }; } diff --git a/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java index f180e6091..730aab0cb 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeePassXcPasswordManager.java @@ -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 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 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.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); } diff --git a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java index 57c00faf9..1460ad955 100644 --- a/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/KeeperPasswordManager.java @@ -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 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() diff --git a/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java index 161c5393c..002a51f4b 100644 --- a/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/OnePasswordManager.java @@ -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 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) { diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java index 390eb79b5..137e240a5 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManager.java @@ -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) { diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerAgentStrategy.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerAgentStrategy.java index 018680f6a..84d73b85d 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerAgentStrategy.java @@ -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 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 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 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>(); l.add(Inline.class); l.add(Agent.class); + l.add(KeePassXcOpenSshAgent.class); + l.add(KeePassXcPageant.class); return l; } } diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java index 4ace4458c..dcf720c48 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java @@ -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 diff --git a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java index 094be3c7b..5d6ccde38 100644 --- a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java @@ -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()); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java index 6048915ac..246b68cc6 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/identity/IdentityChoiceBuilder.java @@ -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); diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index 1bc12baf4..500755680 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -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.