EncryptedFileKeyPairProvider.java

/*
 * 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.internal.transport.sshd;

import static java.text.MessageFormat.format;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.security.auth.DestroyFailedException;

import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
import org.eclipse.jgit.internal.transport.sshd.RepeatingFilePasswordProvider.ResourceDecodeResult;

/**
 * A {@link FileKeyPairProvider} that asks repeatedly for a passphrase for an
 * encrypted private key if the {@link FilePasswordProvider} is a
 * {@link RepeatingFilePasswordProvider}.
 */
public abstract class EncryptedFileKeyPairProvider extends FileKeyPairProvider {

	// TODO: remove this class once we're based on sshd > 2.1.0. See upstream
	// issue SSHD-850 https://issues.apache.org/jira/browse/SSHD-850 and commit
	// https://github.com/apache/mina-sshd/commit/f19bd2e34

	/**
	 * Creates a new {@link EncryptedFileKeyPairProvider} for the given
	 * {@link Path}s.
	 *
	 * @param paths
	 *            to read keys from
	 */
	public EncryptedFileKeyPairProvider(List<Path> paths) {
		super(paths);
	}

	@Override
	protected KeyPair doLoadKey(String resourceKey, InputStream inputStream,
			FilePasswordProvider provider)
			throws IOException, GeneralSecurityException {
		if (!(provider instanceof RepeatingFilePasswordProvider)) {
			return super.doLoadKey(resourceKey, inputStream, provider);
		}
		KeyPairResourceParser parser = SecurityUtils.getKeyPairResourceParser();
		if (parser == null) {
			// This is an internal configuration error, thus no translation.
			throw new NoSuchProviderException(
					"No registered key-pair resource parser"); //$NON-NLS-1$
		}
		RepeatingFilePasswordProvider realProvider = (RepeatingFilePasswordProvider) provider;
		// Read the stream now so that we can process the content several
		// times.
		List<String> lines = IoUtils.readAllLines(inputStream);
		Collection<KeyPair> ids = null;
		while (ids == null) {
			try {
				ids = parser.loadKeyPairs(resourceKey, realProvider, lines);
				realProvider.handleDecodeAttemptResult(resourceKey, "", null); //$NON-NLS-1$
				// No exception; success. Exit the loop even if ids is still
				// null!
				break;
			} catch (IOException | GeneralSecurityException
					| RuntimeException e) {
				ResourceDecodeResult loadResult = realProvider
						.handleDecodeAttemptResult(resourceKey, "", e); //$NON-NLS-1$
				if (loadResult == null
						|| loadResult == ResourceDecodeResult.TERMINATE) {
					throw e;
				} else if (loadResult == ResourceDecodeResult.RETRY) {
					continue;
				}
				// IGNORE doesn't make any sense here, but OK, let's ignore it.
				// ids == null, so we'll throw an exception below.
				break;
			}
		}
		if (ids == null) {
			// The javadoc on loadKeyPairs says it might return null if no
			// key pair found. Bad API.
			throw new InvalidKeyException(
					format(SshdText.get().identityFileNoKey, resourceKey));
		}
		Iterator<KeyPair> keys = ids.iterator();
		if (!keys.hasNext()) {
			throw new InvalidKeyException(format(
					SshdText.get().identityFileUnsupportedFormat, resourceKey));
		}
		KeyPair result = keys.next();
		if (keys.hasNext()) {
			log.warn(format(SshdText.get().identityFileMultipleKeys,
					resourceKey));
			keys.forEachRemaining(k -> {
				PrivateKey pk = k.getPrivate();
				if (pk != null) {
					try {
						pk.destroy();
					} catch (DestroyFailedException e) {
						// Ignore
					}
				}
			});
		}
		return result;
	}
}