From c98fc955b5034bd66dc9f91a6f0524a6a8aee332 Mon Sep 17 00:00:00 2001 From: crschnick Date: Wed, 22 Apr 2026 17:47:53 +0000 Subject: [PATCH] Merge branch 'security-keys' into 23-release --- .../xpipe/app/cred/CustomAgentStrategy.java | 11 +- .../app/cred/CustomPkcs11LibraryStrategy.java | 122 ----------- .../io/xpipe/app/cred/GpgAgentStrategy.java | 10 +- .../xpipe/app/cred/OpenSshAgentStrategy.java | 10 +- .../app/cred/OtherExternalAgentStrategy.java | 8 +- .../io/xpipe/app/cred/PageantStrategy.java | 10 +- .../cred/PasswordManagerAgentStrategy.java | 3 +- .../PasswordManagerInPlaceKeyStrategy.java | 83 ------- .../io/xpipe/app/cred/SecurityKeyImpl.java | 206 ++++++++++++++++++ .../xpipe/app/cred/SecurityKeyStrategy.java | 118 ++++++++++ .../io/xpipe/app/cred/SshAgentKeyList.java | 12 +- .../xpipe/app/cred/SshAgentKeyListComp.java | 13 +- .../io/xpipe/app/cred/SshAgentTestComp.java | 4 +- .../app/cred/SshIdentityAgentStrategy.java | 14 +- .../app/cred/SshIdentityKeyListStrategy.java | 15 ++ .../xpipe/app/cred/SshIdentityStrategy.java | 7 +- .../io/xpipe/app/cred/YubikeyPivStrategy.java | 87 -------- .../xpipe/app/ext/ProcessControlProvider.java | 2 + .../PasswordManagerKeyConfiguration.java | 8 +- .../app/pwman/PasswordManagerKeyStrategy.java | 20 +- .../io/xpipe/app/util/AppJacksonModule.java | 2 + lang/strings/fixed_en.properties | 1 + lang/strings/translations_en.properties | 8 +- 23 files changed, 439 insertions(+), 335 deletions(-) delete mode 100644 app/src/main/java/io/xpipe/app/cred/CustomPkcs11LibraryStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/PasswordManagerInPlaceKeyStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SecurityKeyImpl.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SecurityKeyStrategy.java create mode 100644 app/src/main/java/io/xpipe/app/cred/SshIdentityKeyListStrategy.java delete mode 100644 app/src/main/java/io/xpipe/app/cred/YubikeyPivStrategy.java 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 52a95f79f..0c557843e 100644 --- a/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/CustomAgentStrategy.java @@ -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)); } diff --git a/app/src/main/java/io/xpipe/app/cred/CustomPkcs11LibraryStrategy.java b/app/src/main/java/io/xpipe/app/cred/CustomPkcs11LibraryStrategy.java deleted file mode 100644 index 8ca31a892..000000000 --- a/app/src/main/java/io/xpipe/app/cred/CustomPkcs11LibraryStrategy.java +++ /dev/null @@ -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 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 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; - } -} 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 06ed055a0..700a718ab 100644 --- a/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/GpgAgentStrategy.java @@ -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)); } 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 f19a65341..e18407f62 100644 --- a/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/OpenSshAgentStrategy.java @@ -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)); } 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 53697fdaf..5cb5859e5 100644 --- a/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/OtherExternalAgentStrategy.java @@ -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; } 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 36d688a11..a699f3805 100644 --- a/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/PageantStrategy.java @@ -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)); } diff --git a/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java index cf85eadd8..737db3534 100644 --- a/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/PasswordManagerAgentStrategy.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/cred/PasswordManagerInPlaceKeyStrategy.java b/app/src/main/java/io/xpipe/app/cred/PasswordManagerInPlaceKeyStrategy.java deleted file mode 100644 index 17ec3c37a..000000000 --- a/app/src/main/java/io/xpipe/app/cred/PasswordManagerInPlaceKeyStrategy.java +++ /dev/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 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 configOptions(ShellControl sc) { - return List.of(); - } - - @Override - public PublicKeyStrategy getPublicKeyStrategy() { - return null; - } - - @Override - public FilePath determinetAgentSocketLocation(ShellControl parent) { - return null; - } -} diff --git a/app/src/main/java/io/xpipe/app/cred/SecurityKeyImpl.java b/app/src/main/java/io/xpipe/app/cred/SecurityKeyImpl.java new file mode 100644 index 000000000..c3e36873e --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SecurityKeyImpl.java @@ -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> getClasses() { + var l = new ArrayList>(); + l.add(OpenSc.class); + l.add(YubikeyPiv.class); + l.add(MacOsKeychain.class); + l.add(Custom.class); + return l; + } + + static List> getAvailable() { + var l = new ArrayList>(); + 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 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); + } + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SecurityKeyStrategy.java b/app/src/main/java/io/xpipe/app/cred/SecurityKeyStrategy.java new file mode 100644 index 000000000..c0b1fe54c --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SecurityKeyStrategy.java @@ -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 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 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); + } +} diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java index c3b619018..189460611 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyList.java @@ -30,7 +30,7 @@ public class SshAgentKeyList { } public static Entry findAgentIdentity( - DataStoreEntryRef ref, SshIdentityAgentStrategy strategy, String identifier) throws Exception { + DataStoreEntryRef 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 listAgentIdentities(DataStoreEntryRef ref, SshIdentityAgentStrategy strategy) + public static List listAgentIdentities(DataStoreEntryRef 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(); diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java index d052ae038..ce163a955 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentKeyListComp.java @@ -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> ref; - private final ObservableValue sshIdentityStrategy; + private final ObservableValue sshIdentityStrategy; private final StringProperty value; private final boolean useKeyNames; public SshAgentKeyListComp( ObservableValue> ref, - ObservableValue sshIdentityStrategy, + ObservableValue sshIdentityStrategy, StringProperty value, boolean useKeyNames) { this.ref = ref; @@ -50,6 +51,14 @@ public class SshAgentKeyListComp extends SimpleRegionBuilder { field.apply(struc -> struc.setPromptText( useKeyNames ? "" : "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIBmhLUTJiP...== ")); 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 refToUse = ref != null && ref.getValue() != null diff --git a/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java index 54570d73b..64669aad1 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java +++ b/app/src/main/java/io/xpipe/app/cred/SshAgentTestComp.java @@ -24,10 +24,10 @@ import atlantafx.base.theme.Styles; public class SshAgentTestComp extends SimpleRegionBuilder { private final Runnable beforeTest; - private final ObservableValue sshIdentityStrategy; + private final ObservableValue sshIdentityStrategy; public SshAgentTestComp( - Runnable beforeTest, ObservableValue sshIdentityStrategy) { + Runnable beforeTest, ObservableValue sshIdentityStrategy) { this.beforeTest = beforeTest; this.sshIdentityStrategy = sshIdentityStrategy; } diff --git a/app/src/main/java/io/xpipe/app/cred/SshIdentityAgentStrategy.java b/app/src/main/java/io/xpipe/app/cred/SshIdentityAgentStrategy.java index e0dcc1d22..1c834949a 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshIdentityAgentStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/SshIdentityAgentStrategy.java @@ -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; } diff --git a/app/src/main/java/io/xpipe/app/cred/SshIdentityKeyListStrategy.java b/app/src/main/java/io/xpipe/app/cred/SshIdentityKeyListStrategy.java new file mode 100644 index 000000000..eddfeb400 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/cred/SshIdentityKeyListStrategy.java @@ -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(); +} 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 05f48db76..4d38a18b4 100644 --- a/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java +++ b/app/src/main/java/io/xpipe/app/cred/SshIdentityStrategy.java @@ -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; diff --git a/app/src/main/java/io/xpipe/app/cred/YubikeyPivStrategy.java b/app/src/main/java/io/xpipe/app/cred/YubikeyPivStrategy.java deleted file mode 100644 index 8a00746e1..000000000 --- a/app/src/main/java/io/xpipe/app/cred/YubikeyPivStrategy.java +++ /dev/null @@ -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 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; - } -} 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 acb885ca1..fa59564d4 100644 --- a/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/ProcessControlProvider.java @@ -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 getHttpProxy(DataStoreEntryRef store); + public abstract void addAskpassEnvironment(CommandBuilder b, String prefix, UUID requestId, UUID secretId, String... askpassName); } diff --git a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyConfiguration.java b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyConfiguration.java index 8710eaff1..431e58244 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyConfiguration.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyConfiguration.java @@ -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(); } 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 61eda9454..1202892da 100644 --- a/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java +++ b/app/src/main/java/io/xpipe/app/pwman/PasswordManagerKeyStrategy.java @@ -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 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> getClasses() { var l = new ArrayList>(); 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 7d6f66123..2087713b8 100644 --- a/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java +++ b/app/src/main/java/io/xpipe/app/util/AppJacksonModule.java @@ -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()); diff --git a/lang/strings/fixed_en.properties b/lang/strings/fixed_en.properties index 5253e2005..ad69bc3cc 100644 --- a/lang/strings/fixed_en.properties +++ b/lang/strings/fixed_en.properties @@ -169,4 +169,5 @@ protonPass=Proton Pass passwork=Passwork passworkPlaceholder=Item ID socks5=SOCKS5 +openSc=OpenSC diff --git a/lang/strings/translations_en.properties b/lang/strings/translations_en.properties index ca6435212..1da4020cd 100644 --- a/lang/strings/translations_en.properties +++ b/lang/strings/translations_en.properties @@ -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