blob: 2a5f2ff24923925cc2d4ee653a99266ddc573e63 [file] [log] [blame]
/*
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
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 java.util.concurrent.CancellationException;
import org.eclipse.jgit.annotations.NonNull;
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;
/**
* 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, message);
state.setPassword(pass);
return pass;
}
private char[] getPassword(URIish uri, String message) {
if (provider == null) {
return null;
}
List<CredentialItem> items = new ArrayList<>(2);
items.add(new CredentialItem.InformationalMessage(
format(message, uri)));
CredentialItem.Password password = new CredentialItem.Password(
SshdText.get().keyEncryptedPrompt);
items.add(password);
try {
provider.get(uri, items);
char[] pass = password.getValue();
if (pass == null) {
throw new CancellationException(
SshdText.get().authenticationCanceled);
}
return pass.clone();
} finally {
password.clear();
}
}
/**
* 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;
}
}