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.

IdentityPasswordProvider.java 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292
  1. /*
  2. * Copyright (C) 2018, 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 static java.text.MessageFormat.format;
  12. import java.io.IOException;
  13. import java.security.GeneralSecurityException;
  14. import java.security.InvalidKeyException;
  15. import java.util.ArrayList;
  16. import java.util.Arrays;
  17. import java.util.HashMap;
  18. import java.util.List;
  19. import java.util.Map;
  20. import org.eclipse.jgit.annotations.NonNull;
  21. import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
  22. import org.eclipse.jgit.internal.transport.sshd.SshdText;
  23. import org.eclipse.jgit.transport.CredentialItem;
  24. import org.eclipse.jgit.transport.CredentialsProvider;
  25. import org.eclipse.jgit.transport.URIish;
  26. import org.eclipse.jgit.util.StringUtils;
  27. /**
  28. * A {@link KeyPasswordProvider} based on a {@link CredentialsProvider}.
  29. *
  30. * @since 5.2
  31. */
  32. public class IdentityPasswordProvider implements KeyPasswordProvider {
  33. private CredentialsProvider provider;
  34. /**
  35. * The number of times to ask successively for a password for a given
  36. * identity resource.
  37. */
  38. private int attempts = 1;
  39. /**
  40. * A simple state object for repeated attempts to get a password for a
  41. * resource.
  42. */
  43. protected static class State {
  44. private int count = 0;
  45. private char[] password;
  46. /**
  47. * Obtains the current count. The initial count is zero.
  48. *
  49. * @return the count
  50. */
  51. public int getCount() {
  52. return count;
  53. }
  54. /**
  55. * Increments the current count. Should be called for each new attempt
  56. * to get a password.
  57. *
  58. * @return the incremented count.
  59. */
  60. public int incCount() {
  61. return ++count;
  62. }
  63. /**
  64. * Remembers the password.
  65. *
  66. * @param password
  67. * the password
  68. */
  69. public void setPassword(char[] password) {
  70. if (this.password != null) {
  71. Arrays.fill(this.password, '\000');
  72. }
  73. if (password != null) {
  74. this.password = password.clone();
  75. } else {
  76. this.password = null;
  77. }
  78. }
  79. /**
  80. * Retrieves the password from the current attempt.
  81. *
  82. * @return the password, or {@code null} if none was obtained
  83. */
  84. public char[] getPassword() {
  85. return password;
  86. }
  87. }
  88. /**
  89. * Counts per resource key.
  90. */
  91. private final Map<URIish, State> current = new HashMap<>();
  92. /**
  93. * Creates a new {@link IdentityPasswordProvider} to get the passphrase for
  94. * an encrypted identity.
  95. *
  96. * @param provider
  97. * to use
  98. */
  99. public IdentityPasswordProvider(CredentialsProvider provider) {
  100. this.provider = provider;
  101. }
  102. @Override
  103. public void setAttempts(int numberOfPasswordPrompts) {
  104. if (numberOfPasswordPrompts <= 0) {
  105. throw new IllegalArgumentException(
  106. "Number of password prompts must be >= 1"); //$NON-NLS-1$
  107. }
  108. attempts = numberOfPasswordPrompts;
  109. }
  110. @Override
  111. public int getAttempts() {
  112. return Math.max(1, attempts);
  113. }
  114. @Override
  115. public char[] getPassphrase(URIish uri, int attempt) throws IOException {
  116. return getPassword(uri, attempt,
  117. current.computeIfAbsent(uri, r -> new State()));
  118. }
  119. /**
  120. * Retrieves a password to decrypt a private key.
  121. *
  122. * @param uri
  123. * identifying the resource to obtain a password for
  124. * @param attempt
  125. * number of previous attempts to get a passphrase
  126. * @param state
  127. * encapsulating state information about attempts to get the
  128. * password
  129. * @return the password, or {@code null} or the empty string if none
  130. * available.
  131. * @throws IOException
  132. * if an error occurs
  133. */
  134. protected char[] getPassword(URIish uri, int attempt, @NonNull State state)
  135. throws IOException {
  136. state.setPassword(null);
  137. state.incCount();
  138. String message = state.count == 1 ? SshdText.get().keyEncryptedMsg
  139. : SshdText.get().keyEncryptedRetry;
  140. char[] pass = getPassword(uri, format(message, uri));
  141. state.setPassword(pass);
  142. return pass;
  143. }
  144. /**
  145. * Retrieves the JGit {@link CredentialsProvider} to use for user
  146. * interaction.
  147. *
  148. * @return the {@link CredentialsProvider} or {@code null} if none
  149. * configured
  150. * @since 5.10
  151. */
  152. protected CredentialsProvider getCredentialsProvider() {
  153. return provider;
  154. }
  155. /**
  156. * Obtains the passphrase/password for an encrypted private key via the
  157. * {@link #getCredentialsProvider() configured CredentialsProvider}.
  158. *
  159. * @param uri
  160. * identifying the resource to obtain a password for
  161. * @param message
  162. * optional message text to display; may be {@code null} or empty
  163. * if none
  164. * @return the password entered, or {@code null} if no
  165. * {@link CredentialsProvider} is configured or none was entered
  166. * @throws java.util.concurrent.CancellationException
  167. * if the user canceled the operation
  168. * @since 5.10
  169. */
  170. protected char[] getPassword(URIish uri, String message) {
  171. if (provider == null) {
  172. return null;
  173. }
  174. boolean haveMessage = !StringUtils.isEmptyOrNull(message);
  175. List<CredentialItem> items = new ArrayList<>(haveMessage ? 2 : 1);
  176. if (haveMessage) {
  177. items.add(new CredentialItem.InformationalMessage(message));
  178. }
  179. CredentialItem.Password password = new CredentialItem.Password(
  180. SshdText.get().keyEncryptedPrompt);
  181. items.add(password);
  182. try {
  183. boolean completed = provider.get(uri, items);
  184. char[] pass = password.getValue();
  185. if (!completed) {
  186. cancelAuthentication();
  187. return null;
  188. }
  189. return pass == null ? null : pass.clone();
  190. } finally {
  191. password.clear();
  192. }
  193. }
  194. /**
  195. * Cancels the authentication process. Called by
  196. * {@link #getPassword(URIish, String)} when the user interaction has been
  197. * canceled. If this throws a
  198. * {@link java.util.concurrent.CancellationException}, the authentication
  199. * process is aborted; otherwise it may continue with the next configured
  200. * authentication mechanism, if any.
  201. * <p>
  202. * This default implementation always throws a
  203. * {@link java.util.concurrent.CancellationException}.
  204. * </p>
  205. *
  206. * @throws java.util.concurrent.CancellationException
  207. * always
  208. * @since 5.10
  209. */
  210. protected void cancelAuthentication() {
  211. throw new AuthenticationCanceledException();
  212. }
  213. /**
  214. * Invoked to inform the password provider about the decoding result.
  215. *
  216. * @param uri
  217. * identifying the key resource the key was attempted to be
  218. * loaded from
  219. * @param state
  220. * associated with this key
  221. * @param password
  222. * the password that was attempted
  223. * @param err
  224. * the attempt result - {@code null} for success
  225. * @return how to proceed in case of error
  226. * @throws IOException
  227. * @throws GeneralSecurityException
  228. */
  229. protected boolean keyLoaded(URIish uri,
  230. State state, char[] password, Exception err)
  231. throws IOException, GeneralSecurityException {
  232. if (err == null) {
  233. return false; // Success, don't retry
  234. } else if (err instanceof GeneralSecurityException) {
  235. throw new InvalidKeyException(
  236. format(SshdText.get().identityFileCannotDecrypt, uri), err);
  237. } else {
  238. // Unencrypted key (state == null && password == null), or exception
  239. // before having asked for the password (state != null && password
  240. // == null; might also be a user cancellation), or number of
  241. // attempts exhausted.
  242. if (state == null || password == null
  243. || state.getCount() >= attempts) {
  244. return false;
  245. }
  246. return true;
  247. }
  248. }
  249. @Override
  250. public boolean keyLoaded(URIish uri, int attempt, Exception error)
  251. throws IOException, GeneralSecurityException {
  252. State state = null;
  253. boolean retry = false;
  254. try {
  255. state = current.get(uri);
  256. retry = keyLoaded(uri, state,
  257. state == null ? null : state.getPassword(), error);
  258. } finally {
  259. if (state != null) {
  260. state.setPassword(null);
  261. }
  262. if (!retry) {
  263. current.remove(uri);
  264. }
  265. }
  266. return retry;
  267. }
  268. }