OpenSshServerKeyDatabase.java

  1. /*
  2.  * Copyright (C) 2018, 2019 Thomas Wolf <thomas.wolf@paranor.ch> and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */
  10. package org.eclipse.jgit.internal.transport.sshd;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static java.text.MessageFormat.format;

  13. import java.io.BufferedReader;
  14. import java.io.BufferedWriter;
  15. import java.io.FileNotFoundException;
  16. import java.io.IOException;
  17. import java.io.OutputStreamWriter;
  18. import java.net.InetSocketAddress;
  19. import java.net.SocketAddress;
  20. import java.nio.file.Files;
  21. import java.nio.file.InvalidPathException;
  22. import java.nio.file.Path;
  23. import java.nio.file.Paths;
  24. import java.security.GeneralSecurityException;
  25. import java.security.PublicKey;
  26. import java.security.SecureRandom;
  27. import java.util.ArrayList;
  28. import java.util.Arrays;
  29. import java.util.Collection;
  30. import java.util.Collections;
  31. import java.util.LinkedList;
  32. import java.util.List;
  33. import java.util.Map;
  34. import java.util.TreeSet;
  35. import java.util.concurrent.ConcurrentHashMap;
  36. import java.util.function.Supplier;

  37. import org.apache.sshd.client.config.hosts.HostPatternsHolder;
  38. import org.apache.sshd.client.config.hosts.KnownHostDigest;
  39. import org.apache.sshd.client.config.hosts.KnownHostEntry;
  40. import org.apache.sshd.client.config.hosts.KnownHostHashValue;
  41. import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
  42. import org.apache.sshd.client.session.ClientSession;
  43. import org.apache.sshd.common.NamedFactory;
  44. import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
  45. import org.apache.sshd.common.config.keys.KeyUtils;
  46. import org.apache.sshd.common.config.keys.PublicKeyEntry;
  47. import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
  48. import org.apache.sshd.common.digest.BuiltinDigests;
  49. import org.apache.sshd.common.mac.Mac;
  50. import org.apache.sshd.common.util.io.ModifiableFileWatcher;
  51. import org.apache.sshd.common.util.net.SshdSocketAddress;
  52. import org.eclipse.jgit.annotations.NonNull;
  53. import org.eclipse.jgit.internal.storage.file.LockFile;
  54. import org.eclipse.jgit.transport.CredentialItem;
  55. import org.eclipse.jgit.transport.CredentialsProvider;
  56. import org.eclipse.jgit.transport.URIish;
  57. import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
  58. import org.slf4j.Logger;
  59. import org.slf4j.LoggerFactory;

  60. /**
  61.  * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
  62.  * {@code UserKnownHostsFile} values from the ssh configuration.
  63.  * <p>
  64.  * The verifier can be given default known_hosts files in the constructor, which
  65.  * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
  66.  * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
  67.  * uses the given files in the order given. Non-existing or unreadable files are
  68.  * ignored.
  69.  * <p>
  70.  * {@code StrictHostKeyChecking} accepts the following values:
  71.  * </p>
  72.  * <dl>
  73.  * <dt>ask</dt>
  74.  * <dd>Ask the user whether new or changed keys shall be accepted and be added
  75.  * to the known_hosts file.</dd>
  76.  * <dt>yes/true</dt>
  77.  * <dd>Accept only keys listed in the known_hosts file.</dd>
  78.  * <dt>no/false</dt>
  79.  * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
  80.  * file.</dd>
  81.  * <dt>accept-new</dt>
  82.  * <dd>Silently accept keys for new hosts and add them to the known_hosts
  83.  * file.</dd>
  84.  * </dl>
  85.  * <p>
  86.  * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
  87.  * default value <b>ask</b> is active.
  88.  * </p>
  89.  * <p>
  90.  * This implementation relies on the {@link ClientSession} being a
  91.  * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
  92.  * config file host entry to the session, so it would be unknown here which
  93.  * entry it was and what setting of {@code StrictHostKeyChecking} should be
  94.  * used. If used with some other session type, the implementation assumes
  95.  * "<b>ask</b>".
  96.  * <p>
  97.  * <p>
  98.  * Asking the user is done via a {@link CredentialsProvider} obtained from the
  99.  * session. If none is set, the implementation falls back to strict host key
  100.  * checking ("<b>yes</b>").
  101.  * </p>
  102.  * <p>
  103.  * Note that adding a key to the known hosts file may create the file. You can
  104.  * specify in the constructor whether the user shall be asked about that, too.
  105.  * If the user declines updating the file, but the key was otherwise
  106.  * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
  107.  * active), the key is accepted for this session only.
  108.  * </p>
  109.  * <p>
  110.  * If several known hosts files are specified, a new key is always added to the
  111.  * first file (even if it doesn't exist yet; see the note about file creation
  112.  * above).
  113.  * </p>
  114.  *
  115.  * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
  116.  *      ssh-config</a>
  117.  */
  118. public class OpenSshServerKeyDatabase
  119.         implements ServerKeyDatabase {

  120.     // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
  121.     // files may be large!

  122.     private static final Logger LOG = LoggerFactory
  123.             .getLogger(OpenSshServerKeyDatabase.class);

  124.     /** Can be used to mark revoked known host lines. */
  125.     private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$

  126.     private final boolean askAboutNewFile;

  127.     private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();

  128.     private final List<HostKeyFile> defaultFiles = new ArrayList<>();

  129.     /**
  130.      * Creates a new {@link OpenSshServerKeyDatabase}.
  131.      *
  132.      * @param askAboutNewFile
  133.      *            whether to ask the user, if possible, about creating a new
  134.      *            non-existing known_hosts file
  135.      * @param defaultFiles
  136.      *            typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
  137.      *            empty or {@code null}, in which case no default files are
  138.      *            installed. The files need not exist.
  139.      */
  140.     public OpenSshServerKeyDatabase(boolean askAboutNewFile,
  141.             List<Path> defaultFiles) {
  142.         if (defaultFiles != null) {
  143.             for (Path file : defaultFiles) {
  144.                 HostKeyFile newFile = new HostKeyFile(file);
  145.                 knownHostsFiles.put(file, newFile);
  146.                 this.defaultFiles.add(newFile);
  147.             }
  148.         }
  149.         this.askAboutNewFile = askAboutNewFile;
  150.     }

  151.     private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
  152.         List<HostKeyFile> filesToUse = defaultFiles;
  153.         List<HostKeyFile> userFiles = addUserHostKeyFiles(
  154.                 config.getUserKnownHostsFiles());
  155.         if (!userFiles.isEmpty()) {
  156.             filesToUse = userFiles;
  157.         }
  158.         return filesToUse;
  159.     }

  160.     @Override
  161.     public List<PublicKey> lookup(@NonNull String connectAddress,
  162.             @NonNull InetSocketAddress remoteAddress,
  163.             @NonNull Configuration config) {
  164.         List<HostKeyFile> filesToUse = getFilesToUse(config);
  165.         List<PublicKey> result = new ArrayList<>();
  166.         Collection<SshdSocketAddress> candidates = getCandidates(
  167.                 connectAddress, remoteAddress);
  168.         for (HostKeyFile file : filesToUse) {
  169.             for (HostEntryPair current : file.get()) {
  170.                 KnownHostEntry entry = current.getHostEntry();
  171.                 for (SshdSocketAddress host : candidates) {
  172.                     if (entry.isHostMatch(host.getHostName(), host.getPort())) {
  173.                         result.add(current.getServerKey());
  174.                         break;
  175.                     }
  176.                 }
  177.             }
  178.         }
  179.         return result;
  180.     }

  181.     @Override
  182.     public boolean accept(@NonNull String connectAddress,
  183.             @NonNull InetSocketAddress remoteAddress,
  184.             @NonNull PublicKey serverKey,
  185.             @NonNull Configuration config, CredentialsProvider provider) {
  186.         List<HostKeyFile> filesToUse = getFilesToUse(config);
  187.         AskUser ask = new AskUser(config, provider);
  188.         HostEntryPair[] modified = { null };
  189.         Path path = null;
  190.         Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
  191.                 remoteAddress);
  192.         for (HostKeyFile file : filesToUse) {
  193.             try {
  194.                 if (find(candidates, serverKey, file.get(), modified)) {
  195.                     return true;
  196.                 }
  197.             } catch (RevokedKeyException e) {
  198.                 ask.revokedKey(remoteAddress, serverKey, file.getPath());
  199.                 return false;
  200.             }
  201.             if (path == null && modified[0] != null) {
  202.                 // Remember the file in which we might need to update the
  203.                 // entry
  204.                 path = file.getPath();
  205.             }
  206.         }
  207.         if (modified[0] != null) {
  208.             // We found an entry, but with a different key
  209.             AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
  210.                     remoteAddress, modified[0].getServerKey(),
  211.                     serverKey, path);
  212.             if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
  213.                 try {
  214.                     updateModifiedServerKey(serverKey, modified[0], path);
  215.                     knownHostsFiles.get(path).resetReloadAttributes();
  216.                 } catch (IOException e) {
  217.                     LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
  218.                             path));
  219.                 }
  220.             }
  221.             if (toDo == AskUser.ModifiedKeyHandling.DENY) {
  222.                 return false;
  223.             }
  224.             // TODO: OpenSsh disables password and keyboard-interactive
  225.             // authentication in this case. Also agent and local port forwarding
  226.             // are switched off. (Plus a few other things such as X11 forwarding
  227.             // that are of no interest to a git client.)
  228.             return true;
  229.         } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
  230.             if (!filesToUse.isEmpty()) {
  231.                 HostKeyFile toUpdate = filesToUse.get(0);
  232.                 path = toUpdate.getPath();
  233.                 try {
  234.                     if (Files.exists(path) || !askAboutNewFile
  235.                             || ask.createNewFile(path)) {
  236.                         updateKnownHostsFile(candidates, serverKey, path,
  237.                                 config);
  238.                         toUpdate.resetReloadAttributes();
  239.                     }
  240.                 } catch (Exception e) {
  241.                     LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
  242.                             path), e);
  243.                 }
  244.             }
  245.             return true;
  246.         }
  247.         return false;
  248.     }

  249.     private static class RevokedKeyException extends Exception {
  250.         private static final long serialVersionUID = 1L;
  251.     }

  252.     private boolean find(Collection<SshdSocketAddress> candidates,
  253.             PublicKey serverKey, List<HostEntryPair> entries,
  254.             HostEntryPair[] modified) throws RevokedKeyException {
  255.         for (HostEntryPair current : entries) {
  256.             KnownHostEntry entry = current.getHostEntry();
  257.             for (SshdSocketAddress host : candidates) {
  258.                 if (entry.isHostMatch(host.getHostName(), host.getPort())) {
  259.                     boolean isRevoked = MARKER_REVOKED
  260.                             .equals(entry.getMarker());
  261.                     if (KeyUtils.compareKeys(serverKey,
  262.                             current.getServerKey())) {
  263.                         // Exact match
  264.                         if (isRevoked) {
  265.                             throw new RevokedKeyException();
  266.                         }
  267.                         modified[0] = null;
  268.                         return true;
  269.                     } else if (!isRevoked) {
  270.                         // Server sent a different key
  271.                         modified[0] = current;
  272.                         // Keep going -- maybe there's another entry for this
  273.                         // host
  274.                     }
  275.                 }
  276.             }
  277.         }
  278.         return false;
  279.     }

  280.     private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
  281.         if (fileNames == null || fileNames.isEmpty()) {
  282.             return Collections.emptyList();
  283.         }
  284.         List<HostKeyFile> userFiles = new ArrayList<>();
  285.         for (String name : fileNames) {
  286.             try {
  287.                 Path path = Paths.get(name);
  288.                 HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
  289.                         p -> new HostKeyFile(path));
  290.                 userFiles.add(file);
  291.             } catch (InvalidPathException e) {
  292.                 LOG.warn(format(SshdText.get().knownHostsInvalidPath,
  293.                         name));
  294.             }
  295.         }
  296.         return userFiles;
  297.     }

  298.     private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
  299.             PublicKey serverKey, Path path, Configuration config)
  300.             throws Exception {
  301.         String newEntry = createHostKeyLine(candidates, serverKey, config);
  302.         if (newEntry == null) {
  303.             return;
  304.         }
  305.         LockFile lock = new LockFile(path.toFile());
  306.         if (lock.lockForAppend()) {
  307.             try {
  308.                 try (BufferedWriter writer = new BufferedWriter(
  309.                         new OutputStreamWriter(lock.getOutputStream(),
  310.                                 UTF_8))) {
  311.                     writer.newLine();
  312.                     writer.write(newEntry);
  313.                     writer.newLine();
  314.                 }
  315.                 lock.commit();
  316.             } catch (IOException e) {
  317.                 lock.unlock();
  318.                 throw e;
  319.             }
  320.         } else {
  321.             LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
  322.                     path));
  323.         }
  324.     }

  325.     private void updateModifiedServerKey(PublicKey serverKey,
  326.             HostEntryPair entry, Path path)
  327.             throws IOException {
  328.         KnownHostEntry hostEntry = entry.getHostEntry();
  329.         String oldLine = hostEntry.getConfigLine();
  330.         if (oldLine == null) {
  331.             return;
  332.         }
  333.         String newLine = updateHostKeyLine(oldLine, serverKey);
  334.         if (newLine == null || newLine.isEmpty()) {
  335.             return;
  336.         }
  337.         if (oldLine.isEmpty() || newLine.equals(oldLine)) {
  338.             // Shouldn't happen.
  339.             return;
  340.         }
  341.         LockFile lock = new LockFile(path.toFile());
  342.         if (lock.lock()) {
  343.             try {
  344.                 try (BufferedWriter writer = new BufferedWriter(
  345.                         new OutputStreamWriter(lock.getOutputStream(), UTF_8));
  346.                         BufferedReader reader = Files.newBufferedReader(path,
  347.                                 UTF_8)) {
  348.                     boolean done = false;
  349.                     String line;
  350.                     while ((line = reader.readLine()) != null) {
  351.                         String toWrite = line;
  352.                         if (!done) {
  353.                             int pos = line.indexOf('#');
  354.                             String toTest = pos < 0 ? line
  355.                                     : line.substring(0, pos);
  356.                             if (toTest.trim().equals(oldLine)) {
  357.                                 toWrite = newLine;
  358.                                 done = true;
  359.                             }
  360.                         }
  361.                         writer.write(toWrite);
  362.                         writer.newLine();
  363.                     }
  364.                 }
  365.                 lock.commit();
  366.             } catch (IOException e) {
  367.                 lock.unlock();
  368.                 throw e;
  369.             }
  370.         } else {
  371.             LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
  372.                     path));
  373.         }
  374.     }

  375.     private static class AskUser {

  376.         public enum ModifiedKeyHandling {
  377.             DENY, ALLOW, ALLOW_AND_STORE
  378.         }

  379.         private enum Check {
  380.             ASK, DENY, ALLOW;
  381.         }

  382.         private final @NonNull Configuration config;

  383.         private final CredentialsProvider provider;

  384.         public AskUser(@NonNull Configuration config,
  385.                 CredentialsProvider provider) {
  386.             this.config = config;
  387.             this.provider = provider;
  388.         }

  389.         private static boolean askUser(CredentialsProvider provider, URIish uri,
  390.                 String prompt, String... messages) {
  391.             List<CredentialItem> items = new ArrayList<>(messages.length + 1);
  392.             for (String message : messages) {
  393.                 items.add(new CredentialItem.InformationalMessage(message));
  394.             }
  395.             if (prompt != null) {
  396.                 CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
  397.                         prompt);
  398.                 items.add(answer);
  399.                 return provider.get(uri, items) && answer.getValue();
  400.             }
  401.             return provider.get(uri, items);
  402.         }

  403.         private Check checkMode(SocketAddress remoteAddress, boolean changed) {
  404.             if (!(remoteAddress instanceof InetSocketAddress)) {
  405.                 return Check.DENY;
  406.             }
  407.             switch (config.getStrictHostKeyChecking()) {
  408.             case REQUIRE_MATCH:
  409.                 return Check.DENY;
  410.             case ACCEPT_ANY:
  411.                 return Check.ALLOW;
  412.             case ACCEPT_NEW:
  413.                 return changed ? Check.DENY : Check.ALLOW;
  414.             default:
  415.                 return provider == null ? Check.DENY : Check.ASK;
  416.             }
  417.         }

  418.         public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
  419.                 Path path) {
  420.             if (provider == null) {
  421.                 return;
  422.             }
  423.             InetSocketAddress remote = (InetSocketAddress) remoteAddress;
  424.             URIish uri = JGitUserInteraction.toURI(config.getUsername(),
  425.                     remote);
  426.             String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
  427.                     serverKey);
  428.             String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
  429.             String keyAlgorithm = serverKey.getAlgorithm();
  430.             askUser(provider, uri, null, //
  431.                     format(SshdText.get().knownHostsRevokedKeyMsg,
  432.                             remote.getHostString(), path),
  433.                     format(SshdText.get().knownHostsKeyFingerprints,
  434.                             keyAlgorithm),
  435.                     md5, sha256);
  436.         }

  437.         public boolean acceptUnknownKey(SocketAddress remoteAddress,
  438.                 PublicKey serverKey) {
  439.             Check check = checkMode(remoteAddress, false);
  440.             if (check != Check.ASK) {
  441.                 return check == Check.ALLOW;
  442.             }
  443.             InetSocketAddress remote = (InetSocketAddress) remoteAddress;
  444.             // Ask the user
  445.             String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
  446.                     serverKey);
  447.             String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
  448.             String keyAlgorithm = serverKey.getAlgorithm();
  449.             String remoteHost = remote.getHostString();
  450.             URIish uri = JGitUserInteraction.toURI(config.getUsername(),
  451.                     remote);
  452.             String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
  453.             return askUser(provider, uri, prompt, //
  454.                     format(SshdText.get().knownHostsUnknownKeyMsg,
  455.                             remoteHost),
  456.                     format(SshdText.get().knownHostsKeyFingerprints,
  457.                             keyAlgorithm),
  458.                     md5, sha256);
  459.         }

  460.         public ModifiedKeyHandling acceptModifiedServerKey(
  461.                 InetSocketAddress remoteAddress, PublicKey expected,
  462.                 PublicKey actual, Path path) {
  463.             Check check = checkMode(remoteAddress, true);
  464.             if (check == Check.ALLOW) {
  465.                 // Never auto-store on CHECK.ALLOW
  466.                 return ModifiedKeyHandling.ALLOW;
  467.             }
  468.             String keyAlgorithm = actual.getAlgorithm();
  469.             String remoteHost = remoteAddress.getHostString();
  470.             URIish uri = JGitUserInteraction.toURI(config.getUsername(),
  471.                     remoteAddress);
  472.             List<String> messages = new ArrayList<>();
  473.             String warning = format(
  474.                     SshdText.get().knownHostsModifiedKeyWarning,
  475.                     keyAlgorithm, expected.getAlgorithm(), remoteHost,
  476.                     KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
  477.                     KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
  478.                     KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
  479.                     KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
  480.             messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$

  481.             if (check == Check.DENY) {
  482.                 if (provider != null) {
  483.                     messages.add(format(
  484.                             SshdText.get().knownHostsModifiedKeyDenyMsg, path));
  485.                     askUser(provider, uri, null,
  486.                             messages.toArray(new String[0]));
  487.                 }
  488.                 return ModifiedKeyHandling.DENY;
  489.             }
  490.             // ASK -- two questions: procceed? and store?
  491.             List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
  492.             for (String message : messages) {
  493.                 items.add(new CredentialItem.InformationalMessage(message));
  494.             }
  495.             CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
  496.                     SshdText.get().knownHostsModifiedKeyAcceptPrompt);
  497.             CredentialItem.YesNoType store = new CredentialItem.YesNoType(
  498.                     SshdText.get().knownHostsModifiedKeyStorePrompt);
  499.             items.add(proceed);
  500.             items.add(store);
  501.             if (provider.get(uri, items) && proceed.getValue()) {
  502.                 return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
  503.                         : ModifiedKeyHandling.ALLOW;
  504.             }
  505.             return ModifiedKeyHandling.DENY;
  506.         }

  507.         public boolean createNewFile(Path path) {
  508.             if (provider == null) {
  509.                 // We can't ask, so don't create the file
  510.                 return false;
  511.             }
  512.             URIish uri = new URIish().setPath(path.toString());
  513.             return askUser(provider, uri, //
  514.                     format(SshdText.get().knownHostsUserAskCreationPrompt,
  515.                             path), //
  516.                     format(SshdText.get().knownHostsUserAskCreationMsg, path));
  517.         }
  518.     }

  519.     private static class HostKeyFile extends ModifiableFileWatcher
  520.             implements Supplier<List<HostEntryPair>> {

  521.         private List<HostEntryPair> entries = Collections.emptyList();

  522.         public HostKeyFile(Path path) {
  523.             super(path);
  524.         }

  525.         @Override
  526.         public List<HostEntryPair> get() {
  527.             Path path = getPath();
  528.             try {
  529.                 if (checkReloadRequired()) {
  530.                     if (!Files.exists(path)) {
  531.                         // Has disappeared.
  532.                         resetReloadAttributes();
  533.                         return Collections.emptyList();
  534.                     }
  535.                     LockFile lock = new LockFile(path.toFile());
  536.                     if (lock.lock()) {
  537.                         try {
  538.                             entries = reload(getPath());
  539.                         } finally {
  540.                             lock.unlock();
  541.                         }
  542.                     } else {
  543.                         LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
  544.                                 path));
  545.                     }
  546.                 }
  547.             } catch (IOException e) {
  548.                 LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
  549.             }
  550.             return Collections.unmodifiableList(entries);
  551.         }

  552.         private List<HostEntryPair> reload(Path path) throws IOException {
  553.             try {
  554.                 List<KnownHostEntry> rawEntries = KnownHostEntryReader
  555.                         .readFromFile(path);
  556.                 updateReloadAttributes();
  557.                 if (rawEntries == null || rawEntries.isEmpty()) {
  558.                     return Collections.emptyList();
  559.                 }
  560.                 List<HostEntryPair> newEntries = new LinkedList<>();
  561.                 for (KnownHostEntry entry : rawEntries) {
  562.                     AuthorizedKeyEntry keyPart = entry.getKeyEntry();
  563.                     if (keyPart == null) {
  564.                         continue;
  565.                     }
  566.                     try {
  567.                         PublicKey serverKey = keyPart.resolvePublicKey(null,
  568.                                 PublicKeyEntryResolver.IGNORING);
  569.                         if (serverKey == null) {
  570.                             LOG.warn(format(
  571.                                     SshdText.get().knownHostsUnknownKeyType,
  572.                                     path, entry.getConfigLine()));
  573.                         } else {
  574.                             newEntries.add(new HostEntryPair(entry, serverKey));
  575.                         }
  576.                     } catch (GeneralSecurityException e) {
  577.                         LOG.warn(format(SshdText.get().knownHostsInvalidLine,
  578.                                 path, entry.getConfigLine()));
  579.                     }
  580.                 }
  581.                 return newEntries;
  582.             } catch (FileNotFoundException e) {
  583.                 resetReloadAttributes();
  584.                 return Collections.emptyList();
  585.             }
  586.         }
  587.     }

  588.     private int parsePort(String s) {
  589.         try {
  590.             return Integer.parseInt(s);
  591.         } catch (NumberFormatException e) {
  592.             return -1;
  593.         }
  594.     }

  595.     private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
  596.         String host = null;
  597.         int port = 0;
  598.         if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
  599.                 .charAt(0)) {
  600.             int end = address.indexOf(
  601.                     HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
  602.             if (end <= 1) {
  603.                 return null; // Invalid
  604.             }
  605.             host = address.substring(1, end);
  606.             if (end < address.length() - 1
  607.                     && HostPatternsHolder.PORT_VALUE_DELIMITER == address
  608.                             .charAt(end + 1)) {
  609.                 port = parsePort(address.substring(end + 2));
  610.             }
  611.         } else {
  612.             int i = address
  613.                     .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER);
  614.             if (i > 0) {
  615.                 port = parsePort(address.substring(i + 1));
  616.                 host = address.substring(0, i);
  617.             } else {
  618.                 host = address;
  619.             }
  620.         }
  621.         if (port < 0 || port > 65535) {
  622.             return null;
  623.         }
  624.         return new SshdSocketAddress(host, port);
  625.     }

  626.     private Collection<SshdSocketAddress> getCandidates(
  627.             @NonNull String connectAddress,
  628.             @NonNull InetSocketAddress remoteAddress) {
  629.         Collection<SshdSocketAddress> candidates = new TreeSet<>(
  630.                 SshdSocketAddress.BY_HOST_AND_PORT);
  631.         candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
  632.         SshdSocketAddress address = toSshdSocketAddress(connectAddress);
  633.         if (address != null) {
  634.             candidates.add(address);
  635.         }
  636.         return candidates;
  637.     }

  638.     private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
  639.             PublicKey key, Configuration config) throws Exception {
  640.         StringBuilder result = new StringBuilder();
  641.         if (config.getHashKnownHosts()) {
  642.             // SHA1 is the only algorithm for host name hashing known to OpenSSH
  643.             // or to Apache MINA sshd.
  644.             NamedFactory<Mac> digester = KnownHostDigest.SHA1;
  645.             Mac mac = digester.create();
  646.             SecureRandom prng = new SecureRandom();
  647.             byte[] salt = new byte[mac.getDefaultBlockSize()];
  648.             for (SshdSocketAddress address : patterns) {
  649.                 if (result.length() > 0) {
  650.                     result.append(',');
  651.                 }
  652.                 prng.nextBytes(salt);
  653.                 KnownHostHashValue.append(result, digester, salt,
  654.                         KnownHostHashValue.calculateHashValue(
  655.                                 address.getHostName(), address.getPort(), mac,
  656.                                 salt));
  657.             }
  658.         } else {
  659.             for (SshdSocketAddress address : patterns) {
  660.                 if (result.length() > 0) {
  661.                     result.append(',');
  662.                 }
  663.                 KnownHostHashValue.appendHostPattern(result,
  664.                         address.getHostName(), address.getPort());
  665.             }
  666.         }
  667.         result.append(' ');
  668.         PublicKeyEntry.appendPublicKeyEntry(result, key);
  669.         return result.toString();
  670.     }

  671.     private String updateHostKeyLine(String line, PublicKey newKey)
  672.             throws IOException {
  673.         // Replaces an existing public key by the new key
  674.         int pos = line.indexOf(' ');
  675.         if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
  676.             // We're at the end of the marker. Skip ahead to the next blank.
  677.             pos = line.indexOf(' ', pos + 1);
  678.         }
  679.         if (pos < 0) {
  680.             // Don't update if bogus format
  681.             return null;
  682.         }
  683.         StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
  684.         PublicKeyEntry.appendPublicKeyEntry(result, newKey);
  685.         return result.toString();
  686.     }

  687. }