AuthenticationLogger.java

/*
 * Copyright (C) 2022 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.internal.transport.sshd;

import static org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider.getKeyId;

import java.security.KeyPair;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
import org.apache.sshd.client.auth.password.UserAuthPassword;
import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.config.keys.KeyUtils;

/**
 * Provides a log of authentication attempts for a {@link ClientSession}.
 */
public class AuthenticationLogger {

	private final List<String> messages = new ArrayList<>();

	// We're interested in this log only in the failure case, so we don't need
	// to log authentication success.

	private final PublicKeyAuthenticationReporter pubkeyLogger = new PublicKeyAuthenticationReporter() {

		private boolean hasAttempts;

		@Override
		public void signalAuthenticationAttempt(ClientSession session,
				String service, KeyPair identity, String signature)
				throws Exception {
			hasAttempts = true;
			String message;
			if (identity.getPrivate() == null) {
				// SSH agent key
				message = MessageFormat.format(
						SshdText.get().authPubkeyAttemptAgent,
						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
						getKeyId(session, identity), signature);
			} else {
				message = MessageFormat.format(
						SshdText.get().authPubkeyAttempt,
						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
						getKeyId(session, identity), signature);
			}
			messages.add(message);
		}

		@Override
		public void signalAuthenticationExhausted(ClientSession session,
				String service) throws Exception {
			String message;
			if (hasAttempts) {
				message = MessageFormat.format(
						SshdText.get().authPubkeyExhausted,
						UserAuthPublicKey.NAME);
			} else {
				message = MessageFormat.format(SshdText.get().authPubkeyNoKeys,
						UserAuthPublicKey.NAME);
			}
			messages.add(message);
			hasAttempts = false;
		}

		@Override
		public void signalAuthenticationFailure(ClientSession session,
				String service, KeyPair identity, boolean partial,
				List<String> serverMethods) throws Exception {
			String message;
			if (partial) {
				message = MessageFormat.format(
						SshdText.get().authPubkeyPartialSuccess,
						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
						getKeyId(session, identity), serverMethods);
			} else {
				message = MessageFormat.format(
						SshdText.get().authPubkeyFailure,
						UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
						getKeyId(session, identity));
			}
			messages.add(message);
		}
	};

	private final PasswordAuthenticationReporter passwordLogger = new PasswordAuthenticationReporter() {

		private int attempts;

		@Override
		public void signalAuthenticationAttempt(ClientSession session,
				String service, String oldPassword, boolean modified,
				String newPassword) throws Exception {
			attempts++;
			String message;
			if (modified) {
				message = MessageFormat.format(
						SshdText.get().authPasswordChangeAttempt,
						UserAuthPassword.NAME, Integer.valueOf(attempts));
			} else {
				message = MessageFormat.format(
						SshdText.get().authPasswordAttempt,
						UserAuthPassword.NAME, Integer.valueOf(attempts));
			}
			messages.add(message);
		}

		@Override
		public void signalAuthenticationExhausted(ClientSession session,
				String service) throws Exception {
			String message;
			if (attempts > 0) {
				message = MessageFormat.format(
						SshdText.get().authPasswordExhausted,
						UserAuthPassword.NAME);
			} else {
				message = MessageFormat.format(
						SshdText.get().authPasswordNotTried,
						UserAuthPassword.NAME);
			}
			messages.add(message);
			attempts = 0;
		}

		@Override
		public void signalAuthenticationFailure(ClientSession session,
				String service, String password, boolean partial,
				List<String> serverMethods) throws Exception {
			String message;
			if (partial) {
				message = MessageFormat.format(
						SshdText.get().authPasswordPartialSuccess,
						UserAuthPassword.NAME, serverMethods);
			} else {
				message = MessageFormat.format(
						SshdText.get().authPasswordFailure,
						UserAuthPassword.NAME);
			}
			messages.add(message);
		}
	};

	private final GssApiWithMicAuthenticationReporter gssLogger = new GssApiWithMicAuthenticationReporter() {

		private boolean hasAttempts;

		@Override
		public void signalAuthenticationAttempt(ClientSession session,
				String service, String mechanism) {
			hasAttempts = true;
			String message = MessageFormat.format(
					SshdText.get().authGssApiAttempt,
					GssApiWithMicAuthFactory.NAME, mechanism);
			messages.add(message);
		}

		@Override
		public void signalAuthenticationExhausted(ClientSession session,
				String service) {
			String message;
			if (hasAttempts) {
				message = MessageFormat.format(
						SshdText.get().authGssApiExhausted,
						GssApiWithMicAuthFactory.NAME);
			} else {
				message = MessageFormat.format(
						SshdText.get().authGssApiNotTried,
						GssApiWithMicAuthFactory.NAME);
			}
			messages.add(message);
			hasAttempts = false;
		}

		@Override
		public void signalAuthenticationFailure(ClientSession session,
				String service, String mechanism, boolean partial,
				List<String> serverMethods) {
			String message;
			if (partial) {
				message = MessageFormat.format(
						SshdText.get().authGssApiPartialSuccess,
						GssApiWithMicAuthFactory.NAME, mechanism,
						serverMethods);
			} else {
				message = MessageFormat.format(
						SshdText.get().authGssApiFailure,
						GssApiWithMicAuthFactory.NAME, mechanism);
			}
			messages.add(message);
		}
	};

	/**
	 * Creates a new {@link AuthenticationLogger} and configures the
	 * {@link ClientSession} to report authentication attempts through this
	 * instance.
	 *
	 * @param session
	 *            to configure
	 */
	public AuthenticationLogger(ClientSession session) {
		session.setPublicKeyAuthenticationReporter(pubkeyLogger);
		session.setPasswordAuthenticationReporter(passwordLogger);
		session.setAttribute(
				GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER,
				gssLogger);
		// TODO: keyboard-interactive? sshd 2.8.0 has no callback
		// interface for it.
	}

	/**
	 * Retrieves the log messages for the authentication attempts.
	 *
	 * @return the messages as an unmodifiable list
	 */
	public List<String> getLog() {
		return Collections.unmodifiableList(messages);
	}

	/**
	 * Drops all previously recorded log messages.
	 */
	public void clear() {
		messages.clear();
	}
}