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:
Raymond Lai
2020-09-09 15:51:17 +08:00
committed by GitHub
parent 4458332cbf
commit 143069e3e0
33 changed files with 722 additions and 82 deletions

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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(),

View File

@@ -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() {

View File

@@ -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

View File

@@ -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();

View File

@@ -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(),

View File

@@ -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);
}
} }

View File

@@ -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);
} }

View File

@@ -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) {
}
} }

View File

@@ -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));
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD><EFBFBD>}<7D><>GL<47><01>8a<<3C>ųǁ<C5B3>0ݺ8s<>1<EFBFBD><31>

View File

@@ -0,0 +1 @@
<EFBFBD><EFBFBD>ѝ<EFBFBD>e<EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>7<01><>>n<><50>/<1C>A<EFBFBD><41><EFBFBD><15>_<EFBFBD><5F><EFBFBD>Ş{f<><66>.<2E>~f<>5<EFBFBD><35><EFBFBD><EFBFBD><EFBFBD>ZhM9<4D><1A>;

View 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><6C><DABA><EFBFBD>M<EFBFBD><4D><EFBFBD>~OEδ\<5C>'`<60>

Binary file not shown.