This is an automated email from the ASF dual-hosted git repository. lgoldstein pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit 0ba876638c690cc1441a2cc55266715dc73e260f Author: Matt Sicker <boa...@gmail.com> AuthorDate: Mon May 18 11:14:49 2020 -0500 [SSHD-506] Add support for AES-GCM ciphers --- CHANGES.md | 1 + README.md | 4 +- .../org/apache/sshd/common/cipher/BaseCipher.java | 14 ++- .../apache/sshd/common/cipher/BaseGCMCipher.java | 103 +++++++++++++++++++++ .../apache/sshd/common/cipher/BaseRC4Cipher.java | 2 +- .../apache/sshd/common/cipher/BuiltinCiphers.java | 51 +++++++--- .../java/org/apache/sshd/common/cipher/Cipher.java | 37 ++++++++ .../sshd/common/cipher/CipherInformation.java | 5 + .../org/apache/sshd/common/cipher/CipherNone.java | 10 ++ .../sshd/common/util/buffer/BufferUtils.java | 34 +++++++ .../apache/sshd/common/cipher/AES128GCMTest.java | 35 +++++++ .../apache/sshd/common/cipher/AES256GCMTest.java | 35 +++++++ .../common/cipher/BaseAuthenticatedCipherTest.java | 70 ++++++++++++++ .../java/org/apache/sshd/common/BaseBuilder.java | 2 + .../common/session/helpers/AbstractSession.java | 99 ++++++++++++++------ .../sshd/common/cipher/BuiltinCiphersTest.java | 9 +- 16 files changed, 463 insertions(+), 48 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b50f202..b55e550 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,6 +12,7 @@ ## Major code re-factoring +* [SSHD-506](https://issues.apache.org/jira/browse/SSHD-506) Added support for AES-GCM ciphers. * [SSHD-1034](https://issues.apache.org/jira/browse/SSHD-1034) Rename `org.apache.sshd.common.ForwardingFilter` to `Forwarder`. * [SSHD-1035](https://issues.apache.org/jira/browse/SSHD-1035) Move property definitions to common locations. * [SSHD-1038](https://issues.apache.org/jira/browse/SSHD-1035) Refactor packages from a module into a cleaner hierarchy. diff --git a/README.md b/README.md index 8e79789..82778d7 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ based applications requiring SSH support. * [RFC 4716 - The Secure Shell (SSH) Public Key File Format](https://tools.ietf.org/html/rfc4716) * [RFC 5208 - Public-Key Cryptography Standards (PKCS) #8 - version 1.2](https://tools.ietf.org/html/rfc5208) * [RFC 5480 - Elliptic Curve Cryptography Subject Public Key Information](https://tools.ietf.org/html/rfc5480) +* [RFC 5647 - AES Galois Counter Mode for the Secure Shell Transport Layer Protocol](https://tools.ietf.org/html/rfc5647) * [RFC 5656 - Elliptic Curve Algorithm Integration in the Secure Shell Transport Layer](https://tools.ietf.org/html/rfc5656) * [RFC 5915 - Elliptic Curve Private Key Structure](https://tools.ietf.org/html/rfc5915) * [RFC 6668 - SHA-2 Data Integrity Verification for the Secure Shell (SSH) Transport Layer Protocol](https://tools.ietf.org/html/rfc6668) @@ -53,7 +54,8 @@ based applications requiring SSH support. ## Implemented/available support -* **Ciphers**: aes128cbc, aes128ctr, aes192cbc, aes192ctr, aes256cbc, aes256ctr, arcfour128, arcfour256, blowfishcbc, tripledescbc +* **Ciphers**: aes128cbc, aes128ctr, aes192cbc, aes192ctr, aes256cbc, aes256ctr, arcfour128, arcfour256, blowfishcbc, tripledescbc, +aes128-...@openssh.com, aes256-...@openssh.com * **Digests**: md5, sha1, sha224, sha256, sha384, sha512 * **Macs**: hmacmd5, hmacmd596, hmacsha1, hmacsha196, hmacsha256, hmacsha512, hmac-sha2-256-...@openssh.com , hmac-sha2-512-...@openssh.com, hmac-sha1-...@openssh.com diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseCipher.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseCipher.java index 7a855d1..1288af0 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseCipher.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseCipher.java @@ -33,6 +33,7 @@ public class BaseCipher implements Cipher { private javax.crypto.Cipher cipher; private final int ivsize; + private final int authSize; private final int kdfSize; private final String algorithm; private final int keySize; @@ -41,9 +42,10 @@ public class BaseCipher implements Cipher { private String s; public BaseCipher( - int ivsize, int kdfSize, String algorithm, + int ivsize, int authSize, int kdfSize, String algorithm, int keySize, String transformation, int blkSize) { this.ivsize = ivsize; + this.authSize = authSize; this.kdfSize = kdfSize; this.algorithm = ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm"); this.keySize = keySize; @@ -72,6 +74,11 @@ public class BaseCipher implements Cipher { } @Override + public int getAuthenticationTagSize() { + return authSize; + } + + @Override public int getKdfSize() { return kdfSize; } @@ -116,6 +123,11 @@ public class BaseCipher implements Cipher { cipher.update(input, inputOffset, inputLen, input, inputOffset); } + @Override + public void updateAAD(byte[] data, int offset, int length) throws Exception { + throw new UnsupportedOperationException(getClass() + " does not support AAD operations"); + } + protected static byte[] resize(byte[] data, int size) { if (data.length > size) { byte[] tmp = new byte[size]; diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseGCMCipher.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseGCMCipher.java new file mode 100644 index 0000000..c73cf32 --- /dev/null +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseGCMCipher.java @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.cipher; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +public class BaseGCMCipher extends BaseCipher { + + protected Mode mode; + protected boolean initialized; + protected CounterGCMParameterSpec parameters; + protected SecretKey secretKey; + + public BaseGCMCipher( + int ivsize, int authSize, int kdfSize, String algorithm, int keySize, String transformation, + int blkSize) { + super(ivsize, authSize, kdfSize, algorithm, keySize, transformation, blkSize); + } + + @Override + protected Cipher createCipherInstance(Mode mode, byte[] key, byte[] iv) throws Exception { + this.mode = mode; + secretKey = new SecretKeySpec(key, getAlgorithm()); + parameters = new CounterGCMParameterSpec(getAuthenticationTagSize() * Byte.SIZE, iv); + return SecurityUtils.getCipher(getTransformation()); + } + + protected Cipher getInitializedCipherInstance() throws Exception { + Cipher cipher = getCipherInstance(); + if (!initialized) { + cipher.init(mode == Mode.Encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, secretKey, parameters); + initialized = true; + } + return cipher; + } + + @Override + public void updateAAD(byte[] data, int offset, int length) throws Exception { + getInitializedCipherInstance().updateAAD(data, offset, length); + } + + @Override + public void update(byte[] input, int inputOffset, int inputLen) throws Exception { + if (mode == Mode.Decrypt) { + inputLen += getAuthenticationTagSize(); + } + Cipher cipher = getInitializedCipherInstance(); + cipher.doFinal(input, inputOffset, inputLen, input, inputOffset); + 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 - Long.BYTES; + long counter = BufferUtils.getLong(iv, off, Long.BYTES); + BufferUtils.putLong(Math.addExact(counter, 1L), iv, off, Long.BYTES); + } + + @Override + public byte[] getIV() { + // JCE implementation of GCM will complain if the reference doesn't change between inits + return iv.clone(); + } + } + +} diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseRC4Cipher.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseRC4Cipher.java index 10ebb0f..a8b1f14 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseRC4Cipher.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BaseRC4Cipher.java @@ -29,7 +29,7 @@ public class BaseRC4Cipher extends BaseCipher { public static final int SKIP_SIZE = 1536; public BaseRC4Cipher(int ivsize, int kdfSize, int keySize, int blkSize) { - super(ivsize, kdfSize, "ARCFOUR", keySize, "RC4", blkSize); + super(ivsize, 0, kdfSize, "ARCFOUR", keySize, "RC4", blkSize); } @Override diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java index 0f8a136..c66bc64 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/BuiltinCiphers.java @@ -46,32 +46,48 @@ import org.apache.sshd.common.util.ValidateUtils; * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public enum BuiltinCiphers implements CipherFactory { - none(Constants.NONE, 0, 0, "None", 0, "None", 0) { + none(Constants.NONE, 0, 0, 0, "None", 0, "None", 0) { @Override public Cipher create() { return new CipherNone(); } }, - aes128cbc(Constants.AES128_CBC, 16, 16, "AES", 128, "AES/CBC/NoPadding", 16), - aes128ctr(Constants.AES128_CTR, 16, 16, "AES", 128, "AES/CTR/NoPadding", 16), - aes192cbc(Constants.AES192_CBC, 16, 24, "AES", 192, "AES/CBC/NoPadding", 16), - aes192ctr(Constants.AES192_CTR, 16, 24, "AES", 192, "AES/CTR/NoPadding", 16), - aes256cbc(Constants.AES256_CBC, 16, 32, "AES", 256, "AES/CBC/NoPadding", 16), - aes256ctr(Constants.AES256_CTR, 16, 32, "AES", 256, "AES/CTR/NoPadding", 16), - arcfour128(Constants.ARCFOUR128, 8, 16, "ARCFOUR", 128, "RC4", 16) { + aes128cbc(Constants.AES128_CBC, 16, 0, 16, "AES", 128, "AES/CBC/NoPadding", 16), + aes128ctr(Constants.AES128_CTR, 16, 0, 16, "AES", 128, "AES/CTR/NoPadding", 16), + aes128gcm(Constants.AES128_GCM, 12, 16, 16, "AES", 128, "AES/GCM/NoPadding", 16) { + @Override + public Cipher create() { + return new BaseGCMCipher( + getIVSize(), getAuthenticationTagSize(), getKdfSize(), getAlgorithm(), + getKeySize(), getTransformation(), getCipherBlockSize()); + } + }, + aes256gcm(Constants.AES256_GCM, 12, 16, 32, "AES", 256, "AES/GCM/NoPadding", 16) { + @Override + public Cipher create() { + return new BaseGCMCipher( + getIVSize(), getAuthenticationTagSize(), getKdfSize(), getAlgorithm(), + getKeySize(), getTransformation(), getCipherBlockSize()); + } + }, + aes192cbc(Constants.AES192_CBC, 16, 0, 24, "AES", 192, "AES/CBC/NoPadding", 16), + aes192ctr(Constants.AES192_CTR, 16, 0, 24, "AES", 192, "AES/CTR/NoPadding", 16), + aes256cbc(Constants.AES256_CBC, 16, 0, 32, "AES", 256, "AES/CBC/NoPadding", 16), + aes256ctr(Constants.AES256_CTR, 16, 0, 32, "AES", 256, "AES/CTR/NoPadding", 16), + arcfour128(Constants.ARCFOUR128, 8, 0, 16, "ARCFOUR", 128, "RC4", 16) { @Override public Cipher create() { return new BaseRC4Cipher(getIVSize(), getKdfSize(), getKeySize(), getCipherBlockSize()); } }, - arcfour256(Constants.ARCFOUR256, 8, 32, "ARCFOUR", 256, "RC4", 32) { + arcfour256(Constants.ARCFOUR256, 8, 0, 32, "ARCFOUR", 256, "RC4", 32) { @Override public Cipher create() { return new BaseRC4Cipher(getIVSize(), getKdfSize(), getKeySize(), getCipherBlockSize()); } }, - blowfishcbc(Constants.BLOWFISH_CBC, 8, 16, "Blowfish", 128, "Blowfish/CBC/NoPadding", 8), - tripledescbc(Constants.TRIPLE_DES_CBC, 8, 24, "DESede", 192, "DESede/CBC/NoPadding", 8); + blowfishcbc(Constants.BLOWFISH_CBC, 8, 0, 16, "Blowfish", 128, "Blowfish/CBC/NoPadding", 8), + tripledescbc(Constants.TRIPLE_DES_CBC, 8, 0, 24, "DESede", 192, "DESede/CBC/NoPadding", 8); public static final Set<BuiltinCiphers> VALUES = Collections.unmodifiableSet(EnumSet.allOf(BuiltinCiphers.class)); @@ -79,6 +95,7 @@ public enum BuiltinCiphers implements CipherFactory { private final String factoryName; private final int ivsize; + private final int authSize; private final int kdfSize; private final int keysize; private final int blkSize; @@ -87,10 +104,11 @@ public enum BuiltinCiphers implements CipherFactory { private final boolean supported; BuiltinCiphers( - String factoryName, int ivsize, int kdfSize, + String factoryName, int ivsize, int authSize, int kdfSize, String algorithm, int keySize, String transformation, int blkSize) { this.factoryName = factoryName; this.ivsize = ivsize; + this.authSize = authSize; this.kdfSize = kdfSize; this.keysize = keySize; this.algorithm = algorithm; @@ -134,6 +152,11 @@ public enum BuiltinCiphers implements CipherFactory { } @Override + public int getAuthenticationTagSize() { + return authSize; + } + + @Override public int getKdfSize() { return kdfSize; } @@ -156,7 +179,7 @@ public enum BuiltinCiphers implements CipherFactory { @Override public Cipher create() { return new BaseCipher( - getIVSize(), getKdfSize(), getAlgorithm(), + getIVSize(), getAuthenticationTagSize(), getKdfSize(), getAlgorithm(), getKeySize(), getTransformation(), getCipherBlockSize()); } @@ -319,10 +342,12 @@ public enum BuiltinCiphers implements CipherFactory { public static final String AES128_CBC = "aes128-cbc"; public static final String AES128_CTR = "aes128-ctr"; + public static final String AES128_GCM = "aes128-...@openssh.com"; public static final String AES192_CBC = "aes192-cbc"; public static final String AES192_CTR = "aes192-ctr"; public static final String AES256_CBC = "aes256-cbc"; public static final String AES256_CTR = "aes256-ctr"; + public static final String AES256_GCM = "aes256-...@openssh.com"; public static final String ARCFOUR128 = "arcfour128"; public static final String ARCFOUR256 = "arcfour256"; public static final String BLOWFISH_CBC = "blowfish-cbc"; diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/Cipher.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/Cipher.java index 019f26e..09e5aaf 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/Cipher.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/Cipher.java @@ -65,6 +65,43 @@ public interface Cipher extends CipherInformation { void update(byte[] input, int inputOffset, int inputLen) throws Exception; /** + * Adds the provided input data as additional authenticated data during encryption or decryption. + * + * @param data The data to authenticate + * @throws Exception If failed to execute + */ + default void updateAAD(byte[] data) throws Exception { + updateAAD(data, 0, NumberUtils.length(data)); + } + + /** + * 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 + * @throws Exception If failed to execute + */ + void updateAAD(byte[] data, int offset, int length) throws Exception; + + /** + * 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 + * @throws Exception If failed to execute + */ + default void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) throws Exception { + updateAAD(input, offset, aadLen); + update(input, offset + aadLen, inputLen); + } + + /** * @param xform The full cipher transformation - e.g., AES/CBC/NoPadding - never {@code null}/empty * @param keyLength The required key length in bits - always positive * @return {@code true} if the cipher transformation <U>and</U> required key length are supported diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherInformation.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherInformation.java index 743b3b2..96a74c5 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherInformation.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherInformation.java @@ -39,6 +39,11 @@ public interface CipherInformation extends AlgorithmNameProvider, KeySizeIndicat int getIVSize(); /** + * @return Size of the authentication tag (AT) in bytes or 0 if this cipher does not support authentication + */ + int getAuthenticationTagSize(); + + /** * @return Size of block data used by the cipher (in bytes). For stream ciphers this value is (currently) used to * indicate some average work buffer size to be used for the automatic re-keying mechanism described in * <a href="https://tools.ietf.org/html/rfc4253#section-9">RFC 4253 - Section 9</a> diff --git a/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherNone.java b/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherNone.java index 90ddb73..0a312a2 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherNone.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/cipher/CipherNone.java @@ -50,6 +50,11 @@ public class CipherNone implements Cipher { } @Override + public int getAuthenticationTagSize() { + return 0; + } + + @Override public int getKdfSize() { return 16; // dummy - not zero in order to avoid some code that uses it as divisor } @@ -65,6 +70,11 @@ public class CipherNone implements Cipher { } @Override + public void updateAAD(byte[] data, int offset, int length) throws Exception { + // ignored - always succeeds + } + + @Override public void update(byte[] input, int inputOffset, int inputLen) throws Exception { // ignored - always succeeds } diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java index 8a2aa58..a7e3a59 100644 --- a/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java +++ b/sshd-common/src/main/java/org/apache/sshd/common/util/buffer/BufferUtils.java @@ -385,6 +385,23 @@ public final class BufferUtils { return l; } + public static long getLong(byte[] buf, int off, int len) { + if (len < Long.BYTES) { + throw new IllegalArgumentException("Not enough data for a long: required=" + Long.BYTES + ", 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; + } + /** * Writes a 32-bit value in network order (i.e., MSB 1st) * @@ -488,6 +505,23 @@ public final class BufferUtils { return Integer.BYTES; } + public static int putLong(long value, byte[] buf, int off, int len) { + if (len < Long.BYTES) { + throw new IllegalArgumentException("Not enough data for a long: required=" + Long.BYTES + ", 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 Long.BYTES; + } + public static boolean equals(byte[] a1, byte[] a2) { int len1 = NumberUtils.length(a1); int len2 = NumberUtils.length(a2); diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES128GCMTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES128GCMTest.java new file mode 100644 index 0000000..217ba35 --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES128GCMTest.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.cipher; + +import org.junit.Test; + +public class AES128GCMTest extends BaseAuthenticatedCipherTest { + + public AES128GCMTest() { + super(); + } + + @Test + public void testEncryptDecrypt() throws Exception { + ensureFullCipherInformationSupported(BuiltinCiphers.aes128gcm); + testAuthenticatedEncryptDecrypt(BuiltinCiphers.aes128gcm); + } +} diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256GCMTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256GCMTest.java new file mode 100644 index 0000000..5a8f23a --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/AES256GCMTest.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.cipher; + +import org.junit.Test; + +public class AES256GCMTest extends BaseAuthenticatedCipherTest { + + public AES256GCMTest() { + super(); + } + + @Test + public void testEncryptDecrypt() throws Exception { + ensureFullCipherInformationSupported(BuiltinCiphers.aes256gcm); + testAuthenticatedEncryptDecrypt(BuiltinCiphers.aes256gcm); + } +} diff --git a/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseAuthenticatedCipherTest.java b/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseAuthenticatedCipherTest.java new file mode 100644 index 0000000..dd20287 --- /dev/null +++ b/sshd-common/src/test/java/org/apache/sshd/common/cipher/BaseAuthenticatedCipherTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.cipher; + +import java.nio.charset.StandardCharsets; + +import javax.crypto.AEADBadTagException; + +import org.apache.sshd.common.NamedFactory; + +public abstract class BaseAuthenticatedCipherTest extends BaseCipherTest { + + protected BaseAuthenticatedCipherTest() { + super(); + } + + protected void testAuthenticatedEncryptDecrypt(NamedFactory<Cipher> factory) throws Exception { + String factoryName = factory.getName(); + Cipher enc = factory.create(); + byte[] key = new byte[enc.getKdfSize()]; + 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 " + factoryName + ']'; + 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 (AEADBadTagException e) { + assertNotNull(e); + } + } + +} diff --git a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java index d6c6bb2..ee352fc 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/BaseBuilder.java @@ -72,6 +72,8 @@ public class BaseBuilder<T extends AbstractFactoryManager, S extends BaseBuilder BuiltinCiphers.aes128ctr, BuiltinCiphers.aes192ctr, BuiltinCiphers.aes256ctr, + BuiltinCiphers.aes128gcm, + BuiltinCiphers.aes256gcm, BuiltinCiphers.arcfour256, BuiltinCiphers.arcfour128, BuiltinCiphers.aes128cbc, diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java index 8e2655e..dcc9f40 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/AbstractSession.java @@ -1108,9 +1108,11 @@ public abstract class AbstractSession extends SessionHelper { // Since the caller claims to know how many bytes they will need // increase their request to account for our headers/footers if // they actually send exactly this amount. - boolean etmMode = (outMac == null) ? false : outMac.isEncryptThenMac(); - int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode); - len = SshConstants.SSH_PACKET_HEADER_LEN + len + pad + Byte.BYTES /* the pad length byte */; + boolean etmMode = outMac != null && outMac.isEncryptThenMac(); + int authLen = outCipher != null ? outCipher.getAuthenticationTagSize() : 0; + boolean authMode = authLen > 0; + int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode || authMode); + len += SshConstants.SSH_PACKET_HEADER_LEN + pad + authLen; if (outMac != null) { len += outMacSize; } @@ -1204,10 +1206,14 @@ public abstract class AbstractSession extends SessionHelper { } // Compute padding length - boolean etmMode = (outMac == null) ? false : outMac.isEncryptThenMac(); - int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode); + boolean etmMode = outMac != null && outMac.isEncryptThenMac(); + int authSize = outCipher != null ? outCipher.getAuthenticationTagSize() : 0; + boolean authMode = authSize > 0; int oldLen = len; - len = len + pad + Byte.BYTES /* the pad length byte */; + + int pad = PacketWriter.calculatePadLength(len, outCipherSize, etmMode || authMode); + + len += Byte.BYTES + pad; if (traceEnabled) { log.trace("encode({}) packet #{} command={}[{}] len={}, pad={}, mac={}", @@ -1224,7 +1230,11 @@ public abstract class AbstractSession extends SessionHelper { random.fill(buffer.array(), buffer.wpos() - pad, pad); } - if (etmMode) { + if (authMode) { + int wpos = buffer.wpos(); + buffer.wpos(wpos + authSize); + aeadOutgoingBuffer(buffer, off, len); + } else if (etmMode) { // Do not encrypt the length field encryptOutgoingBuffer(buffer, off + Integer.BYTES, len); appendOutgoingMac(buffer, off, len); @@ -1250,6 +1260,16 @@ public abstract class AbstractSession extends SessionHelper { } } + protected void aeadOutgoingBuffer(Buffer buf, int offset, int len) throws Exception { + if (outCipher == null || outCipher.getAuthenticationTagSize() == 0) { + throw new IllegalArgumentException("AEAD mode requires an AEAD cipher"); + } + byte[] data = buf.array(); + outCipher.updateWithAAD(data, offset, Integer.BYTES, len); + int blocksCount = len / outCipherSize; + outBlocksCount.addAndGet(Math.max(1, blocksCount)); + } + protected void appendOutgoingMac(Buffer buf, int offset, int len) throws Exception { if (outMac == null) { return; @@ -1284,7 +1304,11 @@ public abstract class AbstractSession extends SessionHelper { protected void decode() throws Exception { // Decoding loop for (;;) { - boolean etmMode = (inMac == null) ? false : inMac.isEncryptThenMac(); + + int authSize = inCipher != null ? inCipher.getAuthenticationTagSize() : 0; + boolean authMode = authSize > 0; + int macSize = inMac != null ? inMacSize : 0; + boolean etmMode = inMac != null && inMac.isEncryptThenMac(); // Wait for beginning of packet if (decoderState == 0) { // The read position should always be 0 at this point because we have compacted this buffer @@ -1298,11 +1322,14 @@ public abstract class AbstractSession extends SessionHelper { * However, we currently do not have ciphers with a block size of less than 8 we avoid un-necessary * Math.max(minBufLen, 8) for each and every packet */ - int minBufLen = etmMode ? Integer.BYTES : inCipherSize; + int minBufLen = etmMode || authMode ? Integer.BYTES : inCipherSize; // If we have received enough bytes, start processing those if (decoderBuffer.available() > minBufLen) { - // Decrypt the first bytes so we can extract the packet length - if ((inCipher != null) && (!etmMode)) { + if (authMode) { + // RFC 5647: packet length encoded in additional data + inCipher.updateAAD(decoderBuffer.array(), 0, Integer.BYTES); + } else if ((inCipher != null) && (!etmMode)) { + // Decrypt the first bytes so we can extract the packet length inCipher.update(decoderBuffer.array(), 0, inCipherSize); int blocksCount = inCipherSize / inCipher.getCipherBlockSize(); @@ -1333,11 +1360,15 @@ public abstract class AbstractSession extends SessionHelper { } else if (decoderState == 1) { // The read position should always be after reading the packet length at this point assert decoderBuffer.rpos() == Integer.BYTES; - int macSize = (inMac != null) ? inMacSize : 0; // Check if the packet has been fully received - if (decoderBuffer.available() >= (decoderLength + macSize)) { + if (decoderBuffer.available() >= (decoderLength + macSize + authSize)) { byte[] data = decoderBuffer.array(); - if (etmMode) { + if (authMode) { + inCipher.update(data, Integer.BYTES /* packet length is handled by AAD */, decoderLength); + + int blocksCount = decoderLength / inCipherSize; + inBlocksCount.addAndGet(Math.max(1, blocksCount)); + } else if (etmMode) { validateIncomingMac(data, 0, decoderLength + Integer.BYTES); if (inCipher != null) { @@ -1400,7 +1431,7 @@ public abstract class AbstractSession extends SessionHelper { handleMessage(packet); // Set ready to handle next packet - decoderBuffer.rpos(decoderLength + Integer.BYTES + macSize); + decoderBuffer.rpos(decoderLength + Integer.BYTES + macSize + authSize); decoderBuffer.wpos(wpos); decoderBuffer.compact(); decoderState = 0; @@ -1611,13 +1642,18 @@ public abstract class AbstractSession extends SessionHelper { e_s2c = resizeKey(e_s2c, s2ccipher.getKdfSize(), hash, k, h); s2ccipher.init(serverSession ? Cipher.Mode.Encrypt : Cipher.Mode.Decrypt, e_s2c, iv_s2c); - value = getNegotiatedKexParameter(KexProposalOption.S2CMAC); - Mac s2cmac = NamedFactory.create(getMacFactories(), value); - if (s2cmac == null) { - throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "Unknown s2c MAC: " + value); + Mac s2cmac; + if (s2ccipher.getAuthenticationTagSize() == 0) { + value = getNegotiatedKexParameter(KexProposalOption.S2CMAC); + s2cmac = NamedFactory.create(getMacFactories(), value); + if (s2cmac == null) { + throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "Unknown s2c MAC: " + value); + } + mac_s2c = resizeKey(mac_s2c, s2cmac.getBlockSize(), hash, k, h); + s2cmac.init(mac_s2c); + } else { + s2cmac = null; } - mac_s2c = resizeKey(mac_s2c, s2cmac.getBlockSize(), hash, k, h); - s2cmac.init(mac_s2c); value = getNegotiatedKexParameter(KexProposalOption.S2CCOMP); Compression s2ccomp = NamedFactory.create(getCompressionFactories(), value); @@ -1631,13 +1667,18 @@ public abstract class AbstractSession extends SessionHelper { e_c2s = resizeKey(e_c2s, c2scipher.getKdfSize(), hash, k, h); c2scipher.init(serverSession ? Cipher.Mode.Decrypt : Cipher.Mode.Encrypt, e_c2s, iv_c2s); - value = getNegotiatedKexParameter(KexProposalOption.C2SMAC); - Mac c2smac = NamedFactory.create(getMacFactories(), value); - if (c2smac == null) { - throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "Unknown c2s MAC: " + value); + Mac c2smac; + if (c2scipher.getAuthenticationTagSize() == 0) { + value = getNegotiatedKexParameter(KexProposalOption.C2SMAC); + c2smac = NamedFactory.create(getMacFactories(), value); + if (c2smac == null) { + throw new SshException(SshConstants.SSH2_DISCONNECT_MAC_ERROR, "Unknown c2s MAC: " + value); + } + mac_c2s = resizeKey(mac_c2s, c2smac.getBlockSize(), hash, k, h); + c2smac.init(mac_c2s); + } else { + c2smac = null; } - mac_c2s = resizeKey(mac_c2s, c2smac.getBlockSize(), hash, k, h); - c2smac.init(mac_c2s); value = getNegotiatedKexParameter(KexProposalOption.C2SCOMP); Compression c2scomp = NamedFactory.create(getCompressionFactories(), value); @@ -1662,12 +1703,12 @@ public abstract class AbstractSession extends SessionHelper { } outCipherSize = outCipher.getCipherBlockSize(); - outMacSize = outMac.getBlockSize(); + outMacSize = outMac != null ? outMac.getBlockSize() : 0; // TODO add support for configurable compression level outCompression.init(Compression.Type.Deflater, -1); inCipherSize = inCipher.getCipherBlockSize(); - inMacSize = inMac.getBlockSize(); + inMacSize = inMac != null ? inMac.getBlockSize() : 0; inMacResult = new byte[inMacSize]; // TODO add support for configurable compression level inCompression.init(Compression.Type.Inflater, -1); diff --git a/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java b/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java index 1022f6c..33f1810 100644 --- a/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java +++ b/sshd-core/src/test/java/org/apache/sshd/common/cipher/BuiltinCiphersTest.java @@ -37,6 +37,7 @@ import org.apache.sshd.common.NamedFactory; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.cipher.BuiltinCiphers.ParseResult; import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.server.SshServer; import org.apache.sshd.util.test.BaseTestSupport; import org.junit.FixMethodOrder; @@ -193,10 +194,12 @@ public class BuiltinCiphersTest extends BaseTestSupport { rnd.nextBytes(iv); cipher.init(Cipher.Mode.Encrypt, key, iv); - byte[] data = new byte[cipher.getCipherBlockSize()]; - rnd.nextBytes(data); + byte[] data = new byte[cipher.getCipherBlockSize() + cipher.getAuthenticationTagSize()]; + for (int i = 0; i < cipher.getCipherBlockSize(); i += Integer.BYTES) { + BufferUtils.putUInt(Integer.toUnsignedLong(rnd.nextInt()), data, i, Integer.BYTES); + } - cipher.update(data); + cipher.update(data, 0, cipher.getCipherBlockSize()); } @Test