diff --git a/src/main/java/net/schmizz/sshj/common/Buffer.java b/src/main/java/net/schmizz/sshj/common/Buffer.java index c11852d7..30b9995e 100644 --- a/src/main/java/net/schmizz/sshj/common/Buffer.java +++ b/src/main/java/net/schmizz/sshj/common/Buffer.java @@ -453,11 +453,14 @@ public class Buffer> { public T putSensitiveString(char[] str) { if (str == null) return putString(""); - putUInt32(str.length); - ensureCapacity(str.length); - for (char c : str) - data[wpos++] = (byte) c; - Arrays.fill(str, ' '); + // RFC 4252, Section 8 says: passwords should be encoded as UTF-8. + // RFC 4256, Section 3.4 says: keyboard-interactive information responses should be encoded as UTF-8. + byte[] utf8 = ByteArrayUtils.encodeSensitiveStringToUtf8(str); + putUInt32(utf8.length); + ensureCapacity(utf8.length); + for (byte c : utf8) + data[wpos++] = c; + Arrays.fill(utf8, (byte) 0); return (T) this; } diff --git a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java index 4d96c3e6..265943a3 100644 --- a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java +++ b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java @@ -15,6 +15,12 @@ */ package net.schmizz.sshj.common; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.Arrays; + /** Utility functions for byte arrays. */ public class ByteArrayUtils { @@ -124,4 +130,26 @@ public class ByteArrayUtils { } throw new IllegalArgumentException("Digit '" + c + "' out of bounds [0-9a-fA-F]"); } + + /** + * Converts a char-array to UTF-8 byte-array and then blanks out source array and all intermediate arrays. + *

+ * This is useful when a plaintext password needs to be encoded as UTF-8. + * + * @param str A not-null string as a character array. + * + * @return UTF-8 bytes of the string + */ + public static byte[] encodeSensitiveStringToUtf8(char[] str) { + CharsetEncoder charsetEncoder = Charset.forName("UTF-8").newEncoder(); + ByteBuffer utf8Buffer = ByteBuffer.allocate((int) (str.length * charsetEncoder.maxBytesPerChar())); + assert utf8Buffer.hasArray(); + charsetEncoder.encode(CharBuffer.wrap(str), utf8Buffer, true); + Arrays.fill(str, ' '); + + byte[] utf8Bytes = new byte[utf8Buffer.position()]; + System.arraycopy(utf8Buffer.array(), 0, utf8Bytes, 0, utf8Bytes.length); + Arrays.fill(utf8Buffer.array(), (byte) 0); + return utf8Bytes; + } } diff --git a/src/test/java/com/hierynomus/sshj/userauth/method/AuthKeyboardInteractiveTest.java b/src/test/java/com/hierynomus/sshj/userauth/method/AuthKeyboardInteractiveTest.java new file mode 100644 index 00000000..dbf81b0d --- /dev/null +++ b/src/test/java/com/hierynomus/sshj/userauth/method/AuthKeyboardInteractiveTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * Licensed 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 com.hierynomus.sshj.userauth.method; + +import com.hierynomus.sshj.test.SshFixture; +import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.userauth.method.AuthKeyboardInteractive; +import net.schmizz.sshj.userauth.method.ChallengeResponseProvider; +import net.schmizz.sshj.userauth.password.Resource; +import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.server.auth.UserAuth; +import org.apache.sshd.server.auth.keyboard.UserAuthKeyboardInteractive; +import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.session.ServerSession; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; + +public class AuthKeyboardInteractiveTest { + @Rule + public SshFixture fixture = new SshFixture(false); + + @Before + public void setKeyboardInteractiveAuthenticator() throws IOException { + fixture.getServer().setUserAuthFactories(Collections.>singletonList(new NamedFactory() { + @Override + public String getName() { + return UserAuthKeyboardInteractiveFactory.NAME; + } + + @Override + public UserAuth get() { + return new UserAuthKeyboardInteractive(); + } + + @Override + public UserAuth create() { + return get(); + } + })); + fixture.getServer().setPasswordAuthenticator(new PasswordAuthenticator() { + @Override + public boolean authenticate(String username, String password, ServerSession session) { + return password.equals(username); + } + }); + fixture.getServer().start(); + } + + @Test + public void shouldEncodePasswordsAsUtf8() throws IOException { + SSHClient sshClient = fixture.setupConnectedDefaultClient(); + final String userAndPassword = "øæå"; + sshClient.auth(userAndPassword, new AuthKeyboardInteractive(new ChallengeResponseProvider() { + @Override + public List getSubmethods() { + return new ArrayList(); + } + + @Override + public void init(Resource resource, String name, String instruction) { + // nothing + } + + @Override + public char[] getResponse(String prompt, boolean echo) { + return userAndPassword.toCharArray(); + } + + @Override + public boolean shouldRetry() { + return false; + } + })); + assertThat("Should have been authenticated", sshClient.isAuthenticated()); + } +} diff --git a/src/test/java/com/hierynomus/sshj/userauth/method/AuthPasswordTest.java b/src/test/java/com/hierynomus/sshj/userauth/method/AuthPasswordTest.java index acf4c2ea..33ba2c79 100644 --- a/src/test/java/com/hierynomus/sshj/userauth/method/AuthPasswordTest.java +++ b/src/test/java/com/hierynomus/sshj/userauth/method/AuthPasswordTest.java @@ -144,6 +144,14 @@ public class AuthPasswordTest { assertThat("Should have been authenticated", sshClient.isAuthenticated()); } + @Test + public void shouldEncodePasswordsAsUtf8() throws IOException { + SSHClient sshClient = fixture.setupConnectedDefaultClient(); + String userAndPassword = "øæå"; + sshClient.authPassword(userAndPassword, userAndPassword); + assertThat("Should have been authenticated", sshClient.isAuthenticated()); + } + private static class StaticPasswordUpdateProvider implements PasswordUpdateProvider { private Stack newPasswords = new Stack();