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

Reply via email to