Merge branch 'security-keys' into 23-release

This commit is contained in:
crschnick
2026-04-22 17:47:53 +00:00
parent d790b378b5
commit c98fc955b5
23 changed files with 439 additions and 335 deletions
@@ -4,12 +4,14 @@ 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.ext.ValidationException;
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.app.util.DocumentationLink;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
@@ -95,6 +97,11 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
String publicKey;
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(AppPrefs.get().defaultSshAgentSocket().getValue());
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
if (parent.isLocal()) {
@@ -107,7 +114,7 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
if (!sc.isLocal() || sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -136,7 +143,7 @@ public class CustomAgentStrategy implements SshIdentityAgentStrategy {
KeyValue.raw("IdentityFile", file.isPresent() ? file.get().toString() : "none"),
KeyValue.raw("PKCS11Provider", "none")));
var agent = determinetAgentSocketLocation(sc);
var agent = determineAgentSocketLocation(sc);
if (agent != null) {
l.add(KeyValue.escape("IdentityAgent", agent));
}
@@ -1,122 +0,0 @@
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.util.List;
@Value
@Jacksonized
@Builder
@JsonTypeName("customPkcs11")
@AllArgsConstructor
public class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {
@SuppressWarnings("unused")
public static String getOptionsNameKey() {
return "customPkcs11Library";
}
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<CustomPkcs11LibraryStrategy> p, SshIdentityStrategyChoiceConfig config) {
var file =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getFile() : null);
return new OptionsBuilder()
.nameAndDescription("pkcs11Library")
.addComp(
new ContextualFileReferenceChoiceComp(
config.getFileSystem() != null
? config.getFileSystem()
: new ReadOnlyObjectWrapper<>(
DataStorage.get().local().ref()),
file,
null,
List.of(),
e -> {
if (config.getFileSystem() == null) {
return e.equals(DataStorage.get().local());
}
var fs = config.getFileSystem().getValue();
if (fs == null) {
return e.equals(DataStorage.get().local());
} else {
return e.equals(fs.get());
}
},
false),
file)
.nonNull()
.bind(
() -> {
return new CustomPkcs11LibraryStrategy(file.get());
},
p);
}
FilePath file;
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(file);
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
parent.requireLicensedFeature(LicenseProvider.get().getFeature("pkcs11Identity"));
if (!parent.getShellDialect()
.createFileExistsCommand(parent, file.toString())
.executeAndCheck()) {
throw ErrorEventFactory.expected(new IOException("PKCS11 library at " + file + " not found"));
}
}
@Override
public void buildCommand(CommandBuilder builder) {
builder.setup(sc -> {
var dir = file.getParent();
if (sc.getOsType() == OsType.WINDOWS) {
builder.addToPath(dir, true);
} else {
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
}
});
}
@Override
public List<KeyValue> configOptions(ShellControl sc) {
return List.of(
KeyValue.raw("IdentitiesOnly", "no"),
KeyValue.escape("PKCS11Provider", file),
KeyValue.raw("IdentityFile", "none"),
KeyValue.raw("IdentityAgent", "none"));
}
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
}
@@ -2,6 +2,7 @@ package io.xpipe.app.cred;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
@@ -67,6 +68,11 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
String publicKey;
@Override
public void checkComplete() throws ValidationException {
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
parent.requireLicensedFeature(LicenseProvider.get().getFeature("gpgAgent"));
@@ -76,7 +82,7 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
if (sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -100,7 +106,7 @@ public class GpgAgentStrategy implements SshIdentityAgentStrategy {
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
KeyValue.raw("PKCS11Provider", "none")));
var agent = determinetAgentSocketLocation(sc);
var agent = determineAgentSocketLocation(sc);
if (agent != null) {
l.add(KeyValue.escape("IdentityAgent", agent));
}
@@ -1,6 +1,7 @@
package io.xpipe.app.cred;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
@@ -58,7 +59,12 @@ public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl sc) throws Exception {
public void checkComplete() throws ValidationException {
}
@Override
public FilePath determineAgentSocketLocation(ShellControl sc) throws Exception {
if (sc.getOsType() == OsType.WINDOWS) {
return null;
}
@@ -84,7 +90,7 @@ public class OpenSshAgentStrategy implements SshIdentityAgentStrategy {
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
KeyValue.raw("PKCS11Provider", "none")));
var agent = determinetAgentSocketLocation(sc);
var agent = determineAgentSocketLocation(sc);
if (agent != null) {
l.add(KeyValue.escape("IdentityAgent", agent));
}
@@ -1,5 +1,6 @@
package io.xpipe.app.cred;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
@@ -49,7 +50,12 @@ public class OtherExternalAgentStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
public void checkComplete() throws ValidationException {
}
@Override
public FilePath determineAgentSocketLocation(ShellControl parent) {
return null;
}
@@ -1,6 +1,7 @@
package io.xpipe.app.cred;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
@@ -87,7 +88,7 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl sc) {
public FilePath determineAgentSocketLocation(ShellControl sc) {
if (sc.isLocal() && sc.getOsType() == OsType.WINDOWS) {
return FilePath.of(getPageantWindowsPipe());
}
@@ -95,6 +96,11 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
return null;
}
@Override
public void checkComplete() throws ValidationException {
}
@Override
public void buildCommand(CommandBuilder builder) {}
@@ -106,7 +112,7 @@ public class PageantStrategy implements SshIdentityAgentStrategy {
KeyValue.escape("IdentityFile", file.isPresent() ? file.get() : "none"),
KeyValue.raw("PKCS11Provider", "none")));
var agent = determinetAgentSocketLocation(sc);
var agent = determineAgentSocketLocation(sc);
if (agent != null) {
l.add(KeyValue.escape("IdentityAgent", agent));
}
@@ -11,7 +11,6 @@ import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.pwman.PasswordManagerKeyConfiguration;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
@@ -126,7 +125,7 @@ public class PasswordManagerAgentStrategy implements SshIdentityAgentStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
public FilePath determineAgentSocketLocation(ShellControl parent) {
var config = getConfig();
return config != null ? FilePath.of(config.getDefaultSocketLocation()) : null;
}
@@ -1,83 +0,0 @@
package io.xpipe.app.cred;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.App;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@JsonTypeName("passwordManagerInPlaceKey")
@Value
@Jacksonized
@Builder
public class PasswordManagerInPlaceKeyStrategy implements SshIdentityAgentStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<PasswordManagerInPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
var options = new OptionsBuilder();
var prefs = AppPrefs.get();
var keyProperty = options.map(p, PasswordManagerInPlaceKeyStrategy::getKey);
var field = new TextFieldComp(keyProperty).apply(struc -> struc.promptTextProperty()
.bind(Bindings.createStringBinding(
() -> {
return prefs.passwordManager().getValue() != null
? prefs.passwordManager().getValue().getKeyPlaceholder()
: "?";
},
prefs.passwordManager())));
var button = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
AppPrefs.get().selectCategory("passwordManager");
App.getApp().getStage().requestFocus();
});
var content = new InputGroupComp(List.of(field, button));
content.setMainReference(field);
return options.nameAndDescription("passwordManagerInPlaceKeyKey")
.addComp(content, keyProperty)
.nonNull()
.bind(
() -> {
return PasswordManagerInPlaceKeyStrategy.builder()
.key(keyProperty.get())
.build();
},
p);
}
String key;
@Override
public void prepareParent(ShellControl parent) {}
@Override
public void buildCommand(CommandBuilder builder) {}
@Override
public List<KeyValue> configOptions(ShellControl sc) {
return List.of();
}
@Override
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) {
return null;
}
}
@@ -0,0 +1,206 @@
package io.xpipe.app.cred;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
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;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface SecurityKeyImpl {
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
l.add(OpenSc.class);
l.add(YubikeyPiv.class);
l.add(MacOsKeychain.class);
l.add(Custom.class);
return l;
}
static List<Class<?>> getAvailable() {
var l = new ArrayList<Class<?>>();
l.add(OpenSc.class);
l.add(YubikeyPiv.class);
if (OsType.ofLocal() == OsType.MACOS) {
l.add(MacOsKeychain.class);
}
l.add(Custom.class);
return l;
}
default void checkComplete() throws ValidationException {}
FilePath determineLibraryPath(ShellControl sc) throws Exception;
@JsonTypeName("yubikeyPiv")
@Value
@Jacksonized
@Builder
class YubikeyPiv implements SecurityKeyImpl {
@Override
public FilePath determineLibraryPath(ShellControl sc) throws Exception {
var file =
switch (sc.getOsType()) {
case OsType.MacOs ignored -> FilePath.of("/usr/local/lib/libykcs11.dylib");
case OsType.Windows ignored -> {
var x64 = FilePath.of(
sc.view().getEnvironmentVariableOrThrow("ProgramFiles"),
"Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll");
if (sc.view().fileExists(x64)) {
yield x64;
}
var x86 = FilePath.of(
sc.view().getEnvironmentVariableOrThrow("ProgramFiles(x86)"),
"Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll");
if (sc.view().fileExists(x86)) {
yield x86;
}
yield x64;
}
default -> FilePath.of("/usr/local/lib/libykcs11.so");
};
return file;
}
}
@JsonTypeName("openSc")
@Value
@Jacksonized
@Builder
class OpenSc implements SecurityKeyImpl {
@Override
public FilePath determineLibraryPath(ShellControl sc) throws Exception {
var file =
switch (sc.getOsType()) {
case OsType.MacOs ignored -> FilePath.of("/Library/OpenSC/lib/opensc-pkcs11.so");
case OsType.Windows ignored -> {
var x64 = FilePath.of(
sc.view().getEnvironmentVariableOrThrow("ProgramFiles"),
"OpenSC Project\\OpenSC\\pkcs11\\opensc-pkcs11.dll");
if (sc.view().fileExists(x64)) {
yield x64;
}
var x86 = FilePath.of(
sc.view().getEnvironmentVariableOrThrow("ProgramFiles(x86)"),
"OpenSC Project\\OpenSC\\pkcs11\\opensc-pkcs11.dll");
if (sc.view().fileExists(x86)) {
yield x86;
}
yield x64;
}
default -> FilePath.of("/usr/lib/pkcs11/opensc-pkcs11.so");
};
return file;
}
}
@JsonTypeName("macOsKeychain")
@Value
@Jacksonized
@Builder
class MacOsKeychain implements SecurityKeyImpl {
@Override
public FilePath determineLibraryPath(ShellControl sc) throws Exception {
var file =
switch (sc.getOsType()) {
case OsType.MacOs ignored -> FilePath.of("/usr/lib/ssh-keychain.dylib");
default -> throw ErrorEventFactory.expected(new UnsupportedOperationException("macOS keychain is not supported on other operating systems"));
};
return file;
}
}
@JsonTypeName("customLibrary")
@Value
@Jacksonized
@Builder
class Custom implements SecurityKeyImpl {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<Custom> p, SshIdentityStrategyChoiceConfig config) {
var file = new SimpleObjectProperty<>(p.getValue().getFile());
return new OptionsBuilder()
.nameAndDescription("pkcs11Library")
.addComp(
new ContextualFileReferenceChoiceComp(
config.getFileSystem() != null
? config.getFileSystem()
: new ReadOnlyObjectWrapper<>(
DataStorage.get().local().ref()),
file,
null,
List.of(),
e -> {
if (config.getFileSystem() == null) {
return e.equals(DataStorage.get().local());
}
var fs = config.getFileSystem().getValue();
if (fs == null) {
return e.equals(DataStorage.get().local());
} else {
return e.equals(fs.get());
}
},
false),
file)
.nonNull()
.bind(
() -> {
return new Custom(file.get());
},
p);
}
FilePath file;
@Override
public FilePath determineLibraryPath(ShellControl sc) {
return file;
}
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(file);
}
}
}
@@ -0,0 +1,118 @@
package io.xpipe.app.cred;
import com.fasterxml.jackson.annotation.JsonTypeName;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.core.AppInstallation;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ValidationException;
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.ShellControl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.util.List;
@Value
@Jacksonized
@Builder
@JsonTypeName("hardwareSecurityKey")
@AllArgsConstructor
public class SecurityKeyStrategy implements SshIdentityKeyListStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<SecurityKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
var publicKey = new SimpleStringProperty(p.getValue().getPublicKey());
var securityKey = new SimpleObjectProperty<>(p.getValue().getSecurityKey());
var choice = OptionsChoiceBuilder.builder().property(securityKey).available(SecurityKeyImpl.getAvailable()).customConfiguration(config).build().build();
return new OptionsBuilder()
.nameAndDescription("pkcs11Impl")
.sub(choice, securityKey)
.nonNull()
.nameAndDescription("publicKey")
.documentationLink(DocumentationLink.SSH_AGENT_PUBLIC_KEYS)
.addComp(new SshAgentKeyListComp(config.getFileSystem(), p, publicKey, false), publicKey)
.bind(
() -> {
return SecurityKeyStrategy.builder().securityKey(securityKey.get()).publicKey(publicKey.get()).build();
},
p);
}
SecurityKeyImpl securityKey;
String publicKey;
@Override
public void checkComplete() throws ValidationException {
Validators.nonNull(securityKey);
securityKey.checkComplete();
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
parent.requireLicensedFeature(LicenseProvider.get().getFeature("pkcs11Identity"));
var file = securityKey.determineLibraryPath(parent);
if (!parent.view().fileExists(file)) {
throw ErrorEventFactory.expected(new IOException("PKCS11 library at " + file + " not found"));
}
}
@Override
public CommandBuilder createListCommand() {
var cmd = CommandBuilder.of().add("ssh-keygen", "-D")
.addFile(sc -> securityKey.determineLibraryPath(sc).toUnix()).add("-e")
.fixedEnvironment("SSH_ASKPASS", AppInstallation.ofCurrent().getCliExecutablePath().toString())
.fixedEnvironment("SSH_ASKPASS_REQUIRE", "force");
ProcessControlProvider.get().addAskpassEnvironment(cmd, "[ssh-keygen]", null, null);
return cmd;
}
@Override
public void buildCommand(CommandBuilder builder) {
builder.setup(sc -> {
var dir = securityKey.determineLibraryPath(sc).getParent();
if (sc.getOsType() == OsType.WINDOWS) {
builder.addToPath(dir, true);
} else {
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
}
});
}
@Override
public List<KeyValue> configOptions(ShellControl sc) throws Exception {
var file = securityKey.determineLibraryPath(sc);
var key = SshIdentityStrategy.getPublicKeyPath(sc, publicKey);
return List.of(
new KeyValue("PKCS11Provider", "\"" + file.toString() + "\""),
new KeyValue("IdentitiesOnly", key.isPresent() ? "yes" : "no"),
new KeyValue("IdentityFile", key.isPresent() ? key.get().toString() : "none"),
new KeyValue("IdentityAgent", "none"));
}
@Override
public PublicKeyStrategy getPublicKeyStrategy() {
return PublicKeyStrategy.Fixed.of(publicKey);
}
}
@@ -30,7 +30,7 @@ public class SshAgentKeyList {
}
public static Entry findAgentIdentity(
DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy, String identifier) throws Exception {
DataStoreEntryRef<ShellStore> ref, SshIdentityKeyListStrategy strategy, String identifier) throws Exception {
var all = listAgentIdentities(ref, strategy);
var list = all.stream()
.filter(entry -> {
@@ -73,16 +73,14 @@ public class SshAgentKeyList {
return list.getFirst();
}
public static List<Entry> listAgentIdentities(DataStoreEntryRef<ShellStore> ref, SshIdentityAgentStrategy strategy)
public static List<Entry> listAgentIdentities(DataStoreEntryRef<ShellStore> ref, SshIdentityKeyListStrategy strategy)
throws Exception {
var session = ref != null ? ref.getStore().getOrStartSession() : LocalShell.getShell();
strategy.prepareParent(session);
var socket = strategy.determinetAgentSocketLocation(session);
var out = session.command(CommandBuilder.of()
.add("ssh-add", "-L")
.fixedEnvironment("SSH_AUTH_SOCK", socket != null ? socket.toString() : null))
.readStdoutOrThrow();
var cmd = strategy.createListCommand();
strategy.buildCommand(cmd);
var out = session.command(cmd).readStdoutOrThrow();
var pattern = Pattern.compile("([^ ]+) ([^ ]+)\\s*(?: (.+))?");
var lines = out.lines().toList();
var list = new ArrayList<Entry>();
@@ -12,6 +12,7 @@ import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
@@ -29,13 +30,13 @@ import java.util.List;
public class SshAgentKeyListComp extends SimpleRegionBuilder {
private final ObservableValue<DataStoreEntryRef<ShellStore>> ref;
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
private final ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy;
private final StringProperty value;
private final boolean useKeyNames;
public SshAgentKeyListComp(
ObservableValue<DataStoreEntryRef<ShellStore>> ref,
ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy,
ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy,
StringProperty value,
boolean useKeyNames) {
this.ref = ref;
@@ -50,6 +51,14 @@ public class SshAgentKeyListComp extends SimpleRegionBuilder {
field.apply(struc -> struc.setPromptText(
useKeyNames ? "<name>" : "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== <key comment>"));
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2m-magnify-scan"), null);
button.disable(Bindings.createBooleanBinding(() -> {
try {
sshIdentityStrategy.getValue().checkComplete();
return false;
} catch (Exception e) {
return true;
}
}, sshIdentityStrategy));
button.apply(struc -> {
struc.setOnAction(event -> {
DataStoreEntryRef<ShellStore> refToUse = ref != null && ref.getValue() != null
@@ -24,10 +24,10 @@ import atlantafx.base.theme.Styles;
public class SshAgentTestComp extends SimpleRegionBuilder {
private final Runnable beforeTest;
private final ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy;
private final ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy;
public SshAgentTestComp(
Runnable beforeTest, ObservableValue<? extends SshIdentityAgentStrategy> sshIdentityStrategy) {
Runnable beforeTest, ObservableValue<? extends SshIdentityKeyListStrategy> sshIdentityStrategy) {
this.beforeTest = beforeTest;
this.sshIdentityStrategy = sshIdentityStrategy;
}
@@ -1,11 +1,19 @@
package io.xpipe.app.cred;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
public interface SshIdentityAgentStrategy extends SshIdentityStrategy {
public interface SshIdentityAgentStrategy extends SshIdentityKeyListStrategy {
void prepareParent(ShellControl parent) throws Exception;
@Override
default CommandBuilder createListCommand() {
return CommandBuilder.of().add("ssh-add", "-L")
.environment("SSH_AUTH_SOCK", sc -> {
var socket = determineAgentSocketLocation(sc);
return socket != null ? socket.toString() : null;
});
}
FilePath determinetAgentSocketLocation(ShellControl parent) throws Exception;
FilePath determineAgentSocketLocation(ShellControl parent) throws Exception;
}
@@ -0,0 +1,15 @@
package io.xpipe.app.cred;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.core.FilePath;
public interface SshIdentityKeyListStrategy extends SshIdentityStrategy {
void checkComplete() throws ValidationException;
void prepareParent(ShellControl parent) throws Exception;
CommandBuilder createListCommand();
}
@@ -36,8 +36,7 @@ public interface SshIdentityStrategy {
if (PageantStrategy.isSupported()) {
l.add(PageantStrategy.class);
}
l.add(YubikeyPivStrategy.class);
l.add(CustomPkcs11LibraryStrategy.class);
l.add(SecurityKeyStrategy.class);
l.add(OtherExternalAgentStrategy.class);
return l;
@@ -50,12 +49,10 @@ public interface SshIdentityStrategy {
l.add(KeyFileStrategy.class);
l.add(OpenSshAgentStrategy.class);
l.add(PasswordManagerAgentStrategy.class);
l.add(PasswordManagerInPlaceKeyStrategy.class);
l.add(CustomAgentStrategy.class);
l.add(GpgAgentStrategy.class);
l.add(PageantStrategy.class);
l.add(YubikeyPivStrategy.class);
l.add(CustomPkcs11LibraryStrategy.class);
l.add(SecurityKeyStrategy.class);
l.add(OtherExternalAgentStrategy.class);
return l;
@@ -1,87 +0,0 @@
package io.xpipe.app.cred;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.core.FilePath;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@Value
@Jacksonized
@Builder
@JsonTypeName("yubikeyPiv")
@AllArgsConstructor
public class YubikeyPivStrategy implements SshIdentityStrategy {
private String getFile(ShellControl sc) {
var file =
switch (sc.getOsType()) {
case OsType.MacOs ignored -> "/usr/local/lib/libykcs11.dylib";
case OsType.Windows ignored -> {
var x64 = "C:\\Program Files\\Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll";
if (Files.exists(Path.of(x64))) {
yield x64;
}
var x86 = "C:\\Program Files (x86)\\Yubico\\Yubico PIV Tool\\bin\\libykcs11.dll";
if (Files.exists(Path.of(x86))) {
yield x86;
}
yield x64;
}
default -> "/usr/local/lib/libykcs11.so";
};
return file;
}
@Override
public void prepareParent(ShellControl parent) throws Exception {
parent.requireLicensedFeature(LicenseProvider.get().getFeature("pkcs11Identity"));
var file = getFile(parent);
if (!parent.getShellDialect().createFileExistsCommand(parent, file).executeAndCheck()) {
throw ErrorEventFactory.expected(new IOException("Yubikey PKCS11 library at " + file + " not found"));
}
}
@Override
public void buildCommand(CommandBuilder builder) {
builder.setup(sc -> {
var file = getFile(sc);
var dir = FilePath.of(file).getParent();
if (sc.getOsType() == OsType.WINDOWS) {
builder.addToPath(dir, true);
} else {
builder.addToEnvironmentPath("LD_LIBRARY_PATH", dir, true);
}
});
}
@Override
public List<KeyValue> configOptions(ShellControl sc) {
return List.of(
KeyValue.raw("IdentitiesOnly", "no"),
KeyValue.escape("PKCS11Provider", getFile(sc)),
KeyValue.raw("IdentityFile", "none"),
KeyValue.raw("IdentityAgent", "none"));
}
@Override
public PublicKeyStrategy getPublicKeyStrategy() {
return null;
}
}
@@ -23,6 +23,7 @@ import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.UUID;
public abstract class ProcessControlProvider {
@@ -87,4 +88,5 @@ public abstract class ProcessControlProvider {
public abstract void pullRepository(Path target) throws Exception;
public abstract Optional<HttpProxy> getHttpProxy(DataStoreEntryRef<?> store);
public abstract void addAskpassEnvironment(CommandBuilder b, String prefix, UUID requestId, UUID secretId, String... askpassName);
}
@@ -1,6 +1,6 @@
package io.xpipe.app.pwman;
import io.xpipe.app.cred.SshIdentityAgentStrategy;
import io.xpipe.app.cred.SshIdentityKeyListStrategy;
import java.nio.file.Path;
@@ -29,7 +29,7 @@ public interface PasswordManagerKeyConfiguration {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return strategy.getSshIdentityStrategy(publicKey, forward);
}
@@ -58,7 +58,7 @@ public interface PasswordManagerKeyConfiguration {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return null;
}
@@ -75,7 +75,7 @@ public interface PasswordManagerKeyConfiguration {
boolean supportsAgentKeyNames();
SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward);
SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward);
Path getDefaultSocketLocation();
}
@@ -3,6 +3,7 @@ package io.xpipe.app.pwman;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.cred.*;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.process.CommandBuilder;
@@ -47,7 +48,7 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return null;
}
}
@@ -113,7 +114,7 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return PasswordManagerKeyStrategy.getAgentSshIdentityStrategy(
publicKey, forward, (socket) -> SshIdentityStateManager.prepareLocalExternalAgent(socket));
}
@@ -175,7 +176,7 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return PasswordManagerKeyStrategy.getAgentSshIdentityStrategy(publicKey, forward, (socket) -> {
try {
SshIdentityStateManager.prepareLocalExternalAgent(socket);
@@ -191,12 +192,15 @@ public interface PasswordManagerKeyStrategy {
}
}
private static SshIdentityAgentStrategy getAgentSshIdentityStrategy(
private static SshIdentityKeyListStrategy getAgentSshIdentityStrategy(
String publicKey, boolean forward, FailableConsumer<FilePath, Exception> con) {
var pwman = AppPrefs.get().passwordManager().getValue();
var socket = pwman != null ? FilePath.of(pwman.getKeyConfiguration().getDefaultSocketLocation()) : null;
return new SshIdentityAgentStrategy() {
@Override
public void checkComplete() throws ValidationException {}
@Override
public void prepareParent(ShellControl parent) throws Exception {
if (parent.isLocal()) {
@@ -205,7 +209,7 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public FilePath determinetAgentSocketLocation(ShellControl parent) throws Exception {
public FilePath determineAgentSocketLocation(ShellControl parent) throws Exception {
return socket != null ? socket.resolveTildeHome(parent.view().userHome()) : null;
}
@@ -258,7 +262,7 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return OpenSshAgentStrategy.builder().build();
}
}
@@ -288,14 +292,14 @@ public interface PasswordManagerKeyStrategy {
}
@Override
public SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
public SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward) {
return PageantStrategy.builder().build();
}
}
boolean useAgent();
SshIdentityAgentStrategy getSshIdentityStrategy(String publicKey, boolean forward);
SshIdentityKeyListStrategy getSshIdentityStrategy(String publicKey, boolean forward);
static List<Class<?>> getClasses() {
var l = new ArrayList<Class<?>>();
@@ -1,5 +1,6 @@
package io.xpipe.app.util;
import io.xpipe.app.cred.SecurityKeyImpl;
import io.xpipe.app.cred.SshIdentityStrategy;
import io.xpipe.app.ext.HostAddress;
import io.xpipe.app.process.ShellDialect;
@@ -78,6 +79,7 @@ public class AppJacksonModule extends SimpleModule {
context.registerSubtypes(new NamedType(t.getClass()));
}
context.registerSubtypes(SecurityKeyImpl.getClasses());
context.registerSubtypes(SshIdentityStrategy.getClasses());
context.registerSubtypes(PasswordManagerKeyStrategy.getClasses());
context.registerSubtypes(PasswordManager.getClasses());
+1
View File
@@ -169,4 +169,5 @@ protonPass=Proton Pass
passwork=Passwork
passworkPlaceholder=Item ID
socks5=SOCKS5
openSc=OpenSC
+7 -1
View File
@@ -841,7 +841,8 @@ sshAgent=OpenSSH agent
#context: nothing selected
none=None
index=Index ...
otherExternal=Other external agent
#force
otherExternal=Other external auth
sync=Sync
vaultSync=Vault sync
customUsername=Username
@@ -1640,6 +1641,8 @@ k8sEditPodTitle=Apply changes
k8sEditPodContent=Do you want to apply the changes made via the command kubectl apply? A restart is likely required for changes to apply.
virshEditDomainTitle=Apply changes
virshEditDomainContent=Do you want to apply the changes to the domain? A restart is likely required for changes to apply.
pkcs11Impl=PKCS#11 implementation
pkcs11ImplDescription=The PKCS#11 implementation to use to retrieve hardware keys
pkcs11Library=PKCS#11 library
pkcs11LibraryDescription=The path of the dynamically linked library file
sshAgentSocket=Custom SSH agent socket
@@ -2100,3 +2103,6 @@ addProxy=Add proxy ...
httpProxyError=Configure HTTP proxy settings
httpProxyErrorDescription=Add HTTP proxy authentication details in the settings menu
systemDefault=System default
hardwareSecurityKey=Hardware security key (PKCS#11)
macOsKeychain=macOS keychain
customLibrary=Custom library