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

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