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 88f81a47ec899657e38d0b90ac77526f0a68bfa7 Author: Lyor Goldstein <lgoldst...@apache.org> AuthorDate: Thu Oct 17 10:26:28 2019 +0300 [SSHD-949] Session should use cipher block size and not IV size to calculate padding --- CHANGES.md | 4 +- .../apache/sshd/common/cipher/AES192CTRTest.java | 2 +- .../apache/sshd/common/cipher/AES256CBCTest.java | 2 +- .../apache/sshd/common/cipher/ARCFOUR256Test.java | 2 +- .../apache/sshd/common/cipher/BaseCipherTest.java | 40 +++-- .../org/apache/sshd/common/io/PacketWriter.java | 25 +++ .../common/session/helpers/AbstractSession.java | 179 +++++++++++++-------- .../sshd/common/session/helpers/SessionHelper.java | 6 +- .../sshd/common/cipher/BuiltinCiphersTest.java | 3 +- .../org/apache/sshd/common/cipher/CipherTest.java | 24 ++- 10 files changed, 201 insertions(+), 86 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6ac150d..9193bbf 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,7 +35,7 @@ the standard does not specifically specify the behavior regarding symbolic links `createKeyExchange` method that accepts the session instance through which the request is made. * `Signature` methods accept a `SessionContext` argument representing the session context -of their invocation (if any) +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. @@ -93,3 +93,5 @@ for the server's identification before sending KEX-INIT message. * [SSHD-948](https://issues.apache.org/jira/browse/SSHD-948) - Do not accept password authentication if the session is not encrypted. +* [SSHD-949](https://issues.apache.org/jira/browse/SSHD-949) - Session should use cipher block size and not IV size to calculate padding. + diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES192CTRTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES192CTRTest.java index 6611702..bdd220a 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES192CTRTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES192CTRTest.java @@ -35,7 +35,7 @@ public class AES192CTRTest extends BaseCipherTest { @Test public void testEncryptDecrypt() throws Exception { // for AES 256 bits we need the JCE unlimited strength policy - ensureKeySizeSupported(16, 24, "AES", "AES/CTR/NoPadding"); + ensureFullCipherInformationSupported(BuiltinCiphers.aes192ctr); testEncryptDecrypt(BuiltinCiphers.aes192ctr); } } diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256CBCTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256CBCTest.java index dd39fd4..6503b23 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256CBCTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256CBCTest.java @@ -35,7 +35,7 @@ public class AES256CBCTest extends BaseCipherTest { @Test public void testEncryptDecrypt() throws Exception { // for AES 256 bits we need the JCE unlimited strength policy - ensureKeySizeSupported(16, 32, "AES", "AES/CBC/NoPadding"); + ensureFullCipherInformationSupported(BuiltinCiphers.aes256cbc); testEncryptDecrypt(BuiltinCiphers.aes256cbc); } } diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR256Test.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR256Test.java index 5511e0f..e9dddf8 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR256Test.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/ARCFOUR256Test.java @@ -35,7 +35,7 @@ public class ARCFOUR256Test extends BaseCipherTest { @Test public void testEncryptDecrypt() throws Exception { // for RC4 256 bits we need the JCE unlimited strength policy - ensureKeySizeSupported(32, "ARCFOUR", "RC4"); + ensureCipherInformationKeySizeSupported(BuiltinCiphers.arcfour256); testEncryptDecrypt(BuiltinCiphers.arcfour256); } } diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseCipherTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseCipherTest.java index 8dc68ea..b503761 100644 --- a/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseCipherTest.java +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseCipherTest.java @@ -43,9 +43,17 @@ public abstract class BaseCipherTest extends JUnitTestSupport { super(); } - protected void ensureKeySizeSupported(int bsize, String algorithm, String transformation) throws GeneralSecurityException { + protected void ensureCipherInformationKeySizeSupported(CipherInformation cipher) + throws GeneralSecurityException { + ensureKeySizeSupported( + cipher.getCipherBlockSize(), cipher.getAlgorithm(), cipher.getTransformation()); + } + + protected void ensureKeySizeSupported(int bsize, String algorithm, String transformation) + throws GeneralSecurityException { try { - javax.crypto.Cipher cipher = SecurityUtils.getCipher(transformation); + javax.crypto.Cipher cipher = + SecurityUtils.getCipher(transformation); byte[] key = new byte[bsize]; cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, new SecretKeySpec(key, algorithm)); } catch (GeneralSecurityException e) { @@ -57,12 +65,23 @@ public abstract class BaseCipherTest extends JUnitTestSupport { } } - protected void ensureKeySizeSupported(int ivsize, int bsize, String algorithm, String transformation) throws GeneralSecurityException { + protected void ensureFullCipherInformationSupported(CipherInformation cipher) + throws GeneralSecurityException { + ensureKeySizeSupported( + cipher.getIVSize(), cipher.getCipherBlockSize(), cipher.getAlgorithm(), cipher.getTransformation()); + } + + protected void ensureKeySizeSupported( + int ivsize, int bsize, String algorithm, String transformation) + throws GeneralSecurityException { try { - javax.crypto.Cipher cipher = SecurityUtils.getCipher(transformation); + javax.crypto.Cipher cipher = + SecurityUtils.getCipher(transformation); byte[] key = new byte[bsize]; byte[] iv = new byte[ivsize]; - cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, new SecretKeySpec(key, algorithm), new IvParameterSpec(iv)); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, + new SecretKeySpec(key, algorithm), + new IvParameterSpec(iv)); } catch (GeneralSecurityException e) { if (e instanceof InvalidKeyException) { Assume.assumeTrue(algorithm + "/" + transformation + "[" + bsize + "/" + ivsize + "]", false /* force exception */); @@ -81,15 +100,16 @@ public abstract class BaseCipherTest extends JUnitTestSupport { byte[] iv = new byte[ivSize]; enc.init(Mode.Encrypt, key, iv); - byte[] expected = facName.getBytes(StandardCharsets.UTF_8); - byte[] workBuf = expected.clone(); // need to clone since the cipher works in-line + String expected = getClass().getName() + "[" + facName + "]"; + byte[] expBytes = expected.getBytes(StandardCharsets.UTF_8); + byte[] workBuf = expBytes.clone(); // need to clone since the cipher works in-line enc.update(workBuf, 0, workBuf.length); Cipher dec = factory.create(); dec.init(Mode.Decrypt, key, iv); - byte[] actual = workBuf.clone(); - dec.update(actual, 0, actual.length); + byte[] actBytes = workBuf.clone(); // need to clone since the cipher works in-line + dec.update(actBytes, 0, actBytes.length); - assertArrayEquals(facName, expected, actual); + assertArrayEquals(facName, expBytes, actBytes); } } 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 59f66b0..1be1c88 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 @@ -46,11 +46,36 @@ public interface PacketWriter extends Channel { * @return The required padding length */ static int calculatePadLength(int len, int blockSize, boolean etmMode) { + /* + * Note: according to RFC-4253 section 6: + * + * The minimum size of a packet is 16 (or the cipher block size, + * whichever is larger) bytes (plus 'mac'). + * + * Since all out ciphers, MAC(s), etc. have a block size > 8 then + * the minimum size of the packet will be at least 16 due to the + * padding at the very least - so even packets that contain an opcode + * with no arguments will be above this value. This avoids an un-necessary + * call to Math.max(len, 16) for each and every packet + */ + len++; // the pad length if (!etmMode) { len += Integer.BYTES; } + /* + * Note: according to RFC-4253 section 6: + * + * Note that the length of the concatenation of 'packet_length', + * 'padding_length', 'payload', and 'random padding' MUST be a multiple + * of the cipher block size or 8, whichever is larger. + * + * However, we currently do not have ciphers with a block size of less than + * 8 so we do not take this into account in order to accelerate the calculation + * and avoiding an un-necessary call to Math.max(blockSize, 8) for each and every + * packet. + */ int pad = (-len) & (blockSize - 1); if (pad < blockSize) { pad += blockSize; 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 241162e..fcf2c7d 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 @@ -161,6 +161,8 @@ public abstract class AbstractSession extends SessionHelper { protected int inCipherSize = 8; protected Mac outMac; protected Mac inMac; + protected int outMacSize; + protected int inMacSize; protected byte[] inMacResult; protected Compression outCompression; protected Compression inCompression; @@ -197,16 +199,18 @@ public abstract class AbstractSession extends SessionHelper { protected long ignorePacketsFrequency = FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY; protected int ignorePacketsVariance = FactoryManager.DEFAULT_IGNORE_MESSAGE_VARIANCE; - protected final AtomicLong maxRekeyBlocks = new AtomicLong(FactoryManager.DEFAULT_REKEY_BYTES_LIMIT / 16); - protected final AtomicLong ignorePacketsCount = new AtomicLong(FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY); + protected final AtomicLong maxRekeyBlocks = + new AtomicLong(FactoryManager.DEFAULT_REKEY_BYTES_LIMIT / 16); + protected final AtomicLong ignorePacketsCount = + new AtomicLong(FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY); /** * Used to wait for global requests result synchronous wait */ private final AtomicReference<Object> requestResult = new AtomicReference<>(); - private byte[] clientKexData; // the payload of the client's SSH_MSG_KEXINIT - private byte[] serverKexData; // the payload of the factoryManager's SSH_MSG_KEXINIT + private byte[] clientKexData; // the payload of the client's SSH_MSG_KEXINIT + private byte[] serverKexData; // the payload of the server's SSH_MSG_KEXINIT /** * Create a new session. @@ -215,7 +219,8 @@ public abstract class AbstractSession extends SessionHelper { * @param factoryManager the factory manager * @param ioSession the underlying I/O session */ - protected AbstractSession(boolean serverSession, FactoryManager factoryManager, IoSession ioSession) { + protected AbstractSession( + boolean serverSession, FactoryManager factoryManager, IoSession ioSession) { super(serverSession, factoryManager, ioSession); this.decoderBuffer = new SessionWorkBuffer(this); @@ -223,15 +228,20 @@ public abstract class AbstractSession extends SessionHelper { attachSession(ioSession, this); Factory<Random> factory = - ValidateUtils.checkNotNull(factoryManager.getRandomFactory(), "No random factory for %s", ioSession); - random = ValidateUtils.checkNotNull(factory.create(), "No randomizer instance for %s", ioSession); + ValidateUtils.checkNotNull( + factoryManager.getRandomFactory(), "No random factory for %s", ioSession); + random = ValidateUtils.checkNotNull( + factory.create(), "No randomizer instance for %s", ioSession); refreshConfiguration(); ClassLoader loader = getClass().getClassLoader(); - sessionListenerProxy = EventListenerUtils.proxyWrapper(SessionListener.class, loader, sessionListeners); - channelListenerProxy = EventListenerUtils.proxyWrapper(ChannelListener.class, loader, channelListeners); - tunnelListenerProxy = EventListenerUtils.proxyWrapper(PortForwardingEventListener.class, loader, tunnelListeners); + sessionListenerProxy = EventListenerUtils.proxyWrapper( + SessionListener.class, loader, sessionListeners); + channelListenerProxy = EventListenerUtils.proxyWrapper( + ChannelListener.class, loader, channelListeners); + tunnelListenerProxy = EventListenerUtils.proxyWrapper( + PortForwardingEventListener.class, loader, tunnelListeners); try { signalSessionEstablished(ioSession); @@ -345,19 +355,24 @@ public abstract class AbstractSession extends SessionHelper { protected void refreshConfiguration() { synchronized (random) { // re-keying configuration - maxRekeyBytes = this.getLongProperty(FactoryManager.REKEY_BYTES_LIMIT, maxRekeyBytes); - maxRekeyInterval = this.getLongProperty(FactoryManager.REKEY_TIME_LIMIT, maxRekeyInterval); - maxRekyPackets = this.getLongProperty(FactoryManager.REKEY_PACKETS_LIMIT, maxRekyPackets); + maxRekeyBytes = getLongProperty(FactoryManager.REKEY_BYTES_LIMIT, maxRekeyBytes); + maxRekeyInterval = getLongProperty(FactoryManager.REKEY_TIME_LIMIT, maxRekeyInterval); + maxRekyPackets = getLongProperty(FactoryManager.REKEY_PACKETS_LIMIT, maxRekyPackets); // intermittent SSH_MSG_IGNORE stream padding - ignorePacketDataLength = this.getIntProperty(FactoryManager.IGNORE_MESSAGE_SIZE, FactoryManager.DEFAULT_IGNORE_MESSAGE_SIZE); - ignorePacketsFrequency = this.getLongProperty(FactoryManager.IGNORE_MESSAGE_FREQUENCY, FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY); - ignorePacketsVariance = this.getIntProperty(FactoryManager.IGNORE_MESSAGE_VARIANCE, FactoryManager.DEFAULT_IGNORE_MESSAGE_VARIANCE); + ignorePacketDataLength = getIntProperty( + FactoryManager.IGNORE_MESSAGE_SIZE, FactoryManager.DEFAULT_IGNORE_MESSAGE_SIZE); + ignorePacketsFrequency = getLongProperty( + FactoryManager.IGNORE_MESSAGE_FREQUENCY, FactoryManager.DEFAULT_IGNORE_MESSAGE_FREQUENCY); + ignorePacketsVariance = getIntProperty( + FactoryManager.IGNORE_MESSAGE_VARIANCE, FactoryManager.DEFAULT_IGNORE_MESSAGE_VARIANCE); if (ignorePacketsVariance >= ignorePacketsFrequency) { ignorePacketsVariance = 0; } - ignorePacketsCount.set(calculateNextIgnorePacketCount(random, ignorePacketsFrequency, ignorePacketsVariance)); + long countValue = calculateNextIgnorePacketCount( + random, ignorePacketsFrequency, ignorePacketsVariance); + ignorePacketsCount.set(countValue); } } @@ -399,7 +414,8 @@ public abstract class AbstractSession extends SessionHelper { protected void doHandleMessage(Buffer buffer) throws Exception { int cmd = buffer.getUByte(); if (log.isTraceEnabled()) { - log.trace("doHandleMessage({}) process {}", this, SshConstants.getCommandMessageName(cmd)); + log.trace("doHandleMessage({}) process {}", + this, SshConstants.getCommandMessageName(cmd)); } switch (cmd) { @@ -541,7 +557,8 @@ public abstract class AbstractSession extends SessionHelper { * + As the next packet following the server's first SSH_MSG_NEWKEYS. */ KexExtensionHandler extHandler = getKexExtensionHandler(); - if ((extHandler == null) || (!extHandler.isKexExtensionsAvailable(this, AvailabilityPhase.NEWKEYS))) { + if ((extHandler == null) + || (!extHandler.isKexExtensionsAvailable(this, AvailabilityPhase.NEWKEYS))) { return future; } @@ -555,14 +572,16 @@ public abstract class AbstractSession extends SessionHelper { boolean debugEnabled = log.isDebugEnabled(); if (kex.next(cmd, buffer)) { if (debugEnabled) { - log.debug("handleKexMessage({})[{}] KEX processing complete after cmd={}", this, kex.getName(), cmd); + log.debug("handleKexMessage({})[{}] KEX processing complete after cmd={}", + this, kex.getName(), cmd); } checkKeys(); sendNewKeys(); kexState.set(KexState.KEYS); } else { if (debugEnabled) { - log.debug("handleKexMessage({})[{}] more KEX packets expected after cmd={}", this, kex.getName(), cmd); + log.debug("handleKexMessage({})[{}] more KEX packets expected after cmd={}", + this, kex.getName(), cmd); } } } @@ -620,7 +639,8 @@ public abstract class AbstractSession extends SessionHelper { log.debug("handleServiceRequest({}) Accepted service {}", this, serviceName); } - Buffer response = createBuffer(SshConstants.SSH_MSG_SERVICE_ACCEPT, Byte.SIZE + GenericUtils.length(serviceName)); + Buffer response = createBuffer( + SshConstants.SSH_MSG_SERVICE_ACCEPT, Byte.SIZE + GenericUtils.length(serviceName)); response.putString(serviceName); writePacket(response); return true; @@ -747,8 +767,9 @@ public abstract class AbstractSession extends SessionHelper { protected void validateKexState(int cmd, KexState expected) { KexState actual = kexState.get(); if (!expected.equals(actual)) { - throw new IllegalStateException("Received KEX command=" + SshConstants.getCommandMessageName(cmd) - + " while in state=" + actual + " instead of " + expected); + throw new IllegalStateException( + "Received KEX command=" + SshConstants.getCommandMessageName(cmd) + + " while in state=" + actual + " instead of " + expected); } } @@ -803,7 +824,8 @@ public abstract class AbstractSession extends SessionHelper { @Override public <T extends Service> T getService(Class<T> clazz) { Collection<? extends Service> registeredServices = getServices(); - ValidateUtils.checkState(GenericUtils.isNotEmpty(registeredServices), "No registered services to look for %s", clazz.getSimpleName()); + ValidateUtils.checkState(GenericUtils.isNotEmpty(registeredServices), + "No registered services to look for %s", clazz.getSimpleName()); for (Service s : registeredServices) { if (clazz.isInstance(s)) { @@ -822,9 +844,10 @@ public abstract class AbstractSession extends SessionHelper { int cmd = bufData[buffer.rpos()] & 0xFF; if (cmd > SshConstants.SSH_MSG_KEX_LAST) { String cmdName = SshConstants.getCommandMessageName(cmd); + boolean debugEnabled = log.isDebugEnabled(); synchronized (pendingPackets) { if (!KexState.DONE.equals(kexState.get())) { - if (pendingPackets.isEmpty()) { + if (pendingPackets.isEmpty() && debugEnabled) { log.debug("writePacket({})[{}] Start flagging packets as pending until key exchange is done", this, cmdName); } PendingWriteFuture future = new PendingWriteFuture(cmdName, buffer); @@ -898,7 +921,9 @@ public abstract class AbstractSession extends SessionHelper { } protected int resolveIgnoreBufferDataLength() { - if ((ignorePacketDataLength <= 0) || (ignorePacketsFrequency <= 0L) || (ignorePacketsVariance < 0)) { + if ((ignorePacketDataLength <= 0) + || (ignorePacketsFrequency <= 0L) + || (ignorePacketsVariance < 0)) { return 0; } @@ -908,7 +933,8 @@ public abstract class AbstractSession extends SessionHelper { } synchronized (random) { - count = calculateNextIgnorePacketCount(random, ignorePacketsFrequency, ignorePacketsVariance); + count = calculateNextIgnorePacketCount( + random, ignorePacketsFrequency, ignorePacketsVariance); ignorePacketsCount.set(count); return ignorePacketDataLength + random.random(ignorePacketDataLength); } @@ -990,16 +1016,11 @@ public abstract class AbstractSession extends SessionHelper { // Since the caller claims to know how many bytes they will need // increase their request to account for our headers/footers if // they actually send exactly this amount. - // - int bsize = outCipherSize; - len += SshConstants.SSH_PACKET_HEADER_LEN; - int pad = (-len) & (bsize - 1); - if (pad < bsize) { - pad += bsize; - } - len = len + pad - 4; + boolean etmMode = (outMac == null) ? false : outMac.isEncryptThenMac(); + int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode); + len = SshConstants.SSH_PACKET_HEADER_LEN + len + pad + Byte.BYTES /* the pad length byte */; if (outMac != null) { - len += outMac.getBlockSize(); + len += outMacSize; } return prepareBuffer(cmd, new ByteArrayBuffer(new byte[len + Byte.SIZE], false)); @@ -1026,8 +1047,10 @@ public abstract class AbstractSession extends SessionHelper { */ protected <B extends Buffer> B validateTargetBuffer(int cmd, B buffer) { ValidateUtils.checkNotNull(buffer, "No target buffer to examine for command=%d", cmd); - ValidateUtils.checkTrue(buffer != decoderBuffer, "Not allowed to use the internal decoder buffer for command=%d", cmd); - ValidateUtils.checkTrue(buffer != uncompressBuffer, "Not allowed to use the internal uncompress buffer for command=%d", cmd); + ValidateUtils.checkTrue( + buffer != decoderBuffer, "Not allowed to use the internal decoder buffer for command=%d", cmd); + ValidateUtils.checkTrue( + buffer != uncompressBuffer, "Not allowed to use the internal uncompress buffer for command=%d", cmd); return buffer; } @@ -1068,7 +1091,8 @@ public abstract class AbstractSession extends SessionHelper { // Debug log the packet boolean traceEnabled = log.isTraceEnabled(); if (traceEnabled) { - buffer.dumpHex(getSimplifiedLogger(), Level.FINEST, "encode(" + this + ") packet #" + seqo, this); + buffer.dumpHex(getSimplifiedLogger(), Level.FINEST, + "encode(" + this + ") packet #" + seqo, this); } // Compress the packet if needed @@ -1116,9 +1140,11 @@ public abstract class AbstractSession extends SessionHelper { // Increment packet id seqo = (seqo + 1L) & 0x0ffffffffL; - // Update stats + + // Update counters used to track re-keying outPacketsCount.incrementAndGet(); outBytesCount.addAndGet(len); + // Make buffer ready to be read buffer.rpos(off); return buffer; @@ -1134,10 +1160,9 @@ public abstract class AbstractSession extends SessionHelper { return; } - int macSize = outMac.getBlockSize(); int l = buf.wpos(); // ensure enough room for MAC in outgoing buffer - buf.wpos(l + macSize); + buf.wpos(l + outMacSize); // Include sequence number outMac.updateUInt(seqo); // Include the length field in the MAC calculation @@ -1152,7 +1177,7 @@ public abstract class AbstractSession extends SessionHelper { } outCipher.update(buf.array(), offset, len); - int blocksCount = len / outCipher.getCipherBlockSize(); + int blocksCount = len / outCipherSize; outBlocksCount.addAndGet(Math.max(1, blocksCount)); } @@ -1169,10 +1194,19 @@ public abstract class AbstractSession extends SessionHelper { if (decoderState == 0) { // The read position should always be 0 at this point because we have compacted this buffer assert decoderBuffer.rpos() == 0; + /* + * Note: according to RFC-4253 section 6: + * + * Implementations SHOULD decrypt the length after receiving + * the first 8 (or cipher block size whichever is larger) bytes + * + * However, we currently do not have ciphers with a block size of less than + * 8 we avoid un-necessary Math.max(minBufLen, 8) for each and every packet + */ int minBufLen = etmMode ? Integer.BYTES : inCipherSize; // If we have received enough bytes, start processing those if (decoderBuffer.available() > minBufLen) { - // Decrypt the first bytes + // Decrypt the first bytes so we can extract the packet length if ((inCipher != null) && (!etmMode)) { inCipher.update(decoderBuffer.array(), 0, inCipherSize); @@ -1188,7 +1222,8 @@ 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(), Level.FINEST, "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); } @@ -1202,7 +1237,7 @@ public abstract class AbstractSession extends SessionHelper { } else if (decoderState == 1) { // 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; + int macSize = (inMac != null) ? inMacSize : 0; // Check if the packet has been fully received if (decoderBuffer.available() >= (decoderLength + macSize)) { byte[] data = decoderBuffer.array(); @@ -1210,18 +1245,21 @@ public abstract class AbstractSession extends SessionHelper { validateIncomingMac(data, 0, decoderLength + Integer.BYTES); if (inCipher != null) { - inCipher.update(data, Integer.BYTES, decoderLength); + inCipher.update(data, Integer.BYTES /* packet length is unencrypted */, decoderLength); - int blocksCount = decoderLength / inCipher.getCipherBlockSize(); + int blocksCount = decoderLength / inCipherSize; inBlocksCount.addAndGet(Math.max(1, blocksCount)); } } else { - // Decrypt the remaining of the packet + /* + * Decrypt the remaining of the packet - skip the block we + * already decoded in order to extract the packet length + */ if (inCipher != null) { int updateLen = decoderLength + Integer.BYTES - inCipherSize; inCipher.update(data, inCipherSize, updateLen); - int blocksCount = updateLen / inCipher.getCipherBlockSize(); + int blocksCount = updateLen / inCipherSize; inBlocksCount.addAndGet(Math.max(1, blocksCount)); } @@ -1230,6 +1268,7 @@ public abstract class AbstractSession extends SessionHelper { // Increment incoming packet sequence number seqi = (seqi + 1L) & 0x0ffffffffL; + // Get padding int pad = decoderBuffer.getUByte(); Buffer packet; @@ -1253,14 +1292,17 @@ public abstract class AbstractSession extends SessionHelper { } if (log.isTraceEnabled()) { - packet.dumpHex(getSimplifiedLogger(), Level.FINEST, "decode(" + this + ") packet #" + seqi, this); + packet.dumpHex(getSimplifiedLogger(), Level.FINEST, + "decode(" + this + ") packet #" + seqi, this); } - // Update stats + // Update counters used to track re-keying inPacketsCount.incrementAndGet(); inBytesCount.addAndGet(packet.available()); + // Process decoded packet handleMessage(packet); + // Set ready to handle next packet decoderBuffer.rpos(decoderLength + Integer.BYTES + macSize); decoderBuffer.wpos(wpos); @@ -1287,7 +1329,7 @@ public abstract class AbstractSession extends SessionHelper { 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())) { + if (!BufferUtils.equals(inMacResult, 0, data, offset + len, inMacSize)) { throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "MAC Error"); } } @@ -1475,7 +1517,8 @@ public abstract class AbstractSession extends SessionHelper { boolean serverSession = isServerSession(); String value = getNegotiatedKexParameter(KexProposalOption.S2CENC); - Cipher s2ccipher = ValidateUtils.checkNotNull(NamedFactory.create(getCipherFactories(), value), "Unknown s2c cipher: %s", value); + Cipher s2ccipher = ValidateUtils.checkNotNull( + NamedFactory.create(getCipherFactories(), value), "Unknown s2c cipher: %s", value); e_s2c = resizeKey(e_s2c, s2ccipher.getKdfSize(), hash, k, h); s2ccipher.init(serverSession ? Cipher.Mode.Encrypt : Cipher.Mode.Decrypt, e_s2c, iv_s2c); @@ -1494,7 +1537,8 @@ public abstract class AbstractSession extends SessionHelper { } value = getNegotiatedKexParameter(KexProposalOption.C2SENC); - Cipher c2scipher = ValidateUtils.checkNotNull(NamedFactory.create(getCipherFactories(), value), "Unknown c2s cipher: %s", value); + Cipher c2scipher = ValidateUtils.checkNotNull( + NamedFactory.create(getCipherFactories(), value), "Unknown c2s cipher: %s", value); e_c2s = resizeKey(e_c2s, c2scipher.getKdfSize(), hash, k, h); c2scipher.init(serverSession ? Cipher.Mode.Decrypt : Cipher.Mode.Encrypt, e_c2s, iv_c2s); @@ -1527,22 +1571,26 @@ public abstract class AbstractSession extends SessionHelper { inMac = s2cmac; inCompression = s2ccomp; } - outCipherSize = outCipher.getIVSize(); + + outCipherSize = outCipher.getCipherBlockSize(); + outMacSize = outMac.getBlockSize(); // TODO add support for configurable compression level outCompression.init(Compression.Type.Deflater, -1); - inCipherSize = inCipher.getIVSize(); - inMacResult = new byte[inMac.getBlockSize()]; + inCipherSize = inCipher.getCipherBlockSize(); + inMacSize = inMac.getBlockSize(); + inMacResult = new byte[inMacSize]; // TODO add support for configurable compression level inCompression.init(Compression.Type.Inflater, -1); // see https://tools.ietf.org/html/rfc4344#section-3.2 - int inBlockSize = inCipher.getCipherBlockSize(); - int outBlockSize = outCipher.getCipherBlockSize(); // select the lowest cipher size - int avgCipherBlockSize = Math.min(inBlockSize, outBlockSize); - long recommendedByteRekeyBlocks = 1L << Math.min((avgCipherBlockSize * Byte.SIZE) / 4, 63); // in case (block-size / 4) > 63 - maxRekeyBlocks.set(this.getLongProperty(FactoryManager.REKEY_BLOCKS_LIMIT, recommendedByteRekeyBlocks)); + int avgCipherBlockSize = Math.min(inCipherSize, outCipherSize); + long recommendedByteRekeyBlocks = + 1L << Math.min((avgCipherBlockSize * Byte.SIZE) / 4, 63); // in case (block-size / 4) > 63 + long effectiveRekyBlocksCount = + getLongProperty(FactoryManager.REKEY_BLOCKS_LIMIT, recommendedByteRekeyBlocks); + maxRekeyBlocks.set(effectiveRekyBlocksCount); if (debugEnabled) { log.debug("receiveNewKeys({}) inCipher={}, outCipher={}, recommended blocks limit={}, actual={}", this, inCipher, outCipher, recommendedByteRekeyBlocks, maxRekeyBlocks); @@ -1984,7 +2032,8 @@ public abstract class AbstractSession extends SessionHelper { return false; // disabled } - boolean rekey = (inPacketsCount.get() > maxRekyPackets) || (outPacketsCount.get() > maxRekyPackets); + boolean rekey = (inPacketsCount.get() > maxRekyPackets) + || (outPacketsCount.get() > maxRekyPackets); if (rekey) { if (log.isDebugEnabled()) { log.debug("isRekeyPacketCountsExceeded({}) re-keying: in={}, out={}, max={}", 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 3a24647..d18d803 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 @@ -687,7 +687,9 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements * @return the resized key * @throws Exception if a problem occur while resizing the key */ - protected byte[] resizeKey(byte[] e, int kdfSize, Digest hash, byte[] k, byte[] h) throws Exception { + protected byte[] resizeKey( + byte[] e, int kdfSize, Digest hash, byte[] k, byte[] h) + throws Exception { for (Buffer buffer = null; kdfSize > e.length; buffer = BufferUtils.clear(buffer)) { if (buffer == null) { buffer = new ByteArrayBuffer(); @@ -697,12 +699,14 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements buffer.putRawBytes(h); buffer.putRawBytes(e); hash.update(buffer.array(), 0, buffer.available()); + byte[] foo = hash.digest(); byte[] bar = new byte[e.length + foo.length]; System.arraycopy(e, 0, bar, 0, e.length); System.arraycopy(foo, 0, bar, e.length, foo.length); e = bar; } + return e; } diff --git a/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java b/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java index 394fff5..efb7f60 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java @@ -201,7 +201,8 @@ public class BuiltinCiphersTest extends BaseTestSupport { @Test public void testParseCiphersList() { List<String> builtin = NamedResource.getNameList(BuiltinCiphers.VALUES); - List<String> unknown = Arrays.asList(getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName()); + List<String> unknown = Arrays.asList( + getClass().getPackage().getName(), getClass().getSimpleName(), getCurrentTestName()); Random rnd = new Random(); for (int index = 0; index < (builtin.size() + unknown.size()); index++) { Collections.shuffle(builtin, rnd); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/cipher/CipherTest.java b/sshd-core/src/test/java/org/apache/sshd/common/cipher/CipherTest.java index 949e3df..e9c3d09 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/cipher/CipherTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/cipher/CipherTest.java @@ -71,10 +71,13 @@ public class CipherTest extends BaseTestSupport { Collections.unmodifiableList( Arrays.asList( new Object[]{BuiltinCiphers.aes128cbc, com.jcraft.jsch.jce.AES128CBC.class, NUM_LOADTEST_ROUNDS}, + new Object[]{BuiltinCiphers.aes128ctr, com.jcraft.jsch.jce.AES128CTR.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.tripledescbc, com.jcraft.jsch.jce.TripleDESCBC.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.blowfishcbc, com.jcraft.jsch.jce.BlowfishCBC.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.aes192cbc, com.jcraft.jsch.jce.AES192CBC.class, NUM_LOADTEST_ROUNDS}, + new Object[]{BuiltinCiphers.aes192ctr, com.jcraft.jsch.jce.AES192CTR.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.aes256cbc, com.jcraft.jsch.jce.AES256CBC.class, NUM_LOADTEST_ROUNDS}, + new Object[]{BuiltinCiphers.aes256ctr, com.jcraft.jsch.jce.AES256CTR.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.arcfour128, com.jcraft.jsch.jce.ARCFOUR128.class, NUM_LOADTEST_ROUNDS}, new Object[]{BuiltinCiphers.arcfour256, com.jcraft.jsch.jce.ARCFOUR256.class, NUM_LOADTEST_ROUNDS} )); @@ -94,7 +97,10 @@ public class CipherTest extends BaseTestSupport { private final Class<? extends com.jcraft.jsch.Cipher> jschCipher; private final int loadTestRounds; - public CipherTest(BuiltinCiphers builtInCipher, Class<? extends com.jcraft.jsch.Cipher> jschCipher, int loadTestRounds) { + public CipherTest( + BuiltinCiphers builtInCipher, + Class<? extends com.jcraft.jsch.Cipher> jschCipher, + int loadTestRounds) { this.builtInCipher = builtInCipher; this.jschCipher = jschCipher; this.loadTestRounds = loadTestRounds; @@ -126,7 +132,8 @@ public class CipherTest extends BaseTestSupport { @Test public void testBuiltinCipherSession() throws Exception { - Assume.assumeTrue("No internal support for " + builtInCipher.getName(), builtInCipher.isSupported() && checkCipher(jschCipher.getName())); + Assume.assumeTrue("No internal support for " + builtInCipher.getName(), + builtInCipher.isSupported() && checkCipher(jschCipher.getName())); sshd.setCipherFactories(Collections.singletonList(builtInCipher)); runJschTest(port); } @@ -170,7 +177,9 @@ public class CipherTest extends BaseTestSupport { loadTest(builtInCipher, random, loadTestRounds); } - private static void loadTest(NamedFactory<Cipher> factory, Random random, int numRounds) throws Exception { + private static void loadTest( + NamedFactory<Cipher> factory, Random random, int numRounds) + throws Exception { Cipher cipher = factory.create(); byte[] key = new byte[cipher.getKdfSize()]; byte[] iv = new byte[cipher.getIVSize()]; @@ -185,7 +194,10 @@ public class CipherTest extends BaseTestSupport { cipher.update(input, 0, input.length); } long t1 = System.currentTimeMillis(); - System.err.println(factory.getName() + "[" + numRounds + "]: " + (t1 - t0) + " ms"); + System.err.append(CipherTest.class.getSimpleName()) + .append(" - ").append(factory.getName()) + .append('[').append(Integer.toString(numRounds)).append(']') + .append(": ").append(Long.toString(t1 - t0)).println(" ms"); } static boolean checkCipher(String cipher) { @@ -197,7 +209,9 @@ public class CipherTest extends BaseTestSupport { new byte[jschCipher.getIVSize()]); return true; } catch (Exception e) { - System.err.println("checkCipher(" + cipher + ") " + e.getClass().getSimpleName() + ": " + e.getMessage()); + System.err.println("checkCipher(" + cipher + ")" + + " " + e.getClass().getSimpleName() + + ": " + e.getMessage()); return false; } }