mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-08 00:00:54 +03:00
some authentication refactoring esp. keyboard-interactive auth
This commit is contained in:
@@ -49,7 +49,7 @@ import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
|
|||||||
import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper;
|
import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper;
|
||||||
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
|
||||||
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;
|
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;
|
||||||
import net.schmizz.sshj.userauth.method.AuthChallengeResponse;
|
import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive;
|
||||||
import net.schmizz.sshj.userauth.method.AuthMethod;
|
import net.schmizz.sshj.userauth.method.AuthMethod;
|
||||||
import net.schmizz.sshj.userauth.method.AuthPassword;
|
import net.schmizz.sshj.userauth.method.AuthPassword;
|
||||||
import net.schmizz.sshj.userauth.method.AuthPublickey;
|
import net.schmizz.sshj.userauth.method.AuthPublickey;
|
||||||
@@ -262,7 +262,7 @@ public class SSHClient
|
|||||||
*/
|
*/
|
||||||
public void authPassword(String username, PasswordFinder pfinder)
|
public void authPassword(String username, PasswordFinder pfinder)
|
||||||
throws UserAuthException, TransportException {
|
throws UserAuthException, TransportException {
|
||||||
auth(username, new AuthPassword(pfinder), new AuthChallengeResponse(new PasswordResponseProvider(pfinder)));
|
auth(username, new AuthPassword(pfinder), new AuthKeyboardInteractive(new PasswordResponseProvider(pfinder)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -440,19 +440,19 @@ public class Buffer<T extends Buffer<T>> {
|
|||||||
* This is useful when a plaintext password needs to be sent. If {@code passwd} is {@code null}, an empty string is
|
* This is useful when a plaintext password needs to be sent. If {@code passwd} is {@code null}, an empty string is
|
||||||
* written.
|
* written.
|
||||||
*
|
*
|
||||||
* @param passwd (null-ok) the password as a character array
|
* @param str (null-ok) the password as a character array
|
||||||
*
|
*
|
||||||
* @return this
|
* @return this
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public T putPassword(char[] passwd) {
|
public T putSensitiveString(char[] str) {
|
||||||
if (passwd == null)
|
if (str == null)
|
||||||
return putString("");
|
return putString("");
|
||||||
putInt(passwd.length);
|
putInt(str.length);
|
||||||
ensureCapacity(passwd.length);
|
ensureCapacity(str.length);
|
||||||
for (char c : passwd)
|
for (char c : str)
|
||||||
data[wpos++] = (byte) c;
|
data[wpos++] = (byte) c;
|
||||||
Arrays.fill(passwd, ' ');
|
Arrays.fill(str, ' ');
|
||||||
return (T) this;
|
return (T) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import net.schmizz.sshj.common.SSHPacket;
|
|||||||
import net.schmizz.sshj.transport.TransportException;
|
import net.schmizz.sshj.transport.TransportException;
|
||||||
import net.schmizz.sshj.userauth.AuthParams;
|
import net.schmizz.sshj.userauth.AuthParams;
|
||||||
import net.schmizz.sshj.userauth.UserAuthException;
|
import net.schmizz.sshj.userauth.UserAuthException;
|
||||||
|
import net.schmizz.sshj.userauth.password.AccountResource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -80,4 +81,8 @@ public abstract class AbstractAuthMethod
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected AccountResource makeAccountResource() {
|
||||||
|
return new AccountResource(params.getUsername(), params.getTransport().getRemoteHost());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,32 +18,19 @@ package net.schmizz.sshj.userauth.method;
|
|||||||
import net.schmizz.sshj.common.Message;
|
import net.schmizz.sshj.common.Message;
|
||||||
import net.schmizz.sshj.common.SSHPacket;
|
import net.schmizz.sshj.common.SSHPacket;
|
||||||
import net.schmizz.sshj.transport.TransportException;
|
import net.schmizz.sshj.transport.TransportException;
|
||||||
import net.schmizz.sshj.userauth.AuthParams;
|
|
||||||
import net.schmizz.sshj.userauth.UserAuthException;
|
import net.schmizz.sshj.userauth.UserAuthException;
|
||||||
import net.schmizz.sshj.userauth.password.AccountResource;
|
|
||||||
import net.schmizz.sshj.userauth.password.Resource;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** Implements the {@code keyboard-interactive} authentication method. */
|
/** Implements the {@code keyboard-interactive} authentication method. */
|
||||||
public class AuthChallengeResponse
|
public class AuthKeyboardInteractive
|
||||||
extends AbstractAuthMethod {
|
extends AbstractAuthMethod {
|
||||||
|
|
||||||
private final ChallengeResponseProvider provider;
|
private final ChallengeResponseProvider provider;
|
||||||
private Resource resource;
|
|
||||||
|
|
||||||
public AuthChallengeResponse(ChallengeResponseProvider provider) {
|
public AuthKeyboardInteractive(ChallengeResponseProvider provider) {
|
||||||
super("keyboard-interactive");
|
super("keyboard-interactive");
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(AuthParams params) {
|
|
||||||
super.init(params);
|
|
||||||
resource = new AccountResource(params.getUsername(), params.getTransport().getRemoteHost());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SSHPacket buildReq()
|
public SSHPacket buildReq()
|
||||||
throws UserAuthException {
|
throws UserAuthException {
|
||||||
@@ -62,36 +49,39 @@ public class AuthChallengeResponse
|
|||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class CharArrWrap {
|
||||||
|
private final char[] arr;
|
||||||
|
|
||||||
|
private CharArrWrap(char[] arr) {
|
||||||
|
this.arr = arr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(Message cmd, SSHPacket buf)
|
public void handle(Message cmd, SSHPacket buf)
|
||||||
throws UserAuthException, TransportException {
|
throws UserAuthException, TransportException {
|
||||||
if (cmd == Message.USERAUTH_60) {
|
if (cmd != Message.USERAUTH_60) {
|
||||||
provider.init(resource, buf.readString(), buf.readString());
|
super.handle(cmd, buf);
|
||||||
|
} else {
|
||||||
|
provider.init(makeAccountResource(), buf.readString(), buf.readString());
|
||||||
buf.readString(); // lang-tag
|
buf.readString(); // lang-tag
|
||||||
final int numPrompts = buf.readInt();
|
final int numPrompts = buf.readInt();
|
||||||
final List<String> userReplies = new ArrayList<String>(numPrompts);
|
final CharArrWrap[] userReplies = new CharArrWrap[numPrompts];
|
||||||
for (int i = 0; i < numPrompts; i++) {
|
for (int i = 0; i < numPrompts; i++) {
|
||||||
userReplies.add(provider.getResponse(buf.readString(), buf.readBoolean()));
|
userReplies[i] = new CharArrWrap(provider.getResponse(buf.readString(), buf.readBoolean()));
|
||||||
}
|
}
|
||||||
respond(userReplies);
|
respond(userReplies);
|
||||||
} else
|
}
|
||||||
super.handle(cmd, buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void respond(List<String> userReplies)
|
private void respond(CharArrWrap[] userReplies)
|
||||||
throws TransportException {
|
throws TransportException {
|
||||||
final SSHPacket pkt = new SSHPacket(Message.USERAUTH_INFO_RESPONSE)
|
final SSHPacket pkt = new SSHPacket(Message.USERAUTH_INFO_RESPONSE).putInt(userReplies.length);
|
||||||
.putInt(userReplies.size());
|
for (final CharArrWrap response : userReplies)
|
||||||
for (String response : userReplies) {
|
pkt.putSensitiveString(response.arr);
|
||||||
pkt.putString(response);
|
|
||||||
}
|
|
||||||
params.getTransport().write(pkt);
|
params.getTransport().write(pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns {@code true} if the associated {@link net.schmizz.sshj.userauth.password.PasswordFinder} tells that we
|
|
||||||
* should retry with a new password that it will supply.
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldRetry() {
|
public boolean shouldRetry() {
|
||||||
return provider.shouldRetry();
|
return provider.shouldRetry();
|
||||||
@@ -18,42 +18,29 @@ package net.schmizz.sshj.userauth.method;
|
|||||||
import net.schmizz.sshj.common.Message;
|
import net.schmizz.sshj.common.Message;
|
||||||
import net.schmizz.sshj.common.SSHPacket;
|
import net.schmizz.sshj.common.SSHPacket;
|
||||||
import net.schmizz.sshj.transport.TransportException;
|
import net.schmizz.sshj.transport.TransportException;
|
||||||
import net.schmizz.sshj.userauth.AuthParams;
|
|
||||||
import net.schmizz.sshj.userauth.UserAuthException;
|
import net.schmizz.sshj.userauth.UserAuthException;
|
||||||
import net.schmizz.sshj.userauth.password.AccountResource;
|
import net.schmizz.sshj.userauth.password.AccountResource;
|
||||||
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
import net.schmizz.sshj.userauth.password.PasswordFinder;
|
||||||
import net.schmizz.sshj.userauth.password.Resource;
|
|
||||||
|
|
||||||
/** Implements the {@code password} authentication method. Password-change request handling is not currently supported. */
|
/** Implements the {@code password} authentication method. Password-change request handling is not currently supported. */
|
||||||
public class AuthPassword
|
public class AuthPassword
|
||||||
extends AbstractAuthMethod {
|
extends AbstractAuthMethod {
|
||||||
|
|
||||||
private final PasswordFinder pwdf;
|
private final PasswordFinder pwdf;
|
||||||
private Resource resource;
|
|
||||||
|
|
||||||
public AuthPassword(PasswordFinder pwdf) {
|
public AuthPassword(PasswordFinder pwdf) {
|
||||||
super("password");
|
super("password");
|
||||||
this.pwdf = pwdf;
|
this.pwdf = pwdf;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void init(AuthParams params) {
|
|
||||||
super.init(params);
|
|
||||||
resource = new AccountResource(params.getUsername(), params.getTransport().getRemoteHost());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SSHPacket buildReq()
|
public SSHPacket buildReq()
|
||||||
throws UserAuthException {
|
throws UserAuthException {
|
||||||
log.info("Requesting password for {}", resource);
|
final AccountResource accountResource = makeAccountResource();
|
||||||
char[] password = pwdf.reqPassword(resource);
|
log.info("Requesting password for {}", accountResource);
|
||||||
if (password == null)
|
return super.buildReq() // the generic stuff
|
||||||
throw new UserAuthException("Was given null password for " + resource);
|
.putBoolean(false) // no, we are not responding to a CHANGEREQ
|
||||||
else
|
.putSensitiveString(pwdf.reqPassword(accountResource));
|
||||||
return super.buildReq() // the generic stuff
|
|
||||||
.putBoolean(false) // no, we are not responding to a CHANGEREQ
|
|
||||||
.putPassword(password);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -71,7 +58,7 @@ public class AuthPassword
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean shouldRetry() {
|
public boolean shouldRetry() {
|
||||||
return pwdf.shouldRetry(resource);
|
return pwdf.shouldRetry(makeAccountResource());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ public interface ChallengeResponseProvider {
|
|||||||
|
|
||||||
void init(Resource resource, String name, String instruction);
|
void init(Resource resource, String name, String instruction);
|
||||||
|
|
||||||
String getResponse(String prompt, boolean echo);
|
char[] getResponse(String prompt, boolean echo);
|
||||||
|
|
||||||
boolean shouldRetry();
|
boolean shouldRetry();
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class PasswordResponseProvider
|
|||||||
|
|
||||||
private final Logger log = LoggerFactory.getLogger(getClass());
|
private final Logger log = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
private static final char[] EMPTY_RESPONSE = new char[0];
|
||||||
|
|
||||||
private final PasswordFinder pwdf;
|
private final PasswordFinder pwdf;
|
||||||
private Resource resource;
|
private Resource resource;
|
||||||
private boolean gaveOnce;
|
private boolean gaveOnce;
|
||||||
@@ -48,13 +50,13 @@ public class PasswordResponseProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getResponse(String prompt, boolean echo) {
|
public char[] getResponse(String prompt, boolean echo) {
|
||||||
if (!gaveOnce && !echo && prompt.toLowerCase().contains("password")) {
|
if (!gaveOnce && !echo && prompt.toLowerCase().contains("password")) {
|
||||||
gaveOnce = true;
|
gaveOnce = true;
|
||||||
log.debug("prompt = `{}`", prompt);
|
log.debug("prompt = `{}`", prompt);
|
||||||
return new String(pwdf.reqPassword(resource));
|
return pwdf.reqPassword(resource);
|
||||||
}
|
}
|
||||||
return "";
|
return EMPTY_RESPONSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public interface PasswordFinder {
|
|||||||
* Request password for specified resource.
|
* Request password for specified resource.
|
||||||
* <p/>
|
* <p/>
|
||||||
* This method may return {@code null} when the request cannot be serviced, e.g. when the user cancels a password
|
* This method may return {@code null} when the request cannot be serviced, e.g. when the user cancels a password
|
||||||
* prompt. The consequences of returning {@code null} are specific to the requestor.
|
* prompt.
|
||||||
*
|
*
|
||||||
* @param resource the resource for which password is being requested
|
* @param resource the resource for which password is being requested
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import java.security.GeneralSecurityException;
|
|||||||
import static org.junit.Assert.assertArrayEquals;
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
/** Tests {@link SSHPacket} functionality */
|
/** Tests {@link Buffer} functionality */
|
||||||
public class BufferTest {
|
public class BufferTest {
|
||||||
private Buffer.PlainBuffer posBuf;
|
private Buffer.PlainBuffer posBuf;
|
||||||
private Buffer.PlainBuffer handyBuf;
|
private Buffer.PlainBuffer handyBuf;
|
||||||
@@ -69,7 +69,7 @@ public class BufferTest {
|
|||||||
public void testPassword() {
|
public void testPassword() {
|
||||||
char[] pass = "lolcatz".toCharArray();
|
char[] pass = "lolcatz".toCharArray();
|
||||||
// test if put correctly as a string
|
// test if put correctly as a string
|
||||||
assertEquals(new Buffer.PlainBuffer().putPassword(pass).readString(), "lolcatz");
|
assertEquals(new Buffer.PlainBuffer().putSensitiveString(pass).readString(), "lolcatz");
|
||||||
// test that char[] was blanked out
|
// test that char[] was blanked out
|
||||||
assertArrayEquals(pass, " ".toCharArray());
|
assertArrayEquals(pass, " ".toCharArray());
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user