Merge branch keygen

This commit is contained in:
crschnick
2025-12-16 08:15:47 +00:00
parent b5fdbc0f4c
commit afdf3a05ea
34 changed files with 858 additions and 68 deletions
@@ -47,6 +47,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
@Override
public CompStructure<Button> createBase() {
var button = new Button(null);
button.setMnemonicParsing(false);
if (name != null) {
name.subscribe(t -> {
PlatformThread.runLaterIfNeeded(() -> button.setText(t));
@@ -5,6 +5,8 @@ import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import atlantafx.base.layout.InputGroup;
@@ -35,6 +37,7 @@ public class InputGroupComp extends Comp<CompStructure<InputGroup>> {
if (mainReference != null && entries.contains(mainReference)) {
var refIndex = entries.indexOf(mainReference);
var ref = b.getChildren().get(refIndex);
HBox.setHgrow(ref, Priority.ALWAYS);
if (ref instanceof Region refR) {
for (int i = 0; i < entries.size(); i++) {
if (i == refIndex) {
@@ -30,6 +30,7 @@ import atlantafx.base.controls.ModalPane;
import atlantafx.base.layout.ModalBox;
import atlantafx.base.theme.Styles;
import atlantafx.base.util.Animations;
import net.synedra.validatorfx.GraphicDecorationStackPane;
public class ModalOverlayComp extends SimpleComp {
@@ -171,8 +172,10 @@ public class ModalOverlayComp extends SimpleComp {
private Region toBox(ModalPane pane, ModalOverlay newValue) {
Region r = newValue.getContent().createRegion();
var validatorPane = new GraphicDecorationStackPane();
validatorPane.getChildren().add(r);
var content = new VBox(r);
var content = new VBox(validatorPane);
content.getStyleClass().add("content");
content.focusedProperty().addListener((o, old, n) -> {
if (n) {
@@ -232,7 +232,6 @@ public class OptionsComp extends Comp<CompStructure<VBox>> {
}
public record Entry(
String key,
ObservableValue<String> description,
String documentationLink,
ObservableValue<String> name,
@@ -7,8 +7,11 @@ import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.CommandControl;
import io.xpipe.app.process.ShellControl;
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.SecretValue;
import javafx.beans.property.Property;
import java.util.List;
import java.util.ServiceLoader;
@@ -28,6 +31,10 @@ public abstract class ProcessControlProvider {
return INSTANCE;
}
public abstract String generatePublicSshKey(SecretValue privateKey, SecretRetrievalStrategy passphrase);
public abstract void showSshKeygenDialog(String commentDefault, Property<?> identityProperty);
public abstract ShellStore subShellEnvironment(DataStoreEntryRef<ShellStore> s, ShellDialect dialect);
public abstract BrowserStoreSessionTab<?> createVncSession(
@@ -51,7 +51,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
"noCompatibleConnection");
}
private String toName(DataStoreEntry entry) {
protected String toName(DataStoreEntry entry) {
if (entry == null) {
return null;
}
@@ -2,7 +2,10 @@ package io.xpipe.app.platform;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@@ -39,7 +42,7 @@ public class BindingsHelper {
}
}
public static <T, U> ObservableValue<U> map(
public static <T, U> ObjectBinding<U> map(
ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {
return Bindings.createObjectBinding(
() -> {
@@ -48,6 +51,15 @@ public class BindingsHelper {
observableValue);
}
public static <T> BooleanBinding mapBoolean(
ObservableValue<T> observableValue, Function<? super T, Boolean> mapper) {
return Bindings.createBooleanBinding(
() -> {
return mapper.apply(observableValue.getValue());
},
observableValue);
}
public static <T, U> ObservableValue<U> flatMap(
ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
var prop = new SimpleObjectProperty<U>();
@@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.GuiDialog;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.LicenseProvider;
import io.xpipe.core.InPlaceSecretValue;
@@ -30,6 +31,36 @@ import java.util.function.Supplier;
public class OptionsBuilder {
public <V, T> ObjectProperty<T> map(Property<V> prop, Function<V, T> function) {
var mapped = new SimpleObjectProperty<T>();
prop.subscribe(v -> {
if (mappingUpdate.get()) {
return;
}
try (var ignored = new BooleanScope(mappingUpdate).start()) {
mapped.setValue(function.apply(v));
}
});
return mapped;
}
public <V, T, R> ObjectProperty<R> map(Property<V> prop, Function<V, T> function, Function<T, R> subFunction) {
var mapped = new SimpleObjectProperty<R>();
prop.subscribe(v -> {
if (mappingUpdate.get()) {
return;
}
T t = function.apply(v);
R r = t != null ? subFunction.apply(t) : null;
try (var ignored = new BooleanScope(mappingUpdate).start()) {
mapped.setValue(r);
}
});
return mapped;
}
private final Validator ownValidator;
private final List<Validator> allValidators = new ArrayList<>();
private final List<Check> allChecks = new ArrayList<>();
@@ -44,6 +75,8 @@ public class OptionsBuilder {
private ObservableValue<String> lastNameReference;
private boolean focusFirstIncomplete = true;
private BooleanProperty mappingUpdate = new SimpleBooleanProperty();
public OptionsBuilder disableFirstIncompleteFocus() {
focusFirstIncomplete = false;
return this;
@@ -86,6 +119,9 @@ public class OptionsBuilder {
validatorList.get(list.indexOf(newValue)).validate();
}
});
selectedIndex.addListener((observable, oldValue, newValue) -> {
selected.setValue(list.get(newValue.intValue()));
});
var pane = new ChoicePaneComp(list, selected);
if (transformer != null) {
pane.setTransformer(transformer);
@@ -114,7 +150,7 @@ public class OptionsBuilder {
return;
}
var entry = new OptionsComp.Entry(null, description, documentationLink, name, comp);
var entry = new OptionsComp.Entry(description, documentationLink, name, comp);
description = null;
documentationLink = null;
name = null;
@@ -171,7 +207,14 @@ public class OptionsBuilder {
public OptionsBuilder addTitle(String titleKey) {
finishCurrent();
entries.add(new OptionsComp.Entry(
titleKey, null, null, null, new LabelComp(AppI18n.observable(titleKey)).styleClass("title-header")));
null, null, null, new LabelComp(AppI18n.observable(titleKey)).styleClass("title-header")));
return this;
}
public OptionsBuilder addTitle(ObservableValue<String> title) {
finishCurrent();
entries.add(new OptionsComp.Entry(
null, null, null, new LabelComp(title).styleClass("title-header")));
return this;
}
@@ -395,6 +438,10 @@ public class OptionsBuilder {
public final <T, V extends T> OptionsBuilder bind(Supplier<V> creator, Property<T>... toSet) {
props.forEach(prop -> {
prop.addListener((c, o, n) -> {
if (mappingUpdate.get()) {
return;
}
for (Property<T> p : toSet) {
p.setValue(creator.get());
}
@@ -412,12 +459,20 @@ public class OptionsBuilder {
var listener = new ChangeListener<V>() {
@Override
public void changed(ObservableValue<? extends V> observable, V oldValue, V newValue) {
if (mappingUpdate.get()) {
return;
}
toSet.setValue(newValue);
}
};
current.get().addListener(listener);
props.forEach(prop -> {
prop.addListener((c, o, n) -> {
if (mappingUpdate.get()) {
return;
}
current.get().removeListener(listener);
current.set(creator.get());
toSet.setValue(current.get().getValue());
@@ -138,6 +138,7 @@ public class OptionsChoiceBuilder {
var c = sub.get(i);
if (c.isAssignableFrom(newValue.getClass())) {
properties.get(i + (allowNull ? 1 : 0)).setValue(newValue);
selected.setValue(i + (allowNull ? 1 : 0));
}
}
});
@@ -157,7 +158,7 @@ public class OptionsChoiceBuilder {
}
}
return new OptionsBuilder()
var options = new OptionsBuilder()
.choice(selected, map, transformer)
.bindChoice(
() -> {
@@ -169,5 +170,6 @@ public class OptionsChoiceBuilder {
return (Property<? extends T>) prop;
},
s);
return options;
}
}
@@ -21,6 +21,7 @@ public class ShellView {
protected String user;
protected FilePath userHome;
protected Boolean root;
protected Boolean administrator;
protected PasswdFile passwdFile;
protected GroupFile groupFile;
protected final Map<String, Boolean> installedApplications = new HashMap<>();
@@ -254,4 +255,28 @@ public class ShellView {
command.sensitive();
command.execute();
}
public synchronized boolean isAdministrator() throws Exception {
if (shellControl.getOsType() != OsType.WINDOWS) {
return false;
}
if (administrator != null) {
return administrator;
}
if (shellControl.getShellDialect() == ShellDialects.CMD) {
administrator = shellControl.command("net.exe session 1>NUL 2>NUL").executeAndCheck();
} else if (ShellDialects.isPowershell(shellControl)) {
administrator = shellControl.command(String.format(
"$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent());"
+ "try {if (-not $($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {$host.ui"
+ ".WriteErrorLine(\"%s\"); throw \"error\"}} catch {}",
"Not Administrator")).executeAndCheck();
} else {
administrator = false;
}
return administrator;
}
}
@@ -5,6 +5,7 @@ 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.prefs.AppPrefs;
import io.xpipe.app.util.Validators;
import io.xpipe.core.InPlaceSecretValue;
@@ -27,11 +28,10 @@ public class SecretCustomCommandStrategy implements SecretRetrievalStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<SecretCustomCommandStrategy> p, SecretStrategyChoiceConfig config) {
var cmdProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getCommand() : null);
var content = new TextFieldComp(cmdProperty);
return new OptionsBuilder()
.addComp(content, cmdProperty)
var options = new OptionsBuilder();
var cmdProperty = options.map(p, SecretCustomCommandStrategy::getCommand);
return options
.addComp(new TextFieldComp(cmdProperty), cmdProperty)
.nonNull()
.bind(
() -> {
@@ -28,11 +28,9 @@ public class SecretInPlaceStrategy implements SecretRetrievalStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<SecretInPlaceStrategy> p, SecretStrategyChoiceConfig config) {
var options = new OptionsBuilder();
var original = p.getValue() != null ? p.getValue().getValue() : null;
var secretProperty = new SimpleObjectProperty<>(
p.getValue() != null && p.getValue().getValue() != null
? p.getValue().getValue()
: null);
var secretProperty = options.map(p, SecretInPlaceStrategy::getValue);
return new OptionsBuilder()
.addComp(new SecretFieldComp(secretProperty, true), secretProperty)
.nonNull()
@@ -33,9 +33,9 @@ public class SecretPasswordManagerStrategy implements SecretRetrievalStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(
Property<SecretPasswordManagerStrategy> p, SecretStrategyChoiceConfig config) {
var options = new OptionsBuilder();
var prefs = AppPrefs.get();
var keyProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getKey() : null);
var keyProperty = options.map(p, SecretPasswordManagerStrategy::getKey);
var content = new HorizontalComp(List.of(
new TextFieldComp(keyProperty)
.apply(struc -> struc.get()
@@ -63,7 +63,7 @@ public class SecretPasswordManagerStrategy implements SecretRetrievalStrategy {
struc.get().getChildren().getFirst().requestFocus();
}
}));
return new OptionsBuilder()
return options
.nameAndDescription("passwordManagerKey")
.addComp(content, keyProperty)
.nonNull()
@@ -415,6 +415,8 @@ public abstract class DataStorage {
}
entry.setStoreInternal(store, false);
entry.initializeEntry();
saveAsync();
}
public void updateCategory(DataStoreCategory category, DataStoreCategory newCategory) {
@@ -70,6 +70,7 @@ public enum DocumentationLink {
TUNNELS_DYNAMIC("guide/ssh-tunnels#dynamic-tunnels"),
HYPERV("guide/hyperv"),
SSH_MACS("troubleshoot/ssh#no-matching-mac-found"),
SSH_FEATURE_NOT_SUPPORTED("troubleshoot/ssh#requested-feature-not-supported"),
SSH_JUMP_SERVERS("guide/ssh#gateways-and-jump-servers"),
SSH_CUSTOM("guide/ssh-config#custom-ssh-connections"),
SSH_CUSTOM_ORDER("guide/ssh-config#jump-hosts"),
@@ -3,6 +3,10 @@
-fx-background-color: transparent;
}
.store-section-mini-comp .item:disabled {
-fx-opacity: 1.0;
}
.store-section-mini-comp .item {
-fx-padding: 0.25em 0.4em 0.25em 0.4em;
-fx-border-color: transparent;
@@ -199,3 +199,18 @@
-fx-font-family: Roboto;
}
.root .label.success .ikonli-font-icon.graphic {
-fx-icon-color: -color-success-fg;
}
.root .label.warning .ikonli-font-icon.graphic {
-fx-icon-color: -color-warning-fg;
}
.root .label.danger .ikonli-font-icon.graphic {
-fx-icon-color: -color-danger-fg;
}
.monospace {
-fx-font-family: Monospace;
}
@@ -0,0 +1,365 @@
package io.xpipe.ext.base.identity;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.file.BrowserFileOpener;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.hub.comp.StoreChoiceComp;
import io.xpipe.app.hub.comp.StoreViewState;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.BindingsHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.PlatformThread;
import io.xpipe.app.process.CommandBuilder;
import io.xpipe.app.process.ShellControl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.FilePath;
import io.xpipe.core.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import lombok.Data;
import java.util.List;
public class IdentityApplyDialog {
@Data
private static class SystemState {
public static SystemState of(ShellControl sc, IdentityStore identity) throws Exception {
var s = new SystemState();
s.init(sc, identity);
return s;
}
boolean inAuthorizedKeys;
boolean keyAuthEnabled;
boolean keyAuthInMethods;
boolean passwordAuthEnabled;
boolean passwordAuthInMethods;
boolean rootLoginEnabled;
boolean mightRequireAdministratorAuthorizedKeys;
FilePath configFile;
FilePath authorizedKeysFile;
private void init(ShellControl sc, IdentityStore identity) throws Exception {
var hasPassword = identity.getPassword() != null && identity.getPassword().expectsQuery();
var hasIdentity = identity.getSshIdentity() != null && identity.getSshIdentity().getPublicKey() != null;
configFile = getSystemConfigPath(sc);
authorizedKeysFile = getAuthorizedKeysFile(sc);
var configContent = getSshdConfigContent(sc);
keyAuthEnabled = isSet(configContent, "PubkeyAuthentication", "yes", true,false);
passwordAuthEnabled = isSet(configContent, "PasswordAuthentication", "yes", true, false);
rootLoginEnabled = isSet(configContent, "PermitRootLogin", "yes", !hasPassword, false) ||
(!hasPassword &&
!isSet(configContent, "PermitRootLogin", "forced-commands-only", true, false) &&
isSet(configContent, "PermitRootLogin", "prohibit-password", true, true));
keyAuthInMethods = isSet(configContent, "AuthenticationMethods", "publickey", true, false);
passwordAuthInMethods = isSet(configContent, "AuthenticationMethods", "password", true, false);
mightRequireAdministratorAuthorizedKeys = sc.getOsType() == OsType.WINDOWS &&
isSet(configContent, "Match", "Group administrators", false, false) &&
isSet(configContent, "AuthorizedKeysFile", "administrators_authorized_keys", false, false);
if (hasIdentity) {
var authorizedKeysContent = getAuthorizedKeysContent(sc);
var publicKey = identity.getSshIdentity().getPublicKey();
var split = publicKey.split("\\s+");
var basePublicKey = split[0] + " " + split[1];
inAuthorizedKeys = authorizedKeysContent.toLowerCase().contains(basePublicKey.toLowerCase());
} else {
inAuthorizedKeys = true;
}
}
private FilePath getSystemConfigPath(ShellControl sc) throws Exception {
if (sc.getOsType() == OsType.WINDOWS) {
var base = sc.view().getEnvironmentVariableOrThrow("programdata");
return FilePath.of(base).join("ssh", "sshd_config");
}
return FilePath.of("/etc/ssh/sshd_config");
}
private FilePath getAuthorizedKeysFile(ShellControl sc) throws Exception {
var v = sc.view();
var authorizedKeysFile = v.userHome().join(".ssh", "authorized_keys");
return authorizedKeysFile;
}
private String getAuthorizedKeysContent(ShellControl sc) throws Exception {
var v = sc.view();
var authorizedKeysContent = v.fileExists(authorizedKeysFile) ? v.readTextFile(authorizedKeysFile) : "";
return authorizedKeysContent;
}
private String getSshdConfigContent(ShellControl sc) throws Exception {
var v = sc.view();
var configContent = v.fileExists(configFile) ? v.readTextFile(configFile) : "";
return configContent;
}
private boolean isSet(String config, String name, String value, boolean notFoundDef, boolean notSpecifiedDef) {
var found = config.lines().filter(s -> {
return !s.strip().startsWith("#");
}).filter(s -> {
return s.toLowerCase().contains(name.toLowerCase());
}).toList();
if (found.isEmpty()) {
return notFoundDef;
}
for (String line : found) {
var matches = line.toLowerCase().contains(value.toLowerCase());
if (matches) {
return true;
}
}
return notSpecifiedDef;
}
}
private static void addPublicKey(SystemState systemState, ShellControl sc, String publicKey) throws Exception {
var v = sc.view();
var authorizedKeysFile = systemState.getAuthorizedKeysFile();
v.mkdir(authorizedKeysFile.getParent());
String authorizedKeysContent;
if (v.fileExists(authorizedKeysFile)) {
var text = v.readTextFile(authorizedKeysFile).strip();
authorizedKeysContent = text.isBlank() ? publicKey + "\n" : text + "\n" + publicKey + "\n";
} else {
authorizedKeysContent = publicKey + "\n";
}
v.writeTextFile(authorizedKeysFile, authorizedKeysContent);
if (sc.getOsType() != OsType.WINDOWS) {
sc.command(CommandBuilder.of().add("chmod", "600").addFile(authorizedKeysFile)).execute();
}
}
private static Comp<?> success() {
var graphic = new LabelGraphic.IconGraphic("mdi2c-checkbox-marked-outline");
return new LabelComp(AppI18n.observable("valid"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.SUCCESS).apply(struc -> {
AppFontSizes.lg(struc.get());
});
}
private static Comp<?> warning() {
var graphic = new LabelGraphic.IconGraphic("mdi2a-alert-box-outline");
return new LabelComp(AppI18n.observable("warning"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.WARNING).apply(struc -> {
AppFontSizes.lg(struc.get());
});
}
private static Comp<?> fail(Comp<?> fixComp) {
var graphic = new LabelGraphic.IconGraphic("mdi2c-close-box-outline");
var label = new LabelComp(AppI18n.observable("notValid"), new ReadOnlyObjectWrapper<>(graphic)).styleClass(Styles.DANGER);
label.apply(struc -> {
AppFontSizes.lg(struc.get());
});
if (fixComp != null) {
var hbox = new HorizontalComp(List.of(label, fixComp, Comp.hspacer()));
hbox.spacing(10);
return hbox;
} else {
return label;
}
}
private static Comp<?> createAuthorizedKeysOptions(Property<DataStoreEntryRef<ShellStore>> system, ObjectProperty<SystemState> systemState, IdentityStore identity, BooleanProperty busy) {
var showAddAuthorizedHost = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && !s.isInAuthorizedKeys();
});
var editButton = new ButtonComp(AppI18n.observable("identityApplyEditAuthorizedKeysButton"), () -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
var sc = system.getValue().getStore().getOrStartSession();
var file = systemState.get().getAuthorizedKeysFile();
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(system.getValue(), null, m -> file.getParent(), null, false);
var found = model.findFile(file);
if (found.isEmpty()) {
model.getFileSystem().touch(file);
if (sc.getOsType() != OsType.WINDOWS) {
sc.command(CommandBuilder.of().add("chmod", "600").addFile(file)).execute();
}
model.refreshSync();
found = model.findFile(file);
}
if (found.isPresent()) {
BrowserFileOpener.openInTextEditor(model, found.get());
}
});
});
}).padding(new Insets(4, 8, 4, 8));
var addButton = new ButtonComp(AppI18n.observable("identityApplyAuthorizedHostButton"),
() -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
var sc = system.getValue().getStore().getOrStartSession();
addPublicKey(systemState.get(), sc, identity.getSshIdentity().getPublicKey());
systemState.setValue(SystemState.of(sc, identity));
});
});
}).padding(new Insets(4, 8, 4, 8));
var options = new OptionsBuilder()
.nameAndDescription("identityApplyAuthorizedHost")
.addComp(success())
.hide(showAddAuthorizedHost)
.nameAndDescription("identityApplyAuthorizedHost")
.addComp(fail(addButton))
.hide(showAddAuthorizedHost.not())
.nameAndDescription("identityApplyEditAuthorizedKeys")
.addComp(editButton);
return options.buildComp().hide(Bindings.isNull(systemState));
}
private static Comp<?> createConfigOptions(Property<DataStoreEntryRef<ShellStore>> system, Property<SystemState> systemState, IdentityStore identity, BooleanProperty busy) {
var showAdminWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && identity.getSshIdentity() != null && identity.getSshIdentity().providesKey() && s.isMightRequireAdministratorAuthorizedKeys();
});
var showPasswordEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && (identity.getPassword() == null || !identity.getPassword().expectsQuery()) && s.isPasswordAuthEnabled() && s.isPasswordAuthInMethods();
});
var showPasswordDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && identity.getPassword() != null && identity.getPassword().expectsQuery() && (!s.isPasswordAuthEnabled() || !s.isPasswordAuthInMethods());
});
var showKeyEnabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && (identity.getSshIdentity() == null || !identity.getSshIdentity().providesKey()) && s.keyAuthEnabled;
});
var showKeyDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && identity.getSshIdentity() != null && identity.getSshIdentity().providesKey() && !s.keyAuthEnabled;
});
var showRootDisabledWarning = BindingsHelper.mapBoolean(systemState, s -> {
return s != null && identity.getUsername().getFixedUsername().map(u -> u.equals("root")).orElse(false) && !s.rootLoginEnabled;
});
var showConfigSection = showKeyEnabledWarning.or(showRootDisabledWarning).or(showPasswordEnabledWarning)
.or(showPasswordDisabledWarning).or(showKeyDisabledWarning).or(showAdminWarning);
var editButton = new ButtonComp(AppI18n.observable("identityApplyEditConfigButton"), () -> {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
var file = systemState.getValue().getConfigFile();
var model = BrowserFullSessionModel.DEFAULT.openFileSystemSync(system.getValue(), null, m -> file.getParent(), null, false);
var found = model.findFile(file);
if (found.isEmpty()) {
return;
}
BrowserFileOpener.openInTextEditor(model, found.get());
});
});
}).padding(new Insets(4, 8, 4, 8));
var options = new OptionsBuilder()
.nameAndDescription("identityApplyConfigPasswordEnabled")
.addComp(warning())
.hide(showPasswordEnabledWarning.not())
.nameAndDescription("identityApplyConfigPasswordDisabled")
.addComp(warning())
.hide(showPasswordDisabledWarning.not())
.nameAndDescription("identityApplyConfigKeyEnabled")
.addComp(warning())
.hide(showKeyEnabledWarning.not())
.nameAndDescription("identityApplyConfigKeyDisabled")
.addComp(warning())
.hide(showKeyDisabledWarning.not())
.nameAndDescription("identityApplyConfigRootDisabledWarning")
.addComp(fail(null))
.hide(showRootDisabledWarning.not())
.nameAndDescription("identityApplyConfigAdminWarning")
.documentationLink("https://learn.microsoft.com/en-us/windows-server/administration/openssh/openssh_keymanagement#administrative-user")
.addComp(warning())
.hide(showAdminWarning.not())
.nameAndDescription("identityApplyEditConfig")
.addComp(editButton).buildComp()
.hide(showConfigSection.not());
return options;
}
public static void show(DataStoreEntryRef<IdentityStore> identity) {
var busy = new SimpleBooleanProperty();
var system = new SimpleObjectProperty<DataStoreEntryRef<ShellStore>>();
var systemState = new SimpleObjectProperty<SystemState>();
var showSetIdentityButton = new SimpleBooleanProperty();
var showIdentityAlreadySet = new SimpleBooleanProperty();
system.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
systemState.setValue(null);
showSetIdentityButton.set(false);
showIdentityAlreadySet.set(false);
return;
}
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
var sc = newValue.getStore().getOrStartSession();
systemState.setValue(SystemState.of(sc, identity.getStore()));
showSetIdentityButton.set(newValue.getStore() instanceof IdentitySwitchStore iss && !iss.getIdentity().unwrap().equals(identity.getStore()));
showIdentityAlreadySet.set(newValue.getStore() instanceof IdentitySwitchStore iss && iss.getIdentity().unwrap().equals(identity.getStore()));
});
});
});
var systemChoice = new StoreChoiceComp<>(null, system, ShellStore.class, null, StoreViewState.get().getAllConnectionsCategory()) {
@Override
protected String toName(DataStoreEntry entry) {
if (entry == null) {
return null;
}
return DataStorage.get().getStoreEntryDisplayName(entry) + " -> " + IdentitySummary.createSummary(identity.getStore());
}
};
var systemChoiceBusy = new LoadingOverlayComp(systemChoice, busy, false);
var applyButton = new ButtonComp(AppI18n.observable("identityApplySetStoreIdentityButton"),
() -> {
DataStorage.get().updateEntryStore(system.get().get(),
((IdentitySwitchStore) system.get().getStore()).withIdentity(new IdentityValue.Ref(identity)));
showSetIdentityButton.set(false);
showIdentityAlreadySet.set(true);
});
applyButton.padding(new Insets(4, 8, 4, 8));
var options = new OptionsBuilder()
.nameAndDescription("identityApplyTargetHost")
.addComp(systemChoiceBusy, system)
.addComp(createAuthorizedKeysOptions(system, systemState, identity.getStore(), busy))
.addComp(createConfigOptions(system, systemState, identity.getStore(), busy))
.nameAndDescription("identityApplySetStoreIdentity")
.addComp(success())
.hide(showIdentityAlreadySet.not())
.nameAndDescription("identityApplySetStoreIdentity")
.addComp(fail(applyButton))
.hide(showSetIdentityButton.not());
var modal = ModalOverlay.of("identityApplyTitle", options.buildComp().prefWidth(600).prefHeight(500));
modal.persist();
modal.addButton(ModalButton.cancel());
modal.addButton(ModalButton.ok().augment(button -> {
button.disableProperty().bind(PlatformThread.sync(busy));
}));
modal.show();
}
}
@@ -0,0 +1,83 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.action.AbstractAction;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.hub.action.HubLeafProvider;
import io.xpipe.app.hub.action.StoreAction;
import io.xpipe.app.hub.action.StoreActionCategory;
import io.xpipe.app.hub.comp.StoreCreationDialog;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.process.ShellTtyState;
import io.xpipe.app.process.SystemState;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;
import javafx.beans.value.ObservableValue;
import lombok.experimental.SuperBuilder;
import lombok.extern.jackson.Jacksonized;
public class IdentityApplyHubLeafProvider implements HubLeafProvider<IdentityStore> {
@Override
public StoreActionCategory getCategory() {
return StoreActionCategory.OPEN;
}
@Override
public boolean isMajor() {
return true;
}
@Override
public boolean isApplicable(DataStoreEntryRef<IdentityStore> o) {
var state = o.get().getStorePersistentState();
if (state instanceof SystemState systemState) {
return (systemState.getShellDialect() == null
|| systemState.getShellDialect().getDumbMode().supportsAnyPossibleInteraction())
&& (systemState.getTtyState() == null || systemState.getTtyState() == ShellTtyState.NONE);
} else {
return true;
}
}
@Override
public ObservableValue<String> getName(DataStoreEntryRef<IdentityStore> store) {
return AppI18n.observable("applyIdentityToHost");
}
@Override
public LabelGraphic getIcon(DataStoreEntryRef<IdentityStore> store) {
return new LabelGraphic.IconGraphic("mdi2e-export");
}
@Override
public Class<?> getApplicableClass() {
return IdentityStore.class;
}
@Override
public AbstractAction createAction(DataStoreEntryRef<IdentityStore> ref) {
return Action.builder().ref(ref).build();
}
@Override
public String getId() {
return "applyIdentity";
}
@Jacksonized
@SuperBuilder
public static class Action extends StoreAction<IdentityStore> {
@Override
public void executeImpl() {
if (ref.getStore().getSshIdentity() != null && !(ref.getStore().getSshIdentity() instanceof NoIdentityStrategy) && ref.getStore().getSshIdentity().getPublicKey() == null) {
AppDialog.confirm("identityApplyMissingPublicKey");
StoreCreationDialog.showEdit(ref.get());
return;
}
IdentityApplyDialog.show(ref);
}
}
}
@@ -1,5 +1,13 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ChoicePaneComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.secret.EncryptedValue;
@@ -11,10 +19,14 @@ import io.xpipe.ext.base.identity.ssh.SshIdentityStrategyChoiceConfig;
import javafx.beans.property.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.HBox;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import java.util.List;
@Value
@Builder
@AllArgsConstructor
@@ -41,6 +53,26 @@ public class IdentityChoiceBuilder {
return i.build();
}
public static OptionsBuilder keyAuthChoice(Property<SshIdentityStrategy> identity, SshIdentityStrategyChoiceConfig config) {
return OptionsChoiceBuilder.builder()
.allowNull(false)
.property(identity)
.customConfiguration(config)
.available(SshIdentityStrategy.getSubclasses())
.transformer(entryComboBox -> {
var button = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2k-key-plus"), () -> {
ProcessControlProvider.get().showSshKeygenDialog(null, identity);
});
button.tooltipKey("generateKey");
var comboComp = Comp.of(() -> entryComboBox);
var hbox = new InputGroupComp(List.of(comboComp, button));
hbox.setMainReference(comboComp);
return hbox.createRegion();
})
.build()
.build();
}
public OptionsBuilder build() {
var existing = identity.getValue();
var user = new SimpleStringProperty(
@@ -88,15 +120,7 @@ public class IdentityChoiceBuilder {
options.name("keyAuthentication")
.description("keyAuthenticationDescription")
.documentationLink(DocumentationLink.SSH_KEYS)
.sub(
OptionsChoiceBuilder.builder()
.allowNull(false)
.property(identityStrategy)
.customConfiguration(sshIdentityChoiceConfig)
.available(SshIdentityStrategy.getSubclasses())
.build()
.build(),
identityStrategy)
.sub(keyAuthChoice(identityStrategy, sshIdentityChoiceConfig), identityStrategy)
.nonNullIf(inPlaceSelected)
.disable(refSelected)
.hide(refSelected);
@@ -41,12 +41,6 @@ public abstract class IdentityStoreProvider implements DataStoreProvider {
@Override
public ObservableValue<String> informationString(StoreSection section) {
var st = (IdentityStore) section.getWrapper().getStore().getValue();
var user = st.getUsername().hasUser()
? st.getUsername().getFixedUsername().map(s -> "User " + s).orElse("User")
: "Anonymous User";
var s = user
+ (st.getPassword() == null || st.getPassword() instanceof SecretNoneStrategy ? "" : " + Password")
+ (st.getSshIdentity() == null || st.getSshIdentity() instanceof NoIdentityStrategy ? "" : " + Key");
return new SimpleStringProperty(s);
return new SimpleStringProperty(IdentitySummary.createSummary(st));
}
}
@@ -0,0 +1,17 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.secret.SecretNoneStrategy;
import io.xpipe.ext.base.identity.ssh.NoIdentityStrategy;
public class IdentitySummary {
public static String createSummary(IdentityStore st) {
var user = st.getUsername().hasUser()
? st.getUsername().getFixedUsername().map(s -> "User " + s).orElse("User")
: "Anonymous User";
var s = user
+ (st.getPassword() == null || st.getPassword() instanceof SecretNoneStrategy ? "" : " + Password")
+ (st.getSshIdentity() == null || st.getSshIdentity() instanceof NoIdentityStrategy ? "" : " + Key");
return s;
}
}
@@ -0,0 +1,13 @@
package io.xpipe.ext.base.identity;
import io.xpipe.app.ext.HostAddress;
import io.xpipe.ext.base.host.HostAddressStore;
import java.util.Optional;
public interface IdentitySwitchStore extends HostAddressStore {
IdentityValue getIdentity();
IdentitySwitchStore withIdentity(IdentityValue identity);
}
@@ -64,15 +64,7 @@ public class LocalIdentityStoreProvider extends IdentityStoreProvider {
.name("keyAuthentication")
.description("keyAuthenticationDescription")
.documentationLink(DocumentationLink.SSH_KEYS)
.sub(
OptionsChoiceBuilder.builder()
.allowNull(false)
.property(identity)
.customConfiguration(sshIdentityChoiceConfig)
.available(SshIdentityStrategy.getSubclasses())
.build()
.build(),
identity)
.sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)
.bind(
() -> {
return LocalIdentityStore.builder()
@@ -96,15 +96,7 @@ public class SyncedIdentityStoreProvider extends IdentityStoreProvider {
.name("keyAuthentication")
.description("keyAuthenticationDescription")
.documentationLink(DocumentationLink.SSH_KEYS)
.sub(
OptionsChoiceBuilder.builder()
.allowNull(false)
.property(identity)
.customConfiguration(sshIdentityChoiceConfig)
.available(SshIdentityStrategy.getSubclasses())
.build()
.build(),
identity)
.sub(IdentityChoiceBuilder.keyAuthChoice(identity, sshIdentityChoiceConfig), identity)
.check(val -> Validator.create(val, AppI18n.observable("keyNotSynced"), identity, i -> {
var wrong = i instanceof KeyFileStrategy f
&& f.getFile() != null
@@ -94,4 +94,9 @@ public class CustomPkcs11LibraryStrategy implements SshIdentityStrategy {
new KeyValue("IdentityFile", "none"),
new KeyValue("IdentityAgent", "none"));
}
@Override
public String getPublicKey() {
return null;
}
}
@@ -1,8 +1,16 @@
package io.xpipe.ext.base.identity.ssh;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.InputGroupComp;
import io.xpipe.app.comp.base.TextAreaComp;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.process.CommandBuilder;
@@ -13,6 +21,7 @@ import io.xpipe.app.util.LocalFileTracker;
import io.xpipe.app.util.Validators;
import io.xpipe.core.*;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
@@ -22,6 +31,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.charset.StandardCharsets;
import java.util.List;
@@ -36,10 +46,11 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
@SuppressWarnings("unused")
public static OptionsBuilder createOptions(Property<InPlaceKeyStrategy> p, SshIdentityStrategyChoiceConfig config) {
var key = new SimpleStringProperty(
p.getValue().getKey() != null ? p.getValue().getKey().getSecretValue() : null);
var keyPasswordProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
var options = new OptionsBuilder();
var key = options.map(p, InPlaceKeyStrategy::getKey, SecretValue::getSecretValue);
var publicKey = options.map(p, InPlaceKeyStrategy::getPublicKey);
var keyPasswordProperty = options.map(p, InPlaceKeyStrategy::getPassword);
var passwordChoice = OptionsChoiceBuilder.builder()
.allowNull(false)
@@ -49,8 +60,28 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
.available(SecretRetrievalStrategy.getSubclasses())
.build()
.build();
var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
struc.get().promptTextProperty().bind(Bindings.createStringBinding(() -> {
return "ssh-... ABCDEF.... (" + AppI18n.get("publicKeyGenerateNotice") + ")";
}, AppI18n.activeLanguage()));
struc.get().setEditable(false);
});
var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
var generated = ProcessControlProvider.get().generatePublicSshKey(InPlaceSecretValue.of(key.get()), keyPasswordProperty.get());
if (generated != null) {
publicKey.set(generated);
}
}).tooltipKey("generatePublicKey").disable(key.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyText(publicKey.get());
})
.disable(publicKey.isNull())
.tooltipKey("copyPublicKey");
return new OptionsBuilder()
var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
publicKeyBox.setMainReference(publicKeyField);
return options
.nameAndDescription("inPlaceKeyText")
.addComp(
new TextAreaComp(key).apply(struc -> {
@@ -59,8 +90,10 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
"""
-----BEGIN ... PRIVATE KEY-----
-----END ... PRIVATE KEY-----
-----END ... PRIVATE KEY-----
""");
struc.getTextArea().setPrefRowCount(4);
}),
key)
.nonNull()
@@ -68,16 +101,22 @@ public class InPlaceKeyStrategy implements SshIdentityStrategy {
.description("sshConfigHost.identityPassphraseDescription")
.sub(passwordChoice, keyPasswordProperty)
.nonNull()
.nameAndDescription("inPlacePublicKey")
.addComp(
publicKeyBox,
publicKey)
.bind(
() -> {
return new InPlaceKeyStrategy(
key.get() != null ? InPlaceSecretValue.of(key.get()) : null,
keyPasswordProperty.get());
key.getValue() != null ? InPlaceSecretValue.of(key.getValue()) : null,
publicKey.get(),
keyPasswordProperty.getValue());
},
p);
}
SecretValue key;
String publicKey;
SecretRetrievalStrategy password;
public void checkComplete() throws ValidationException {
@@ -1,10 +1,13 @@
package io.xpipe.ext.base.identity.ssh;
import io.xpipe.app.comp.base.ContextualFileReferenceChoiceComp;
import io.xpipe.app.comp.base.ContextualFileReferenceSync;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppSystemInfo;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.ClipboardHelper;
import io.xpipe.app.platform.LabelGraphic;
import io.xpipe.app.platform.OptionsBuilder;
import io.xpipe.app.platform.OptionsChoiceBuilder;
import io.xpipe.app.process.CommandBuilder;
@@ -13,11 +16,15 @@ import io.xpipe.app.secret.SecretRetrievalStrategy;
import io.xpipe.app.secret.SecretStrategyChoiceConfig;
import io.xpipe.app.storage.ContextualFileReference;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.app.util.Validators;
import io.xpipe.core.FilePath;
import io.xpipe.core.InPlaceSecretValue;
import io.xpipe.core.KeyValue;
import io.xpipe.core.OsType;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
@@ -27,7 +34,10 @@ import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@Value
@@ -62,6 +72,7 @@ public class KeyFileStrategy implements SshIdentityStrategy {
});
var keyPasswordProperty =
new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPassword() : null);
var publicKey = new SimpleObjectProperty<>(p.getValue() != null ? p.getValue().getPublicKey() : null);
var sync = ContextualFileReferenceSync.of(
DataStorage.get().getDataDir().resolve("keys"),
@@ -77,6 +88,45 @@ public class KeyFileStrategy implements SshIdentityStrategy {
.build()
.build();
var publicKeyField = new TextFieldComp(publicKey).apply(struc -> {
struc.get().promptTextProperty().bind(Bindings.createStringBinding(() -> {
return "ssh-... ABCDEF.... (" + AppI18n.get("publicKeyGenerateNotice") + ")";
}, AppI18n.activeLanguage()));
struc.get().setEditable(false);
});
var generateButton = new ButtonComp(null, new LabelGraphic.IconGraphic("mdi2c-cog-refresh-outline"), () -> {
ThreadHelper.runFailableAsync(() -> {
Path path = keyPath.get().asLocalPath();
if (!Files.exists(path)) {
return;
}
var pubKeyPath = Path.of(path + ".pub");
if (Files.exists(pubKeyPath)) {
var contents = Files.readString(pubKeyPath).strip();
Platform.runLater(() -> {
publicKey.set(contents);
});
}
var contents = Files.readAllBytes(path);
var generated = ProcessControlProvider.get().generatePublicSshKey(InPlaceSecretValue.of(contents), keyPasswordProperty.get());
if (generated != null) {
Platform.runLater(() -> {
publicKey.set(generated);
});
}
});
}).tooltipKey("generatePublicKey").disable(keyPath.isNull().or(publicKey.isNotNull()).or(keyPasswordProperty.isNull()));
var copyButton = new ButtonComp(null, new FontIcon("mdi2c-clipboard-multiple-outline"), () -> {
ClipboardHelper.copyText(publicKey.get());
})
.disable(publicKey.isNull())
.tooltipKey("copyPublicKey");
var publicKeyBox = new InputGroupComp(List.of(publicKeyField, copyButton, generateButton));
publicKeyBox.setMainReference(publicKeyField);
return new OptionsBuilder()
.name("location")
.description("locationDescription")
@@ -94,16 +144,21 @@ public class KeyFileStrategy implements SshIdentityStrategy {
.description("sshConfigHost.identityPassphraseDescription")
.sub(passwordChoice, keyPasswordProperty)
.nonNull()
.nameAndDescription("inPlacePublicKey")
.addComp(
publicKeyBox,
publicKey)
.bind(
() -> {
return new KeyFileStrategy(
ContextualFileReference.of(keyPath.get()), keyPasswordProperty.get());
ContextualFileReference.of(keyPath.get()), keyPasswordProperty.get(), publicKey.get());
},
p);
}
ContextualFileReference file;
SecretRetrievalStrategy password;
String publicKey;
public void checkComplete() throws ValidationException {
Validators.nonNull(file);
@@ -28,4 +28,14 @@ public class NoIdentityStrategy implements SshIdentityStrategy {
new KeyValue("IdentityFile", "none"),
new KeyValue("PKCS11Provider", "none"));
}
@Override
public String getPublicKey() {
return null;
}
@Override
public boolean providesKey() {
return false;
}
}
@@ -39,8 +39,8 @@ public interface SshIdentityStrategy {
static List<Class<?>> getSubclasses() {
var l = new ArrayList<Class<?>>();
l.add(NoIdentityStrategy.class);
l.add(KeyFileStrategy.class);
l.add(InPlaceKeyStrategy.class);
l.add(KeyFileStrategy.class);
l.add(OpenSshAgentStrategy.class);
if (OsType.ofLocal() != OsType.WINDOWS) {
l.add(CustomAgentStrategy.class);
@@ -85,6 +85,10 @@ public interface SshIdentityStrategy {
}
}
default boolean providesKey() {
return true;
}
default void checkComplete() throws ValidationException {}
void prepareParent(ShellControl parent) throws Exception;
@@ -96,4 +100,6 @@ public interface SshIdentityStrategy {
default SecretRetrievalStrategy getAskpassStrategy() {
return new SecretNoneStrategy();
}
String getPublicKey();
}
@@ -79,4 +79,9 @@ public class YubikeyPivStrategy implements SshIdentityStrategy {
new KeyValue("IdentityFile", "none"),
new KeyValue("IdentityAgent", "none"));
}
@Override
public String getPublicKey() {
return null;
}
}
+2
View File
@@ -6,6 +6,7 @@ import io.xpipe.ext.base.host.AbstractHostCreationActionProvider;
import io.xpipe.ext.base.host.AbstractHostStoreProvider;
import io.xpipe.ext.base.host.HostAddressSwitchBranchProvider;
import io.xpipe.ext.base.identity.*;
import io.xpipe.ext.base.identity.IdentityApplyHubLeafProvider;
import io.xpipe.ext.base.script.*;
import io.xpipe.ext.base.service.*;
import io.xpipe.ext.base.store.*;
@@ -35,6 +36,7 @@ open module io.xpipe.ext.base {
requires javafx.base;
provides ActionProvider with
IdentityApplyHubLeafProvider,
AbstractHostCreationActionProvider,
HostAddressSwitchBranchProvider,
LocalIdentityConvertHubLeafProvider,
+4 -1
View File
@@ -149,4 +149,7 @@ tailscale=Tailscale
netbird=Netbird
appImageDist=AppImage
nixDist=Nix
antigravity=Antigravity
antigravity=Antigravity
rsa=RSA
ed25519=ED25519
ed25519Sk=ED25519 (FIDO2)
+60 -2
View File
@@ -808,7 +808,8 @@ location=Location
keyAuthentication=Key-based authentication
keyAuthenticationDescription=The authentication method to use if key-based authentication is required
locationDescription=The file path of your corresponding private key
keyFile=Key file
#force
keyFile=Local key file
keyPassword=Passphrase
key=Key
yubikeyPiv=Yubikey PIV
@@ -1682,7 +1683,8 @@ largeFileWarningTitle=Large file edit
largeFileWarningContent=The file you want to edit is quite large with $SIZE$. Do you really want to open this file in your text editor?
rdpAskpassUser=RDP username for host $HOST$
rdpAskpassPassword=Password for user $USER$
inPlaceKey=In-place key
#force
inPlaceKey=Key
inPlaceKeyText=Private key content
inPlaceKeyTextDescription=The private key contents
netbirdSelfhosted=Self-hosted netbird instance
@@ -1708,3 +1710,59 @@ tags=Tags
tag=Tag
addNewTag=Create new tag
createTag=Create tag ...
inPlacePublicKey=Public key
inPlacePublicKeyDescription=The associated public key for the private key
sshKeygenTitle=Generate new SSH key
sshKeygenAlgorithm=Algorithm
sshKeygenAlgorithmDescription=The asymmetric keygen algorithm to use for the key
rsaBits=Bits
rsaBitsDescription=Number of bits in generated key
sshKeygenComment=Comment
sshKeygenCommentDescription=The optional comment for this key
sshKeygenPassphrase=Passphrase
sshKeygenPassphraseDescription=The optional passphrase for this key
ed25519SkResident=Make resident key
ed25519SkResidentDescription=Store private key on the hardware security key
ed25519SkResidentKeyName=Resident key label
ed25519SkResidentKeyNameDescription=Give the resident key a label. Needed when storing multiple keys on the security key
ed25519SkPinRequired=Require PIN
ed25519SkPinRequiredDescription=Require PIN entry on use
ed25519SkUserPresenceRequired=Require user presence
ed25519SkUserPresenceRequiredDescription=Require touch or similar on use. Some security keys require this to be enabled
copyPublicKey=Copy public key
generatePublicKey=Generate public key
publicKeyGenerateNotice=Can be generated from public key
identityApplyTargetHost=Target
identityApplyTargetHostDescription=The system to apply the identity to
identityApplyAuthorizedHost=SSH key authorized
identityApplyAuthorizedHostDescription=The SSH key is added to authorized hosts file
identityApplyAuthorizedHostButton=Append key to file
applyIdentityToHost=Apply identity to host ...
identityApplyMissingPublicKeyTitle=Missing public key
identityApplyMissingPublicKeyContent=The identity's SSH key does not have a public key associated with it. Check out the configuration for details.
valid=Valid
notValid=Not valid
warning=Warning
identityApplyTitle=Apply identity
identityApplyConfigPasswordEnabled=Password auth enabled
identityApplyConfigPasswordEnabledDescription=Password authentication is still enabled in the sshd config
identityApplyConfigPasswordDisabled=Password auth disabled
identityApplyConfigPasswordDisabledDescription=Password authentication is disabled in the sshd config
identityApplyConfigKeyEnabled=Key auth enabled
identityApplyConfigKeyEnabledDescription=Key-based authentication is still enabled in the sshd config
identityApplyConfigKeyDisabled=Key auth disabled
identityApplyConfigKeyDisabledDescription=Key-based authentication is disabled in the sshd config
identityApplyConfigRootDisabledWarning=Root login disabled
identityApplyConfigRootDisabledWarningDescription=Root login is not enabled in the sshd config
identityApplyConfigAdminWarning=Administrator keys configured
identityApplyConfigAdminWarningDescription=The key might have to be added to administrators_authorized_keys instead for admin users
identityApplyEditConfig=Edit config
identityApplyEditConfigDescription=Open the sshd config in the file browser to fix any issues
identityApplyEditAuthorizedKeys=Edit authorized keys
identityApplyEditAuthorizedKeysDescription=Open the authorized_keys file in the file browser to edit or remove other keys
identityApplyEditConfigButton=Open sshd_config
identityApplyEditAuthorizedKeysButton=Open authorized_keys
identityApplySetStoreIdentity=Connection identity set
identityApplySetStoreIdentityDescription=The identity has been configured to be used by the connection
identityApplySetStoreIdentityButton=Apply identity
generateKey=Generate key