OpenSshServerKeyDatabase.java
- /*
- * Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.internal.transport.sshd;
- import static java.nio.charset.StandardCharsets.UTF_8;
- import static java.text.MessageFormat.format;
- import java.io.BufferedReader;
- import java.io.BufferedWriter;
- import java.io.FileNotFoundException;
- import java.io.IOException;
- import java.io.OutputStreamWriter;
- import java.net.InetSocketAddress;
- import java.net.SocketAddress;
- import java.nio.file.Files;
- import java.nio.file.InvalidPathException;
- import java.nio.file.Path;
- import java.nio.file.Paths;
- import java.security.GeneralSecurityException;
- import java.security.PublicKey;
- import java.security.SecureRandom;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collection;
- import java.util.Collections;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Map;
- import java.util.TreeSet;
- import java.util.concurrent.ConcurrentHashMap;
- import java.util.function.Supplier;
- import org.apache.sshd.client.config.hosts.HostPatternsHolder;
- import org.apache.sshd.client.config.hosts.KnownHostDigest;
- import org.apache.sshd.client.config.hosts.KnownHostEntry;
- import org.apache.sshd.client.config.hosts.KnownHostHashValue;
- import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
- import org.apache.sshd.client.session.ClientSession;
- import org.apache.sshd.common.NamedFactory;
- import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
- import org.apache.sshd.common.config.keys.KeyUtils;
- import org.apache.sshd.common.config.keys.PublicKeyEntry;
- import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
- import org.apache.sshd.common.digest.BuiltinDigests;
- import org.apache.sshd.common.mac.Mac;
- import org.apache.sshd.common.util.io.ModifiableFileWatcher;
- import org.apache.sshd.common.util.net.SshdSocketAddress;
- import org.eclipse.jgit.annotations.NonNull;
- import org.eclipse.jgit.internal.storage.file.LockFile;
- import org.eclipse.jgit.transport.CredentialItem;
- import org.eclipse.jgit.transport.CredentialsProvider;
- import org.eclipse.jgit.transport.URIish;
- import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- /**
- * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
- * {@code UserKnownHostsFile} values from the ssh configuration.
- * <p>
- * The verifier can be given default known_hosts files in the constructor, which
- * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
- * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
- * uses the given files in the order given. Non-existing or unreadable files are
- * ignored.
- * <p>
- * {@code StrictHostKeyChecking} accepts the following values:
- * </p>
- * <dl>
- * <dt>ask</dt>
- * <dd>Ask the user whether new or changed keys shall be accepted and be added
- * to the known_hosts file.</dd>
- * <dt>yes/true</dt>
- * <dd>Accept only keys listed in the known_hosts file.</dd>
- * <dt>no/false</dt>
- * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
- * file.</dd>
- * <dt>accept-new</dt>
- * <dd>Silently accept keys for new hosts and add them to the known_hosts
- * file.</dd>
- * </dl>
- * <p>
- * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
- * default value <b>ask</b> is active.
- * </p>
- * <p>
- * This implementation relies on the {@link ClientSession} being a
- * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
- * config file host entry to the session, so it would be unknown here which
- * entry it was and what setting of {@code StrictHostKeyChecking} should be
- * used. If used with some other session type, the implementation assumes
- * "<b>ask</b>".
- * <p>
- * <p>
- * Asking the user is done via a {@link CredentialsProvider} obtained from the
- * session. If none is set, the implementation falls back to strict host key
- * checking ("<b>yes</b>").
- * </p>
- * <p>
- * Note that adding a key to the known hosts file may create the file. You can
- * specify in the constructor whether the user shall be asked about that, too.
- * If the user declines updating the file, but the key was otherwise
- * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
- * active), the key is accepted for this session only.
- * </p>
- * <p>
- * If several known hosts files are specified, a new key is always added to the
- * first file (even if it doesn't exist yet; see the note about file creation
- * above).
- * </p>
- *
- * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
- * ssh-config</a>
- */
- public class OpenSshServerKeyDatabase
- implements ServerKeyDatabase {
- // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
- // files may be large!
- private static final Logger LOG = LoggerFactory
- .getLogger(OpenSshServerKeyDatabase.class);
- /** Can be used to mark revoked known host lines. */
- private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
- private final boolean askAboutNewFile;
- private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
- private final List<HostKeyFile> defaultFiles = new ArrayList<>();
- /**
- * Creates a new {@link OpenSshServerKeyDatabase}.
- *
- * @param askAboutNewFile
- * whether to ask the user, if possible, about creating a new
- * non-existing known_hosts file
- * @param defaultFiles
- * typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
- * empty or {@code null}, in which case no default files are
- * installed. The files need not exist.
- */
- public OpenSshServerKeyDatabase(boolean askAboutNewFile,
- List<Path> defaultFiles) {
- if (defaultFiles != null) {
- for (Path file : defaultFiles) {
- HostKeyFile newFile = new HostKeyFile(file);
- knownHostsFiles.put(file, newFile);
- this.defaultFiles.add(newFile);
- }
- }
- this.askAboutNewFile = askAboutNewFile;
- }
- private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
- List<HostKeyFile> filesToUse = defaultFiles;
- List<HostKeyFile> userFiles = addUserHostKeyFiles(
- config.getUserKnownHostsFiles());
- if (!userFiles.isEmpty()) {
- filesToUse = userFiles;
- }
- return filesToUse;
- }
- @Override
- public List<PublicKey> lookup(@NonNull String connectAddress,
- @NonNull InetSocketAddress remoteAddress,
- @NonNull Configuration config) {
- List<HostKeyFile> filesToUse = getFilesToUse(config);
- List<PublicKey> result = new ArrayList<>();
- Collection<SshdSocketAddress> candidates = getCandidates(
- connectAddress, remoteAddress);
- for (HostKeyFile file : filesToUse) {
- for (HostEntryPair current : file.get()) {
- KnownHostEntry entry = current.getHostEntry();
- for (SshdSocketAddress host : candidates) {
- if (entry.isHostMatch(host.getHostName(), host.getPort())) {
- result.add(current.getServerKey());
- break;
- }
- }
- }
- }
- return result;
- }
- @Override
- public boolean accept(@NonNull String connectAddress,
- @NonNull InetSocketAddress remoteAddress,
- @NonNull PublicKey serverKey,
- @NonNull Configuration config, CredentialsProvider provider) {
- List<HostKeyFile> filesToUse = getFilesToUse(config);
- AskUser ask = new AskUser(config, provider);
- HostEntryPair[] modified = { null };
- Path path = null;
- Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
- remoteAddress);
- for (HostKeyFile file : filesToUse) {
- try {
- if (find(candidates, serverKey, file.get(), modified)) {
- return true;
- }
- } catch (RevokedKeyException e) {
- ask.revokedKey(remoteAddress, serverKey, file.getPath());
- return false;
- }
- if (path == null && modified[0] != null) {
- // Remember the file in which we might need to update the
- // entry
- path = file.getPath();
- }
- }
- if (modified[0] != null) {
- // We found an entry, but with a different key
- AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
- remoteAddress, modified[0].getServerKey(),
- serverKey, path);
- if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
- try {
- updateModifiedServerKey(serverKey, modified[0], path);
- knownHostsFiles.get(path).resetReloadAttributes();
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
- path));
- }
- }
- if (toDo == AskUser.ModifiedKeyHandling.DENY) {
- return false;
- }
- // TODO: OpenSsh disables password and keyboard-interactive
- // authentication in this case. Also agent and local port forwarding
- // are switched off. (Plus a few other things such as X11 forwarding
- // that are of no interest to a git client.)
- return true;
- } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
- if (!filesToUse.isEmpty()) {
- HostKeyFile toUpdate = filesToUse.get(0);
- path = toUpdate.getPath();
- try {
- if (Files.exists(path) || !askAboutNewFile
- || ask.createNewFile(path)) {
- updateKnownHostsFile(candidates, serverKey, path,
- config);
- toUpdate.resetReloadAttributes();
- }
- } catch (Exception e) {
- LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
- path), e);
- }
- }
- return true;
- }
- return false;
- }
- private static class RevokedKeyException extends Exception {
- private static final long serialVersionUID = 1L;
- }
- private boolean find(Collection<SshdSocketAddress> candidates,
- PublicKey serverKey, List<HostEntryPair> entries,
- HostEntryPair[] modified) throws RevokedKeyException {
- for (HostEntryPair current : entries) {
- KnownHostEntry entry = current.getHostEntry();
- for (SshdSocketAddress host : candidates) {
- if (entry.isHostMatch(host.getHostName(), host.getPort())) {
- boolean isRevoked = MARKER_REVOKED
- .equals(entry.getMarker());
- if (KeyUtils.compareKeys(serverKey,
- current.getServerKey())) {
- // Exact match
- if (isRevoked) {
- throw new RevokedKeyException();
- }
- modified[0] = null;
- return true;
- } else if (!isRevoked) {
- // Server sent a different key
- modified[0] = current;
- // Keep going -- maybe there's another entry for this
- // host
- }
- }
- }
- }
- return false;
- }
- private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
- if (fileNames == null || fileNames.isEmpty()) {
- return Collections.emptyList();
- }
- List<HostKeyFile> userFiles = new ArrayList<>();
- for (String name : fileNames) {
- try {
- Path path = Paths.get(name);
- HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
- p -> new HostKeyFile(path));
- userFiles.add(file);
- } catch (InvalidPathException e) {
- LOG.warn(format(SshdText.get().knownHostsInvalidPath,
- name));
- }
- }
- return userFiles;
- }
- private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
- PublicKey serverKey, Path path, Configuration config)
- throws Exception {
- String newEntry = createHostKeyLine(candidates, serverKey, config);
- if (newEntry == null) {
- return;
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lockForAppend()) {
- try {
- try (BufferedWriter writer = new BufferedWriter(
- new OutputStreamWriter(lock.getOutputStream(),
- UTF_8))) {
- writer.newLine();
- writer.write(newEntry);
- writer.newLine();
- }
- lock.commit();
- } catch (IOException e) {
- lock.unlock();
- throw e;
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
- path));
- }
- }
- private void updateModifiedServerKey(PublicKey serverKey,
- HostEntryPair entry, Path path)
- throws IOException {
- KnownHostEntry hostEntry = entry.getHostEntry();
- String oldLine = hostEntry.getConfigLine();
- if (oldLine == null) {
- return;
- }
- String newLine = updateHostKeyLine(oldLine, serverKey);
- if (newLine == null || newLine.isEmpty()) {
- return;
- }
- if (oldLine.isEmpty() || newLine.equals(oldLine)) {
- // Shouldn't happen.
- return;
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lock()) {
- try {
- try (BufferedWriter writer = new BufferedWriter(
- new OutputStreamWriter(lock.getOutputStream(), UTF_8));
- BufferedReader reader = Files.newBufferedReader(path,
- UTF_8)) {
- boolean done = false;
- String line;
- while ((line = reader.readLine()) != null) {
- String toWrite = line;
- if (!done) {
- int pos = line.indexOf('#');
- String toTest = pos < 0 ? line
- : line.substring(0, pos);
- if (toTest.trim().equals(oldLine)) {
- toWrite = newLine;
- done = true;
- }
- }
- writer.write(toWrite);
- writer.newLine();
- }
- }
- lock.commit();
- } catch (IOException e) {
- lock.unlock();
- throw e;
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
- path));
- }
- }
- private static class AskUser {
- public enum ModifiedKeyHandling {
- DENY, ALLOW, ALLOW_AND_STORE
- }
- private enum Check {
- ASK, DENY, ALLOW;
- }
- private final @NonNull Configuration config;
- private final CredentialsProvider provider;
- public AskUser(@NonNull Configuration config,
- CredentialsProvider provider) {
- this.config = config;
- this.provider = provider;
- }
- private static boolean askUser(CredentialsProvider provider, URIish uri,
- String prompt, String... messages) {
- List<CredentialItem> items = new ArrayList<>(messages.length + 1);
- for (String message : messages) {
- items.add(new CredentialItem.InformationalMessage(message));
- }
- if (prompt != null) {
- CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
- prompt);
- items.add(answer);
- return provider.get(uri, items) && answer.getValue();
- }
- return provider.get(uri, items);
- }
- private Check checkMode(SocketAddress remoteAddress, boolean changed) {
- if (!(remoteAddress instanceof InetSocketAddress)) {
- return Check.DENY;
- }
- switch (config.getStrictHostKeyChecking()) {
- case REQUIRE_MATCH:
- return Check.DENY;
- case ACCEPT_ANY:
- return Check.ALLOW;
- case ACCEPT_NEW:
- return changed ? Check.DENY : Check.ALLOW;
- default:
- return provider == null ? Check.DENY : Check.ASK;
- }
- }
- public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
- Path path) {
- if (provider == null) {
- return;
- }
- InetSocketAddress remote = (InetSocketAddress) remoteAddress;
- URIish uri = JGitUserInteraction.toURI(config.getUsername(),
- remote);
- String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
- serverKey);
- String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
- String keyAlgorithm = serverKey.getAlgorithm();
- askUser(provider, uri, null, //
- format(SshdText.get().knownHostsRevokedKeyMsg,
- remote.getHostString(), path),
- format(SshdText.get().knownHostsKeyFingerprints,
- keyAlgorithm),
- md5, sha256);
- }
- public boolean acceptUnknownKey(SocketAddress remoteAddress,
- PublicKey serverKey) {
- Check check = checkMode(remoteAddress, false);
- if (check != Check.ASK) {
- return check == Check.ALLOW;
- }
- InetSocketAddress remote = (InetSocketAddress) remoteAddress;
- // Ask the user
- String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
- serverKey);
- String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
- String keyAlgorithm = serverKey.getAlgorithm();
- String remoteHost = remote.getHostString();
- URIish uri = JGitUserInteraction.toURI(config.getUsername(),
- remote);
- String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
- return askUser(provider, uri, prompt, //
- format(SshdText.get().knownHostsUnknownKeyMsg,
- remoteHost),
- format(SshdText.get().knownHostsKeyFingerprints,
- keyAlgorithm),
- md5, sha256);
- }
- public ModifiedKeyHandling acceptModifiedServerKey(
- InetSocketAddress remoteAddress, PublicKey expected,
- PublicKey actual, Path path) {
- Check check = checkMode(remoteAddress, true);
- if (check == Check.ALLOW) {
- // Never auto-store on CHECK.ALLOW
- return ModifiedKeyHandling.ALLOW;
- }
- String keyAlgorithm = actual.getAlgorithm();
- String remoteHost = remoteAddress.getHostString();
- URIish uri = JGitUserInteraction.toURI(config.getUsername(),
- remoteAddress);
- List<String> messages = new ArrayList<>();
- String warning = format(
- SshdText.get().knownHostsModifiedKeyWarning,
- keyAlgorithm, expected.getAlgorithm(), remoteHost,
- KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
- KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
- KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
- KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
- messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
- if (check == Check.DENY) {
- if (provider != null) {
- messages.add(format(
- SshdText.get().knownHostsModifiedKeyDenyMsg, path));
- askUser(provider, uri, null,
- messages.toArray(new String[0]));
- }
- return ModifiedKeyHandling.DENY;
- }
- // ASK -- two questions: procceed? and store?
- List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
- for (String message : messages) {
- items.add(new CredentialItem.InformationalMessage(message));
- }
- CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
- SshdText.get().knownHostsModifiedKeyAcceptPrompt);
- CredentialItem.YesNoType store = new CredentialItem.YesNoType(
- SshdText.get().knownHostsModifiedKeyStorePrompt);
- items.add(proceed);
- items.add(store);
- if (provider.get(uri, items) && proceed.getValue()) {
- return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
- : ModifiedKeyHandling.ALLOW;
- }
- return ModifiedKeyHandling.DENY;
- }
- public boolean createNewFile(Path path) {
- if (provider == null) {
- // We can't ask, so don't create the file
- return false;
- }
- URIish uri = new URIish().setPath(path.toString());
- return askUser(provider, uri, //
- format(SshdText.get().knownHostsUserAskCreationPrompt,
- path), //
- format(SshdText.get().knownHostsUserAskCreationMsg, path));
- }
- }
- private static class HostKeyFile extends ModifiableFileWatcher
- implements Supplier<List<HostEntryPair>> {
- private List<HostEntryPair> entries = Collections.emptyList();
- public HostKeyFile(Path path) {
- super(path);
- }
- @Override
- public List<HostEntryPair> get() {
- Path path = getPath();
- try {
- if (checkReloadRequired()) {
- if (!Files.exists(path)) {
- // Has disappeared.
- resetReloadAttributes();
- return Collections.emptyList();
- }
- LockFile lock = new LockFile(path.toFile());
- if (lock.lock()) {
- try {
- entries = reload(getPath());
- } finally {
- lock.unlock();
- }
- } else {
- LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
- path));
- }
- }
- } catch (IOException e) {
- LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
- }
- return Collections.unmodifiableList(entries);
- }
- private List<HostEntryPair> reload(Path path) throws IOException {
- try {
- List<KnownHostEntry> rawEntries = KnownHostEntryReader
- .readFromFile(path);
- updateReloadAttributes();
- if (rawEntries == null || rawEntries.isEmpty()) {
- return Collections.emptyList();
- }
- List<HostEntryPair> newEntries = new LinkedList<>();
- for (KnownHostEntry entry : rawEntries) {
- AuthorizedKeyEntry keyPart = entry.getKeyEntry();
- if (keyPart == null) {
- continue;
- }
- try {
- PublicKey serverKey = keyPart.resolvePublicKey(null,
- PublicKeyEntryResolver.IGNORING);
- if (serverKey == null) {
- LOG.warn(format(
- SshdText.get().knownHostsUnknownKeyType,
- path, entry.getConfigLine()));
- } else {
- newEntries.add(new HostEntryPair(entry, serverKey));
- }
- } catch (GeneralSecurityException e) {
- LOG.warn(format(SshdText.get().knownHostsInvalidLine,
- path, entry.getConfigLine()));
- }
- }
- return newEntries;
- } catch (FileNotFoundException e) {
- resetReloadAttributes();
- return Collections.emptyList();
- }
- }
- }
- private int parsePort(String s) {
- try {
- return Integer.parseInt(s);
- } catch (NumberFormatException e) {
- return -1;
- }
- }
- private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
- String host = null;
- int port = 0;
- if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
- .charAt(0)) {
- int end = address.indexOf(
- HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
- if (end <= 1) {
- return null; // Invalid
- }
- host = address.substring(1, end);
- if (end < address.length() - 1
- && HostPatternsHolder.PORT_VALUE_DELIMITER == address
- .charAt(end + 1)) {
- port = parsePort(address.substring(end + 2));
- }
- } else {
- int i = address
- .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER);
- if (i > 0) {
- port = parsePort(address.substring(i + 1));
- host = address.substring(0, i);
- } else {
- host = address;
- }
- }
- if (port < 0 || port > 65535) {
- return null;
- }
- return new SshdSocketAddress(host, port);
- }
- private Collection<SshdSocketAddress> getCandidates(
- @NonNull String connectAddress,
- @NonNull InetSocketAddress remoteAddress) {
- Collection<SshdSocketAddress> candidates = new TreeSet<>(
- SshdSocketAddress.BY_HOST_AND_PORT);
- candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
- SshdSocketAddress address = toSshdSocketAddress(connectAddress);
- if (address != null) {
- candidates.add(address);
- }
- return candidates;
- }
- private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
- PublicKey key, Configuration config) throws Exception {
- StringBuilder result = new StringBuilder();
- if (config.getHashKnownHosts()) {
- // SHA1 is the only algorithm for host name hashing known to OpenSSH
- // or to Apache MINA sshd.
- NamedFactory<Mac> digester = KnownHostDigest.SHA1;
- Mac mac = digester.create();
- SecureRandom prng = new SecureRandom();
- byte[] salt = new byte[mac.getDefaultBlockSize()];
- for (SshdSocketAddress address : patterns) {
- if (result.length() > 0) {
- result.append(',');
- }
- prng.nextBytes(salt);
- KnownHostHashValue.append(result, digester, salt,
- KnownHostHashValue.calculateHashValue(
- address.getHostName(), address.getPort(), mac,
- salt));
- }
- } else {
- for (SshdSocketAddress address : patterns) {
- if (result.length() > 0) {
- result.append(',');
- }
- KnownHostHashValue.appendHostPattern(result,
- address.getHostName(), address.getPort());
- }
- }
- result.append(' ');
- PublicKeyEntry.appendPublicKeyEntry(result, key);
- return result.toString();
- }
- private String updateHostKeyLine(String line, PublicKey newKey)
- throws IOException {
- // Replaces an existing public key by the new key
- int pos = line.indexOf(' ');
- if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
- // We're at the end of the marker. Skip ahead to the next blank.
- pos = line.indexOf(' ', pos + 1);
- }
- if (pos < 0) {
- // Don't update if bogus format
- return null;
- }
- StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
- PublicKeyEntry.appendPublicKeyEntry(result, newKey);
- return result.toString();
- }
- }