From 143069e3e0a14cf5f589ad4b6e3ca5ee7a625bc4 Mon Sep 17 00:00:00 2001 From: Raymond Lai Date: Wed, 9 Sep 2020 15:51:17 +0800 Subject: [PATCH] Implement AES-GCM cipher support (#630) * Implement AES-GCM cipher support Fixes #217. A port of AES-GCM cipher support from Apache MINA-SSHD, based on https://github.com/apache/mina-sshd/pull/132. Included tests for decoding SSH packets sent from Apache MINA-SSHD and OpenSSH (Version 7.9p1 as used by Debian 10). Manual tests also done on OpenSSH server 7.9p1 running Debian 10 with its available ciphers, including 3des-cbc, aes128-cbc, aes192-cbc, aes256-cbc, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com and aes256-gcm@openssh.com. * Changes per PR feedback - Fixed variable/statement whitespaces and add back missing braces per coding standard requirement - Moved Buffer.putLong() and Buffer.getLong() into GcmCipher.CounterGCMParameterSpec since it's the only user - Moved BaseCipher.authSize into GcmCipher since it is the only cipher that would return a non-zero. BaseCipher will keep return 0 instead - Made BaseCipher.cipher protected instead of making it publicly accessible - Combined the three decoding modes in Decoder.decode() into one single method, to reduce code duplication - Added integration test for the ciphers, along with the newly implemented AES-GCM ciphers --- .../docker-image/test-container/sshd_config | 4 +- .../sshj/transport/cipher/CipherSpec.groovy | 54 ++++++ .../sshj/transport/cipher/GcmCipher.java | 164 ++++++++++++++++++ .../sshj/transport/cipher/GcmCiphers.java | 74 ++++++++ .../java/net/schmizz/sshj/DefaultConfig.java | 3 + .../net/schmizz/sshj/transport/Converter.java | 7 +- .../net/schmizz/sshj/transport/Decoder.java | 112 +++++------- .../net/schmizz/sshj/transport/Encoder.java | 27 ++- .../schmizz/sshj/transport/KeyExchanger.java | 25 ++- .../sshj/transport/cipher/BaseCipher.java | 23 ++- .../schmizz/sshj/transport/cipher/Cipher.java | 31 ++++ .../sshj/transport/cipher/NoneCipher.java | 19 ++ .../GcmCipherDecryptSshPacketTest.java | 69 ++++++++ .../sshj/transport/GcmCipherTest.java | 73 ++++++++ .../DecoderDecryptGcmCipherSshPacketTest.java | 116 +++++++++++++ .../gcm/mina-sshd/client.decrypted.1.bin | Bin 0 -> 1024 bytes .../gcm/mina-sshd/client.decrypted.2.bin | Bin 0 -> 1024 bytes .../gcm/mina-sshd/client.decrypted.3.bin | Bin 0 -> 1024 bytes .../gcm/mina-sshd/client.receive.1.bin | Bin 0 -> 1024 bytes .../gcm/mina-sshd/client.receive.2.bin | Bin 0 -> 1024 bytes .../gcm/mina-sshd/client.receive.3.bin | Bin 0 -> 1024 bytes .../ssh-packets/gcm/mina-sshd/s2c.iv.bin | 1 + .../ssh-packets/gcm/mina-sshd/s2c.key.bin | 1 + .../gcm/openssh/client.decrypted.1.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.decrypted.2.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.decrypted.3.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.decrypted.4.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.receive.1.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.receive.2.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.receive.3.bin | Bin 0 -> 1024 bytes .../gcm/openssh/client.receive.4.bin | Bin 0 -> 1024 bytes .../ssh-packets/gcm/openssh/s2c.iv.bin | 1 + .../ssh-packets/gcm/openssh/s2c.key.bin | Bin 0 -> 64 bytes 33 files changed, 722 insertions(+), 82 deletions(-) create mode 100644 src/itest/groovy/com/hierynomus/sshj/transport/cipher/CipherSpec.groovy create mode 100644 src/main/java/com/hierynomus/sshj/transport/cipher/GcmCipher.java create mode 100644 src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java create mode 100644 src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java create mode 100644 src/test/java/com/hierynomus/sshj/transport/GcmCipherTest.java create mode 100644 src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.1.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.2.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.3.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.1.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.2.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.3.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin create mode 100644 src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.decrypted.1.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.decrypted.2.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.decrypted.3.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.decrypted.4.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.receive.1.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.receive.2.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.receive.3.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/client.receive.4.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin create mode 100644 src/test/resources/ssh-packets/gcm/openssh/s2c.key.bin diff --git a/src/itest/docker-image/test-container/sshd_config b/src/itest/docker-image/test-container/sshd_config index 96b90dde..48a51331 100644 --- a/src/itest/docker-image/test-container/sshd_config +++ b/src/itest/docker-image/test-container/sshd_config @@ -131,4 +131,6 @@ Subsystem sftp /usr/lib/ssh/sftp-server KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1 macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com -TrustedUserCAKeys /etc/ssh/users_rsa_ca.pub \ No newline at end of file +TrustedUserCAKeys /etc/ssh/users_rsa_ca.pub + +Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com \ No newline at end of file diff --git a/src/itest/groovy/com/hierynomus/sshj/transport/cipher/CipherSpec.groovy b/src/itest/groovy/com/hierynomus/sshj/transport/cipher/CipherSpec.groovy new file mode 100644 index 00000000..76461aee --- /dev/null +++ b/src/itest/groovy/com/hierynomus/sshj/transport/cipher/CipherSpec.groovy @@ -0,0 +1,54 @@ +/* + * 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.transport.cipher + +import com.hierynomus.sshj.IntegrationBaseSpec +import net.schmizz.sshj.DefaultConfig +import spock.lang.Unroll + +class CipherSpec extends IntegrationBaseSpec { + + @Unroll + def "should correctly connect with #cipher Cipher"() { + given: + def cfg = new DefaultConfig() + cfg.setCipherFactories(cipherFactory) + def client = getConnectedClient(cfg) + + when: + client.authPublickey(USERNAME, KEYFILE) + + then: + client.authenticated + + cleanup: + client.disconnect() + + where: + cipherFactory << [BlockCiphers.TripleDESCBC(), + BlockCiphers.BlowfishCBC(), + BlockCiphers.AES128CBC(), + BlockCiphers.AES128CTR(), + BlockCiphers.AES192CBC(), + BlockCiphers.AES192CTR(), + BlockCiphers.AES256CBC(), + BlockCiphers.AES256CTR(), + GcmCiphers.AES128GCM(), + GcmCiphers.AES256GCM()] + cipher = cipherFactory.name + } + +} diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCipher.java b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCipher.java new file mode 100644 index 00000000..e6d41227 --- /dev/null +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCipher.java @@ -0,0 +1,164 @@ +/* + * 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.transport.cipher; + +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.transport.cipher.BaseCipher; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; + +public class GcmCipher extends BaseCipher { + + protected int authSize; + protected Mode mode; + protected boolean initialized; + protected CounterGCMParameterSpec parameters; + protected SecretKey secretKey; + + public GcmCipher(int ivsize, int authSize, int bsize, String algorithm, String transformation) { + super(ivsize, bsize, algorithm, transformation); + this.authSize = authSize; + } + + @Override + public int getAuthenticationTagSize() { + return authSize; + } + + protected Cipher getInitializedCipherInstance() throws GeneralSecurityException { + if (!initialized) { + cipher.init(mode == Mode.Encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, parameters); + initialized = true; + } + return cipher; + } + + @Override + protected void initCipher(Cipher cipher, Mode mode, byte[] key, byte[] iv) throws InvalidKeyException, InvalidAlgorithmParameterException { + this.mode = mode; + this.secretKey = getKeySpec(key); + this.parameters = new CounterGCMParameterSpec(getAuthenticationTagSize() * Byte.SIZE, iv); + cipher.init(getMode(mode), secretKey, parameters); + initialized = true; + } + + @Override + public void updateAAD(byte[] data, int offset, int length) { + try { + Cipher cipher = getInitializedCipherInstance(); + cipher.updateAAD(data, offset, length); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException("Error updating data through cipher", e); + } + } + + @Override + public void update(byte[] input, int inputOffset, int inputLen) { + if (mode == Mode.Decrypt) { + inputLen += getAuthenticationTagSize(); + } + try { + Cipher cipher = getInitializedCipherInstance(); + cipher.doFinal(input, inputOffset, inputLen, input, inputOffset); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException("Error updating data through cipher", e); + } + /* + * As the RFC stated, the IV used in AES-GCM cipher for SSH is a 4-byte constant plus a 8-byte invocation + * counter. After cipher.doFinal() is called, IV invocation counter is increased by 1, and changes to the IV + * requires reinitializing the cipher, so initialized have to be false here, for the next invocation. + * + * Refer to RFC5647, Section 7.1 + */ + parameters.incrementCounter(); + initialized = false; + } + + /** + * Algorithm parameters for AES/GCM that assumes the IV uses an 8-byte counter field as its most significant bytes. + */ + protected static class CounterGCMParameterSpec extends GCMParameterSpec { + protected final byte[] iv; + + protected CounterGCMParameterSpec(int tLen, byte[] src) { + super(tLen, src); + if (src.length != 12) { + throw new IllegalArgumentException("GCM nonce must be 12 bytes, but given len=" + src.length); + } + iv = src.clone(); + } + + protected void incrementCounter() { + int off = iv.length - 8; + long counter = getLong(iv, off, 8); + putLong(addExact(counter, 1L), iv, off, 8); + } + + @Override + public byte[] getIV() { + // JCE implementation of GCM will complain if the reference doesn't change between inits + return iv.clone(); + } + + static long addExact(long var0, long var2) { + long var4 = var0 + var2; + if (((var0 ^ var4) & (var2 ^ var4)) < 0L) { + throw new ArithmeticException("long overflow"); + } else { + return var4; + } + } + + static long getLong(byte[] buf, int off, int len) { + if (len < 8) { + throw new IllegalArgumentException("Not enough data for a long: required=8, available=" + len); + } + + long l = (long) buf[off] << 56; + l |= ((long) buf[off + 1] & 0xff) << 48; + l |= ((long) buf[off + 2] & 0xff) << 40; + l |= ((long) buf[off + 3] & 0xff) << 32; + l |= ((long) buf[off + 4] & 0xff) << 24; + l |= ((long) buf[off + 5] & 0xff) << 16; + l |= ((long) buf[off + 6] & 0xff) << 8; + l |= (long) buf[off + 7] & 0xff; + + return l; + } + + static int putLong(long value, byte[] buf, int off, int len) { + if (len < 8) { + throw new IllegalArgumentException("Not enough data for a long: required=8, available=" + len); + } + + buf[off] = (byte) (value >> 56); + buf[off + 1] = (byte) (value >> 48); + buf[off + 2] = (byte) (value >> 40); + buf[off + 3] = (byte) (value >> 32); + buf[off + 4] = (byte) (value >> 24); + buf[off + 5] = (byte) (value >> 16); + buf[off + 6] = (byte) (value >> 8); + buf[off + 7] = (byte) value; + + return 8; + } + } +} diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java new file mode 100644 index 00000000..2a288516 --- /dev/null +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java @@ -0,0 +1,74 @@ +/* + * 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.transport.cipher; + +import net.schmizz.sshj.transport.cipher.Cipher; + +public class GcmCiphers { + + public static final String GALOIS_COUNTER_MODE = "GCM"; + + public static Factory AES128GCM() { + return new Factory(12, 16, 128, "aes128-gcm@openssh.com", "AES", GALOIS_COUNTER_MODE); + } + + public static Factory AES256GCM() { + return new Factory(12, 16, 256, "aes256-gcm@openssh.com", "AES", GALOIS_COUNTER_MODE); + } + + /** Named factory for BlockCipher */ + public static class Factory + implements net.schmizz.sshj.common.Factory.Named { + + private int keysize; + private int authSize; + private String cipher; + private String mode; + private String name; + private int ivsize; + + /** + * @param ivsize + * @param keysize The keysize used in bits. + * @param name + * @param cipher + * @param mode + */ + public Factory(int ivsize, int authSize, int keysize, String name, String cipher, String mode) { + this.name = name; + this.keysize = keysize; + this.cipher = cipher; + this.mode = mode; + this.ivsize = ivsize; + this.authSize = authSize; + } + + @Override + public Cipher create() { + return new GcmCipher(ivsize, authSize, keysize / 8, cipher, cipher + "/" + mode + "/NoPadding"); + } + + @Override + public String getName() { + return name; + } + + @Override + public String toString() { + return getName(); + } + } +} diff --git a/src/main/java/net/schmizz/sshj/DefaultConfig.java b/src/main/java/net/schmizz/sshj/DefaultConfig.java index 746148c7..49e7c95e 100644 --- a/src/main/java/net/schmizz/sshj/DefaultConfig.java +++ b/src/main/java/net/schmizz/sshj/DefaultConfig.java @@ -18,6 +18,7 @@ package net.schmizz.sshj; import com.hierynomus.sshj.key.KeyAlgorithm; import com.hierynomus.sshj.key.KeyAlgorithms; import com.hierynomus.sshj.transport.cipher.BlockCiphers; +import com.hierynomus.sshj.transport.cipher.GcmCiphers; import com.hierynomus.sshj.transport.cipher.StreamCiphers; import com.hierynomus.sshj.transport.kex.DHGroups; import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory; @@ -171,6 +172,8 @@ public class DefaultConfig BlockCiphers.AES192CTR(), BlockCiphers.AES256CBC(), BlockCiphers.AES256CTR(), + GcmCiphers.AES128GCM(), + GcmCiphers.AES256GCM(), BlockCiphers.BlowfishCBC(), BlockCiphers.BlowfishCTR(), BlockCiphers.Cast128CBC(), diff --git a/src/main/java/net/schmizz/sshj/transport/Converter.java b/src/main/java/net/schmizz/sshj/transport/Converter.java index 0e817e2e..9d532f96 100644 --- a/src/main/java/net/schmizz/sshj/transport/Converter.java +++ b/src/main/java/net/schmizz/sshj/transport/Converter.java @@ -45,6 +45,7 @@ abstract class Converter { protected long seq = -1; protected boolean authed; protected boolean etm; + protected boolean authMode; long getSequenceNumber() { return seq; @@ -57,7 +58,11 @@ abstract class Converter { if (compression != null) compression.init(getCompressionType()); this.cipherSize = cipher.getIVSize(); - this.etm = mac.isEtm(); + this.etm = this.mac != null && mac.isEtm(); + if(cipher.getAuthenticationTagSize() > 0) { + this.cipherSize = cipher.getAuthenticationTagSize(); + this.authMode = true; + } } void setAuthenticated() { diff --git a/src/main/java/net/schmizz/sshj/transport/Decoder.java b/src/main/java/net/schmizz/sshj/transport/Decoder.java index c3619cd5..02309c1b 100644 --- a/src/main/java/net/schmizz/sshj/transport/Decoder.java +++ b/src/main/java/net/schmizz/sshj/transport/Decoder.java @@ -70,87 +70,41 @@ final class Decoder * * @return number of bytes needed before further decoding possible */ - private int decode() - throws SSHException { - - if (etm) { - return decodeEtm(); - } else { - return decodeMte(); - } - } - - /** - * Decode an Encrypt-Then-Mac packet. - */ - private int decodeEtm() throws SSHException { - int bytesNeeded; - while (true) { - if (packetLength == -1) { - assert inputBuffer.rpos() == 0 : "buffer cleared"; - bytesNeeded = 4 - inputBuffer.available(); - if (bytesNeeded <= 0) { - // In Encrypt-Then-Mac, the packetlength is sent unencrypted. - packetLength = inputBuffer.readUInt32AsInt(); - checkPacketLength(packetLength); - } else { - // Needs more data - break; - } - } else { - assert inputBuffer.rpos() == 4 : "packet length read"; - bytesNeeded = packetLength + mac.getBlockSize() - inputBuffer.available(); - if (bytesNeeded <= 0) { - seq = seq + 1 & 0xffffffffL; - checkMAC(inputBuffer.array()); - decryptBuffer(4, packetLength); - inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte()); - final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer; - if (log.isTraceEnabled()) { - log.trace("Received packet #{}: {}", seq, plain.printHex()); - } - packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet - inputBuffer.clear(); - packetLength = -1; - } else { - // Needs more data - break; - } - } - } - return bytesNeeded; - } - - /** - * Decode a Mac-Then-Encrypt packet - * @return - * @throws SSHException - */ - private int decodeMte() throws SSHException { + private int decode() throws SSHException { int need; - - /* Decoding loop */ - for (; ; ) - + for(;;) { if (packetLength == -1) { // Waiting for beginning of packet assert inputBuffer.rpos() == 0 : "buffer cleared"; need = cipherSize - inputBuffer.available(); if (need <= 0) { - packetLength = decryptLength(); + if (authMode) { + packetLength = decryptLengthAAD(); + } else if (etm) { + packetLength = inputBuffer.readUInt32AsInt(); + checkPacketLength(packetLength); + } else { + packetLength = decryptLength(); + } } else { // Need more data break; } } else { assert inputBuffer.rpos() == 4 : "packet length read"; - need = packetLength + (mac != null ? mac.getBlockSize() : 0) - inputBuffer.available(); + need = (authMode) ? packetLength + cipherSize - inputBuffer.available() : packetLength + (mac != null ? mac.getBlockSize() : 0) - inputBuffer.available(); if (need <= 0) { - decryptBuffer(cipherSize, packetLength + 4 - cipherSize); // Decrypt the rest of the payload seq = seq + 1 & 0xffffffffL; - if (mac != null) { + if (authMode) { + cipher.update(inputBuffer.array(), 4, packetLength); + } else if (etm) { checkMAC(inputBuffer.array()); + decryptBuffer(4, packetLength); + } else { + decryptBuffer(cipherSize, packetLength + 4 - cipherSize); // Decrypt the rest of the payload + if (mac != null) { + checkMAC(inputBuffer.array()); + } } - // Exclude the padding & MAC inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte()); final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer; if (log.isTraceEnabled()) { @@ -160,16 +114,20 @@ final class Decoder inputBuffer.clear(); packetLength = -1; } else { - // Need more data + // Needs more data break; } } - + } return need; } private void checkMAC(final byte[] data) throws TransportException { + if (mac == null) { + return; + } + mac.update(seq); // seq num mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac mac.doFinal(macResult, 0); // compute @@ -186,6 +144,20 @@ final class Decoder return uncompressBuffer; } + private int decryptLengthAAD() throws TransportException { + cipher.updateAAD(inputBuffer.array(), 0, 4); + + final int len; + try { + len = inputBuffer.readUInt32AsInt(); + } catch (Buffer.BufferException be) { + throw new TransportException(be); + } + checkPacketLength(len); + + return len; + } + private int decryptLength() throws TransportException { decryptBuffer(0, cipherSize); @@ -237,7 +209,9 @@ final class Decoder @Override void setAlgorithms(Cipher cipher, MAC mac, Compression compression) { super.setAlgorithms(cipher, mac, compression); - macResult = new byte[mac.getBlockSize()]; + if (mac != null) { + macResult = new byte[mac.getBlockSize()]; + } } @Override diff --git a/src/main/java/net/schmizz/sshj/transport/Encoder.java b/src/main/java/net/schmizz/sshj/transport/Encoder.java index 9ece2041..f9cc1749 100644 --- a/src/main/java/net/schmizz/sshj/transport/Encoder.java +++ b/src/main/java/net/schmizz/sshj/transport/Encoder.java @@ -15,6 +15,7 @@ */ package net.schmizz.sshj.transport; +import net.schmizz.sshj.common.Buffer; import net.schmizz.sshj.common.LoggerFactory; import net.schmizz.sshj.common.SSHPacket; import net.schmizz.sshj.transport.cipher.Cipher; @@ -83,7 +84,7 @@ final class Encoder // Compute padding length int padLen = cipherSize - (lengthWithoutPadding % cipherSize); - if (padLen < 4) { + if (padLen < 4 || (authMode && padLen < cipherSize)) { padLen += cipherSize; } @@ -94,6 +95,14 @@ final class Encoder padLen += cipherSize; packetLen = 1 + payloadSize + padLen; } + /* + * In AES-GCM ciphers, they require packets must be a multiple of 16 bytes (which is also block size of AES) + * as mentioned in RFC5647 Section 7.2. So we are calculating the extra padding as necessary here + */ + if (authMode && packetLen % cipherSize != 0) { + padLen += cipherSize - (packetLen % cipherSize); + packetLen = 1 + payloadSize + padLen; + } final int endOfPadding = startOfPacket + 4 + packetLen; @@ -101,6 +110,7 @@ final class Encoder buffer.wpos(startOfPacket); buffer.putUInt32(packetLen); buffer.putByte((byte) padLen); + // Now wpos will mark end of padding buffer.wpos(endOfPadding); @@ -109,14 +119,17 @@ final class Encoder seq = seq + 1 & 0xffffffffL; - if (etm) { + if (authMode) { + int wpos = buffer.wpos(); + buffer.wpos(wpos + cipherSize); + aeadOutgoingBuffer(buffer, startOfPacket, packetLen); + } else if (etm) { cipher.update(buffer.array(), startOfPacket + 4, packetLen); putMAC(buffer, startOfPacket, endOfPadding); } else { if (mac != null) { putMAC(buffer, startOfPacket, endOfPadding); } - cipher.update(buffer.array(), startOfPacket, 4 + packetLen); } buffer.rpos(startOfPacket); // Make ready-to-read @@ -127,6 +140,14 @@ final class Encoder } } + protected void aeadOutgoingBuffer(Buffer buf, int offset, int len) { + if (cipher == null || cipher.getAuthenticationTagSize() == 0) { + throw new IllegalArgumentException("AEAD mode requires an AEAD cipher"); + } + byte[] data = buf.array(); + cipher.updateWithAAD(data, offset, 4, len); + } + @Override void setAlgorithms(Cipher cipher, MAC mac, Compression compression) { encodeLock.lock(); diff --git a/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java index eedaccae..bc53a732 100644 --- a/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java +++ b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java @@ -15,7 +15,6 @@ */ package net.schmizz.sshj.transport; -import com.hierynomus.sshj.key.KeyAlgorithm; import net.schmizz.concurrent.ErrorDeliveryUtil; import net.schmizz.concurrent.Event; import net.schmizz.sshj.common.*; @@ -323,13 +322,25 @@ final class KeyExchanger resizedKey(encryptionKey_S2C, cipher_S2C.getBlockSize(), hash, kex.getK(), kex.getH()), initialIV_S2C); - final MAC mac_C2S = Factory.Named.Util.create(transport.getConfig().getMACFactories(), negotiatedAlgs - .getClient2ServerMACAlgorithm()); - mac_C2S.init(resizedKey(integrityKey_C2S, mac_C2S.getBlockSize(), hash, kex.getK(), kex.getH())); + /* + * For AES-GCM ciphers, MAC will also be AES-GCM, so it is handled by the cipher itself. + * In that case, both s2c and c2s MACs are ignored. + * + * Refer to RFC5647 Section 5.1 + */ + MAC mac_C2S = null; + if(cipher_C2S.getAuthenticationTagSize() == 0) { + mac_C2S = Factory.Named.Util.create(transport.getConfig().getMACFactories(), negotiatedAlgs + .getClient2ServerMACAlgorithm()); + mac_C2S.init(resizedKey(integrityKey_C2S, mac_C2S.getBlockSize(), hash, kex.getK(), kex.getH())); + } - final MAC mac_S2C = Factory.Named.Util.create(transport.getConfig().getMACFactories(), - negotiatedAlgs.getServer2ClientMACAlgorithm()); - mac_S2C.init(resizedKey(integrityKey_S2C, mac_S2C.getBlockSize(), hash, kex.getK(), kex.getH())); + MAC mac_S2C = null; + if(cipher_S2C.getAuthenticationTagSize() == 0) { + mac_S2C = Factory.Named.Util.create(transport.getConfig().getMACFactories(), + negotiatedAlgs.getServer2ClientMACAlgorithm()); + mac_S2C.init(resizedKey(integrityKey_S2C, mac_S2C.getBlockSize(), hash, kex.getK(), kex.getH())); + } final Compression compression_S2C = Factory.Named.Util.create(transport.getConfig().getCompressionFactories(), diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java index 6133b85a..b50f18c1 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/BaseCipher.java @@ -42,7 +42,7 @@ public abstract class BaseCipher private final String algorithm; private final String transformation; - private javax.crypto.Cipher cipher; + protected javax.crypto.Cipher cipher; public BaseCipher(int ivsize, int bsize, String algorithm, String transformation) { this.ivsize = ivsize; @@ -61,6 +61,11 @@ public abstract class BaseCipher return ivsize; } + @Override + public int getAuthenticationTagSize() { + return 0; + } + @Override public void init(Mode mode, byte[] key, byte[] iv) { key = BaseCipher.resize(key, bsize); @@ -75,6 +80,7 @@ public abstract class BaseCipher } protected abstract void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) throws InvalidKeyException, InvalidAlgorithmParameterException; + protected SecretKeySpec getKeySpec(byte[] key) { return new SecretKeySpec(key, algorithm); } @@ -92,4 +98,19 @@ public abstract class BaseCipher } } + @Override + public void updateAAD(byte[] data, int offset, int length) { + throw new UnsupportedOperationException(getClass() + " does not support AAD operations"); + } + + @Override + public void updateAAD(byte[] data) { + updateAAD(data, 0, data.length); + } + + @Override + public void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) { + updateAAD(input, offset, aadLen); + update(input, offset + aadLen, inputLen); + } } diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java index 6a6e51bd..c7f0ad73 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/Cipher.java @@ -29,6 +29,9 @@ public interface Cipher { /** @return the size of the initialization vector */ int getIVSize(); + /** @return Size of the authentication tag (AT) in bytes or 0 if this cipher does not support authentication */ + int getAuthenticationTagSize(); + /** * Initialize the cipher for encryption or decryption with the given private key and initialization vector * @@ -47,4 +50,32 @@ public interface Cipher { */ void update(byte[] input, int inputOffset, int inputLen); + /** + * Adds the provided input data as additional authenticated data during encryption or decryption. + * + * @param data The additional data to authenticate + * @param offset The offset of the additional data in the buffer + * @param length The number of bytes in the buffer to use for authentication + */ + void updateAAD(byte[] data, int offset, int length); + + /** + * Adds the provided input data as additional authenticated data during encryption or decryption. + * + * @param data The data to authenticate + */ + void updateAAD(byte[] data); + + /** + * Performs in-place authenticated encryption or decryption with additional data (AEAD). Authentication tags are + * implicitly appended after the output ciphertext or implicitly verified after the input ciphertext. Header data + * indicated by the {@code aadLen} parameter are authenticated but not encrypted/decrypted, while payload data + * indicated by the {@code inputLen} parameter are authenticated and encrypted/decrypted. + * + * @param input The input/output bytes + * @param offset The offset of the data in the input buffer + * @param aadLen The number of bytes to use as additional authenticated data - starting at offset + * @param inputLen The number of bytes to update - starting at offset + aadLen + */ + void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen); } diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java b/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java index d576d3c6..246bf707 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/NoneCipher.java @@ -44,6 +44,11 @@ public class NoneCipher return 8; } + @Override + public int getAuthenticationTagSize() { + return 0; + } + @Override public void init(Mode mode, byte[] bytes, byte[] bytes1) { // Nothing to do @@ -54,4 +59,18 @@ public class NoneCipher // Nothing to do } + @Override + public void updateAAD(byte[] data, int offset, int length) { + + } + + @Override + public void updateAAD(byte[] data) { + + } + + @Override + public void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) { + + } } diff --git a/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java b/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java new file mode 100644 index 00000000..985d7688 --- /dev/null +++ b/src/test/java/com/hierynomus/sshj/transport/GcmCipherDecryptSshPacketTest.java @@ -0,0 +1,69 @@ +/* + * 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.transport; + +import com.hierynomus.sshj.transport.cipher.GcmCiphers; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.cipher.Cipher; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.BeforeClass; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; + +import java.io.File; +import java.io.FileInputStream; +import java.security.Security; +import java.util.Arrays; + +import static org.junit.Assert.assertArrayEquals; + +/** + * Unit test to decrypt SSH traffic with OpenSSH and Apache Mina SSHD (master) using AES-GCM ciphers, for verifying + * cipher behaviour. + */ +@RunWith(Theories.class) +public class GcmCipherDecryptSshPacketTest { + + @DataPoints + public static final String sets[][] = new String[][]{{"mina-sshd", "3"}, {"openssh", "4"}}; + + @BeforeClass + public static void setupBeforeClass() { + Security.addProvider(new BouncyCastleProvider()); + } + + @Theory + public void testDecryptPacket(String[] args) throws Exception { + ClassLoader classLoader = getClass().getClassLoader(); + byte[] iv = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/"+args[0]+"/s2c.iv.bin")).toByteArray(); + byte[] key = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/"+args[0]+"/s2c.key.bin")).toByteArray(); + Cipher cipher = GcmCiphers.AES128GCM().create(); + cipher.init(Cipher.Mode.Decrypt, key, iv); + for(int i=1; i<=Integer.parseInt(args[1]); i++) { + byte[] data = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/"+args[0]+"/client.receive."+i+".bin")).toByteArray(); + SSHPacket inputBuffer = new SSHPacket(data); + cipher.updateAAD(inputBuffer.array(), 0, 4); + int size = inputBuffer.readUInt32AsInt(); + cipher.update(inputBuffer.array(), 4, size); + byte[] expected = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/"+args[0]+"/client.decrypted."+i+".bin")).toByteArray(); + assertArrayEquals(Arrays.copyOfRange(expected, 0, size+4), + Arrays.copyOfRange(inputBuffer.array(), 0, size+4)); + } + } +} diff --git a/src/test/java/com/hierynomus/sshj/transport/GcmCipherTest.java b/src/test/java/com/hierynomus/sshj/transport/GcmCipherTest.java new file mode 100644 index 00000000..b9784d0b --- /dev/null +++ b/src/test/java/com/hierynomus/sshj/transport/GcmCipherTest.java @@ -0,0 +1,73 @@ +/* + * 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.transport; + +import com.hierynomus.sshj.transport.cipher.GcmCiphers; +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.transport.cipher.Cipher; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; + +import javax.crypto.AEADBadTagException; +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.*; + +@RunWith(Theories.class) +public class GcmCipherTest { + + public static final @DataPoints + GcmCiphers.Factory[] cipherFactories = { GcmCiphers.AES128GCM(), GcmCiphers.AES256GCM() }; + + @Theory + public void testEncryptDecrypt(GcmCiphers.Factory factory) throws Exception { + Cipher enc = factory.create(); + byte[] key = new byte[enc.getBlockSize()]; + byte[] iv = new byte[enc.getIVSize()]; + enc.init(Cipher.Mode.Encrypt, key, iv); + + byte[] aad = getClass().getName().getBytes(StandardCharsets.UTF_8); + enc.updateAAD(aad); + String plaintext = "[Secret authenticated message using AES-GCM"; + byte[] ptBytes = plaintext.getBytes(StandardCharsets.UTF_8); + byte[] output = new byte[ptBytes.length + enc.getAuthenticationTagSize()]; + System.arraycopy(ptBytes, 0, output, 0, ptBytes.length); + enc.update(output, 0, ptBytes.length); + + Cipher dec = factory.create(); + dec.init(Cipher.Mode.Decrypt, key, iv); + dec.updateAAD(aad); + byte[] input = output.clone(); + dec.update(input, 0, ptBytes.length); + assertEquals(getClass().getName(), new String(aad, StandardCharsets.UTF_8)); + assertEquals(plaintext, new String(input, 0, ptBytes.length, StandardCharsets.UTF_8)); + + byte[] corrupted = output.clone(); + corrupted[corrupted.length - 1] += 1; + Cipher failingDec = factory.create(); + failingDec.init(Cipher.Mode.Decrypt, key, iv); + try { + failingDec.updateAAD(aad.clone()); + failingDec.update(corrupted, 0, ptBytes.length); + fail("Modified authentication tag should not validate"); + } catch (SSHRuntimeException e) { + assertNotNull(e); + assertEquals(AEADBadTagException.class, e.getCause().getClass()); + } + } +} diff --git a/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java b/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java new file mode 100644 index 00000000..6387e4d4 --- /dev/null +++ b/src/test/java/net/schmizz/sshj/transport/DecoderDecryptGcmCipherSshPacketTest.java @@ -0,0 +1,116 @@ +/* + * 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 net.schmizz.sshj.transport; + +import com.hierynomus.sshj.transport.cipher.GcmCiphers; +import net.schmizz.sshj.Config; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.LoggerFactory; +import net.schmizz.sshj.common.SSHException; +import net.schmizz.sshj.common.SSHPacket; +import net.schmizz.sshj.transport.cipher.Cipher; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Before; +import org.junit.Test; + +import java.security.SecureRandom; +import java.security.Security; + +import static org.junit.Assert.assertArrayEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +public class DecoderDecryptGcmCipherSshPacketTest { + + private int PACKET_LENGTH; + + private byte[] key; + + private byte[] iv; + + private byte[] data; + + private byte[] decrypted; + + private Decoder decoder; + + @Before + public void setUp() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + ClassLoader classLoader = DecoderDecryptGcmCipherSshPacketTest.class.getClassLoader(); + iv = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/s2c.iv.bin" )).toByteArray(); + key = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/s2c.key.bin" )).toByteArray(); + data = IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/client.receive.1.bin" )).toByteArray(); + + SSHPacket packet = new SSHPacket(IOUtils.readFully(classLoader.getResourceAsStream("ssh-packets/gcm/mina-sshd/client.decrypted.1.bin" )).toByteArray()); + PACKET_LENGTH = packet.readUInt32AsInt(); + decrypted = new byte[PACKET_LENGTH]; + System.arraycopy(packet.array(), 0, decrypted, 0, PACKET_LENGTH); + + Config config = mock(Config.class); + Transport transport = mock(Transport.class); + when(transport.getConfig()).thenReturn(config); + when(config.getLoggerFactory()).thenReturn(LoggerFactory.DEFAULT); + doAnswer(invocation -> { + SSHPacket p = invocation.getArgument(1); + byte[] verify = new byte[PACKET_LENGTH]; + System.arraycopy(p.array(), 0, verify, 0, PACKET_LENGTH); + assertArrayEquals(decrypted, verify); + return null; + }).when(transport).handle(any(), any()); + + decoder = new Decoder(transport); + Cipher cipher = GcmCiphers.AES128GCM().create(); + cipher.init(Cipher.Mode.Decrypt, key, iv); + decoder.setAlgorithms(cipher, null, null); + } + + @Test + public void testDecodeInOneGo() throws SSHException { + decoder.received(data, data.length); + } + + @Test + public void testDecodeInConstantChunks() throws SSHException { + int chunkSize = 16; + int remain = PACKET_LENGTH; + int pos = 0; + while(remain >= 0) { + byte[] chunk = new byte[chunkSize]; + System.arraycopy(data, pos, chunk, 0, chunkSize); + decoder.received(chunk, chunk.length); + pos += chunkSize; + remain -= chunkSize; + } + } + + @Test + public void testDecodeInRandomChunks() throws SSHException { + SecureRandom sr = new SecureRandom(); + int remain = PACKET_LENGTH; + int pos = 0; + while(remain >= 0) { + int chunkSize = sr.nextInt(10); + if (chunkSize - remain < 0) + chunkSize = remain; + byte[] chunk = new byte[chunkSize]; + System.arraycopy(data, pos, chunk, 0, chunkSize); + decoder.received(chunk, chunk.length); + pos += chunkSize; + remain -= chunkSize; + } + } +} diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.1.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.1.bin new file mode 100644 index 0000000000000000000000000000000000000000..34e6826c5c9fb41cf810f3207ab71ec713b0ea27 GIT binary patch literal 1024 zcmZQzU@(wlV_;z5DK5^?EiFzhN-QnOaI^dG8ju?AvtI9k(#Ky9-FAA0{#D@--@D+3 z<43jE4@^EjziV9+GikH*A9EII28LzE;$@++p-&s{Kk>Zi8}YfbG~?<|o+m3Sv%WQ@ zD@QNkwffmzc%fT=RQCd%*2=;tS@?gl^T7ptflDu3+PQj`U1(@&>%Gf85>qC1sAb8p zUKE`do3>rrNw)Eax58q{rzd@DOznQ;$%kjCo)Fu*)k?mTQPijV_Q(0f%j9xSHa$Em z-?vcr)SeUjiy1Fv{a%{L({WGbX$z+rz-w%+2e-i~Lkq1~4?81AXX ziOxKpAF5lNk!YlAWNN0HT9WIKUyzyybf;c&ey&bNZelV-*3{4lr>r4P8L+9SroefI z5LRxADMCos(hM#NWCO!#F2V_e(}7ZX`FW{2RXLeSU;;V#fX>G+3se9FA1XWE9azt*dB zQnr2NdSLSL`CaRpm`R(Z|CqB#GcYVO7B35p4Sm{p|B2^C--yqhr5RU$@;q5tnf0wH zT{(IQul48RXho?rjvM|x@2d)^{r~2F&(@uu+r!Lp7Xa@ zgWC1glrwG!#S7i~qq-O9v{n{I$-@7WoewVH3tW2P($3Yh>_S6JTkl=&k(e^6LoG{& z^`hv!*tG4^PO^w?ED=UM81wvgzSj z`M!m^r}muKU(9$Z>-W+`o{oDeC;x4^Y|1k0((*lR8Rm&Ad2aOCZ3^1ebJ9j@maYHP z)oXzc=?7x=;^GY5qT)m#v!TEIs@L&FUyt!=ORU~gxyOIEGuNrh3m^2~5w^2&w5&TP zoA}wc-dRuYS@xq}`#tm39{!pY)v%v8yG`d}(MNV&#ZB9oUesPW8Z+C8_1dv~cSkpO z_uF%81>_x9*Rt*T_T{!v*6&3pSSMN(OLGeJoNlgtkS@Sse^9pG;;-J0XRe{$mo*sf zsl|!TJf0t_Tbz+-q-$hqrkh%l>yTfNng?{JUUGh}PDXBGGDOzY&yF zd4>>HZi*>FNY~N~E(&A=!)Y$U34_ytQhE7#sXA3TnMq&*IrxCi$1e+100kc|q%qI# QKAZIDp|Z_tnBZWi0b-w1XaE2J literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.3.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/client.decrypted.3.bin new file mode 100644 index 0000000000000000000000000000000000000000..31ce9c05ccb31adaf77fe94eca88b67dd8df76b6 GIT binary patch literal 1024 zcmZQzU{H`VVNR{)w?3>>W@WRw-Xh}k@yl1H3ASD;X)Q7GZK-~+?P6O<>2F=xzHcYP z)*sJ(`@rPm^SjnHF_Sh+|1oEgW?)!mEM68G8~U{I{u9rOz7d~0OEa$isBlIPDW9m?%N;d7cY~`Iob5^ ztbE@>-BWu`>@Q}#l=XXQB2ULXm6QLrTsCEyb!qvYwhZ&cl{`24>^24M>N#nnHOtn2 z>gu&Xhx7w6dvS4wZc%X}klD~*e%0&vqOZsJv?W&Wsodkg+nMXs<%JLW?+Dx3I9k@7 zlTG~WTkou=_bmI-ul=6+Y7c+SifY);o86{!vFIbauHvR`OfPD$9F3Xn#Cq*mzPqEF zyZi0AwF2^vt83ZzeEV`+DC_s46RZ<0ilsRPdQLajK1dhfusuayA*5?*1{Vdgf#Eb4;e^5IK&ia^yi}d4oXjLJfgF55=i`?JDu99y7t)w# Rcb`pq^ibJmHB4}@(*V%sR#*T4 literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.1.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.1.bin new file mode 100644 index 0000000000000000000000000000000000000000..2234c87d5109c57008b445f3f18b302240301754 GIT binary patch literal 1024 zcmZQzU@*v9UGwDm7s-X^xo5fB&fmKGQWMXuWjU;KC(GpOrM*5nzifL(!KT)`EM+%k z8!|PG9y~Dl`24PQP0Xat(tpfZq!}2N8H<;N#)dv^y#K`WqHo0K&eDvlKY5<4tjzk> zl&&1TgxC6WakQe;8OII(p7&J+)c$|-KWGJq?~NoAF@1&MiYRVZmgyMy6{ZZY^z`)ST!YEnzf3owz1$==^FI?KWdX`;iXld)c%RLfPCUvM~ z$*^7&ofn(7UD`>u@rSp{Um%wAlap<7g(2xK<&mtXZdzUb>QK5dEBdn)(%?{?-ob$Q`~{yV~UHjb8c z=VTK<`_?<_={?JS^lQIozS_fIv!WXI^Jcf{TrB#?uB*6d8`F#0D@S8yJF#9nmhbN9 z=I(xbZmod4S0E7Rvg)=mhITi(+X`fu7UNwGYw-IP4F~)?575+wsgbwEMCK z!#%Y)(V55dLv@QY5{-0?OwDvtOL86Z3sUod?$k@p&(+DuO-zQ!ni?A6lr_XD12z@a z6gbZi!pcoCMF{Cyn!!bZY+yLeML1z_I#4PvKQC3MDkn1uOdtmz(E0dffeN7D!-X{F S+1+Q89z9gHSq&2$>@)yv4p(ge literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.2.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.2.bin new file mode 100644 index 0000000000000000000000000000000000000000..4e2bfca81efa5c7148f6fde41b744c032a816bbb GIT binary patch literal 1024 zcmZQzU{Kg%pmfubXY!&pjtPBQg$>2;w(VQl9`&W>YdvGsoWrW;SQz)2{+YHR?639e zoRn=}xgMB&e16xuCT7xR=|AQy(hLmCjK#}BV?&=d-hbkG(Kq6AXKBXOpFB@iR%U%` zN>`3v!fXAxI9gHajN^uX&-_pTp}dUT|jzvuid z)}VHMHRX&OLh(Yk{;2L{U|?uvVU#TVKiT=<0=~ed7cT8wJcRO>Py1ei~{~cjF8%N8! zbFzt_ee0d|^qyru`nBIPU+v+qSy2u9d9&MeE*5=c*HzrKjp;@0m7_7Uomj6O%XfEl zb9cWzw^l&jadj=*o^M}n3uXOYbb@uFMX@xeK+oys+6Un;B3?Re%I+I?Ar z;htKY=*;8!p}NHxiAK6cre?aSCAkjy1*v&Jcj_hQ=jvqSCMH8mrm6MqSCXj;<=zRRLKm}0n;X)eo S?C!Hkj~*)9tcD2=b{YVxt5(DS literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.3.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/client.receive.3.bin new file mode 100644 index 0000000000000000000000000000000000000000..97c7a1cf8f5d27c13b834cf78c314d9aa78fd211 GIT binary patch literal 1024 zcmZQzU{JWh6nY{qv+KQ8@du^PZl7IQJ=DZKa}5tS9!OPxmvzB^+r_q!(%-tWecw)o ztv{ao_JPUA=Xb4ZVkT{t{$tJ}&A_nCSiCGWHuP!Z{U@FmeIq`1mS$Z2$@64oW!AT* zbmizJyw;zKqZOsjIBxj&yss*t_WzszK`S_XZzP$B=`*DGIlSKD_12Yr!b+d}d(Pit z4Qkg{Q_i>{6fbn^kLq3q28LD^M#;kelbsJP;0s)O;nL34v+P1cOIz<sY5MG zhV`Q8yx6qu(oV9CKfDzdOFlj6TVrbXBTqg&L-mB%)~#0Zos6PB-M2r^FJ2~>bF%5- zS^2(&x~KM>*k8(cT)Z5if?D|v48*=-8i)pOEDYnHA5 z)YWT&4(SJC_Tu6U-J;?|AhV&r{HoXSMPHBcX-ll$Q@O`~w=>tN%L^a$-x0R6akQ*E zC!6@$x87M#?^*VvU;91t)gJzu71gkxH@i*eV$nx-UByk?m|oOgIT|zDiS^pCe0N7T zclX}1H)-9!U==Zfl_(-d8s;8Ihjdd0y+4A&c`ncQ~(7ZE~GKf R?mnCJ=%KRBYM9_)rvV1YS0MlZ literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin new file mode 100644 index 00000000..388868e4 --- /dev/null +++ b/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin @@ -0,0 +1 @@ +}GL8a<ųǁ0ݺ8s1 _NZX!2 1+VPܤ \ No newline at end of file diff --git a/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin b/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin new file mode 100644 index 00000000..dee166c6 --- /dev/null +++ b/src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin @@ -0,0 +1 @@ +ѝe_7>nPÚ/A_Ş{f.~f5ZhM9; \ No newline at end of file diff --git a/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.1.bin b/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.1.bin new file mode 100644 index 0000000000000000000000000000000000000000..2ee5b9319e4edb2f68ceb00b99ec3e7c7e0e9dd4 GIT binary patch literal 1024 zcmZQzU{K&=V_;z5DK5^?EiFzhN-QnOSg}Mh^W+pScI`EGVc(5KmD*Ed6+S#{3S*!~{Ox_ofb zQMN_iSmOLJ^u?Y{U=c^tRga=R1PUa_6FzVDxE`Da*IE|g}-yBsRzJZDPPs*)oI zp3iX6$!U*vdH9i|?ceq9D}PvR*>1}kvt(n>cixoTE>k?mlPP8ngNA9fmo4Y$DfA@UP{xB%}boR_F311 zwwv6dt1EULI3Rax^)0@|($9d36snjrTOS5jz4YN$-*_|2xT;67Aus%9w6lq}#l1yd z|2Tnaz<`HK6vkv=NY2mINlYyU+NGPGoa>NZkeUaK06m~EP>n87#?Z(DT?V&?tEIV# z$+~7Hx~U~7X6b;%K}MlVWPmh){S0yfZdp@9Bb>5^7&1_2AbSVy1QbCO>va%3pmlJb mAuuF{t;a<3^7B%4s&X=uki!M&W1JF*v;$!gtq@%!3LgLuEMxZo literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.2.bin b/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.2.bin new file mode 100644 index 0000000000000000000000000000000000000000..9be5517ed1db93faa34d20085365c93ad98c9c46 GIT binary patch literal 1024 zcmZQzU{GK;W?*0tDkx3L$xO~pt<)(&yx!8(HM+d_X-mre3kA6wY_9&F+-=# zm%oK5DdlNMEzPlswfo{f<#F6{%k55Fd&PFz`o4dv<)2|;xlo!R?{cV=^PDMFt4fX> zcs|2LC#OBy<>5z;wtv^Zul!-PWxFkF%#w{g-+5DVlUIFR{MT;f^3VzF=RAsc=+0={ zkl(QI1kfQVKrEb^oKl>qTbz+-q??ynTvA|UY6cYc1Y$*o9e*AscqvUgHZO7N+Gkx4 z+HP`-uCCa1;DFq%)wlQ-OFsiDQmA6iY<(D9_0or1edEn6dHH#%I#oHDNyy;>^f68eMB0I{h*pTM5rq!`iv4CO literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.3.bin b/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.3.bin new file mode 100644 index 0000000000000000000000000000000000000000..1c984897a5fdd9850bb3e6d5ad2be2835799b93f GIT binary patch literal 1024 zcmZQzU{GK;W?*0tDkx3L$xO~pt<)(&yx!8(HM+d_X-mre3kA6wY_9&F+-=# zm%oK5DdlNMEzPlswfo{f<#F6{%k55Fd&PFz`o4dv<)2|;xlo!R?{cV=^PDMFt4fX> zcs|2LC#OBy<>5z;wtv^Zul!-PWxFkF%#w{g-+5DVlUIFR{MT;f^3VzF=RAsc=+0={ zkl(QI1kfQVKrEb^oKl>qTbz+-q??ynTvA|UY6cYc1Y$*o9e*AscqvUgHZO7N+Gkx4 z+HP`-uCCa1;DFq%)wlQ-OFsiDQmA6iY<(D9_0or1edEn6dHH#%I#oHDNyy;>^f68eMB0I{h*pTM5rq!`SO{mZ literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.4.bin b/src/test/resources/ssh-packets/gcm/openssh/client.decrypted.4.bin new file mode 100644 index 0000000000000000000000000000000000000000..8150320385ec383ef358aa79bfc56691ee79206c GIT binary patch literal 1024 zcmZQzU=ZLlDfT@UzJC%^b>)v$`KE5mPIQJ8F1qorvPH1(nbo?PcNYY`zEE>H^}(0s zrsFMl&g>I@+jFr-zs*!<-R1R`rmoTDy-!o_2^jrXLSp3H?6J$3)D*V-^JFtoBT{(5v9g z2Pa+r7NVq-ry;d8$0pY9i~p3zamy{YJ8|t5+iC0j{;8IKhK1!qX@Tf8_Gp)fA356oUH`uFht-ztwyZHrHuikyP03AO^>y)IyOqmBC$OLMDBhtv zqisWe!@?6lhok_paB6Z&aiVTl0BHdG8RP`qvZjVcIAsknWT4JK_72<$D1s=~>mYbQ p>)<>?U`PyGkBR2x=cVdY~scUq3@6(o)`xgpwH`rYLKga9sf{+juZLTf{*9A|) zIt~bJ<2@;-Cv#y%Pu>6PwKfb446Q7TzaHIImt0=aeB;uEBpU<9zT@9R-fm<2dob(r z!AY0Dg(xZIX-F;2v5B?&;y>ka+;Yq9PF#D%cG~*Bf2!r5VPUyYnj!CUsFd@ZDOIaV zjvRPC!$l{jJ=*2rM~=3C*T1j)VYOwuEo;n@jXmFaQ*x76eO>(5ZsqdO3GC-Qig)PF zXxotAus>d@SD-jCfXMF z7J2>S1gZf89xhQBlYt>QKUXI)wHRoZZhCUALw-SOUU6}T9#9ylMi(e!Xk>vdgImMZ z(%i&kT{9Ei)Djf4bim>uqtGQXKpMb)1~~z@tf`?9PFX_?8K^Umy#sdwiXe*hItU)n pIylb|7!t$QW1@Nad8s;8Ihjew;R5tAP6o_2^jrXLSp3H?6J$3)D*V-^JFtoBT{(5v9g z2Pa+r7NVq-ry;d8$0pY9i~p3zamy{YJ8|t5+iC0j{;8IKhK1!qX@Tf8_Gp)fA356oUH`uFht-ztwyZHrHuikyP03AO^>y)IyOqmBC$OLMDBhtv zqisWe!@?6lhok_paB6Z&aiVTl0BHdG8RP`qvZjVcIAsknWT4JK_72<$D1s=~>mYbQ p>)<>?U`PyGkBR2x=cVdYkBoPQy+Y3 zZaUs_=gdCgw>=kY^xI5z)?Hq2Y3dqX-utvA<^F|&+zmEY|IhJyyC5V)MVqV3!F9ot zu#N*l+jvjP>B(GJ(Np*TdaVru14AndrUS-}Ub+e^_nVZp#|8WMj{F-jv+rRbLnXwOhG7bOQT1kK!G= zGuk%fH!M5>bVv#i3#TTh6esEyXCxZw=4BR_6d0MB0fjw*Sdn4JpN9!vO4E+bOPsp) zS=WQMo7|$SD|Q_?Aa`r^ExyIl&wz>)s+cod9|l*w^x;yTfNnpa$$p$8NOs?i0?7#dli%iz{< zwKO*|S=Y=&H?;)CEFG{o$S8D)43Gw}pFvK*Eo*9Mgj3cKLk8*$WbeS8fFg)uy$*s0 qv<}WQ1ct=0^_Xa0eqO3hRZeCSa<~9}j8g)Ub|5UG6{2fI;R68Bdu}QK literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/openssh/client.receive.4.bin b/src/test/resources/ssh-packets/gcm/openssh/client.receive.4.bin new file mode 100644 index 0000000000000000000000000000000000000000..97416f9868c4f84eb0f7fdbe9c67df17081fee30 GIT binary patch literal 1024 zcmZQzU=XOW)eH>_lfs;i4P=Dq95mo>{G%d3Qn3>kBoPQy+Y3 zZaUs_=gdCgw>=kY^xI5z)?Hq2Y3dqX-utvA<^F|&+zmEY|IhJyyC5V)MVqV3!F9ot zu#N*l+jvjP>B(GJ(Np*TdaVru14AndrUS-}Ub+e^_nVZp#|8WMj{F-jv+rRbLnXwOhG7bOQT1kK!G= zGuk%fH!M5>bVv#i3#TTh6esEyXCxZw=4BR_6d0MB0fjw*Sdn4JpN9!vO4E+bOPsp) zS=WQMo7|$SD|Q_?Aa`r^ExyIl&wz>)s+cod9|l*w^x;yTfNnpa$$p$8NOs?i0?7#dli%iz{< zwKO*|S=Y=&H?;)CEFG{o$S8D)43Gw}pFvK*Eo*9Mgj3cKLk8*$WbeS8fFg)uy$*s0 qv<}WQ1ct=0^_Xa0eqO3hRZeCSa<~9}j8g)Ub|5UG6{2fI;R67t>Tccu literal 0 HcmV?d00001 diff --git a/src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin b/src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin new file mode 100644 index 00000000..276ab9b8 --- /dev/null +++ b/src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin @@ -0,0 +1 @@ +K䪳@B/Y*Psu"]0tE `erlںM~OEδ\'` \ No newline at end of file diff --git a/src/test/resources/ssh-packets/gcm/openssh/s2c.key.bin b/src/test/resources/ssh-packets/gcm/openssh/s2c.key.bin new file mode 100644 index 0000000000000000000000000000000000000000..07f7dfd46a3f370968170b656fa239a763c74514 GIT binary patch literal 64 zcmV-G0KfkfOAJjK1q(2QcxF={Zrt6aK-dMtK80|w3ArMRd>lAjAaMo&5qtzM`la&c Wo|rsF_urrl3h)Wfl-0TrpZM