]> source.dussan.org Git - jgit.git/blob
5bc1115f6247b051dea60ced10384e6a4ee7a98a
[jgit.git] /
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
12 import static java.nio.charset.StandardCharsets.UTF_8;
13 import static java.text.MessageFormat.format;
14
15 import java.io.BufferedReader;
16 import java.io.BufferedWriter;
17 import java.io.FileNotFoundException;
18 import java.io.IOException;
19 import java.io.OutputStreamWriter;
20 import java.net.InetSocketAddress;
21 import java.net.SocketAddress;
22 import java.nio.file.Files;
23 import java.nio.file.InvalidPathException;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.security.GeneralSecurityException;
27 import java.security.PublicKey;
28 import java.security.SecureRandom;
29 import java.util.ArrayList;
30 import java.util.Arrays;
31 import java.util.Collection;
32 import java.util.Collections;
33 import java.util.LinkedList;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.TreeSet;
37 import java.util.concurrent.ConcurrentHashMap;
38 import java.util.function.Supplier;
39
40 import org.apache.sshd.client.config.hosts.HostPatternsHolder;
41 import org.apache.sshd.client.config.hosts.KnownHostDigest;
42 import org.apache.sshd.client.config.hosts.KnownHostEntry;
43 import org.apache.sshd.client.config.hosts.KnownHostHashValue;
44 import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryPair;
45 import org.apache.sshd.client.session.ClientSession;
46 import org.apache.sshd.common.NamedFactory;
47 import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
48 import org.apache.sshd.common.config.keys.KeyUtils;
49 import org.apache.sshd.common.config.keys.PublicKeyEntry;
50 import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
51 import org.apache.sshd.common.digest.BuiltinDigests;
52 import org.apache.sshd.common.mac.Mac;
53 import org.apache.sshd.common.util.io.ModifiableFileWatcher;
54 import org.apache.sshd.common.util.net.SshdSocketAddress;
55 import org.eclipse.jgit.annotations.NonNull;
56 import org.eclipse.jgit.internal.storage.file.LockFile;
57 import org.eclipse.jgit.transport.CredentialItem;
58 import org.eclipse.jgit.transport.CredentialsProvider;
59 import org.eclipse.jgit.transport.URIish;
60 import org.eclipse.jgit.transport.sshd.ServerKeyDatabase;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * A sever host key verifier that honors the {@code StrictHostKeyChecking} and
66  * {@code UserKnownHostsFile} values from the ssh configuration.
67  * <p>
68  * The verifier can be given default known_hosts files in the constructor, which
69  * will be used if the ssh config does not specify a {@code UserKnownHostsFile}.
70  * If the ssh config <em>does</em> set {@code UserKnownHostsFile}, the verifier
71  * uses the given files in the order given. Non-existing or unreadable files are
72  * ignored.
73  * <p>
74  * {@code StrictHostKeyChecking} accepts the following values:
75  * </p>
76  * <dl>
77  * <dt>ask</dt>
78  * <dd>Ask the user whether new or changed keys shall be accepted and be added
79  * to the known_hosts file.</dd>
80  * <dt>yes/true</dt>
81  * <dd>Accept only keys listed in the known_hosts file.</dd>
82  * <dt>no/false</dt>
83  * <dd>Silently accept all new or changed keys, add new keys to the known_hosts
84  * file.</dd>
85  * <dt>accept-new</dt>
86  * <dd>Silently accept keys for new hosts and add them to the known_hosts
87  * file.</dd>
88  * </dl>
89  * <p>
90  * If {@code StrictHostKeyChecking} is not set, or set to any other value, the
91  * default value <b>ask</b> is active.
92  * </p>
93  * <p>
94  * This implementation relies on the {@link ClientSession} being a
95  * {@link JGitClientSession}. By default Apache MINA sshd does not forward the
96  * config file host entry to the session, so it would be unknown here which
97  * entry it was and what setting of {@code StrictHostKeyChecking} should be
98  * used. If used with some other session type, the implementation assumes
99  * "<b>ask</b>".
100  * <p>
101  * <p>
102  * Asking the user is done via a {@link CredentialsProvider} obtained from the
103  * session. If none is set, the implementation falls back to strict host key
104  * checking ("<b>yes</b>").
105  * </p>
106  * <p>
107  * Note that adding a key to the known hosts file may create the file. You can
108  * specify in the constructor whether the user shall be asked about that, too.
109  * If the user declines updating the file, but the key was otherwise
110  * accepted (user confirmed for "<b>ask</b>", or "no" or "accept-new" are
111  * active), the key is accepted for this session only.
112  * </p>
113  * <p>
114  * If several known hosts files are specified, a new key is always added to the
115  * first file (even if it doesn't exist yet; see the note about file creation
116  * above).
117  * </p>
118  *
119  * @see <a href="http://man.openbsd.org/OpenBSD-current/man5/ssh_config.5">man
120  *      ssh-config</a>
121  */
122 public class OpenSshServerKeyDatabase
123                 implements ServerKeyDatabase {
124
125         // TODO: GlobalKnownHostsFile? May need some kind of LRU caching; these
126         // files may be large!
127
128         private static final Logger LOG = LoggerFactory
129                         .getLogger(OpenSshServerKeyDatabase.class);
130
131         /** Can be used to mark revoked known host lines. */
132         private static final String MARKER_REVOKED = "revoked"; //$NON-NLS-1$
133
134         private final boolean askAboutNewFile;
135
136         private final Map<Path, HostKeyFile> knownHostsFiles = new ConcurrentHashMap<>();
137
138         private final List<HostKeyFile> defaultFiles = new ArrayList<>();
139
140         /**
141          * Creates a new {@link OpenSshServerKeyDatabase}.
142          *
143          * @param askAboutNewFile
144          *            whether to ask the user, if possible, about creating a new
145          *            non-existing known_hosts file
146          * @param defaultFiles
147          *            typically ~/.ssh/known_hosts and ~/.ssh/known_hosts2. May be
148          *            empty or {@code null}, in which case no default files are
149          *            installed. The files need not exist.
150          */
151         public OpenSshServerKeyDatabase(boolean askAboutNewFile,
152                         List<Path> defaultFiles) {
153                 if (defaultFiles != null) {
154                         for (Path file : defaultFiles) {
155                                 HostKeyFile newFile = new HostKeyFile(file);
156                                 knownHostsFiles.put(file, newFile);
157                                 this.defaultFiles.add(newFile);
158                         }
159                 }
160                 this.askAboutNewFile = askAboutNewFile;
161         }
162
163         private List<HostKeyFile> getFilesToUse(@NonNull Configuration config) {
164                 List<HostKeyFile> filesToUse = defaultFiles;
165                 List<HostKeyFile> userFiles = addUserHostKeyFiles(
166                                 config.getUserKnownHostsFiles());
167                 if (!userFiles.isEmpty()) {
168                         filesToUse = userFiles;
169                 }
170                 return filesToUse;
171         }
172
173         @Override
174         public List<PublicKey> lookup(@NonNull String connectAddress,
175                         @NonNull InetSocketAddress remoteAddress,
176                         @NonNull Configuration config) {
177                 List<HostKeyFile> filesToUse = getFilesToUse(config);
178                 List<PublicKey> result = new ArrayList<>();
179                 Collection<SshdSocketAddress> candidates = getCandidates(
180                                 connectAddress, remoteAddress);
181                 for (HostKeyFile file : filesToUse) {
182                         for (HostEntryPair current : file.get()) {
183                                 KnownHostEntry entry = current.getHostEntry();
184                                 for (SshdSocketAddress host : candidates) {
185                                         if (entry.isHostMatch(host.getHostName(), host.getPort())) {
186                                                 result.add(current.getServerKey());
187                                                 break;
188                                         }
189                                 }
190                         }
191                 }
192                 return result;
193         }
194
195         @Override
196         public boolean accept(@NonNull String connectAddress,
197                         @NonNull InetSocketAddress remoteAddress,
198                         @NonNull PublicKey serverKey,
199                         @NonNull Configuration config, CredentialsProvider provider) {
200                 List<HostKeyFile> filesToUse = getFilesToUse(config);
201                 AskUser ask = new AskUser(config, provider);
202                 HostEntryPair[] modified = { null };
203                 Path path = null;
204                 Collection<SshdSocketAddress> candidates = getCandidates(connectAddress,
205                                 remoteAddress);
206                 for (HostKeyFile file : filesToUse) {
207                         try {
208                                 if (find(candidates, serverKey, file.get(), modified)) {
209                                         return true;
210                                 }
211                         } catch (RevokedKeyException e) {
212                                 ask.revokedKey(remoteAddress, serverKey, file.getPath());
213                                 return false;
214                         }
215                         if (path == null && modified[0] != null) {
216                                 // Remember the file in which we might need to update the
217                                 // entry
218                                 path = file.getPath();
219                         }
220                 }
221                 if (modified[0] != null) {
222                         // We found an entry, but with a different key
223                         AskUser.ModifiedKeyHandling toDo = ask.acceptModifiedServerKey(
224                                         remoteAddress, modified[0].getServerKey(),
225                                         serverKey, path);
226                         if (toDo == AskUser.ModifiedKeyHandling.ALLOW_AND_STORE) {
227                                 try {
228                                         updateModifiedServerKey(serverKey, modified[0], path);
229                                         knownHostsFiles.get(path).resetReloadAttributes();
230                                 } catch (IOException e) {
231                                         LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
232                                                         path));
233                                 }
234                         }
235                         if (toDo == AskUser.ModifiedKeyHandling.DENY) {
236                                 return false;
237                         }
238                         // TODO: OpenSsh disables password and keyboard-interactive
239                         // authentication in this case. Also agent and local port forwarding
240                         // are switched off. (Plus a few other things such as X11 forwarding
241                         // that are of no interest to a git client.)
242                         return true;
243                 } else if (ask.acceptUnknownKey(remoteAddress, serverKey)) {
244                         if (!filesToUse.isEmpty()) {
245                                 HostKeyFile toUpdate = filesToUse.get(0);
246                                 path = toUpdate.getPath();
247                                 try {
248                                         if (Files.exists(path) || !askAboutNewFile
249                                                         || ask.createNewFile(path)) {
250                                                 updateKnownHostsFile(candidates, serverKey, path,
251                                                                 config);
252                                                 toUpdate.resetReloadAttributes();
253                                         }
254                                 } catch (Exception e) {
255                                         LOG.warn(format(SshdText.get().knownHostsCouldNotUpdate,
256                                                         path), e);
257                                 }
258                         }
259                         return true;
260                 }
261                 return false;
262         }
263
264         private static class RevokedKeyException extends Exception {
265                 private static final long serialVersionUID = 1L;
266         }
267
268         private boolean find(Collection<SshdSocketAddress> candidates,
269                         PublicKey serverKey, List<HostEntryPair> entries,
270                         HostEntryPair[] modified) throws RevokedKeyException {
271                 for (HostEntryPair current : entries) {
272                         KnownHostEntry entry = current.getHostEntry();
273                         for (SshdSocketAddress host : candidates) {
274                                 if (entry.isHostMatch(host.getHostName(), host.getPort())) {
275                                         boolean isRevoked = MARKER_REVOKED
276                                                         .equals(entry.getMarker());
277                                         if (KeyUtils.compareKeys(serverKey,
278                                                         current.getServerKey())) {
279                                                 // Exact match
280                                                 if (isRevoked) {
281                                                         throw new RevokedKeyException();
282                                                 }
283                                                 modified[0] = null;
284                                                 return true;
285                                         } else if (!isRevoked) {
286                                                 // Server sent a different key
287                                                 modified[0] = current;
288                                                 // Keep going -- maybe there's another entry for this
289                                                 // host
290                                         }
291                                 }
292                         }
293                 }
294                 return false;
295         }
296
297         private List<HostKeyFile> addUserHostKeyFiles(List<String> fileNames) {
298                 if (fileNames == null || fileNames.isEmpty()) {
299                         return Collections.emptyList();
300                 }
301                 List<HostKeyFile> userFiles = new ArrayList<>();
302                 for (String name : fileNames) {
303                         try {
304                                 Path path = Paths.get(name);
305                                 HostKeyFile file = knownHostsFiles.computeIfAbsent(path,
306                                                 p -> new HostKeyFile(path));
307                                 userFiles.add(file);
308                         } catch (InvalidPathException e) {
309                                 LOG.warn(format(SshdText.get().knownHostsInvalidPath,
310                                                 name));
311                         }
312                 }
313                 return userFiles;
314         }
315
316         private void updateKnownHostsFile(Collection<SshdSocketAddress> candidates,
317                         PublicKey serverKey, Path path, Configuration config)
318                         throws Exception {
319                 String newEntry = createHostKeyLine(candidates, serverKey, config);
320                 if (newEntry == null) {
321                         return;
322                 }
323                 LockFile lock = new LockFile(path.toFile());
324                 if (lock.lockForAppend()) {
325                         try {
326                                 try (BufferedWriter writer = new BufferedWriter(
327                                                 new OutputStreamWriter(lock.getOutputStream(),
328                                                                 UTF_8))) {
329                                         writer.newLine();
330                                         writer.write(newEntry);
331                                         writer.newLine();
332                                 }
333                                 lock.commit();
334                         } catch (IOException e) {
335                                 lock.unlock();
336                                 throw e;
337                         }
338                 } else {
339                         LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
340                                         path));
341                 }
342         }
343
344         private void updateModifiedServerKey(PublicKey serverKey,
345                         HostEntryPair entry, Path path)
346                         throws IOException {
347                 KnownHostEntry hostEntry = entry.getHostEntry();
348                 String oldLine = hostEntry.getConfigLine();
349                 String newLine = updateHostKeyLine(oldLine, serverKey);
350                 if (newLine == null || newLine.isEmpty()) {
351                         return;
352                 }
353                 if (oldLine == null || oldLine.isEmpty() || newLine.equals(oldLine)) {
354                         // Shouldn't happen.
355                         return;
356                 }
357                 LockFile lock = new LockFile(path.toFile());
358                 if (lock.lock()) {
359                         try {
360                                 try (BufferedWriter writer = new BufferedWriter(
361                                                 new OutputStreamWriter(lock.getOutputStream(), UTF_8));
362                                                 BufferedReader reader = Files.newBufferedReader(path,
363                                                                 UTF_8)) {
364                                         boolean done = false;
365                                         String line;
366                                         while ((line = reader.readLine()) != null) {
367                                                 String toWrite = line;
368                                                 if (!done) {
369                                                         int pos = line.indexOf('#');
370                                                         String toTest = pos < 0 ? line
371                                                                         : line.substring(0, pos);
372                                                         if (toTest.trim().equals(oldLine)) {
373                                                                 toWrite = newLine;
374                                                                 done = true;
375                                                         }
376                                                 }
377                                                 writer.write(toWrite);
378                                                 writer.newLine();
379                                         }
380                                 }
381                                 lock.commit();
382                         } catch (IOException e) {
383                                 lock.unlock();
384                                 throw e;
385                         }
386                 } else {
387                         LOG.warn(format(SshdText.get().knownHostsFileLockedUpdate,
388                                         path));
389                 }
390         }
391
392         private static class AskUser {
393
394                 public enum ModifiedKeyHandling {
395                         DENY, ALLOW, ALLOW_AND_STORE
396                 }
397
398                 private enum Check {
399                         ASK, DENY, ALLOW;
400                 }
401
402                 private final @NonNull Configuration config;
403
404                 private final CredentialsProvider provider;
405
406                 public AskUser(@NonNull Configuration config,
407                                 CredentialsProvider provider) {
408                         this.config = config;
409                         this.provider = provider;
410                 }
411
412                 private static boolean askUser(CredentialsProvider provider, URIish uri,
413                                 String prompt, String... messages) {
414                         List<CredentialItem> items = new ArrayList<>(messages.length + 1);
415                         for (String message : messages) {
416                                 items.add(new CredentialItem.InformationalMessage(message));
417                         }
418                         if (prompt != null) {
419                                 CredentialItem.YesNoType answer = new CredentialItem.YesNoType(
420                                                 prompt);
421                                 items.add(answer);
422                                 return provider.get(uri, items) && answer.getValue();
423                         }
424                         return provider.get(uri, items);
425                 }
426
427                 private Check checkMode(SocketAddress remoteAddress, boolean changed) {
428                         if (!(remoteAddress instanceof InetSocketAddress)) {
429                                 return Check.DENY;
430                         }
431                         switch (config.getStrictHostKeyChecking()) {
432                         case REQUIRE_MATCH:
433                                 return Check.DENY;
434                         case ACCEPT_ANY:
435                                 return Check.ALLOW;
436                         case ACCEPT_NEW:
437                                 return changed ? Check.DENY : Check.ALLOW;
438                         default:
439                                 return provider == null ? Check.DENY : Check.ASK;
440                         }
441                 }
442
443                 public void revokedKey(SocketAddress remoteAddress, PublicKey serverKey,
444                                 Path path) {
445                         if (provider == null) {
446                                 return;
447                         }
448                         InetSocketAddress remote = (InetSocketAddress) remoteAddress;
449                         URIish uri = JGitUserInteraction.toURI(config.getUsername(),
450                                         remote);
451                         String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
452                                         serverKey);
453                         String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
454                         String keyAlgorithm = serverKey.getAlgorithm();
455                         askUser(provider, uri, null, //
456                                         format(SshdText.get().knownHostsRevokedKeyMsg,
457                                                         remote.getHostString(), path),
458                                         format(SshdText.get().knownHostsKeyFingerprints,
459                                                         keyAlgorithm),
460                                         md5, sha256);
461                 }
462
463                 public boolean acceptUnknownKey(SocketAddress remoteAddress,
464                                 PublicKey serverKey) {
465                         Check check = checkMode(remoteAddress, false);
466                         if (check != Check.ASK) {
467                                 return check == Check.ALLOW;
468                         }
469                         InetSocketAddress remote = (InetSocketAddress) remoteAddress;
470                         // Ask the user
471                         String sha256 = KeyUtils.getFingerPrint(BuiltinDigests.sha256,
472                                         serverKey);
473                         String md5 = KeyUtils.getFingerPrint(BuiltinDigests.md5, serverKey);
474                         String keyAlgorithm = serverKey.getAlgorithm();
475                         String remoteHost = remote.getHostString();
476                         URIish uri = JGitUserInteraction.toURI(config.getUsername(),
477                                         remote);
478                         String prompt = SshdText.get().knownHostsUnknownKeyPrompt;
479                         return askUser(provider, uri, prompt, //
480                                         format(SshdText.get().knownHostsUnknownKeyMsg,
481                                                         remoteHost),
482                                         format(SshdText.get().knownHostsKeyFingerprints,
483                                                         keyAlgorithm),
484                                         md5, sha256);
485                 }
486
487                 public ModifiedKeyHandling acceptModifiedServerKey(
488                                 InetSocketAddress remoteAddress, PublicKey expected,
489                                 PublicKey actual, Path path) {
490                         Check check = checkMode(remoteAddress, true);
491                         if (check == Check.ALLOW) {
492                                 // Never auto-store on CHECK.ALLOW
493                                 return ModifiedKeyHandling.ALLOW;
494                         }
495                         String keyAlgorithm = actual.getAlgorithm();
496                         String remoteHost = remoteAddress.getHostString();
497                         URIish uri = JGitUserInteraction.toURI(config.getUsername(),
498                                         remoteAddress);
499                         List<String> messages = new ArrayList<>();
500                         String warning = format(
501                                         SshdText.get().knownHostsModifiedKeyWarning,
502                                         keyAlgorithm, expected.getAlgorithm(), remoteHost,
503                                         KeyUtils.getFingerPrint(BuiltinDigests.md5, expected),
504                                         KeyUtils.getFingerPrint(BuiltinDigests.sha256, expected),
505                                         KeyUtils.getFingerPrint(BuiltinDigests.md5, actual),
506                                         KeyUtils.getFingerPrint(BuiltinDigests.sha256, actual));
507                         messages.addAll(Arrays.asList(warning.split("\n"))); //$NON-NLS-1$
508
509                         if (check == Check.DENY) {
510                                 if (provider != null) {
511                                         messages.add(format(
512                                                         SshdText.get().knownHostsModifiedKeyDenyMsg, path));
513                                         askUser(provider, uri, null,
514                                                         messages.toArray(new String[0]));
515                                 }
516                                 return ModifiedKeyHandling.DENY;
517                         }
518                         // ASK -- two questions: procceed? and store?
519                         List<CredentialItem> items = new ArrayList<>(messages.size() + 2);
520                         for (String message : messages) {
521                                 items.add(new CredentialItem.InformationalMessage(message));
522                         }
523                         CredentialItem.YesNoType proceed = new CredentialItem.YesNoType(
524                                         SshdText.get().knownHostsModifiedKeyAcceptPrompt);
525                         CredentialItem.YesNoType store = new CredentialItem.YesNoType(
526                                         SshdText.get().knownHostsModifiedKeyStorePrompt);
527                         items.add(proceed);
528                         items.add(store);
529                         if (provider.get(uri, items) && proceed.getValue()) {
530                                 return store.getValue() ? ModifiedKeyHandling.ALLOW_AND_STORE
531                                                 : ModifiedKeyHandling.ALLOW;
532                         }
533                         return ModifiedKeyHandling.DENY;
534                 }
535
536                 public boolean createNewFile(Path path) {
537                         if (provider == null) {
538                                 // We can't ask, so don't create the file
539                                 return false;
540                         }
541                         URIish uri = new URIish().setPath(path.toString());
542                         return askUser(provider, uri, //
543                                         format(SshdText.get().knownHostsUserAskCreationPrompt,
544                                                         path), //
545                                         format(SshdText.get().knownHostsUserAskCreationMsg, path));
546                 }
547         }
548
549         private static class HostKeyFile extends ModifiableFileWatcher
550                         implements Supplier<List<HostEntryPair>> {
551
552                 private List<HostEntryPair> entries = Collections.emptyList();
553
554                 public HostKeyFile(Path path) {
555                         super(path);
556                 }
557
558                 @Override
559                 public List<HostEntryPair> get() {
560                         Path path = getPath();
561                         try {
562                                 if (checkReloadRequired()) {
563                                         if (!Files.exists(path)) {
564                                                 // Has disappeared.
565                                                 resetReloadAttributes();
566                                                 return Collections.emptyList();
567                                         }
568                                         LockFile lock = new LockFile(path.toFile());
569                                         if (lock.lock()) {
570                                                 try {
571                                                         entries = reload(getPath());
572                                                 } finally {
573                                                         lock.unlock();
574                                                 }
575                                         } else {
576                                                 LOG.warn(format(SshdText.get().knownHostsFileLockedRead,
577                                                                 path));
578                                         }
579                                 }
580                         } catch (IOException e) {
581                                 LOG.warn(format(SshdText.get().knownHostsFileReadFailed, path));
582                         }
583                         return Collections.unmodifiableList(entries);
584                 }
585
586                 private List<HostEntryPair> reload(Path path) throws IOException {
587                         try {
588                                 List<KnownHostEntry> rawEntries = KnownHostEntryReader
589                                                 .readFromFile(path);
590                                 updateReloadAttributes();
591                                 if (rawEntries == null || rawEntries.isEmpty()) {
592                                         return Collections.emptyList();
593                                 }
594                                 List<HostEntryPair> newEntries = new LinkedList<>();
595                                 for (KnownHostEntry entry : rawEntries) {
596                                         AuthorizedKeyEntry keyPart = entry.getKeyEntry();
597                                         if (keyPart == null) {
598                                                 continue;
599                                         }
600                                         try {
601                                                 PublicKey serverKey = keyPart.resolvePublicKey(null,
602                                                                 PublicKeyEntryResolver.IGNORING);
603                                                 if (serverKey == null) {
604                                                         LOG.warn(format(
605                                                                         SshdText.get().knownHostsUnknownKeyType,
606                                                                         path, entry.getConfigLine()));
607                                                 } else {
608                                                         newEntries.add(new HostEntryPair(entry, serverKey));
609                                                 }
610                                         } catch (GeneralSecurityException e) {
611                                                 LOG.warn(format(SshdText.get().knownHostsInvalidLine,
612                                                                 path, entry.getConfigLine()));
613                                         }
614                                 }
615                                 return newEntries;
616                         } catch (FileNotFoundException e) {
617                                 resetReloadAttributes();
618                                 return Collections.emptyList();
619                         }
620                 }
621         }
622
623         private int parsePort(String s) {
624                 try {
625                         return Integer.parseInt(s);
626                 } catch (NumberFormatException e) {
627                         return -1;
628                 }
629         }
630
631         private SshdSocketAddress toSshdSocketAddress(@NonNull String address) {
632                 String host = null;
633                 int port = 0;
634                 if (HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_START_DELIM == address
635                                 .charAt(0)) {
636                         int end = address.indexOf(
637                                         HostPatternsHolder.NON_STANDARD_PORT_PATTERN_ENCLOSURE_END_DELIM);
638                         if (end <= 1) {
639                                 return null; // Invalid
640                         }
641                         host = address.substring(1, end);
642                         if (end < address.length() - 1
643                                         && HostPatternsHolder.PORT_VALUE_DELIMITER == address
644                                                         .charAt(end + 1)) {
645                                 port = parsePort(address.substring(end + 2));
646                         }
647                 } else {
648                         int i = address
649                                         .lastIndexOf(HostPatternsHolder.PORT_VALUE_DELIMITER);
650                         if (i > 0) {
651                                 port = parsePort(address.substring(i + 1));
652                                 host = address.substring(0, i);
653                         } else {
654                                 host = address;
655                         }
656                 }
657                 if (port < 0 || port > 65535) {
658                         return null;
659                 }
660                 return new SshdSocketAddress(host, port);
661         }
662
663         private Collection<SshdSocketAddress> getCandidates(
664                         @NonNull String connectAddress,
665                         @NonNull InetSocketAddress remoteAddress) {
666                 Collection<SshdSocketAddress> candidates = new TreeSet<>(
667                                 SshdSocketAddress.BY_HOST_AND_PORT);
668                 candidates.add(SshdSocketAddress.toSshdSocketAddress(remoteAddress));
669                 SshdSocketAddress address = toSshdSocketAddress(connectAddress);
670                 if (address != null) {
671                         candidates.add(address);
672                 }
673                 return candidates;
674         }
675
676         private String createHostKeyLine(Collection<SshdSocketAddress> patterns,
677                         PublicKey key, Configuration config) throws Exception {
678                 StringBuilder result = new StringBuilder();
679                 if (config.getHashKnownHosts()) {
680                         // SHA1 is the only algorithm for host name hashing known to OpenSSH
681                         // or to Apache MINA sshd.
682                         NamedFactory<Mac> digester = KnownHostDigest.SHA1;
683                         Mac mac = digester.create();
684                         SecureRandom prng = new SecureRandom();
685                         byte[] salt = new byte[mac.getDefaultBlockSize()];
686                         for (SshdSocketAddress address : patterns) {
687                                 if (result.length() > 0) {
688                                         result.append(',');
689                                 }
690                                 prng.nextBytes(salt);
691                                 KnownHostHashValue.append(result, digester, salt,
692                                                 KnownHostHashValue.calculateHashValue(
693                                                                 address.getHostName(), address.getPort(), mac,
694                                                                 salt));
695                         }
696                 } else {
697                         for (SshdSocketAddress address : patterns) {
698                                 if (result.length() > 0) {
699                                         result.append(',');
700                                 }
701                                 KnownHostHashValue.appendHostPattern(result,
702                                                 address.getHostName(), address.getPort());
703                         }
704                 }
705                 result.append(' ');
706                 PublicKeyEntry.appendPublicKeyEntry(result, key);
707                 return result.toString();
708         }
709
710         private String updateHostKeyLine(String line, PublicKey newKey)
711                         throws IOException {
712                 // Replaces an existing public key by the new key
713                 int pos = line.indexOf(' ');
714                 if (pos > 0 && line.charAt(0) == KnownHostEntry.MARKER_INDICATOR) {
715                         // We're at the end of the marker. Skip ahead to the next blank.
716                         pos = line.indexOf(' ', pos + 1);
717                 }
718                 if (pos < 0) {
719                         // Don't update if bogus format
720                         return null;
721                 }
722                 StringBuilder result = new StringBuilder(line.substring(0, pos + 1));
723                 PublicKeyEntry.appendPublicKeyEntry(result, newKey);
724                 return result.toString();
725         }
726
727 }