mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +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:
@@ -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
|
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
|
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
|
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.KeyAlgorithm;
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithms;
|
import com.hierynomus.sshj.key.KeyAlgorithms;
|
||||||
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
|
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.cipher.StreamCiphers;
|
||||||
import com.hierynomus.sshj.transport.kex.DHGroups;
|
import com.hierynomus.sshj.transport.kex.DHGroups;
|
||||||
import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory;
|
import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory;
|
||||||
@@ -171,6 +172,8 @@ public class DefaultConfig
|
|||||||
BlockCiphers.AES192CTR(),
|
BlockCiphers.AES192CTR(),
|
||||||
BlockCiphers.AES256CBC(),
|
BlockCiphers.AES256CBC(),
|
||||||
BlockCiphers.AES256CTR(),
|
BlockCiphers.AES256CTR(),
|
||||||
|
GcmCiphers.AES128GCM(),
|
||||||
|
GcmCiphers.AES256GCM(),
|
||||||
BlockCiphers.BlowfishCBC(),
|
BlockCiphers.BlowfishCBC(),
|
||||||
BlockCiphers.BlowfishCTR(),
|
BlockCiphers.BlowfishCTR(),
|
||||||
BlockCiphers.Cast128CBC(),
|
BlockCiphers.Cast128CBC(),
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ abstract class Converter {
|
|||||||
protected long seq = -1;
|
protected long seq = -1;
|
||||||
protected boolean authed;
|
protected boolean authed;
|
||||||
protected boolean etm;
|
protected boolean etm;
|
||||||
|
protected boolean authMode;
|
||||||
|
|
||||||
long getSequenceNumber() {
|
long getSequenceNumber() {
|
||||||
return seq;
|
return seq;
|
||||||
@@ -57,7 +58,11 @@ abstract class Converter {
|
|||||||
if (compression != null)
|
if (compression != null)
|
||||||
compression.init(getCompressionType());
|
compression.init(getCompressionType());
|
||||||
this.cipherSize = cipher.getIVSize();
|
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() {
|
void setAuthenticated() {
|
||||||
|
|||||||
@@ -70,87 +70,41 @@ final class Decoder
|
|||||||
*
|
*
|
||||||
* @return number of bytes needed before further decoding possible
|
* @return number of bytes needed before further decoding possible
|
||||||
*/
|
*/
|
||||||
private int decode()
|
private int decode() throws SSHException {
|
||||||
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 {
|
|
||||||
int need;
|
int need;
|
||||||
|
for(;;) {
|
||||||
/* Decoding loop */
|
|
||||||
for (; ; )
|
|
||||||
|
|
||||||
if (packetLength == -1) { // Waiting for beginning of packet
|
if (packetLength == -1) { // Waiting for beginning of packet
|
||||||
assert inputBuffer.rpos() == 0 : "buffer cleared";
|
assert inputBuffer.rpos() == 0 : "buffer cleared";
|
||||||
need = cipherSize - inputBuffer.available();
|
need = cipherSize - inputBuffer.available();
|
||||||
if (need <= 0) {
|
if (need <= 0) {
|
||||||
packetLength = decryptLength();
|
if (authMode) {
|
||||||
|
packetLength = decryptLengthAAD();
|
||||||
|
} else if (etm) {
|
||||||
|
packetLength = inputBuffer.readUInt32AsInt();
|
||||||
|
checkPacketLength(packetLength);
|
||||||
|
} else {
|
||||||
|
packetLength = decryptLength();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Need more data
|
// Need more data
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert inputBuffer.rpos() == 4 : "packet length read";
|
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) {
|
if (need <= 0) {
|
||||||
decryptBuffer(cipherSize, packetLength + 4 - cipherSize); // Decrypt the rest of the payload
|
|
||||||
seq = seq + 1 & 0xffffffffL;
|
seq = seq + 1 & 0xffffffffL;
|
||||||
if (mac != null) {
|
if (authMode) {
|
||||||
|
cipher.update(inputBuffer.array(), 4, packetLength);
|
||||||
|
} else if (etm) {
|
||||||
checkMAC(inputBuffer.array());
|
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());
|
inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte());
|
||||||
final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer;
|
final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer;
|
||||||
if (log.isTraceEnabled()) {
|
if (log.isTraceEnabled()) {
|
||||||
@@ -160,16 +114,20 @@ final class Decoder
|
|||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
packetLength = -1;
|
packetLength = -1;
|
||||||
} else {
|
} else {
|
||||||
// Need more data
|
// Needs more data
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return need;
|
return need;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkMAC(final byte[] data)
|
private void checkMAC(final byte[] data)
|
||||||
throws TransportException {
|
throws TransportException {
|
||||||
|
if (mac == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
mac.update(seq); // seq num
|
mac.update(seq); // seq num
|
||||||
mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac
|
mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac
|
||||||
mac.doFinal(macResult, 0); // compute
|
mac.doFinal(macResult, 0); // compute
|
||||||
@@ -186,6 +144,20 @@ final class Decoder
|
|||||||
return uncompressBuffer;
|
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()
|
private int decryptLength()
|
||||||
throws TransportException {
|
throws TransportException {
|
||||||
decryptBuffer(0, cipherSize);
|
decryptBuffer(0, cipherSize);
|
||||||
@@ -237,7 +209,9 @@ final class Decoder
|
|||||||
@Override
|
@Override
|
||||||
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
|
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
|
||||||
super.setAlgorithms(cipher, mac, compression);
|
super.setAlgorithms(cipher, mac, compression);
|
||||||
macResult = new byte[mac.getBlockSize()];
|
if (mac != null) {
|
||||||
|
macResult = new byte[mac.getBlockSize()];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.transport;
|
package net.schmizz.sshj.transport;
|
||||||
|
|
||||||
|
import net.schmizz.sshj.common.Buffer;
|
||||||
import net.schmizz.sshj.common.LoggerFactory;
|
import net.schmizz.sshj.common.LoggerFactory;
|
||||||
import net.schmizz.sshj.common.SSHPacket;
|
import net.schmizz.sshj.common.SSHPacket;
|
||||||
import net.schmizz.sshj.transport.cipher.Cipher;
|
import net.schmizz.sshj.transport.cipher.Cipher;
|
||||||
@@ -83,7 +84,7 @@ final class Encoder
|
|||||||
|
|
||||||
// Compute padding length
|
// Compute padding length
|
||||||
int padLen = cipherSize - (lengthWithoutPadding % cipherSize);
|
int padLen = cipherSize - (lengthWithoutPadding % cipherSize);
|
||||||
if (padLen < 4) {
|
if (padLen < 4 || (authMode && padLen < cipherSize)) {
|
||||||
padLen += cipherSize;
|
padLen += cipherSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +95,14 @@ final class Encoder
|
|||||||
padLen += cipherSize;
|
padLen += cipherSize;
|
||||||
packetLen = 1 + payloadSize + padLen;
|
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;
|
final int endOfPadding = startOfPacket + 4 + packetLen;
|
||||||
|
|
||||||
@@ -101,6 +110,7 @@ final class Encoder
|
|||||||
buffer.wpos(startOfPacket);
|
buffer.wpos(startOfPacket);
|
||||||
buffer.putUInt32(packetLen);
|
buffer.putUInt32(packetLen);
|
||||||
buffer.putByte((byte) padLen);
|
buffer.putByte((byte) padLen);
|
||||||
|
|
||||||
// Now wpos will mark end of padding
|
// Now wpos will mark end of padding
|
||||||
buffer.wpos(endOfPadding);
|
buffer.wpos(endOfPadding);
|
||||||
|
|
||||||
@@ -109,14 +119,17 @@ final class Encoder
|
|||||||
|
|
||||||
seq = seq + 1 & 0xffffffffL;
|
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);
|
cipher.update(buffer.array(), startOfPacket + 4, packetLen);
|
||||||
putMAC(buffer, startOfPacket, endOfPadding);
|
putMAC(buffer, startOfPacket, endOfPadding);
|
||||||
} else {
|
} else {
|
||||||
if (mac != null) {
|
if (mac != null) {
|
||||||
putMAC(buffer, startOfPacket, endOfPadding);
|
putMAC(buffer, startOfPacket, endOfPadding);
|
||||||
}
|
}
|
||||||
|
|
||||||
cipher.update(buffer.array(), startOfPacket, 4 + packetLen);
|
cipher.update(buffer.array(), startOfPacket, 4 + packetLen);
|
||||||
}
|
}
|
||||||
buffer.rpos(startOfPacket); // Make ready-to-read
|
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
|
@Override
|
||||||
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
|
void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
|
||||||
encodeLock.lock();
|
encodeLock.lock();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
package net.schmizz.sshj.transport;
|
package net.schmizz.sshj.transport;
|
||||||
|
|
||||||
import com.hierynomus.sshj.key.KeyAlgorithm;
|
|
||||||
import net.schmizz.concurrent.ErrorDeliveryUtil;
|
import net.schmizz.concurrent.ErrorDeliveryUtil;
|
||||||
import net.schmizz.concurrent.Event;
|
import net.schmizz.concurrent.Event;
|
||||||
import net.schmizz.sshj.common.*;
|
import net.schmizz.sshj.common.*;
|
||||||
@@ -323,13 +322,25 @@ final class KeyExchanger
|
|||||||
resizedKey(encryptionKey_S2C, cipher_S2C.getBlockSize(), hash, kex.getK(), kex.getH()),
|
resizedKey(encryptionKey_S2C, cipher_S2C.getBlockSize(), hash, kex.getK(), kex.getH()),
|
||||||
initialIV_S2C);
|
initialIV_S2C);
|
||||||
|
|
||||||
final MAC mac_C2S = Factory.Named.Util.create(transport.getConfig().getMACFactories(), negotiatedAlgs
|
/*
|
||||||
.getClient2ServerMACAlgorithm());
|
* For AES-GCM ciphers, MAC will also be AES-GCM, so it is handled by the cipher itself.
|
||||||
mac_C2S.init(resizedKey(integrityKey_C2S, mac_C2S.getBlockSize(), hash, kex.getK(), kex.getH()));
|
* 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;
|
||||||
negotiatedAlgs.getServer2ClientMACAlgorithm());
|
if(cipher_S2C.getAuthenticationTagSize() == 0) {
|
||||||
mac_S2C.init(resizedKey(integrityKey_S2C, mac_S2C.getBlockSize(), hash, kex.getK(), kex.getH()));
|
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 =
|
final Compression compression_S2C =
|
||||||
Factory.Named.Util.create(transport.getConfig().getCompressionFactories(),
|
Factory.Named.Util.create(transport.getConfig().getCompressionFactories(),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ public abstract class BaseCipher
|
|||||||
private final String algorithm;
|
private final String algorithm;
|
||||||
private final String transformation;
|
private final String transformation;
|
||||||
|
|
||||||
private javax.crypto.Cipher cipher;
|
protected javax.crypto.Cipher cipher;
|
||||||
|
|
||||||
public BaseCipher(int ivsize, int bsize, String algorithm, String transformation) {
|
public BaseCipher(int ivsize, int bsize, String algorithm, String transformation) {
|
||||||
this.ivsize = ivsize;
|
this.ivsize = ivsize;
|
||||||
@@ -61,6 +61,11 @@ public abstract class BaseCipher
|
|||||||
return ivsize;
|
return ivsize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAuthenticationTagSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Mode mode, byte[] key, byte[] iv) {
|
public void init(Mode mode, byte[] key, byte[] iv) {
|
||||||
key = BaseCipher.resize(key, bsize);
|
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 abstract void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) throws InvalidKeyException, InvalidAlgorithmParameterException;
|
||||||
|
|
||||||
protected SecretKeySpec getKeySpec(byte[] key) {
|
protected SecretKeySpec getKeySpec(byte[] key) {
|
||||||
return new SecretKeySpec(key, algorithm);
|
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 */
|
/** @return the size of the initialization vector */
|
||||||
int getIVSize();
|
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
|
* 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);
|
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;
|
return 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAuthenticationTagSize() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Mode mode, byte[] bytes, byte[] bytes1) {
|
public void init(Mode mode, byte[] bytes, byte[] bytes1) {
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
@@ -54,4 +59,18 @@ public class NoneCipher
|
|||||||
// Nothing to do
|
// 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