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

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