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 25KB

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