You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2019/10/17 06:02:41 UTC
[mina-sshd] 01/06: [SSHD-946] Supporting 'encrypt-then-MAC' mode
This is an automated email from the ASF dual-hosted git repository.
lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 06eeff80a7ca2fe7d6b9cfa96f5c081a6ce8c49e
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Sun Oct 13 11:54:08 2019 +0300
[SSHD-946] Supporting 'encrypt-then-MAC' mode
---
CHANGES.md | 7 +
README.md | 5 +-
.../sshd/cli/client/SshClientCliSupport.java | 3 +-
.../java/org/apache/sshd/common/SshConstants.java | 3 +-
.../apache/sshd/common/kex/KexProposalOption.java | 17 ++
.../java/org/apache/sshd/common/mac/BaseMac.java | 18 ++-
.../org/apache/sshd/common/mac/BuiltinMacs.java | 53 +++++--
.../org/apache/sshd/common/mac/MacInformation.java | 4 +
.../org/apache/sshd/common/util/buffer/Buffer.java | 18 ++-
.../sshd/common/util/buffer/ByteArrayBuffer.java | 5 +
.../apache/sshd/util/test/JUnitTestSupport.java | 8 +
.../java/org/apache/sshd/common/BaseBuilder.java | 7 +-
.../org/apache/sshd/common/io/PacketWriter.java | 22 ++-
.../common/session/helpers/AbstractSession.java | 174 ++++++++++++++-------
.../sshd/common/session/helpers/SessionHelper.java | 3 +-
.../sshd/common/compression/CompressionTest.java | 6 +-
.../apache/sshd/common/mac/EncryptThenMacTest.java | 141 +++++++++++++++++
.../{MacTest.java => MacCompatibilityTest.java} | 21 ++-
.../apache/sshd/server/auth/WelcomeBannerTest.java | 5 +-
19 files changed, 419 insertions(+), 101 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 0f48121..73664a9 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -37,6 +37,9 @@ the standard does not specifically specify the behavior regarding symbolic links
* `Signature` methods accept a `SessionContext` argument representing the session context
of their invocation (if any)
+* Default MAC(s) list is set according to the [ssh_config(5)](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5)
+order as **first** ones, where the supported MAC(s) that do no appear in it come last.
+
## Minor code helpers
* `SessionListener` supports `sessionPeerIdentificationReceived` method that is invoked once successful
@@ -57,6 +60,8 @@ the message type=30 (old request).
* `AbstractSignature#doInitSignature` is now provided also with the `Key` instance for which it is invoked.
+* The `MacInformation` interface has an extra `isEncryptThenMac` method (default=_false_) to enable distinction of this mode.
+
## Behavioral changes and enhancements
* [SSHD-926](https://issues.apache.org/jira/browse/SSHD-930) - Add support for OpenSSH 'lsetstat@openssh.com' SFTP protocol extension.
@@ -77,6 +82,8 @@ exchange via properties.
* [SSHD-945](https://issues.apache.org/jira/browse/SSHD-945) - Added sshd-contrib code that uses SHA1 with DSA regardless of its key length
+* [SSHD-946](https://issues.apache.org/jira/browse/SSHD-946) - Supporting 'encrypt-then-MAC' mode.
+
* [SSHD-947](https://issues.apache.org/jira/browse/SSHD-947) - Added configuration allowing the user to specify whether client should wait
for the server's identification before sending KEX-INIT message.
diff --git a/README.md b/README.md
index ac35e7b..f6d5399 100644
--- a/README.md
+++ b/README.md
@@ -47,8 +47,9 @@ based applications requiring SSH support.
## Implemented/available support
* **Ciphers**: aes128cbc, aes128ctr, aes192cbc, aes192ctr, aes256cbc, aes256ctr, arcfour128, arcfour256, blowfishcbc, tripledescbc
-* **Digests**: md5, sha1, sha224, sha384, sha512
-* **Macs**: hmacmd5, hmacmd596, hmacsha1, hmacsha196, hmacsha256, hmacsha512
+* **Digests**: md5, sha1, sha224, sha256, sha384, sha512
+* **Macs**: hmacmd5, hmacmd596, hmacsha1, hmacsha196, hmacsha256, hmacsha512, hmac-sha2-256-etm@openssh.com
+, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com
* **Key exchange**: diffie-hellman-group1-sha1, diffie-hellman-group-exchange-sha256, diffie-hellman-group14-sha1, diffie-hellman-group14-sha256
, diffie-hellman-group15-sha512, diffie-hellman-group16-sha512, diffie-hellman-group17-sha512, diffie-hellman-group18-sha512
, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521
diff --git a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index 3d52b5f..ca1919b 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -681,7 +681,8 @@ public abstract class SshClientCliSupport extends CliSupport {
: setupMacs(ConfigFileReaderSupport.MACS_CONFIG_PROP, argVal, null, stderr);
}
- public static List<NamedFactory<Mac>> setupMacs(String argName, String argVal, List<NamedFactory<Mac>> current, PrintStream stderr) {
+ public static List<NamedFactory<Mac>> setupMacs(
+ String argName, String argVal, List<NamedFactory<Mac>> current, PrintStream stderr) {
if (GenericUtils.size(current) > 0) {
showError(stderr, argName + " option value re-specified: " + NamedResource.getNames(current));
return null;
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
index 7e73edf..06f7b05 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/SshConstants.java
@@ -122,7 +122,8 @@ public final class SshConstants {
// Some more constants
public static final int SSH_EXTENDED_DATA_STDERR = 1; // see RFC4254 section 5.2
- public static final int SSH_PACKET_HEADER_LEN = 5; // 32-bit length + 8-bit pad length
+ // 32-bit length + 8-bit pad length
+ public static final int SSH_PACKET_HEADER_LEN = Integer.BYTES + Byte.BYTES;
/*
* See https://tools.ietf.org/html/rfc4253#section-6.1:
*
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/kex/KexProposalOption.java b/sshd-common/src/main/java/org/apache/sshd/common/kex/KexProposalOption.java
index ae20311..752b41e 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/kex/KexProposalOption.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/kex/KexProposalOption.java
@@ -19,6 +19,7 @@
package org.apache.sshd.common.kex;
+import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
@@ -42,6 +43,22 @@ public enum KexProposalOption {
C2SLANG(Constants.PROPOSAL_LANG_CTOS, "languages (client to server)"),
S2CLANG(Constants.PROPOSAL_LANG_STOC, "languages (server to client)");
+ public static final Collection<KexProposalOption> CIPHER_PROPOSALS =
+ Collections.unmodifiableSet(
+ EnumSet.of(KexProposalOption.C2SENC, KexProposalOption.S2CENC));
+
+ public static final Collection<KexProposalOption> MAC_PROPOSALS =
+ Collections.unmodifiableSet(
+ EnumSet.of(KexProposalOption.C2SMAC, KexProposalOption.S2CMAC));
+
+ public static final Collection<KexProposalOption> COMPRESSION_PROPOSALS =
+ Collections.unmodifiableSet(
+ EnumSet.of(KexProposalOption.C2SCOMP, KexProposalOption.S2CCOMP));
+
+ public static final Collection<KexProposalOption> LANGUAGE_PROPOSALS =
+ Collections.unmodifiableSet(
+ EnumSet.of(KexProposalOption.C2SLANG, KexProposalOption.S2CLANG));
+
/**
* Compares values according to {@link KexProposalOption#getProposalIndex()}
*/
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/mac/BaseMac.java b/sshd-common/src/main/java/org/apache/sshd/common/mac/BaseMac.java
index e1681d4..036d46f 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/mac/BaseMac.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/mac/BaseMac.java
@@ -33,32 +33,39 @@ public class BaseMac implements Mac {
private final int defbsize;
private final int bsize;
private final byte[] tmp;
+ private final boolean etmMode;
private javax.crypto.Mac mac;
private String s;
- public BaseMac(String algorithm, int bsize, int defbsize) {
+ public BaseMac(String algorithm, int bsize, int defbsize, boolean etmMode) {
this.algorithm = algorithm;
this.bsize = bsize;
this.defbsize = defbsize;
this.tmp = new byte[defbsize];
+ this.etmMode = etmMode;
}
@Override
- public final String getAlgorithm() {
+ public String getAlgorithm() {
return algorithm;
}
@Override
- public final int getBlockSize() {
+ public int getBlockSize() {
return bsize;
}
@Override
- public final int getDefaultBlockSize() {
+ public int getDefaultBlockSize() {
return defbsize;
}
@Override
+ public boolean isEncryptThenMac() {
+ return etmMode;
+ }
+
+ @Override
public void init(byte[] key) throws Exception {
if (key.length > defbsize) {
byte[] tmp = new byte[defbsize];
@@ -102,7 +109,8 @@ public class BaseMac implements Mac {
synchronized (this) {
if (s == null) {
s = getClass().getSimpleName() + "[" + getAlgorithm() + "] - "
- + " block=" + getBlockSize() + "/" + getDefaultBlockSize() + " bytes";
+ + " block=" + getBlockSize() + "/" + getDefaultBlockSize() + " bytes"
+ + ", encrypt-then-mac=" + isEncryptThenMac();
}
}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/mac/BuiltinMacs.java b/sshd-common/src/main/java/org/apache/sshd/common/mac/BuiltinMacs.java
index 3c7beaa..a393082 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/mac/BuiltinMacs.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/mac/BuiltinMacs.java
@@ -46,11 +46,29 @@ public enum BuiltinMacs implements MacFactory {
hmacmd5(Constants.HMAC_MD5, "HmacMD5", 16, 16),
hmacmd596(Constants.HMAC_MD5_96, "HmacMD5", 12, 16),
hmacsha1(Constants.HMAC_SHA1, "HmacSHA1", 20, 20),
+ hmacsha1etm(Constants.ETM_HMAC_SHA1, "HmacSHA1", 20, 20) {
+ @Override
+ public boolean isEncryptThenMac() {
+ return true;
+ }
+ },
hmacsha196(Constants.HMAC_SHA1_96, "HmacSHA1", 12, 20),
/** See <A HREF="https://tools.ietf.org/html/rfc6668">RFC 6668</A> */
hmacsha256(Constants.HMAC_SHA2_256, "HmacSHA256", 32, 32),
+ hmacsha256etm(Constants.ETM_HMAC_SHA2_256, "HmacSHA256", 32, 32) {
+ @Override
+ public boolean isEncryptThenMac() {
+ return true;
+ }
+ },
/** See <A HREF="https://tools.ietf.org/html/rfc6668">RFC 6668</A> */
- hmacsha512(Constants.HMAC_SHA2_512, "HmacSHA512", 64, 64);
+ hmacsha512(Constants.HMAC_SHA2_512, "HmacSHA512", 64, 64),
+ hmacsha512etm(Constants.ETM_HMAC_SHA2_512, "HmacSHA512", 64, 64) {
+ @Override
+ public boolean isEncryptThenMac() {
+ return true;
+ }
+ };
public static final Set<BuiltinMacs> VALUES =
Collections.unmodifiableSet(EnumSet.allOf(BuiltinMacs.class));
@@ -72,7 +90,7 @@ public enum BuiltinMacs implements MacFactory {
@Override
public Mac create() {
- return new BaseMac(getAlgorithm(), getBlockSize(), getDefaultBlockSize());
+ return new BaseMac(getAlgorithm(), getBlockSize(), getDefaultBlockSize(), isEncryptThenMac());
}
@Override
@@ -116,10 +134,12 @@ public enum BuiltinMacs implements MacFactory {
*/
public static void registerExtension(MacFactory extension) {
String name = Objects.requireNonNull(extension, "No extension provided").getName();
- ValidateUtils.checkTrue(fromFactoryName(name) == null, "Extension overrides built-in: %s", name);
+ ValidateUtils.checkTrue(
+ fromFactoryName(name) == null, "Extension overrides built-in: %s", name);
synchronized (EXTENSIONS) {
- ValidateUtils.checkTrue(!EXTENSIONS.containsKey(name), "Extension overrides existing: %s", name);
+ ValidateUtils.checkTrue(
+ !EXTENSIONS.containsKey(name), "Extension overrides existing: %s", name);
EXTENSIONS.put(name, extension);
}
}
@@ -130,7 +150,8 @@ public enum BuiltinMacs implements MacFactory {
*/
public static NavigableSet<MacFactory> getRegisteredExtensions() {
synchronized (EXTENSIONS) {
- return GenericUtils.asSortedSet(NamedResource.BY_NAME_COMPARATOR, EXTENSIONS.values());
+ return GenericUtils.asSortedSet(
+ NamedResource.BY_NAME_COMPARATOR, EXTENSIONS.values());
}
}
@@ -152,8 +173,9 @@ public enum BuiltinMacs implements MacFactory {
/**
* @param s The {@link Enum}'s name - ignored if {@code null}/empty
- * @return The matching {@link org.apache.sshd.common.mac.BuiltinMacs} whose {@link Enum#name()} matches
- * (case <U>insensitive</U>) the provided argument - {@code null} if no match
+ * @return The matching {@link org.apache.sshd.common.mac.BuiltinMacs}
+ * whose {@link Enum#name()} matches (case <U>insensitive</U>) the provided
+ * argument - {@code null} if no match
*/
public static BuiltinMacs fromString(String s) {
if (GenericUtils.isEmpty(s)) {
@@ -193,8 +215,7 @@ public enum BuiltinMacs implements MacFactory {
}
/**
- * @param macs A comma-separated list of MACs' names - ignored
- * if {@code null}/empty
+ * @param macs A comma-separated list of MACs' names - ignored if {@code null}/empty
* @return A {@link ParseResult} containing the successfully parsed
* factories and the unknown ones. <B>Note:</B> it is up to caller to
* ensure that the lists do not contain duplicates
@@ -204,7 +225,9 @@ public enum BuiltinMacs implements MacFactory {
}
public static ParseResult parseMacsList(String... macs) {
- return parseMacsList(GenericUtils.isEmpty((Object[]) macs) ? Collections.emptyList() : Arrays.asList(macs));
+ return parseMacsList(GenericUtils.isEmpty((Object[]) macs)
+ ? Collections.emptyList()
+ : Arrays.asList(macs));
}
public static ParseResult parseMacsList(Collection<String> macs) {
@@ -250,8 +273,10 @@ public enum BuiltinMacs implements MacFactory {
}
}
- public static final class ParseResult extends NamedFactoriesListParseResult<Mac, MacFactory> {
- public static final ParseResult EMPTY = new ParseResult(Collections.emptyList(), Collections.emptyList());
+ public static final class ParseResult
+ extends NamedFactoriesListParseResult<Mac, MacFactory> {
+ public static final ParseResult EMPTY =
+ new ParseResult(Collections.emptyList(), Collections.emptyList());
public ParseResult(List<MacFactory> parsed, List<String> unsupported) {
super(parsed, unsupported);
@@ -266,6 +291,10 @@ public enum BuiltinMacs implements MacFactory {
public static final String HMAC_SHA2_256 = "hmac-sha2-256";
public static final String HMAC_SHA2_512 = "hmac-sha2-512";
+ public static final String ETM_HMAC_SHA1 = "hmac-sha1-etm@openssh.com";
+ public static final String ETM_HMAC_SHA2_256 = "hmac-sha2-256-etm@openssh.com";
+ public static final String ETM_HMAC_SHA2_512 = "hmac-sha2-512-etm@openssh.com";
+
private Constants() {
throw new UnsupportedOperationException("No instance allowed");
}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/mac/MacInformation.java b/sshd-common/src/main/java/org/apache/sshd/common/mac/MacInformation.java
index 07a9321..7d5f6a1 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/mac/MacInformation.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/mac/MacInformation.java
@@ -37,4 +37,8 @@ public interface MacInformation extends AlgorithmNameProvider {
* @return The "natural" MAC block size in bytes
*/
int getDefaultBlockSize();
+
+ default boolean isEncryptThenMac() {
+ return false;
+ }
}
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
index ee2901f..bb29204 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/Buffer.java
@@ -114,6 +114,15 @@ public abstract class Buffer implements Readable {
public abstract byte[] array();
/**
+ * @param pos A position in the <U>raw</U> underlying data bytes
+ * @return The byte at that position
+ */
+ public byte rawByte(int pos) {
+ byte[] data = array();
+ return data[pos];
+ }
+
+ /**
* "Shift" the internal data so that reading starts from
* position zero.
*/
@@ -221,7 +230,8 @@ public abstract class Buffer implements Readable {
}
public void dumpHex(SimplifiedLog logger, Level level, String prefix, PropertyResolver resolver) {
- BufferUtils.dumpHex(logger, level, prefix, resolver, BufferUtils.DEFAULT_HEX_SEPARATOR, array(), rpos(), available());
+ BufferUtils.dumpHex(
+ logger, level, prefix, resolver, BufferUtils.DEFAULT_HEX_SEPARATOR, array(), rpos(), available());
}
/*======================
@@ -482,9 +492,9 @@ public abstract class Buffer implements Readable {
public KeyPair getKeyPair() throws SshException {
try {
- final PublicKey pub;
- final PrivateKey prv;
- final String keyAlg = getString();
+ PublicKey pub;
+ PrivateKey prv;
+ String keyAlg = getString();
if (KeyPairProvider.SSH_RSA.equals(keyAlg)) {
BigInteger e = getMPInt();
BigInteger n = getMPInt();
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
index bb9ff8c..e0dee96 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/ByteArrayBuffer.java
@@ -158,6 +158,11 @@ public class ByteArrayBuffer extends Buffer {
}
@Override
+ public byte rawByte(int pos) {
+ return data[pos];
+ }
+
+ @Override
public void compact() {
int avail = available();
if (avail > 0) {
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
index 9d57473..aa1e16b 100644
--- a/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/JUnitTestSupport.java
@@ -576,6 +576,14 @@ public abstract class JUnitTestSupport extends Assert {
return false;
}
+ /* ---------------------------------------------------------------------------- */
+
+ public static void outputDebugMessage(String format, Object o) {
+ if (OUTPUT_DEBUG_MESSAGES) {
+ outputDebugMessage(String.format(format, o));
+ }
+ }
+
public static void outputDebugMessage(String format, Object... args) {
if (OUTPUT_DEBUG_MESSAGES) {
outputDebugMessage(String.format(format, args));
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
index 904726c..484cc88 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java
@@ -116,10 +116,13 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder
public static final List<BuiltinMacs> DEFAULT_MAC_PREFERENCE =
Collections.unmodifiableList(
Arrays.asList(
- BuiltinMacs.hmacmd5,
- BuiltinMacs.hmacsha1,
+ BuiltinMacs.hmacsha256etm,
+ BuiltinMacs.hmacsha512etm,
+ BuiltinMacs.hmacsha1etm,
BuiltinMacs.hmacsha256,
BuiltinMacs.hmacsha512,
+ BuiltinMacs.hmacsha1,
+ BuiltinMacs.hmacmd5,
BuiltinMacs.hmacsha196,
BuiltinMacs.hmacmd596
));
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/io/PacketWriter.java b/sshd-core/src/main/java/org/apache/sshd/common/io/PacketWriter.java
index 42cda19..59f66b0 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/io/PacketWriter.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/io/PacketWriter.java
@@ -24,8 +24,6 @@ import java.nio.channels.Channel;
import org.apache.sshd.common.util.buffer.Buffer;
/**
- * TODO Add javadoc
- *
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
public interface PacketWriter extends Channel {
@@ -40,4 +38,24 @@ public interface PacketWriter extends Channel {
* @throws IOException if an error occurred when encoding sending the packet
*/
IoWriteFuture writePacket(Buffer buffer) throws IOException;
+
+ /**
+ * @param len The packet payload size
+ * @param blockSize The cipher block size
+ * @param etmMode Whether using "encrypt-then-MAC" mode
+ * @return The required padding length
+ */
+ static int calculatePadLength(int len, int blockSize, boolean etmMode) {
+ len++; // the pad length
+ if (!etmMode) {
+ len += Integer.BYTES;
+ }
+
+ int pad = (-len) & (blockSize - 1);
+ if (pad < blockSize) {
+ pad += blockSize;
+ }
+
+ return pad;
+ }
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
index 27e87a8..241162e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java
@@ -39,6 +39,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.logging.Level;
import org.apache.sshd.common.Closeable;
import org.apache.sshd.common.Factory;
@@ -61,6 +62,7 @@ import org.apache.sshd.common.future.KeyExchangeFuture;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.io.PacketWriter;
import org.apache.sshd.common.kex.KexProposalOption;
import org.apache.sshd.common.kex.KexState;
import org.apache.sshd.common.kex.KeyExchange;
@@ -1030,24 +1032,24 @@ public abstract class AbstractSession extends SessionHelper {
}
/**
- * Encode a buffer into the SSH protocol.
- * This method need to be called into a synchronized block around encodeLock
+ * Encode a buffer into the SSH protocol. <B>Note:</B> This method must be called
+ * inside a {@code synchronized} block using {@code encodeLock}.
*
* @param buffer the buffer to encode
* @return The encoded buffer - may be different than original if input
* buffer does not have enough room for {@link SshConstants#SSH_PACKET_HEADER_LEN},
- * in which a substitute buffer will be created and used.
+ * in which case a substitute buffer will be created and used.
* @throws IOException if an exception occurs during the encoding process
*/
protected Buffer encode(Buffer buffer) throws IOException {
try {
// Check that the packet has some free space for the header
int curPos = buffer.rpos();
+ int cmd = buffer.rawByte(curPos) & 0xFF; // usually the 1st byte is an SSH opcode
if (curPos < SshConstants.SSH_PACKET_HEADER_LEN) {
- byte[] data = buffer.array();
- int cmd = data[curPos] & 0xFF; // usually the 1st byte is an SSH opcode
- log.warn("encode({}) command={} performance cost: available buffer packet header length ({}) below min. required ({})",
- this, SshConstants.getCommandMessageName(cmd), curPos, SshConstants.SSH_PACKET_HEADER_LEN);
+ log.warn("encode({}) command={}[{}] performance cost: available buffer packet header length ({}) below min. required ({})",
+ this, cmd, SshConstants.getCommandMessageName(cmd),
+ curPos, SshConstants.SSH_PACKET_HEADER_LEN);
Buffer nb = new ByteArrayBuffer(buffer.available() + Long.SIZE, false);
nb.wpos(SshConstants.SSH_PACKET_HEADER_LEN);
nb.putBuffer(buffer);
@@ -1057,57 +1059,63 @@ public abstract class AbstractSession extends SessionHelper {
// Grab the length of the packet (excluding the 5 header bytes)
int len = buffer.available();
+ if (log.isDebugEnabled()) {
+ log.debug("encode({}) packet #{} sending command={}[{}] len={}",
+ this, seqo, cmd, SshConstants.getCommandMessageName(cmd), len);
+ }
+
int off = curPos - SshConstants.SSH_PACKET_HEADER_LEN;
// Debug log the packet
- if (log.isTraceEnabled()) {
- buffer.dumpHex(getSimplifiedLogger(), "encode(" + this + ") packet #" + seqo, this);
+ boolean traceEnabled = log.isTraceEnabled();
+ if (traceEnabled) {
+ buffer.dumpHex(getSimplifiedLogger(), Level.FINEST, "encode(" + this + ") packet #" + seqo, this);
}
// Compress the packet if needed
if ((outCompression != null)
&& outCompression.isCompressionExecuted()
&& (isAuthenticated() || (!outCompression.isDelayed()))) {
+ int oldLen = len;
outCompression.compress(buffer);
len = buffer.available();
+ if (traceEnabled) {
+ log.trace("encode({}) packet #{} command={}[{}] compressed {} -> {}",
+ this, seqo, cmd, SshConstants.getCommandMessageName(cmd), oldLen, len);
+ }
}
// Compute padding length
- int bsize = outCipherSize;
+ boolean etmMode = (outMac == null) ? false : outMac.isEncryptThenMac();
+ int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode);
int oldLen = len;
- len += SshConstants.SSH_PACKET_HEADER_LEN;
- int pad = (-len) & (bsize - 1);
- if (pad < bsize) {
- pad += bsize;
+ len = len + pad + Byte.BYTES /* the pad length byte */;
+
+ if (traceEnabled) {
+ log.trace("encode({}) packet #{} command={}[{}] len={}, pad={}, mac={}",
+ this, seqo, cmd, SshConstants.getCommandMessageName(cmd), len, pad, outMac);
}
- len = len + pad - 4;
+
// Write 5 header bytes
buffer.wpos(off);
buffer.putInt(len);
buffer.putByte((byte) pad);
- // Fill padding
+ // Make sure enough room for padding and then fill it
buffer.wpos(off + oldLen + SshConstants.SSH_PACKET_HEADER_LEN + pad);
synchronized (random) {
random.fill(buffer.array(), buffer.wpos() - pad, pad);
}
- // Compute mac
- if (outMac != null) {
- int macSize = outMac.getBlockSize();
- int l = buffer.wpos();
- buffer.wpos(l + macSize);
- outMac.updateUInt(seqo);
- outMac.update(buffer.array(), off, l);
- outMac.doFinal(buffer.array(), l);
+ if (etmMode) {
+ // Do not encrypt the length field
+ encryptOutgoingBuffer(buffer, off + Integer.BYTES, len);
+ appendOutgoingMac(buffer, off, len);
+ } else {
+ appendOutgoingMac(buffer, off, len);
+ encryptOutgoingBuffer(buffer, off, len + Integer.BYTES);
}
- // Encrypt packet, excluding mac
- if (outCipher != null) {
- outCipher.update(buffer.array(), off, len + 4);
- int blocksCount = (len + 4) / outCipher.getCipherBlockSize();
- outBlocksCount.addAndGet(Math.max(1, blocksCount));
- }
// Increment packet id
- seqo = (seqo + 1) & 0xffffffffL;
+ seqo = (seqo + 1L) & 0x0ffffffffL;
// Update stats
outPacketsCount.incrementAndGet();
outBytesCount.addAndGet(len);
@@ -1121,6 +1129,33 @@ public abstract class AbstractSession extends SessionHelper {
}
}
+ protected void appendOutgoingMac(Buffer buf, int offset, int len) throws Exception {
+ if (outMac == null) {
+ return;
+ }
+
+ int macSize = outMac.getBlockSize();
+ int l = buf.wpos();
+ // ensure enough room for MAC in outgoing buffer
+ buf.wpos(l + macSize);
+ // Include sequence number
+ outMac.updateUInt(seqo);
+ // Include the length field in the MAC calculation
+ outMac.update(buf.array(), offset, len + Integer.BYTES);
+ // Append MAC to end of packet
+ outMac.doFinal(buf.array(), l);
+ }
+
+ protected void encryptOutgoingBuffer(Buffer buf, int offset, int len) throws Exception {
+ if (outCipher == null) {
+ return;
+ }
+ outCipher.update(buf.array(), offset, len);
+
+ int blocksCount = len / outCipher.getCipherBlockSize();
+ outBlocksCount.addAndGet(Math.max(1, blocksCount));
+ }
+
/**
* Decode the incoming buffer and handle packets as needed.
*
@@ -1129,14 +1164,16 @@ public abstract class AbstractSession extends SessionHelper {
protected void decode() throws Exception {
// Decoding loop
for (;;) {
+ boolean etmMode = (inMac == null) ? false : inMac.isEncryptThenMac();
// Wait for beginning of packet
if (decoderState == 0) {
// The read position should always be 0 at this point because we have compacted this buffer
assert decoderBuffer.rpos() == 0;
+ int minBufLen = etmMode ? Integer.BYTES : inCipherSize;
// If we have received enough bytes, start processing those
- if (decoderBuffer.available() > inCipherSize) {
+ if (decoderBuffer.available() > minBufLen) {
// Decrypt the first bytes
- if (inCipher != null) {
+ if ((inCipher != null) && (!etmMode)) {
inCipher.update(decoderBuffer.array(), 0, inCipherSize);
int blocksCount = inCipherSize / inCipher.getCipherBlockSize();
@@ -1151,7 +1188,7 @@ public abstract class AbstractSession extends SessionHelper {
if ((decoderLength < SshConstants.SSH_PACKET_HEADER_LEN)
|| (decoderLength > (8 * SshConstants.SSH_REQUIRED_PAYLOAD_PACKET_LENGTH_SUPPORT))) {
log.warn("decode({}) Error decoding packet(invalid length): {}", this, decoderLength);
- decoderBuffer.dumpHex(getSimplifiedLogger(), "decode(" + this + ") invalid length packet", this);
+ decoderBuffer.dumpHex(getSimplifiedLogger(), Level.FINEST, "decode(" + this + ") invalid length packet", this);
throw new SshException(SshConstants.SSH2_DISCONNECT_PROTOCOL_ERROR,
"Invalid packet length: " + decoderLength);
}
@@ -1163,35 +1200,36 @@ public abstract class AbstractSession extends SessionHelper {
}
// We have received the beginning of the packet
} else if (decoderState == 1) {
- // The read position should always be 4 at this point
- assert decoderBuffer.rpos() == 4;
- int macSize = inMac != null ? inMac.getBlockSize() : 0;
+ // The read position should always be after reading the packet length at this point
+ assert decoderBuffer.rpos() == Integer.BYTES;
+ int macSize = (inMac != null) ? inMac.getBlockSize() : 0;
// Check if the packet has been fully received
if (decoderBuffer.available() >= (decoderLength + macSize)) {
byte[] data = decoderBuffer.array();
- // Decrypt the remaining of the packet
- if (inCipher != null) {
- int updateLen = decoderLength + 4 - inCipherSize;
- inCipher.update(data, inCipherSize, updateLen);
+ if (etmMode) {
+ validateIncomingMac(data, 0, decoderLength + Integer.BYTES);
- int blocksCount = updateLen / inCipher.getCipherBlockSize();
- inBlocksCount.addAndGet(Math.max(1, blocksCount));
- }
- // Check the mac of the packet
- if (inMac != null) {
- // Update mac with packet id
- inMac.updateUInt(seqi);
- // Update mac with packet data
- inMac.update(data, 0, decoderLength + 4);
- // Compute mac result
- inMac.doFinal(inMacResult, 0);
- // Check the computed result with the received mac (just after the packet data)
- if (!BufferUtils.equals(inMacResult, 0, data, decoderLength + 4, macSize)) {
- throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "MAC Error");
+ if (inCipher != null) {
+ inCipher.update(data, Integer.BYTES, decoderLength);
+
+ int blocksCount = decoderLength / inCipher.getCipherBlockSize();
+ inBlocksCount.addAndGet(Math.max(1, blocksCount));
+ }
+ } else {
+ // Decrypt the remaining of the packet
+ if (inCipher != null) {
+ int updateLen = decoderLength + Integer.BYTES - inCipherSize;
+ inCipher.update(data, inCipherSize, updateLen);
+
+ int blocksCount = updateLen / inCipher.getCipherBlockSize();
+ inBlocksCount.addAndGet(Math.max(1, blocksCount));
}
+
+ validateIncomingMac(data, 0, decoderLength + Integer.BYTES);
}
+
// Increment incoming packet sequence number
- seqi = (seqi + 1) & 0xffffffffL;
+ seqi = (seqi + 1L) & 0x0ffffffffL;
// Get padding
int pad = decoderBuffer.getUByte();
Buffer packet;
@@ -1210,12 +1248,12 @@ public abstract class AbstractSession extends SessionHelper {
inCompression.uncompress(decoderBuffer, uncompressBuffer);
packet = uncompressBuffer;
} else {
- decoderBuffer.wpos(decoderLength + 4 - pad);
+ decoderBuffer.wpos(decoderLength + Integer.BYTES - pad);
packet = decoderBuffer;
}
if (log.isTraceEnabled()) {
- packet.dumpHex(getSimplifiedLogger(), "decode(" + this + ") packet #" + seqi, this);
+ packet.dumpHex(getSimplifiedLogger(), Level.FINEST, "decode(" + this + ") packet #" + seqi, this);
}
// Update stats
@@ -1224,7 +1262,7 @@ public abstract class AbstractSession extends SessionHelper {
// Process decoded packet
handleMessage(packet);
// Set ready to handle next packet
- decoderBuffer.rpos(decoderLength + 4 + macSize);
+ decoderBuffer.rpos(decoderLength + Integer.BYTES + macSize);
decoderBuffer.wpos(wpos);
decoderBuffer.compact();
decoderState = 0;
@@ -1236,6 +1274,24 @@ public abstract class AbstractSession extends SessionHelper {
}
}
+ protected void validateIncomingMac(byte[] data, int offset, int len) throws Exception {
+ if (inMac == null) {
+ return;
+ }
+
+ // Update mac with packet id
+ inMac.updateUInt(seqi);
+ // Update mac with packet data
+ inMac.update(data, offset, len);
+ // Compute mac result
+ inMac.doFinal(inMacResult, 0);
+
+ // Check the computed result with the received mac (just after the packet data)
+ if (!BufferUtils.equals(inMacResult, 0, data, offset + len, inMac.getBlockSize())) {
+ throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "MAC Error");
+ }
+ }
+
/**
* Read the other side identification.
* This method is specific to the client or server side, but both should call
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
index b9ed2f1..3a24647 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
@@ -921,7 +921,8 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
return proposal;
}
- protected void signalNegotiationStart(Map<KexProposalOption, String> c2sOptions, Map<KexProposalOption, String> s2cOptions) {
+ protected void signalNegotiationStart(
+ Map<KexProposalOption, String> c2sOptions, Map<KexProposalOption, String> s2cOptions) {
try {
invokeSessionSignaller(l -> {
signalNegotiationStart(l, c2sOptions, s2cOptions);
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/compression/CompressionTest.java b/sshd-core/src/test/java/org/apache/sshd/common/compression/CompressionTest.java
index a05b05f..e0fbedb 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/compression/CompressionTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/compression/CompressionTest.java
@@ -28,7 +28,7 @@ import java.util.List;
import org.apache.sshd.common.channel.Channel;
import org.apache.sshd.common.kex.KexProposalOption;
-import org.apache.sshd.common.mac.MacTest;
+import org.apache.sshd.common.mac.MacCompatibilityTest;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.session.SessionListener;
import org.apache.sshd.server.SshServer;
@@ -97,8 +97,8 @@ public class CompressionTest extends BaseTestSupport {
public static void setupClientAndServer() throws Exception {
JSchLogger.init();
- sshd = CoreTestSupportUtils.setupTestServer(MacTest.class);
- sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(MacTest.class));
+ sshd = CoreTestSupportUtils.setupTestServer(MacCompatibilityTest.class);
+ sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(MacCompatibilityTest.class));
sshd.start();
port = sshd.getPort();
}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/mac/EncryptThenMacTest.java b/sshd-core/src/test/java/org/apache/sshd/common/mac/EncryptThenMacTest.java
new file mode 100644
index 0000000..794d308
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/mac/EncryptThenMacTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.common.mac;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.kex.KexProposalOption;
+import org.apache.sshd.server.SshServer;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.CommonTestSupportUtils;
+import org.apache.sshd.util.test.CoreTestSupportUtils;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+public class EncryptThenMacTest extends BaseTestSupport {
+ private static SshServer sshd;
+ private static int port;
+ private static SshClient client;
+
+ private final MacFactory factory;
+
+ public EncryptThenMacTest(MacFactory factory) {
+ this.factory = factory;
+ }
+
+ @BeforeClass
+ public static void setupClientAndServer() throws Exception {
+ sshd = CoreTestSupportUtils.setupTestServer(EncryptThenMacTest.class);
+ sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(EncryptThenMacTest.class));
+ sshd.start();
+ port = sshd.getPort();
+
+ client = CoreTestSupportUtils.setupTestClient(EncryptThenMacTest.class);
+ client.start();
+ }
+
+ @AfterClass
+ public static void tearDownClientAndServer() throws Exception {
+ if (sshd != null) {
+ try {
+ sshd.stop(true);
+ } finally {
+ sshd = null;
+ }
+ }
+
+ if (client != null) {
+ try {
+ client.stop();
+ } finally {
+ client = null;
+ }
+ }
+ }
+
+ @Parameters(name = "{0}")
+ public static Collection<Object[]> parameters() {
+ List<Object[]> ret = new ArrayList<>();
+ for (MacFactory f : BuiltinMacs.VALUES) {
+ if (!f.isSupported()) {
+ outputDebugMessage("Skip unsupported MAC %s", f);
+ continue;
+ }
+
+ // We want only encrypt-then-mac mode
+ if (!f.isEncryptThenMac()) {
+ outputDebugMessage("Skip Mac-Then-Encrypt %s", f);
+ continue;
+ }
+
+ ret.add(new Object[]{f});
+ }
+
+ return ret;
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ sshd.setMacFactories(Collections.singletonList(factory));
+ client.setMacFactories(Collections.singletonList(factory));
+ }
+
+ @Test
+ public void testClientConnection() throws Exception {
+ try (ClientSession session = client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+ .verify(7L, TimeUnit.SECONDS)
+ .getSession()) {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(11L, TimeUnit.SECONDS);
+
+ String expected = factory.getName();
+ for (KexProposalOption opt : KexProposalOption.MAC_PROPOSALS) {
+ String actual = session.getNegotiatedKexParameter(opt);
+ assertEquals("Mismatched " + opt + " negotiation", expected, actual);
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" + factory + "]";
+ }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/mac/MacTest.java b/sshd-core/src/test/java/org/apache/sshd/common/mac/MacCompatibilityTest.java
similarity index 91%
rename from sshd-core/src/test/java/org/apache/sshd/common/mac/MacTest.java
rename to sshd-core/src/test/java/org/apache/sshd/common/mac/MacCompatibilityTest.java
index 3ea363b..9a687f2 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/mac/MacTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/mac/MacCompatibilityTest.java
@@ -55,14 +55,14 @@ import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.ConnectionInfo;
/**
- * Test MAC algorithms.
+ * Test MAC algorithms with other known implementations.
*
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
-public class MacTest extends BaseTestSupport {
+public class MacCompatibilityTest extends BaseTestSupport {
private static final Collection<String> GANYMEDE_MACS =
Collections.unmodifiableSet(
GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, Connection.getAvailableMACs()));
@@ -73,7 +73,7 @@ public class MacTest extends BaseTestSupport {
private final MacFactory factory;
private final String jschMacClass;
- public MacTest(MacFactory factory, String jschMacClass) {
+ public MacCompatibilityTest(MacFactory factory, String jschMacClass) {
this.factory = factory;
this.jschMacClass = jschMacClass;
}
@@ -83,7 +83,13 @@ public class MacTest extends BaseTestSupport {
List<Object[]> ret = new ArrayList<>();
for (MacFactory f : BuiltinMacs.VALUES) {
if (!f.isSupported()) {
- System.out.println("Skip unsupported MAC " + f);
+ outputDebugMessage("Skip unsupported MAC %s", f);
+ continue;
+ }
+
+ // None of the implementations we use support encrypt-then-mac mode
+ if (f.isEncryptThenMac()) {
+ outputDebugMessage("Skip Encrypt-Then-Mac %s", f);
continue;
}
@@ -116,8 +122,8 @@ public class MacTest extends BaseTestSupport {
public static void setupClientAndServer() throws Exception {
JSchLogger.init();
- sshd = CoreTestSupportUtils.setupTestServer(MacTest.class);
- sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(MacTest.class));
+ sshd = CoreTestSupportUtils.setupTestServer(MacCompatibilityTest.class);
+ sshd.setKeyPairProvider(CommonTestSupportUtils.createTestHostKeyProvider(MacCompatibilityTest.class));
sshd.start();
port = sshd.getPort();
}
@@ -180,7 +186,8 @@ public class MacTest extends BaseTestSupport {
try {
conn.setClient2ServerMACs(new String[]{macName});
- ConnectionInfo info = conn.connect(null, (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(11L));
+ ConnectionInfo info = conn.connect(null,
+ (int) TimeUnit.SECONDS.toMillis(5L), (int) TimeUnit.SECONDS.toMillis(11L));
outputDebugMessage("Connected: kex=%s, key-type=%s, c2senc=%s, s2cenc=%s, c2mac=%s, s2cmac=%s",
info.keyExchangeAlgorithm, info.serverHostKeyAlgorithm,
info.clientToServerCryptoAlgorithm, info.serverToClientCryptoAlgorithm,
diff --git a/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java
index 4ae73b6..f3a5400 100644
--- a/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/server/auth/WelcomeBannerTest.java
@@ -90,8 +90,9 @@ public class WelcomeBannerTest extends BaseTestSupport {
@Test
public void testSimpleBanner() throws Exception {
- final String expectedWelcome = "Welcome to SSHD WelcomeBannerTest";
- PropertyResolverUtils.updateProperty(sshd, ServerAuthenticationManager.WELCOME_BANNER, expectedWelcome);
+ String expectedWelcome = "Welcome to SSHD WelcomeBannerTest";
+ PropertyResolverUtils.updateProperty(
+ sshd, ServerAuthenticationManager.WELCOME_BANNER, expectedWelcome);
testBanner(expectedWelcome);
}