123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292 |
- /*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.transport.sshd;
-
- import static java.text.MessageFormat.format;
-
- import java.io.IOException;
- import java.security.GeneralSecurityException;
- import java.security.InvalidKeyException;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- import org.eclipse.jgit.annotations.NonNull;
- import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
- import org.eclipse.jgit.internal.transport.sshd.SshdText;
- import org.eclipse.jgit.transport.CredentialItem;
- import org.eclipse.jgit.transport.CredentialsProvider;
- import org.eclipse.jgit.transport.URIish;
- import org.eclipse.jgit.util.StringUtils;
-
- /**
- * A {@link KeyPasswordProvider} based on a {@link CredentialsProvider}.
- *
- * @since 5.2
- */
- public class IdentityPasswordProvider implements KeyPasswordProvider {
-
- private CredentialsProvider provider;
-
- /**
- * The number of times to ask successively for a password for a given
- * identity resource.
- */
- private int attempts = 1;
-
- /**
- * A simple state object for repeated attempts to get a password for a
- * resource.
- */
- protected static class State {
-
- private int count = 0;
-
- private char[] password;
-
- /**
- * Obtains the current count. The initial count is zero.
- *
- * @return the count
- */
- public int getCount() {
- return count;
- }
-
- /**
- * Increments the current count. Should be called for each new attempt
- * to get a password.
- *
- * @return the incremented count.
- */
- public int incCount() {
- return ++count;
- }
-
- /**
- * Remembers the password.
- *
- * @param password
- * the password
- */
- public void setPassword(char[] password) {
- if (this.password != null) {
- Arrays.fill(this.password, '\000');
- }
- if (password != null) {
- this.password = password.clone();
- } else {
- this.password = null;
- }
- }
-
- /**
- * Retrieves the password from the current attempt.
- *
- * @return the password, or {@code null} if none was obtained
- */
- public char[] getPassword() {
- return password;
- }
- }
-
- /**
- * Counts per resource key.
- */
- private final Map<URIish, State> current = new HashMap<>();
-
- /**
- * Creates a new {@link IdentityPasswordProvider} to get the passphrase for
- * an encrypted identity.
- *
- * @param provider
- * to use
- */
- public IdentityPasswordProvider(CredentialsProvider provider) {
- this.provider = provider;
- }
-
- @Override
- public void setAttempts(int numberOfPasswordPrompts) {
- if (numberOfPasswordPrompts <= 0) {
- throw new IllegalArgumentException(
- "Number of password prompts must be >= 1"); //$NON-NLS-1$
- }
- attempts = numberOfPasswordPrompts;
- }
-
- @Override
- public int getAttempts() {
- return Math.max(1, attempts);
- }
-
- @Override
- public char[] getPassphrase(URIish uri, int attempt) throws IOException {
- return getPassword(uri, attempt,
- current.computeIfAbsent(uri, r -> new State()));
- }
-
- /**
- * Retrieves a password to decrypt a private key.
- *
- * @param uri
- * identifying the resource to obtain a password for
- * @param attempt
- * number of previous attempts to get a passphrase
- * @param state
- * encapsulating state information about attempts to get the
- * password
- * @return the password, or {@code null} or the empty string if none
- * available.
- * @throws IOException
- * if an error occurs
- */
- protected char[] getPassword(URIish uri, int attempt, @NonNull State state)
- throws IOException {
- state.setPassword(null);
- state.incCount();
- String message = state.count == 1 ? SshdText.get().keyEncryptedMsg
- : SshdText.get().keyEncryptedRetry;
- char[] pass = getPassword(uri, format(message, uri));
- state.setPassword(pass);
- return pass;
- }
-
- /**
- * Retrieves the JGit {@link CredentialsProvider} to use for user
- * interaction.
- *
- * @return the {@link CredentialsProvider} or {@code null} if none
- * configured
- * @since 5.10
- */
- protected CredentialsProvider getCredentialsProvider() {
- return provider;
- }
-
- /**
- * Obtains the passphrase/password for an encrypted private key via the
- * {@link #getCredentialsProvider() configured CredentialsProvider}.
- *
- * @param uri
- * identifying the resource to obtain a password for
- * @param message
- * optional message text to display; may be {@code null} or empty
- * if none
- * @return the password entered, or {@code null} if no
- * {@link CredentialsProvider} is configured or none was entered
- * @throws java.util.concurrent.CancellationException
- * if the user canceled the operation
- * @since 5.10
- */
- protected char[] getPassword(URIish uri, String message) {
- if (provider == null) {
- return null;
- }
- boolean haveMessage = !StringUtils.isEmptyOrNull(message);
- List<CredentialItem> items = new ArrayList<>(haveMessage ? 2 : 1);
- if (haveMessage) {
- items.add(new CredentialItem.InformationalMessage(message));
- }
- CredentialItem.Password password = new CredentialItem.Password(
- SshdText.get().keyEncryptedPrompt);
- items.add(password);
- try {
- boolean completed = provider.get(uri, items);
- char[] pass = password.getValue();
- if (!completed) {
- cancelAuthentication();
- return null;
- }
- return pass == null ? null : pass.clone();
- } finally {
- password.clear();
- }
- }
-
- /**
- * Cancels the authentication process. Called by
- * {@link #getPassword(URIish, String)} when the user interaction has been
- * canceled. If this throws a
- * {@link java.util.concurrent.CancellationException}, the authentication
- * process is aborted; otherwise it may continue with the next configured
- * authentication mechanism, if any.
- * <p>
- * This default implementation always throws a
- * {@link java.util.concurrent.CancellationException}.
- * </p>
- *
- * @throws java.util.concurrent.CancellationException
- * always
- * @since 5.10
- */
- protected void cancelAuthentication() {
- throw new AuthenticationCanceledException();
- }
-
- /**
- * Invoked to inform the password provider about the decoding result.
- *
- * @param uri
- * identifying the key resource the key was attempted to be
- * loaded from
- * @param state
- * associated with this key
- * @param password
- * the password that was attempted
- * @param err
- * the attempt result - {@code null} for success
- * @return how to proceed in case of error
- * @throws IOException
- * @throws GeneralSecurityException
- */
- protected boolean keyLoaded(URIish uri,
- State state, char[] password, Exception err)
- throws IOException, GeneralSecurityException {
- if (err == null) {
- return false; // Success, don't retry
- } else if (err instanceof GeneralSecurityException) {
- throw new InvalidKeyException(
- format(SshdText.get().identityFileCannotDecrypt, uri), err);
- } else {
- // Unencrypted key (state == null && password == null), or exception
- // before having asked for the password (state != null && password
- // == null; might also be a user cancellation), or number of
- // attempts exhausted.
- if (state == null || password == null
- || state.getCount() >= attempts) {
- return false;
- }
- return true;
- }
- }
-
- @Override
- public boolean keyLoaded(URIish uri, int attempt, Exception error)
- throws IOException, GeneralSecurityException {
- State state = null;
- boolean retry = false;
- try {
- state = current.get(uri);
- retry = keyLoaded(uri, state,
- state == null ? null : state.getPassword(), error);
- } finally {
- if (state != null) {
- state.setPassword(null);
- }
- if (!retry) {
- current.remove(uri);
- }
- }
- return retry;
- }
- }
|