/*
* Copyright (C) 2021 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import {
get_init_superuser_for_options, split_connection_string, generate_connection_string
} from "./machines/machines";
import * as credentials from "credentials";
import ssh_show_default_key_sh from "../lib/ssh-show-default-key.sh";
import ssh_add_key_sh from "../lib/ssh-add-key.sh";
import React from 'react';
import PropTypes from 'prop-types';
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox/index.js";
import { ClipboardCopy } from "@patternfly/react-core/dist/esm/components/ClipboardCopy/index.js";
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import {
Modal, ModalBody, ModalFooter, ModalHeader
} from '@patternfly/react-core/dist/esm/components/Modal/index.js';
import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
import { Radio } from "@patternfly/react-core/dist/esm/components/Radio/index.js";
import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { OutlinedQuestionCircleIcon, ExternalLinkAltIcon } from "@patternfly/react-icons";
import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
import { Content, ContentVariants } from "@patternfly/react-core/dist/esm/components/Content";
import { FormHelper } from "cockpit-components-form-helper";
import { ModalError } from "cockpit-components-inline-notification.jsx";
import { fmt_to_fragments } from "utils.js";
const _ = cockpit.gettext;
export const HostModalState = () => {
function set_props(props, callback) {
self.modal_properties = props;
self.modal_callback = callback;
self.dispatchEvent("changed");
}
function close_modal() {
set_props(null, null);
}
function show_modal(properties) {
return new Promise((resolve, reject) => {
set_props(properties, result => { resolve(result); return Promise.resolve() });
});
}
const self = {
state: null,
show_modal,
close_modal,
};
return cockpit.event_target(self);
};
/* Activate and jump to a newly added machine, identified by its
connection string.
*/
function jump_to_new_connection_string(shell_state, connection_string) {
// Get the address of the machine from its connection string
const addr = split_connection_string(connection_string).address;
// Tell the Loader that now is the time to start monitoring the
// manifests.
shell_state.loader.connect(addr);
// Navigate to it.
shell_state.jump({ host: addr });
}
export async function add_host(state, shell_state) {
const connection_string = await state.show_modal({ });
if (connection_string)
jump_to_new_connection_string(shell_state, connection_string);
}
export async function edit_host(state, shell_state, machine) {
const { current_machine } = shell_state;
const connection_string = await state.show_modal({ address: machine.address });
if (connection_string) {
const addr = split_connection_string(connection_string).address;
if (machine === current_machine && addr !== machine.address)
jump_to_new_connection_string(shell_state, connection_string);
}
}
export async function connect_host(state, shell_state, machine) {
// We need to trigger the loader for machines that already
// have state "connected". The state of a machine object
// survives a full shell reload, but the loader of course has
// no channel open for it yet. The bridge likely has the SSH
// connection still open, so the loader can do its job right
// away, like triggering the packages reload.
//
// "localhost" is a special case: we can always connect the
// loader without any extra credentials and we never want to
// show any dialogs for it.
//
if (machine.connection_string === "localhost" ||
machine.state === "connected" ||
machine.state === "connecting") {
shell_state.loader.connect(machine.address);
return machine.connection_string;
}
let connection_string = null;
if (machine.problem && codes[machine.problem]) {
// trouble shooting
connection_string = await state.show_modal({
address: machine.address,
template: codes[machine.problem],
});
} else if (!window.sessionStorage.getItem("connection-warning-shown")) {
// connect by launching into the "Connection warning" dialog.
connection_string = await state.show_modal({
address: machine.address,
template: "connect"
});
} else {
// Try to connect without any dialog
try {
await try2Connect(shell_state.machines, machine.connection_string);
connection_string = machine.connection_string;
} catch (err) {
// continue with troubleshooting in the dialog
connection_string = await state.show_modal({
address: machine.address,
template: codes[err.problem] || "change-port",
error_options: err,
});
}
}
if (connection_string) {
// make the rest of the shell aware that the machine is now connected
const parts = split_connection_string(connection_string);
shell_state.loader.connect(parts.address);
shell_state.update();
}
return connection_string;
}
export const codes = {
danger: "connect",
"no-cockpit": "not-supported",
"not-supported": "not-supported",
"protocol-error": "not-supported",
"authentication-not-supported": "change-auth",
"authentication-failed": "change-auth",
"no-forwarding": "change-auth",
"unknown-hostkey": "unknown-hostkey",
"invalid-hostkey": "invalid-hostkey",
"not-found": "add-machine",
"unknown-host": "unknown-host"
};
function full_address(machines_ins, address) {
const machine = machines_ins.lookup(address);
if (machine && machine.address !== "localhost")
return machine.connection_string;
return address;
}
function is_method_supported(methods, method) {
const result = methods[method];
return result ? result !== "no-server-support" : false;
}
function prevent_default(callback) {
return event => {
callback();
event.preventDefault();
return false;
};
}
class NotSupported extends React.Component {
render() {
return (
{ this.props.dialogError && }
{cockpit.format(_("A compatible version of Cockpit is not installed on $0."), this.props.full_address)}
);
}
}
class Connect extends React.Component {
constructor(props) {
super(props);
this.state = {
inProgress: false,
};
}
onConnect() {
window.sessionStorage.setItem("connection-warning-shown", true);
this.setState({ inProgress: true });
this.props.run(try2Connect(this.props.machines_ins, this.props.full_address), ex => {
let keep_message = false;
if (ex.problem === "no-host") {
let host_id_port = this.props.full_address;
let port = "22";
const port_index = host_id_port.lastIndexOf(":");
if (port_index === -1) {
host_id_port = this.props.full_address + ":22";
} else {
port = host_id_port.substring(port_index + 1);
}
ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
ex.problem = "not-found";
keep_message = true;
}
this.setState({ inProgress: false });
this.props.setError(ex, keep_message);
});
}
render() {
return (
{this.props.host})}
titleIconVariant="warning"
/>
{_("Connected hosts can fully control each other. This includes running programs that could harm your system or steal data. Only connect to trusted machines.")}
{_("Read more")}
{_("You will be reminded once per session.")}
);
}
}
class AddMachine extends React.Component {
constructor(props) {
super(props);
let address_parts = null;
if (this.props.full_address)
address_parts = split_connection_string(this.props.full_address);
let host_address = "";
let host_user = "";
if (address_parts) {
host_address = address_parts.address;
if (address_parts.port)
host_address += ":" + address_parts.port;
host_user = address_parts.user;
}
let color = props.machines_ins.unused_color();
let old_machine = null;
if (props.old_address)
old_machine = props.machines_ins.lookup(props.old_address);
if (old_machine)
color = this.rgb2Hex(old_machine.color);
if (old_machine && !old_machine.visible)
old_machine = null;
this.state = {
user: host_user || "",
address: host_address || "",
color,
addressError: "",
inProgress: false,
old_machine,
userChanged: false,
};
this.onAddressChange = this.onAddressChange.bind(this);
this.onAddHost = this.onAddHost.bind(this);
}
rgb2Hex(c) {
function toHex(d) {
return ("0" + (parseInt(d, 10).toString(16)))
.slice(-2);
}
if (c[0] === "#")
return c;
const colors = /rgb\((\d*), (\d*), (\d*)\)/.exec(c);
return "#" + toHex(colors[1]) + toHex(colors[2]) + toHex(colors[3]);
}
onAddressChange() {
let error = "";
if (this.state.address.search(/\s+/) !== -1)
error = _("The IP address or hostname cannot contain whitespace.");
else {
const machine = this.props.machines_ins.lookup(this.state.address);
const machine_address = machine ? full_address(this.props.machines_ins, machine.address) : undefined;
if (machine && machine.on_disk && machine_address !== this.props.old_address) {
if (machine.visible)
error = _("This machine has already been added.");
else if (!this.state.userChanged)
this.setState({ user: machine.user, color: this.rgb2Hex(machine.color) });
} else if (this.state.old_machine && !machine && !this.state.userChanged) { // When editing host by changing its address generate new color
this.setState((_, prevProps) => ({ color: prevProps.machines_ins.unused_color(), userChanged: true }));
}
}
this.setState({ addressError: error });
return error;
}
onAddHost() {
const parts = split_connection_string(this.state.address);
// user in "User name:" field wins over user in connection string
const address = generate_connection_string(this.state.user || parts.user, parts.port, parts.address);
if (this.onAddressChange())
return;
this.props.setAddress(address);
if (this.state.old_machine && address === this.state.old_machine.connection_string) {
this.props.setError(null);
this.setState({ inProgress: true });
this.props.run(this.props.machines_ins.change(this.state.old_machine.key, { color: this.state.color }))
.catch(ex => {
this.setState({ inProgress: false });
throw ex;
});
return;
}
this.props.setError(null);
this.setState({ inProgress: true });
this.props.setGoal(() => {
const parts = split_connection_string(this.state.address);
// user in "User name:" field wins over user in connection string
const address = generate_connection_string(this.state.user || parts.user, parts.port, parts.address);
return new Promise((resolve, reject) => {
this.props.machines_ins.add(address, this.state.color)
.then(() => {
// When changing address of machine, hide the old one
if (this.state.old_machine && this.state.old_machine !== this.props.machines_ins.lookup(address)) {
this.props.machines_ins.change(this.state.old_machine.key, { visible: false })
.then(resolve);
} else {
resolve();
}
})
.catch(ex => {
ex.message = cockpit.format(_("Failed to add machine: $0"), cockpit.message(ex));
this.setState({ dialogError: cockpit.message(ex), inProgress: false });
reject(ex);
});
});
});
if (!window.sessionStorage.getItem("connection-warning-shown")) {
this.props.setError({ problem: "danger", command: "close" });
} else {
this.props.run(try2Connect(this.props.machines_ins, address), ex => {
if (ex.problem === "no-host") {
let host_id_port = address;
let port = "22";
const port_index = host_id_port.lastIndexOf(":");
if (port_index === -1) {
host_id_port = address + ":22";
} else {
port = host_id_port.substring(port_index + 1);
}
ex.message = cockpit.format(_("Unable to contact the given host $0. Make sure it has ssh running on port $1, or specify another port in the address."), host_id_port, port);
ex.problem = "not-found";
}
this.setState({ inProgress: false });
this.props.setError(ex);
});
}
}
render() {
const invisible = this.props.machines_ins.addresses.filter(addr => {
const m = this.props.machines_ins.lookup(addr);
return !m || !m.visible;
});
const callback = this.onAddHost;
const title = this.state.old_machine ? _("Edit host") : _("Add new host");
const submitText = this.state.old_machine ? _("Set") : _("Add");
const body =
;
return (
{ this.props.dialogError && }
{body}
);
}
}
class MachinePort extends React.Component {
constructor(props) {
super(props);
const machine = props.machines_ins.lookup(props.full_address);
if (!machine) {
props.onClose();
return;
}
this.state = {
port: machine.port,
};
this.onChangePort = this.onChangePort.bind(this);
}
onChangePort() {
const promise = new Promise((resolve, reject) => {
const parts = split_connection_string(this.props.full_address);
parts.port = this.state.port;
const address = generate_connection_string(parts.user, parts.port, parts.address);
const self = this;
function update_host(ex) {
self.props.setAddress(address);
self.props.machines_ins.change(parts.address, { port: parts.port })
.then(() => {
// We failed before so try to connect again now that the machine is saved
if (ex) {
try2Connect(self.props.machines_ins, address)
.then(self.props.complete)
.catch(reject);
} else {
resolve();
}
})
.catch(ex => reject(cockpit.format(_("Failed to edit machine: $0"), cockpit.message(ex))));
}
try2Connect(this.props.machines_ins, address)
.then(update_host)
.catch(ex => {
// any other error means progress, so save
if (ex.problem !== 'no-host')
update_host(ex);
else
reject(ex);
});
});
this.props.run(promise);
}
render() {
const callback = this.onChangePort;
const title = cockpit.format(_("Could not contact $0"), this.props.full_address);
const submitText = _("Update");
const body = <>
{cockpit.format(_("Unable to contact $0."), this.props.full_address)}{_("Is sshd running on a different port?")}
>;
return (
{ this.props.dialogError && }
{body}
);
}
}
class HostKey extends React.Component {
constructor(props) {
super(props);
this.state = {
inProgress: false,
verifyExpanded: false,
error_options: props.error_options,
};
this.onAddKey = this.onAddKey.bind(this);
}
componentDidMount() {
if (!this.props.error_options || !this.props.error_options["host-key"]) {
const options = {};
let match_problem = this.props.template;
if (this.props.template === "unknown-host") {
options.session = "private";
match_problem = "unknown-hostkey";
}
try2Connect(this.props.machines_ins, this.props.full_address, options)
.then(this.props.complete)
.catch(ex => {
if (ex.problem !== match_problem) {
this.props.setError(ex);
} else {
this.setState({ error_options: ex });
}
});
}
}
onAddKey() {
this.setState({ inProgress: true });
const key = this.state.error_options["host-key"];
const machine = this.props.machines_ins.lookup(this.props.full_address);
let q;
if (!machine || machine.on_disk) {
q = this.props.machines_ins.add_key(key);
} else {
// When machine isn't saved to disk don't save the key either
q = this.props.machines_ins.change(this.props.full_address, { host_key: key });
}
this.props.run(q.then(() => {
return try2Connect(this.props.machines_ins, this.props.full_address, {})
.catch(ex => {
if ((ex.problem === "invalid-hostkey" || ex.problem === "unknown-hostkey") && machine && !machine.on_disk)
this.props.machines_ins.change(this.props.full_address, { host_key: null });
else {
this.setState({ inProgress: false });
throw ex;
}
});
}));
}
render() {
let key_type = "";
let fp = "";
if (this.state.error_options && this.state.error_options["host-key"]) {
key_type = this.state.error_options["host-key"].split(" ")[1];
fp = this.state.error_options["host-fingerprint"];
}
const scan_cmd = `ssh-keyscan -t ${key_type} localhost | ssh-keygen -lf -`;
const callback = this.onAddKey;
const title = cockpit.format(this.props.template === "invalid-hostkey" ? _("$0 key changed") : _("New host: $0"),
this.props.host);
const submitText = _("Trust and add host");
let unknown = false;
let body = null;
if (!key_type) {
unknown = true;
} else if (this.props.template === "invalid-hostkey") {
body = <>
{_("To ensure that your connection is not intercepted by a malicious third-party, please verify the host key fingerprint:")}
{fp}
({key_type})
{cockpit.format(_("To verify a fingerprint, run the following on $0 while physically sitting at the machine or through a trusted network:"), this.props.host)}
{scan_cmd}
{_("The resulting fingerprint is fine to share via public methods, including email.")}
{_("If the fingerprint matches, click 'Trust and add host'. Otherwise, do not connect and contact your administrator.")}
>;
} else {
const fingerprint_help = ;
body = <>
{cockpit.format(_("You are connecting to $0 for the first time."), this.props.host)}
this.setState({ verifyExpanded }) }>
{_("Run this command over a trusted network or physically on the remote machine:")}
{scan_cmd}
{_("The fingerprint should match:")} {fingerprint_help}
{fp}
>;
}
return (
{ this.props.dialogError && }
{body}
{ unknown ||
}
);
}
}
class ChangeAuth extends React.Component {
constructor(props) {
super(props);
this.state = {
auth: "password",
auto_login: false,
custom_password: "",
custom_password_error: "",
locked_identity_password: "",
locked_identity_password_error: "",
login_setup_new_key_password: "",
login_setup_new_key_password_error: "",
login_setup_new_key_password2: "",
login_setup_new_key_password2_error: "",
user: "",
default_ssh_key: null,
identity_path: null,
inProgress: true, // componentDidMount changes to false once loaded
};
this.keys = null;
if (credentials)
this.keys = credentials.keys_instance();
this.getSupports = this.getSupports.bind(this);
this.updateIdentity = this.updateIdentity.bind(this);
this.login = this.login.bind(this);
this.maybe_create_key = this.maybe_create_key.bind(this);
this.authorize_key = this.authorize_key.bind(this);
this.maybe_unlock_key = this.maybe_unlock_key.bind(this);
}
updateIdentity() {
let identity_path = null;
if (this.props.error_options && this.props.error_options.error && this.props.error_options.error.startsWith("locked identity"))
identity_path = this.props.error_options.error.split(": ")[1];
const default_ssh_key = this.state.default_ssh_key;
if (default_ssh_key && default_ssh_key.encrypted)
default_ssh_key.unaligned_passphrase = identity_path && identity_path === default_ssh_key.name;
this.setState({ identity_path, default_ssh_key });
}
componentDidMount() {
cockpit.user()
.then(user =>
cockpit.script(ssh_show_default_key_sh, [], { })
.then(data => {
let default_ssh_key = null;
const info = data.split("\n");
if (info[0])
default_ssh_key = { name: info[0], exists: true, encrypted: info[1] === "encrypted" };
else
default_ssh_key = { name: user.home + "/.ssh/id_rsa", type: "rsa", exists: false };
return this.setState({ inProgress: false, default_ssh_key, user }, this.updateIdentity);
})
)
.catch(ex => { this.setState({ inProgress: false }); this.props.setError(ex) });
if (!this.props.error_options || this.props.error_options["auth-method-results"] === null) {
try2Connect(this.props.machines_ins, this.props.full_address)
.then(this.props.complete)
.catch(ex => {
this.setState({ inProgress: false });
this.props.setError(ex);
});
}
}
componentWillUnmount() {
if (this.keys)
this.keys.close();
this.keys = null;
}
componentDidUpdate(prevProps) {
if (prevProps.error_options !== this.props.error_options)
this.updateIdentity();
}
getSupports() {
let methods = null;
let available = null;
let offer_login_password = false;
let offer_key_password = false;
if (this.props.error_options) {
available = {};
methods = this.props.error_options["auth-method-results"];
if (methods) {
for (const method in methods) {
if (is_method_supported(methods, method)) {
available[method] = true;
}
}
}
offer_login_password = !!available.password;
offer_key_password = this.state.identity_path !== null;
} else {
offer_login_password = true;
offer_key_password = false;
}
return {
offer_login_password,
offer_key_password,
};
}
maybe_create_key(passphrase) {
if (!this.state.default_ssh_key.exists)
return this.keys.create(this.state.default_ssh_key.name, this.state.default_ssh_key.type, passphrase);
else
return Promise.resolve();
}
authorize_key(host) {
return this.keys.get_pubkey(this.state.default_ssh_key.name)
.then(data => cockpit.script(ssh_add_key_sh, [data.trim()], { host, err: "message" }));
}
maybe_unlock_key() {
const { offer_login_password, offer_key_password } = this.getSupports();
const both = offer_login_password && offer_key_password;
if ((both && this.state.auth === "key") || (!both && offer_key_password))
return this.keys.load(this.state.identity_path, this.state.locked_identity_password);
else
return Promise.resolve();
}
login() {
const options = {};
const user = split_connection_string(this.props.full_address).user || "";
const do_key_password_change = this.state.auto_login && this.state.default_ssh_key.unaligned_passphrase;
let custom_password_error = "";
let locked_identity_password_error = "";
let login_setup_new_key_password_error = "";
let login_setup_new_key_password2_error = "";
const { offer_login_password, offer_key_password } = this.getSupports();
const both = offer_login_password && offer_key_password;
if ((both && this.state.auth === "password") || (!both && offer_login_password)) {
if (!this.state.custom_password)
custom_password_error = _("The password can not be empty");
options.password = this.state.custom_password;
options.session = 'shared';
if (!user) {
/* we don't want to save the default user for everyone
* so we pass current user as an option, but make sure the
* session isn't private
*/
if (this.state.user && this.state.user.name)
options.user = this.state.user.name;
options["temp-session"] = false; /* Compatibility option */
}
}
if ((offer_key_password && !(both && this.state.auth === "password")) && !this.state.locked_identity_password)
locked_identity_password_error = _("The key password can not be empty");
if (this.state.auto_login && !do_key_password_change && this.state.login_setup_new_key_password !== this.state.login_setup_new_key_password2)
login_setup_new_key_password2_error = _("The key passwords do not match");
if (do_key_password_change && !this.state.login_setup_new_key_password)
login_setup_new_key_password_error = _("The new key password can not be empty");
if (do_key_password_change && this.state.login_setup_new_key_password !== this.state.login_setup_new_key_password2)
login_setup_new_key_password2_error = _("The key passwords do not match");
if (custom_password_error || locked_identity_password_error || login_setup_new_key_password_error || login_setup_new_key_password2_error) {
this.setState({
custom_password_error,
locked_identity_password_error,
login_setup_new_key_password_error,
login_setup_new_key_password2_error,
});
return;
}
this.setState({ inProgress: true });
const machine = this.props.machines_ins.lookup(this.props.full_address);
this.props.run(this.maybe_unlock_key()
.then(() => {
return try2Connect(this.props.machines_ins, this.props.full_address, options)
.then(() => {
if (machine)
return this.props.machines_ins.change(machine.address, { user });
else
return Promise.resolve();
})
.then(() => {
if (do_key_password_change)
return this.keys.change(this.state.default_ssh_key.name, this.state.locked_identity_password, this.state.login_setup_new_key_password);
else if (this.state.auto_login)
return this.maybe_create_key(this.state.login_setup_new_key_password)
.then(() => this.authorize_key(this.props.full_address));
else
return Promise.resolve();
});
})
.catch(ex => {
this.setState({ inProgress: false });
throw ex;
}));
}
render() {
const { offer_login_password, offer_key_password } = this.getSupports();
const both = offer_login_password && offer_key_password;
let offer_key_setup = true;
let show_password_advice = true;
if (!this.state.default_ssh_key)
offer_key_setup = false;
else if (this.state.default_ssh_key.unaligned_passphrase)
offer_key_setup = (both && this.state.auth === "key") || (!both && offer_key_password);
else if (this.state.identity_path) {
// This is a locked, non-default identity that will never
// be loaded into the agent, so there is no point in
// offering to change the passphrase.
show_password_advice = false;
offer_key_setup = false;
}
const callback = this.login;
const title = cockpit.format(_("Log in to $0"), this.props.full_address);
const submitText = _("Log in");
let statement = "";
if (!offer_login_password && !offer_key_password)
statement =
{cockpit.format(_("Unable to log in to $0. The host does not accept password login or any of your SSH keys."), this.props.full_address)}
;
else if (offer_login_password && !offer_key_password)
statement =
{cockpit.format(_("Unable to log in to $0 using SSH key authentication. Please provide the password. You may want to set up your SSH keys for automatic login."), this.props.full_address)}
;
else if (offer_key_password && !offer_login_password)
statement = <>
{cockpit.format(_("The SSH key for logging in to $0 is protected by a password, and the host does not allow logging in with a password. Please provide the password of the key at $1."), this.props.full_address, this.state.identity_path)}
{show_password_advice && {_("You may want to change the password of the key for automatic login.")}}
>;
else if (both)
statement = <>
{cockpit.format(_("The SSH key for logging in to $0 is protected. You can log in with either your login password or by providing the password of the key at $1."), this.props.full_address, this.state.identity_path)}
{show_password_advice && {_("You may want to change the password of the key for automatic login.")}}
>;
let auto_text = null;
let auto_details = null;
if (this.state.default_ssh_key) {
const lmach = this.props.machines_ins.lookup(null);
const key = this.state.default_ssh_key.name;
const luser = this.state.user.name;
const lhost = lmach ? lmach.label || lmach.address : "localhost";
const afile = "~/.ssh/authorized_keys";
const ruser = split_connection_string(this.props.full_address).user || this.state.user.name;
const rhost = split_connection_string(this.props.full_address).address;
if (!this.state.default_ssh_key.exists) {
auto_text = _("Create a new SSH key and authorize it");
auto_details = <>
{cockpit.format(_("A new SSH key at $0 will be created for $1 on $2 and it will be added to the $3 file of $4 on $5."), key, luser, lhost, afile, ruser, rhost)}
{cockpit.format(_("In order to allow log in to $0 as $1 without password in the future, use the login password of $2 on $3 as the key password, or leave the key password blank."), rhost, ruser, luser, lhost)}
>;
} else if (this.state.default_ssh_key.unaligned_passphrase) {
auto_text = cockpit.format(_("Change the password of $0"), key);
auto_details = <>
{cockpit.format(_("By changing the password of the SSH key $0 to the login password of $1 on $2, the key will be automatically made available and you can log in to $3 without password in the future."), key, luser, lhost, afile, rhost)}
{cockpit.format(_("In order to allow log in to $0 as $1 without password in the future, use the login password of $2 on $3 as the key password, or leave the key password blank."), rhost, ruser, luser, lhost)}