Skip to content

Commit

Permalink
Implement AES-GCM cipher support (#630)
Browse files Browse the repository at this point in the history
* Implement AES-GCM cipher support

Fixes #217.

A port of AES-GCM cipher support from Apache MINA-SSHD, based on apache/mina-sshd#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
  • Loading branch information
TranceLove committed Sep 9, 2020
1 parent 4458332 commit 143069e
Show file tree
Hide file tree
Showing 33 changed files with 722 additions and 82 deletions.
4 changes: 3 additions & 1 deletion src/itest/docker-image/test-container/sshd_config
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,6 @@ Subsystem sftp /usr/lib/ssh/sftp-server
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1,diffie-hellman-group-exchange-sha1
macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-ripemd160-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-ripemd160,hmac-ripemd160@openssh.com

TrustedUserCAKeys /etc/ssh/users_rsa_ca.pub
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
Original file line number Diff line number Diff line change
@@ -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
}

}
164 changes: 164 additions & 0 deletions src/main/java/com/hierynomus/sshj/transport/cipher/GcmCipher.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/hierynomus/sshj/transport/cipher/GcmCiphers.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
3 changes: 3 additions & 0 deletions src/main/java/net/schmizz/sshj/DefaultConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand Down Expand Up @@ -171,6 +172,8 @@ protected void initCipherFactories() {
BlockCiphers.AES192CTR(),
BlockCiphers.AES256CBC(),
BlockCiphers.AES256CTR(),
GcmCiphers.AES128GCM(),
GcmCiphers.AES256GCM(),
BlockCiphers.BlowfishCBC(),
BlockCiphers.BlowfishCTR(),
BlockCiphers.Cast128CBC(),
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/net/schmizz/sshj/transport/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ abstract class Converter {
protected long seq = -1;
protected boolean authed;
protected boolean etm;
protected boolean authMode;

long getSequenceNumber() {
return seq;
Expand All @@ -57,7 +58,11 @@ void setAlgorithms(Cipher cipher, MAC mac, Compression compression) {
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() {
Expand Down
Loading

0 comments on commit 143069e

Please sign in to comment.