You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

OpenSshServerKeyDatabase.java 23KB

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