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