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.

SshdSessionFactory.java 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575
  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.transport.sshd;
  11. import java.io.Closeable;
  12. import java.io.File;
  13. import java.io.IOException;
  14. import java.nio.file.Files;
  15. import java.nio.file.Path;
  16. import java.security.KeyPair;
  17. import java.time.Duration;
  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.HashSet;
  22. import java.util.List;
  23. import java.util.Map;
  24. import java.util.Set;
  25. import java.util.concurrent.ConcurrentHashMap;
  26. import java.util.concurrent.atomic.AtomicBoolean;
  27. import java.util.stream.Collectors;
  28. import org.apache.sshd.client.ClientBuilder;
  29. import org.apache.sshd.client.SshClient;
  30. import org.apache.sshd.client.auth.UserAuthFactory;
  31. import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
  32. import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyFactory;
  33. import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
  34. import org.apache.sshd.common.compression.BuiltinCompressions;
  35. import org.apache.sshd.common.config.keys.FilePasswordProvider;
  36. import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
  37. import org.apache.sshd.common.keyprovider.KeyIdentityProvider;
  38. import org.eclipse.jgit.annotations.NonNull;
  39. import org.eclipse.jgit.errors.TransportException;
  40. import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
  41. import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
  42. import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
  43. import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
  44. import org.eclipse.jgit.internal.transport.sshd.JGitServerKeyVerifier;
  45. import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
  46. import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
  47. import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction;
  48. import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase;
  49. import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
  50. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  51. import org.eclipse.jgit.transport.CredentialsProvider;
  52. import org.eclipse.jgit.transport.SshConfigStore;
  53. import org.eclipse.jgit.transport.SshConstants;
  54. import org.eclipse.jgit.transport.SshSessionFactory;
  55. import org.eclipse.jgit.transport.URIish;
  56. import org.eclipse.jgit.util.FS;
  57. /**
  58. * A {@link SshSessionFactory} that uses Apache MINA sshd. Classes from Apache
  59. * MINA sshd are kept private to avoid API evolution problems when Apache MINA
  60. * sshd interfaces change.
  61. *
  62. * @since 5.2
  63. */
  64. public class SshdSessionFactory extends SshSessionFactory implements Closeable {
  65. private final AtomicBoolean closing = new AtomicBoolean();
  66. private final Set<SshdSession> sessions = new HashSet<>();
  67. private final Map<Tuple, HostConfigEntryResolver> defaultHostConfigEntryResolver = new ConcurrentHashMap<>();
  68. private final Map<Tuple, ServerKeyDatabase> defaultServerKeyDatabase = new ConcurrentHashMap<>();
  69. private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>();
  70. private final KeyCache keyCache;
  71. private final ProxyDataFactory proxies;
  72. private File sshDirectory;
  73. private File homeDirectory;
  74. /**
  75. * Creates a new {@link SshdSessionFactory} without key cache and a
  76. * {@link DefaultProxyDataFactory}.
  77. */
  78. public SshdSessionFactory() {
  79. this(null, new DefaultProxyDataFactory());
  80. }
  81. /**
  82. * Creates a new {@link SshdSessionFactory} using the given {@link KeyCache}
  83. * and {@link ProxyDataFactory}. The {@code keyCache} is used for all sessions
  84. * created through this session factory; cached keys are destroyed when the
  85. * session factory is {@link #close() closed}.
  86. * <p>
  87. * Caching ssh keys in memory for an extended period of time is generally
  88. * considered bad practice, but there may be circumstances where using a
  89. * {@link KeyCache} is still the right choice, for instance to avoid that a
  90. * user gets prompted several times for the same password for the same key.
  91. * In general, however, it is preferable <em>not</em> to use a key cache but
  92. * to use a {@link #createKeyPasswordProvider(CredentialsProvider)
  93. * KeyPasswordProvider} that has access to some secure storage and can save
  94. * and retrieve passwords from there without user interaction. Another
  95. * approach is to use an ssh agent.
  96. * </p>
  97. * <p>
  98. * Note that the underlying ssh library (Apache MINA sshd) may or may not
  99. * keep ssh keys in memory for unspecified periods of time irrespective of
  100. * the use of a {@link KeyCache}.
  101. * </p>
  102. *
  103. * @param keyCache
  104. * {@link KeyCache} to use for caching ssh keys, or {@code null}
  105. * to not use a key cache
  106. * @param proxies
  107. * {@link ProxyDataFactory} to use, or {@code null} to not use a
  108. * proxy database (in which case connections through proxies will
  109. * not be possible)
  110. */
  111. public SshdSessionFactory(KeyCache keyCache, ProxyDataFactory proxies) {
  112. super();
  113. this.keyCache = keyCache;
  114. this.proxies = proxies;
  115. // sshd limits the number of BCrypt KDF rounds to 255 by default.
  116. // Decrypting such a key takes about two seconds on my machine.
  117. // I consider this limit too low. The time increases linearly with the
  118. // number of rounds.
  119. BCryptKdfOptions.setMaxAllowedRounds(16384);
  120. }
  121. /** A simple general map key. */
  122. private static final class Tuple {
  123. private Object[] objects;
  124. public Tuple(Object[] objects) {
  125. this.objects = objects;
  126. }
  127. @Override
  128. public boolean equals(Object obj) {
  129. if (obj == this) {
  130. return true;
  131. }
  132. if (obj != null && obj.getClass() == Tuple.class) {
  133. Tuple other = (Tuple) obj;
  134. return Arrays.equals(objects, other.objects);
  135. }
  136. return false;
  137. }
  138. @Override
  139. public int hashCode() {
  140. return Arrays.hashCode(objects);
  141. }
  142. }
  143. // We can't really use a single client. Clients need to be stopped
  144. // properly, and we don't really know when to do that. Instead we use
  145. // a dedicated SshClient instance per session. We need a bit of caching to
  146. // avoid re-loading the ssh config and keys repeatedly.
  147. @Override
  148. public SshdSession getSession(URIish uri,
  149. CredentialsProvider credentialsProvider, FS fs, int tms)
  150. throws TransportException {
  151. SshdSession session = null;
  152. try {
  153. session = new SshdSession(uri, () -> {
  154. File home = getHomeDirectory();
  155. if (home == null) {
  156. // Always use the detected filesystem for the user home!
  157. // It makes no sense to have different "user home"
  158. // directories depending on what file system a repository
  159. // is.
  160. home = FS.DETECTED.userHome();
  161. }
  162. File sshDir = getSshDirectory();
  163. if (sshDir == null) {
  164. sshDir = new File(home, SshConstants.SSH_DIR);
  165. }
  166. HostConfigEntryResolver configFile = getHostConfigEntryResolver(
  167. home, sshDir);
  168. KeyIdentityProvider defaultKeysProvider = toKeyIdentityProvider(
  169. getDefaultKeys(sshDir));
  170. KeyPasswordProvider passphrases = createKeyPasswordProvider(
  171. credentialsProvider);
  172. SshClient client = ClientBuilder.builder()
  173. .factory(JGitSshClient::new)
  174. .filePasswordProvider(
  175. createFilePasswordProvider(passphrases))
  176. .hostConfigEntryResolver(configFile)
  177. .serverKeyVerifier(new JGitServerKeyVerifier(
  178. getServerKeyDatabase(home, sshDir)))
  179. .compressionFactories(
  180. new ArrayList<>(BuiltinCompressions.VALUES))
  181. .build();
  182. client.setUserInteraction(
  183. new JGitUserInteraction(credentialsProvider));
  184. client.setUserAuthFactories(getUserAuthFactories());
  185. client.setKeyIdentityProvider(defaultKeysProvider);
  186. // JGit-specific things:
  187. JGitSshClient jgitClient = (JGitSshClient) client;
  188. jgitClient.setKeyCache(getKeyCache());
  189. jgitClient.setCredentialsProvider(credentialsProvider);
  190. jgitClient.setProxyDatabase(proxies);
  191. String defaultAuths = getDefaultPreferredAuthentications();
  192. if (defaultAuths != null) {
  193. jgitClient.setAttribute(
  194. JGitSshClient.PREFERRED_AUTHENTICATIONS,
  195. defaultAuths);
  196. }
  197. // Other things?
  198. return client;
  199. });
  200. session.addCloseListener(s -> unregister(s));
  201. register(session);
  202. session.connect(Duration.ofMillis(tms));
  203. return session;
  204. } catch (Exception e) {
  205. unregister(session);
  206. throw new TransportException(uri, e.getMessage(), e);
  207. }
  208. }
  209. @Override
  210. public void close() {
  211. closing.set(true);
  212. boolean cleanKeys = false;
  213. synchronized (this) {
  214. cleanKeys = sessions.isEmpty();
  215. }
  216. if (cleanKeys) {
  217. KeyCache cache = getKeyCache();
  218. if (cache != null) {
  219. cache.close();
  220. }
  221. }
  222. }
  223. private void register(SshdSession newSession) throws IOException {
  224. if (newSession == null) {
  225. return;
  226. }
  227. if (closing.get()) {
  228. throw new IOException(SshdText.get().sshClosingDown);
  229. }
  230. synchronized (this) {
  231. sessions.add(newSession);
  232. }
  233. }
  234. private void unregister(SshdSession oldSession) {
  235. boolean cleanKeys = false;
  236. synchronized (this) {
  237. sessions.remove(oldSession);
  238. cleanKeys = closing.get() && sessions.isEmpty();
  239. }
  240. if (cleanKeys) {
  241. KeyCache cache = getKeyCache();
  242. if (cache != null) {
  243. cache.close();
  244. }
  245. }
  246. }
  247. /**
  248. * Set a global directory to use as the user's home directory
  249. *
  250. * @param homeDir
  251. * to use
  252. */
  253. public void setHomeDirectory(@NonNull File homeDir) {
  254. if (homeDir.isAbsolute()) {
  255. homeDirectory = homeDir;
  256. } else {
  257. homeDirectory = homeDir.getAbsoluteFile();
  258. }
  259. }
  260. /**
  261. * Retrieves the global user home directory
  262. *
  263. * @return the directory, or {@code null} if not set
  264. */
  265. public File getHomeDirectory() {
  266. return homeDirectory;
  267. }
  268. /**
  269. * Set a global directory to use as the .ssh directory
  270. *
  271. * @param sshDir
  272. * to use
  273. */
  274. public void setSshDirectory(@NonNull File sshDir) {
  275. if (sshDir.isAbsolute()) {
  276. sshDirectory = sshDir;
  277. } else {
  278. sshDirectory = sshDir.getAbsoluteFile();
  279. }
  280. }
  281. /**
  282. * Retrieves the global .ssh directory
  283. *
  284. * @return the directory, or {@code null} if not set
  285. */
  286. public File getSshDirectory() {
  287. return sshDirectory;
  288. }
  289. /**
  290. * Obtain a {@link HostConfigEntryResolver} to read the ssh config file and
  291. * to determine host entries for connections.
  292. *
  293. * @param homeDir
  294. * home directory to use for ~ replacement
  295. * @param sshDir
  296. * to use for looking for the config file
  297. * @return the resolver
  298. */
  299. @NonNull
  300. private HostConfigEntryResolver getHostConfigEntryResolver(
  301. @NonNull File homeDir, @NonNull File sshDir) {
  302. return defaultHostConfigEntryResolver.computeIfAbsent(
  303. new Tuple(new Object[] { homeDir, sshDir }),
  304. t -> new JGitSshConfig(createSshConfigStore(homeDir,
  305. getSshConfig(sshDir), getLocalUserName())));
  306. }
  307. /**
  308. * Determines the ssh config file. The default implementation returns
  309. * ~/.ssh/config. If the file does not exist and is created later it will be
  310. * picked up. To not use a config file at all, return {@code null}.
  311. *
  312. * @param sshDir
  313. * representing ~/.ssh/
  314. * @return the file (need not exist), or {@code null} if no config file
  315. * shall be used
  316. * @since 5.5
  317. */
  318. protected File getSshConfig(@NonNull File sshDir) {
  319. return new File(sshDir, SshConstants.CONFIG);
  320. }
  321. /**
  322. * Obtains a {@link SshConfigStore}, or {@code null} if not SSH config is to
  323. * be used. The default implementation returns {@code null} if
  324. * {@code configFile == null} and otherwise an OpenSSH-compatible store
  325. * reading host entries from the given file.
  326. *
  327. * @param homeDir
  328. * may be used for ~-replacements by the returned config store
  329. * @param configFile
  330. * to use, or {@code null} if none
  331. * @param localUserName
  332. * user name of the current user on the local OS
  333. * @return A {@link SshConfigStore}, or {@code null} if none is to be used
  334. *
  335. * @since 5.8
  336. */
  337. protected SshConfigStore createSshConfigStore(@NonNull File homeDir,
  338. File configFile, String localUserName) {
  339. return configFile == null ? null
  340. : new OpenSshConfigFile(homeDir, configFile, localUserName);
  341. }
  342. /**
  343. * Obtains a {@link ServerKeyDatabase} to verify server host keys. The
  344. * default implementation returns a {@link ServerKeyDatabase} that
  345. * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
  346. * {@code ~/.ssh/known_hosts2} as well as any files configured via the
  347. * {@code UserKnownHostsFile} option in the ssh config file.
  348. *
  349. * @param homeDir
  350. * home directory to use for ~ replacement
  351. * @param sshDir
  352. * representing ~/.ssh/
  353. * @return the {@link ServerKeyDatabase}
  354. * @since 5.5
  355. */
  356. @NonNull
  357. protected ServerKeyDatabase getServerKeyDatabase(@NonNull File homeDir,
  358. @NonNull File sshDir) {
  359. return defaultServerKeyDatabase.computeIfAbsent(
  360. new Tuple(new Object[] { homeDir, sshDir }),
  361. t -> createServerKeyDatabase(homeDir, sshDir));
  362. }
  363. /**
  364. * Creates a {@link ServerKeyDatabase} to verify server host keys. The
  365. * default implementation returns a {@link ServerKeyDatabase} that
  366. * recognizes the two openssh standard files {@code ~/.ssh/known_hosts} and
  367. * {@code ~/.ssh/known_hosts2} as well as any files configured via the
  368. * {@code UserKnownHostsFile} option in the ssh config file.
  369. *
  370. * @param homeDir
  371. * home directory to use for ~ replacement
  372. * @param sshDir
  373. * representing ~/.ssh/
  374. * @return the {@link ServerKeyDatabase}
  375. * @since 5.8
  376. */
  377. @NonNull
  378. protected ServerKeyDatabase createServerKeyDatabase(@NonNull File homeDir,
  379. @NonNull File sshDir) {
  380. return new OpenSshServerKeyDatabase(true,
  381. getDefaultKnownHostsFiles(sshDir));
  382. }
  383. /**
  384. * Gets the list of default user known hosts files. The default returns
  385. * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
  386. * {@code UserKnownHostsFile} overrides this default.
  387. *
  388. * @param sshDir
  389. * @return the possibly empty list of default known host file paths.
  390. */
  391. @NonNull
  392. protected List<Path> getDefaultKnownHostsFiles(@NonNull File sshDir) {
  393. return Arrays.asList(sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS),
  394. sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS + '2'));
  395. }
  396. /**
  397. * Determines the default keys. The default implementation will lazy load
  398. * the {@link #getDefaultIdentities(File) default identity files}.
  399. * <p>
  400. * Subclasses may override and return an {@link Iterable} of whatever keys
  401. * are appropriate. If the returned iterable lazily loads keys, it should be
  402. * an instance of
  403. * {@link org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider
  404. * AbstractResourceKeyPairProvider} so that the session can later pass it
  405. * the {@link #createKeyPasswordProvider(CredentialsProvider) password
  406. * provider} wrapped as a {@link FilePasswordProvider} via
  407. * {@link org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider#setPasswordFinder(FilePasswordProvider)
  408. * AbstractResourceKeyPairProvider#setPasswordFinder(FilePasswordProvider)}
  409. * so that encrypted, password-protected keys can be loaded.
  410. * </p>
  411. * <p>
  412. * The default implementation uses exactly this mechanism; class
  413. * {@link CachingKeyPairProvider} may serve as a model for a customized
  414. * lazy-loading {@link Iterable} implementation
  415. * </p>
  416. * <p>
  417. * If the {@link Iterable} returned has the keys already pre-loaded or
  418. * otherwise doesn't need to decrypt encrypted keys, it can be any
  419. * {@link Iterable}, for instance a simple {@link java.util.List List}.
  420. * </p>
  421. *
  422. * @param sshDir
  423. * to look in for keys
  424. * @return an {@link Iterable} over the default keys
  425. * @since 5.3
  426. */
  427. @NonNull
  428. protected Iterable<KeyPair> getDefaultKeys(@NonNull File sshDir) {
  429. List<Path> defaultIdentities = getDefaultIdentities(sshDir);
  430. return defaultKeys.computeIfAbsent(
  431. new Tuple(defaultIdentities.toArray(new Path[0])),
  432. t -> new CachingKeyPairProvider(defaultIdentities,
  433. getKeyCache()));
  434. }
  435. /**
  436. * Converts an {@link Iterable} of {link KeyPair}s into a
  437. * {@link KeyIdentityProvider}.
  438. *
  439. * @param keys
  440. * to provide via the returned {@link KeyIdentityProvider}
  441. * @return a {@link KeyIdentityProvider} that provides the given
  442. * {@code keys}
  443. */
  444. private KeyIdentityProvider toKeyIdentityProvider(Iterable<KeyPair> keys) {
  445. if (keys instanceof KeyIdentityProvider) {
  446. return (KeyIdentityProvider) keys;
  447. }
  448. return (session) -> keys;
  449. }
  450. /**
  451. * Gets a list of default identities, i.e., private key files that shall
  452. * always be tried for public key authentication. Typically those are
  453. * ~/.ssh/id_dsa, ~/.ssh/id_rsa, and so on. The default implementation
  454. * returns the files defined in {@link SshConstants#DEFAULT_IDENTITIES}.
  455. *
  456. * @param sshDir
  457. * the directory that represents ~/.ssh/
  458. * @return a possibly empty list of paths containing default identities
  459. * (private keys)
  460. */
  461. @NonNull
  462. protected List<Path> getDefaultIdentities(@NonNull File sshDir) {
  463. return Arrays
  464. .asList(SshConstants.DEFAULT_IDENTITIES).stream()
  465. .map(s -> new File(sshDir, s).toPath()).filter(Files::exists)
  466. .collect(Collectors.toList());
  467. }
  468. /**
  469. * Obtains the {@link KeyCache} to use to cache loaded keys.
  470. *
  471. * @return the {@link KeyCache}, or {@code null} if none.
  472. */
  473. protected final KeyCache getKeyCache() {
  474. return keyCache;
  475. }
  476. /**
  477. * Creates a {@link KeyPasswordProvider} for a new session.
  478. *
  479. * @param provider
  480. * the {@link CredentialsProvider} to delegate to for user
  481. * interactions
  482. * @return a new {@link KeyPasswordProvider}
  483. */
  484. @NonNull
  485. protected KeyPasswordProvider createKeyPasswordProvider(
  486. CredentialsProvider provider) {
  487. return new IdentityPasswordProvider(provider);
  488. }
  489. /**
  490. * Creates a {@link FilePasswordProvider} for a new session.
  491. *
  492. * @param provider
  493. * the {@link KeyPasswordProvider} to delegate to
  494. * @return a new {@link FilePasswordProvider}
  495. */
  496. @NonNull
  497. private FilePasswordProvider createFilePasswordProvider(
  498. KeyPasswordProvider provider) {
  499. return new PasswordProviderWrapper(provider);
  500. }
  501. /**
  502. * Gets the user authentication mechanisms (or rather, factories for them).
  503. * By default this returns gssapi-with-mic, public-key, password, and
  504. * keyboard-interactive, in that order. The order is only significant if the
  505. * ssh config does <em>not</em> set {@code PreferredAuthentications}; if it
  506. * is set, the order defined there will be taken.
  507. *
  508. * @return the non-empty list of factories.
  509. */
  510. @NonNull
  511. private List<UserAuthFactory> getUserAuthFactories() {
  512. // About the order of password and keyboard-interactive, see upstream
  513. // bug https://issues.apache.org/jira/projects/SSHD/issues/SSHD-866 .
  514. // Password auth doesn't have this problem.
  515. return Collections.unmodifiableList(
  516. Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
  517. UserAuthPublicKeyFactory.INSTANCE,
  518. JGitPasswordAuthFactory.INSTANCE,
  519. UserAuthKeyboardInteractiveFactory.INSTANCE));
  520. }
  521. /**
  522. * Gets the list of default preferred authentication mechanisms. If
  523. * {@code null} is returned the openssh default list will be in effect. If
  524. * the ssh config defines {@code PreferredAuthentications} the value from
  525. * the ssh config takes precedence.
  526. *
  527. * @return a comma-separated list of mechanism names, or {@code null} if
  528. * none
  529. */
  530. protected String getDefaultPreferredAuthentications() {
  531. return null;
  532. }
  533. }