View Javadoc
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 }