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