/* * Copyright (C) 2022 Thomas Wolf 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 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 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 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 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 getLog() { return Collections.unmodifiableList(messages); } /** * Drops all previously recorded log messages. */ public void clear() { messages.clear(); } }