org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractive Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sshd.client.auth.keyboard;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.client.auth.AbstractUserAuth;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.PropertyResolverUtils;
import org.apache.sshd.common.RuntimeSshException;
import org.apache.sshd.common.SshConstants;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.core.CoreModuleProperties;
/**
* Manages a "keyboard-interactive" exchange according to
* RFC4256
*
* @author Apache MINA SSHD Project
*/
public class UserAuthKeyboardInteractive extends AbstractUserAuth {
public static final String NAME = UserAuthKeyboardInteractiveFactory.NAME;
private final AtomicBoolean requestPending = new AtomicBoolean(false);
private Iterator passwords;
private int maxAttempts;
private int nOfAttempts;
private boolean wasChallenged;
private boolean withUserInteraction;
public UserAuthKeyboardInteractive() {
super(NAME);
}
@Override
public void init(ClientSession session, String service) throws Exception {
super.init(session, service);
passwords = ClientSession.passwordIteratorOf(session);
maxAttempts = Math.max(1, CoreModuleProperties.PASSWORD_PROMPTS.getRequired(session));
nOfAttempts = 0;
wasChallenged = false;
withUserInteraction = false;
}
@Override
protected boolean sendAuthDataRequest(ClientSession session, String service) throws Exception {
String name = getName();
boolean debugEnabled = log.isDebugEnabled();
if (requestPending.get()) {
if (debugEnabled) {
log.debug("sendAuthDataRequest({})[{}] no reply for previous request for {}",
session, service, name);
}
return false;
}
nOfAttempts++;
if (wasChallenged && !withUserInteraction) {
// We did increment on the previous attempt, but then had no user interaction for the challenge(s). The
// count is one too high.
nOfAttempts--;
}
wasChallenged = false;
withUserInteraction = false;
if (!verifyTrialsCount(session, service, SshConstants.SSH_MSG_USERAUTH_REQUEST, nOfAttempts, maxAttempts)) {
return false;
}
String username = session.getUsername();
String lang = getExchangeLanguageTag(session);
String subMethods = getExchangeSubMethods(session);
if (debugEnabled) {
log.debug("sendAuthDataRequest({})[{}] send SSH_MSG_USERAUTH_REQUEST for {}: lang={}, methods={}",
session, service, name, lang, subMethods);
}
int length = username.length() + service.length() + name.length() + GenericUtils.length(lang)
+ GenericUtils.length(subMethods) + Long.SIZE; // A bit extra for the length
Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST, length);
buffer.putString(username);
buffer.putString(service);
buffer.putString(name);
buffer.putString(lang);
buffer.putString(subMethods);
requestPending.set(true);
session.writePacket(buffer);
return true;
}
@Override
protected boolean processAuthDataRequest(ClientSession session, String service, Buffer buffer) throws Exception {
int cmd = buffer.getUByte();
if (cmd != SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST) {
throw new IllegalStateException("processAuthDataRequest(" + session + ")[" + service + "]"
+ " received unknown packet: cmd=" + SshConstants.getCommandMessageName(cmd));
}
requestPending.set(false);
String name = buffer.getString();
String instruction = buffer.getString();
String lang = buffer.getString();
int num = buffer.getInt();
// Protect against malicious or corrupted packets
if ((num < 0) || (num > SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT)) {
log.error("processAuthDataRequest({})[{}] illogical challenges count ({}) for name={}, instruction={}",
session, service, num, name, instruction);
throw new IndexOutOfBoundsException("Illogical challenges count: " + num);
}
boolean debugEnabled = log.isDebugEnabled();
if (debugEnabled) {
log.debug(
"processAuthDataRequest({})[{}] SSH_MSG_USERAUTH_INFO_REQUEST name={}, instruction={}, language={}, num-prompts={}",
session, service, name, instruction, lang, num);
}
String[] prompt = (num > 0) ? new String[num] : GenericUtils.EMPTY_STRING_ARRAY;
boolean[] echo = (num > 0) ? new boolean[num] : GenericUtils.EMPTY_BOOLEAN_ARRAY;
boolean traceEnabled = log.isTraceEnabled();
for (int i = 0; i < num; i++) {
// TODO according to RFC4256: "The prompt field(s) MUST NOT be empty strings."
prompt[i] = buffer.getString();
echo[i] = buffer.getBoolean();
if (traceEnabled) {
log.trace("processAuthDataRequest({})[{}]({}) {}/{}: echo={}, prompt={}",
session, service, name, i + 1, num, echo[i], prompt[i]);
}
}
String[] rep = getUserResponses(name, instruction, lang, prompt, echo);
if (rep == null) {
if (debugEnabled) {
log.debug("processAuthDataRequest({})[{}] no responses for {}", session, service, name);
}
return false;
}
/*
* According to RFC4256:
*
* If the num-responses field does not match the num-prompts field in the request message, the server MUST send
* a failure message.
*
* However it is the server's (!) responsibility to fail, so we only warn...
*/
if (num != rep.length) {
log.warn("processAuthDataRequest({})[{}] Mismatched prompts ({}) vs. responses count ({})",
session, service, num, rep.length);
}
int numResponses = rep.length;
buffer = session.createBuffer(
SshConstants.SSH_MSG_USERAUTH_INFO_RESPONSE, numResponses * Long.SIZE + Byte.SIZE);
buffer.putUInt(numResponses);
for (int index = 0; index < numResponses; index++) {
String r = rep[index];
if (traceEnabled) {
log.trace("processAuthDataRequest({})[{}] response #{}: {}", session, service, index + 1,
(index < num && echo[index]) ? r : "(hidden)");
}
buffer.putString(r);
}
session.writePacket(buffer);
return true;
}
protected String getExchangeLanguageTag(ClientSession session) {
return CoreModuleProperties.INTERACTIVE_LANGUAGE_TAG.getRequired(session);
}
protected String getExchangeSubMethods(ClientSession session) {
return CoreModuleProperties.INTERACTIVE_SUBMETHODS.getRequired(session);
}
protected String getCurrentPasswordCandidate() {
if ((passwords != null) && passwords.hasNext()) {
return passwords.next();
} else {
return null;
}
}
protected boolean verifyTrialsCount(
ClientSession session, String service, int cmd, int nbTrials, int maxAllowed) {
if (log.isDebugEnabled()) {
log.debug("verifyTrialsCount({})[{}] cmd={} - {} out of {}",
session, service, getAuthCommandName(cmd), nbTrials, maxAllowed);
}
return nbTrials <= maxAllowed;
}
/**
* @param name The interaction name - may be empty
* @param instruction The instruction - may be empty
* @param lang The language tag - may be empty
* @param prompt The prompts - may be empty
* @param echo Whether to echo the response for the prompt or not - same length as the prompts
* @return The response for each prompt - if {@code null} then the assumption is that some internal
* error occurred and no response is sent. Note: according to
* RFC4256 the number of responses should be
* exactly the same as the number of prompts. However, since it is the server's
* responsibility to enforce this we do not validate the response (other than logging it as a
* warning...)
*/
protected String[] getUserResponses(String name, String instruction, String lang, String[] prompt, boolean[] echo) {
ClientSession session = getClientSession();
int num = GenericUtils.length(prompt);
boolean debugEnabled = log.isDebugEnabled();
/*
* According to RFC 4256 - section 3.4
*
* In the case that the server sends a `0' num-prompts field in the request message, the client MUST send a
* response message with a `0' num-responses field to complete the exchange.
*/
if (num == 0) {
if (debugEnabled) {
log.debug("getUserResponses({}) no prompts for interaction={}", session, name);
}
return GenericUtils.EMPTY_STRING_ARRAY;
}
wasChallenged = true;
if (PropertyResolverUtils.getBooleanProperty(
session, UserInteraction.AUTO_DETECT_PASSWORD_PROMPT,
UserInteraction.DEFAULT_AUTO_DETECT_PASSWORD_PROMPT)) {
String candidate = getCurrentPasswordCandidate();
if (useCurrentPassword(session, candidate, name, instruction, lang, prompt, echo)) {
if (debugEnabled) {
log.debug("getUserResponses({}) use password candidate for interaction={}", session, name);
}
return new String[] { candidate };
}
}
withUserInteraction = true;
UserInteraction ui = session.getUserInteraction();
try {
if ((ui != null) && ui.isInteractionAllowed(session)) {
return ui.interactive(session, name, instruction, lang, prompt, echo);
}
} catch (Error e) {
warn("getUserResponses({}) failed ({}) to consult interaction: {}",
session, e.getClass().getSimpleName(), e.getMessage(), e);
throw new RuntimeSshException(e);
}
if (debugEnabled) {
log.debug("getUserResponses({}) no user interaction for name={}", session, name);
}
return null;
}
/**
* Checks if we have a candidate password and exactly one prompt is requested with no echo, and the prompt
* matches a configurable pattern.
*
* @param session The {@link ClientSession} through which the request is received
* @param password The current password candidate to use
* @param name The service name
* @param instruction The request instruction
* @param lang The reported language tag
* @param prompt The requested prompts
* @param echo The matching prompts echo flags
* @return Whether to use the password candidate as reply to the prompts
* @see UserInteraction#INTERACTIVE_PASSWORD_PROMPT INTERACTIVE_PASSWORD_PROMPT
* @see UserInteraction#CHECK_INTERACTIVE_PASSWORD_DELIM CHECK_INTERACTIVE_PASSWORD_DELIM
*/
protected boolean useCurrentPassword(
ClientSession session, String password, String name,
String instruction, String lang, String[] prompt, boolean[] echo) {
int num = GenericUtils.length(prompt);
if ((num != 1) || (password == null) || echo[0]) {
return false;
}
// check if prompt is something like "XXX password YYY:"
String value = GenericUtils.trimToEmpty(prompt[0]);
// Don't care about the case
value = value.toLowerCase();
String promptList = PropertyResolverUtils.getStringProperty(
session, UserInteraction.INTERACTIVE_PASSWORD_PROMPT,
UserInteraction.DEFAULT_INTERACTIVE_PASSWORD_PROMPT);
int passPos = UserInteraction.findPromptComponentLastPosition(value, promptList);
if (passPos < 0) { // no password keyword in prompt
return false;
}
String delimList = PropertyResolverUtils.getStringProperty(
session, UserInteraction.CHECK_INTERACTIVE_PASSWORD_DELIM,
UserInteraction.DEFAULT_CHECK_INTERACTIVE_PASSWORD_DELIM);
if (PropertyResolverUtils.isNoneValue(delimList)) {
return true;
}
int sepPos = UserInteraction.findPromptComponentLastPosition(value, delimList);
if (sepPos < passPos) {
return false;
}
return true;
}
public static String getAuthCommandName(int cmd) {
switch (cmd) {
case SshConstants.SSH_MSG_USERAUTH_REQUEST:
return "SSH_MSG_USERAUTH_REQUEST";
case SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST:
return "SSH_MSG_USERAUTH_INFO_REQUEST";
default:
return SshConstants.getCommandMessageName(cmd);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy