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

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