mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 07:10:53 +03:00
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
This commit is contained in:
@@ -132,3 +132,5 @@ KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,
|
||||
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
|
||||
|
||||
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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Cipher> {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
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 (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,8 +209,10 @@ final class Decoder
|
||||
@Override
|
||||
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
|
||||
super.setAlgorithms(cipher, mac, compression);
|
||||
if (mac != null) {
|
||||
macResult = new byte[mac.getBlockSize()];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
Compression.Mode getCompressionType() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
/*
|
||||
* 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(),
|
||||
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(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin
Normal file
1
src/test/resources/ssh-packets/gcm/mina-sshd/s2c.iv.bin
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD><EFBFBD>}<7D><>GL<47><01>8a<<3C>ųǁ<C5B3>0ݺ8s<>1<EFBFBD><31>
|
||||
1
src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin
Normal file
1
src/test/resources/ssh-packets/gcm/mina-sshd/s2c.key.bin
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD><EFBFBD>ѝ<EFBFBD>e<EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>7<01><>>n<>PÚ<50>/<1C>A<EFBFBD><41><EFBFBD><15>_<EFBFBD><5F><EFBFBD>Ş{f<><66>.<2E>~f<>5<EFBFBD><35><EFBFBD><EFBFBD><EFBFBD>ZhM9<4D><1A>;
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.1.bin
Normal file
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.1.bin
Normal file
Binary file not shown.
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.2.bin
Normal file
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.2.bin
Normal file
Binary file not shown.
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.3.bin
Normal file
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.3.bin
Normal file
Binary file not shown.
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.4.bin
Normal file
BIN
src/test/resources/ssh-packets/gcm/openssh/client.receive.4.bin
Normal file
Binary file not shown.
1
src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin
Normal file
1
src/test/resources/ssh-packets/gcm/openssh/s2c.iv.bin
Normal file
@@ -0,0 +1 @@
|
||||
K<EFBFBD><EFBFBD>䪳@B/<2F>Y<EFBFBD><59><EFBFBD>*<2A><>Psu<73><75><EFBFBD><EFBFBD>"<22><>]0t<30>E<0B><><EFBFBD>`<60><>er<65>lں<6C><DABA><EFBFBD>M<EFBFBD><4D><EFBFBD>~OEδ\<5C>'`<60>
|
||||
BIN
src/test/resources/ssh-packets/gcm/openssh/s2c.key.bin
Normal file
BIN
src/test/resources/ssh-packets/gcm/openssh/s2c.key.bin
Normal file
Binary file not shown.
Reference in New Issue
Block a user