This is an automated email from the ASF dual-hosted git repository.

twolf pushed a commit to branch dev_3.0
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git

commit 79a196bc0fe06d88368d390b6796658407312e9d
Author: Thomas Wolf <[email protected]>
AuthorDate: Wed Sep 17 22:37:47 2025 +0200

    ChaCha20-Poly1305: add faster implementations for Java 11+
    
    On Java11+, we can speed up the ChaCha20-Poly1305 cipher considerably
    by using the JDK's built-in ChaCha20 cipher.
    
    A small improvement is also possible in Poly1305Mac by using an
    IntBuffer instead of combining values from bytes using shifts and adds.
---
 sshd-benchmarks/pom.xml                            |   3 +
 .../ciphers/chacha20/ChaChaBenchmark.java          | 127 +++++++++
 sshd-common/pom.xml                                |  15 +-
 .../sshd/common/cipher/AbstractChaCha20Cipher.java |  70 +++++
 .../apache/sshd/common/cipher/BuiltinCiphers.java  |   2 +-
 .../apache/sshd/common/cipher/ChaCha20Cipher.java  |  42 +--
 .../sshd/common/cipher/ChaCha20CipherFactory.java  |  42 +++
 .../sshd/common/cipher/ChaCha20CipherFactory.java  | 202 ++++++++++++++
 .../org/apache/sshd/common/mac/Poly1305Mac.java    | 295 +++++++++++++++++++++
 .../sshd/common/cipher/ChaCha20CipherTest.java     |   2 +-
 10 files changed, 756 insertions(+), 44 deletions(-)

diff --git a/sshd-benchmarks/pom.xml b/sshd-benchmarks/pom.xml
index a18069cf8..62faabac9 100644
--- a/sshd-benchmarks/pom.xml
+++ b/sshd-benchmarks/pom.xml
@@ -138,6 +138,9 @@
                             <transformers>
                                 <transformer 
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                     <mainClass>org.openjdk.jmh.Main</mainClass>
+                                    <manifestEntries>
+                                        <Multi-Release>true</Multi-Release>
+                                    </manifestEntries>
                                 </transformer>
                                 <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"
 />
                             </transformers>
diff --git 
a/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/ciphers/chacha20/ChaChaBenchmark.java
 
b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/ciphers/chacha20/ChaChaBenchmark.java
new file mode 100644
index 000000000..a72c11c4a
--- /dev/null
+++ 
b/sshd-benchmarks/src/main/java/org/apache/sshd/benchmarks/ciphers/chacha20/ChaChaBenchmark.java
@@ -0,0 +1,127 @@
+/*
+ * 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.benchmarks.ciphers.chacha20;
+
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.sshd.common.cipher.ChaCha20Cipher;
+import org.apache.sshd.common.cipher.ChaCha20CipherFactory;
+import org.apache.sshd.common.cipher.Cipher;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.runner.Runner;
+import org.openjdk.jmh.runner.RunnerException;
+import org.openjdk.jmh.runner.options.Options;
+import org.openjdk.jmh.runner.options.OptionsBuilder;
+
+public final class ChaChaBenchmark {
+
+    private ChaChaBenchmark() {
+        super();
+    }
+
+    @State(Scope.Benchmark)
+    public static class CipherBenchmark {
+
+        private static final int SIZE = 32 * 1024;
+
+        private static final Random RND = new SecureRandom();
+
+        private ChaCha20Cipher cipher;
+        private Cipher fromFactory;
+
+        private byte[] a;
+
+        public CipherBenchmark() {
+            super();
+        }
+
+        @Setup(Level.Trial)
+        public void setup() throws Exception {
+            cipher = new ChaCha20Cipher();
+            byte[] key = new byte[cipher.getKdfSize()];
+            byte[] iv = new byte[cipher.getIVSize()];
+            RND.nextBytes(key);
+            iv[iv.length - 1] = 42;
+            cipher.init(Cipher.Mode.Encrypt, key, iv);
+            fromFactory = ChaCha20CipherFactory.INSTANCE.get();
+            if (cipher.getClass().equals(fromFactory.getClass())) {
+                throw new IllegalStateException("Ciphers are equal; 
benchmarking for comparison makes no sense.");
+            }
+            fromFactory.init(Cipher.Mode.Encrypt, key, iv);
+            setupData();
+            encrypt();
+            byte[] old = Arrays.copyOf(a, a.length);
+            setupData();
+            encryptFromFactory();
+            if (!Arrays.equals(old, a)) {
+                throw new IllegalStateException("Encryption error");
+            }
+        }
+
+        @Setup(Level.Iteration)
+        public void setupData() {
+            a = new byte[SIZE + 512];
+            for (int i = 0; i < SIZE; i++) {
+                a[i] = (byte) (i & 0xff);
+            }
+        }
+
+        @Benchmark
+        @Warmup(iterations = 4)
+        @Measurement(iterations = 10)
+        @BenchmarkMode(Mode.AverageTime)
+        @OutputTimeUnit(TimeUnit.NANOSECONDS)
+        public void encryptFromFactory() throws Exception {
+            fromFactory.updateAAD(a, 0, 4);
+            fromFactory.update(a, 4, SIZE);
+        }
+
+        @Benchmark
+        @Warmup(iterations = 4)
+        @Measurement(iterations = 10)
+        @BenchmarkMode(Mode.AverageTime)
+        @OutputTimeUnit(TimeUnit.NANOSECONDS)
+        public void encrypt() throws Exception {
+            cipher.updateAAD(a, 0, 4);
+            cipher.update(a, 4, SIZE);
+        }
+
+    }
+
+    public static void main(String[] args) throws RunnerException {
+        Options opt = new OptionsBuilder()
+                .include(ChaChaBenchmark.class.getSimpleName() + '.' + 
CipherBenchmark.class.getSimpleName()) //
+                .forks(1) //
+                .threads(1) //
+                .build();
+        new Runner(opt).run();
+    }
+}
diff --git a/sshd-common/pom.xml b/sshd-common/pom.xml
index 6ae6e6dbe..5d0fef547 100644
--- a/sshd-common/pom.xml
+++ b/sshd-common/pom.xml
@@ -171,7 +171,20 @@
                 <artifactId>maven-compiler-plugin</artifactId>
                 <executions>
                     <execution>
-                        <id>default-compile-15</id>
+                        <id>compile-11</id>
+                        <goals>
+                            <goal>compile</goal>
+                        </goals>
+                        <configuration>
+                            <compileSourceRoots>
+                                
<compileSourceRoot>${project.basedir}/src/main/java11</compileSourceRoot>
+                            </compileSourceRoots>
+                            <multiReleaseOutput>true</multiReleaseOutput>
+                            <release>11</release>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>compile-15</id>
                         <goals>
                             <goal>compile</goal>
                         </goals>
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/cipher/AbstractChaCha20Cipher.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/AbstractChaCha20Cipher.java
new file mode 100644
index 000000000..b7db12141
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/AbstractChaCha20Cipher.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;
+
+/**
+ * Abstract super class for ChaCha20-Poly1305 implementations.
+ */
+public abstract class AbstractChaCha20Cipher implements Cipher {
+
+    protected AbstractChaCha20Cipher() {
+        super();
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return "ChaCha20";
+    }
+
+    @Override
+    public String getTransformation() {
+        return "ChaCha20";
+    }
+
+    @Override
+    public int getIVSize() {
+        return 8;
+    }
+
+    @Override
+    public int getAuthenticationTagSize() {
+        return 16;
+    }
+
+    @Override
+    public int getCipherBlockSize() {
+        return 8;
+    }
+
+    @Override
+    public int getKdfSize() {
+        return 64;
+    }
+
+    @Override
+    public int getKeySize() {
+        return 512;
+    }
+
+    @Override
+    public String toString() {
+        return "chacha20-poly1305";
+    }
+}
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 4c68051a3..1d54fff77 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
@@ -154,7 +154,7 @@ public enum BuiltinCiphers implements CipherFactory {
     cc20p1305_openssh(Constants.CC20P1305_OPENSSH, 8, 16, "ChaCha", 512, 
"ChaCha", 8) {
         @Override
         public Cipher create() {
-            return new ChaCha20Cipher();
+            return ChaCha20CipherFactory.INSTANCE.get();
         }
 
         @Override
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20Cipher.java 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20Cipher.java
index e82b4bd40..e4ebde8b6 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20Cipher.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20Cipher.java
@@ -34,7 +34,7 @@ import org.apache.sshd.common.util.buffer.BufferUtils;
  * <a 
href="https://github.com/openbsd/src/blob/master/usr.bin/ssh/PROTOCOL.chacha20poly1305";>OpenSSH
  * ChaCha20-Poly1305</a> cipher extension.
  */
-public class ChaCha20Cipher implements Cipher {
+public class ChaCha20Cipher extends AbstractChaCha20Cipher {
     protected final ChaChaEngine headerEngine = new ChaChaEngine();
     protected final ChaChaEngine bodyEngine = new ChaChaEngine();
     protected final Mac mac;
@@ -44,11 +44,6 @@ public class ChaCha20Cipher implements Cipher {
         this.mac = new Poly1305Mac();
     }
 
-    @Override
-    public String getAlgorithm() {
-        return "ChaCha20";
-    }
-
     @Override
     public void init(Mode mode, byte[] key, byte[] iv) throws Exception {
         this.mode = mode;
@@ -103,41 +98,6 @@ public class ChaCha20Cipher implements Cipher {
         mac.init(bodyEngine.polyKey());
     }
 
-    @Override
-    public String getTransformation() {
-        return "ChaCha20";
-    }
-
-    @Override
-    public int getIVSize() {
-        return 8;
-    }
-
-    @Override
-    public int getAuthenticationTagSize() {
-        return 16;
-    }
-
-    @Override
-    public int getCipherBlockSize() {
-        return 8;
-    }
-
-    @Override
-    public int getKdfSize() {
-        return 64;
-    }
-
-    @Override
-    public int getKeySize() {
-        return 512;
-    }
-
-    @Override
-    public String toString() {
-        return "chacha20-poly1305";
-    }
-
     protected static class ChaChaEngine {
         private static final int BLOCK_BYTES = 64;
         private static final int BLOCK_INTS = BLOCK_BYTES / Integer.BYTES;
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
new file mode 100644
index 000000000..91cef70dc
--- /dev/null
+++ 
b/sshd-common/src/main/java/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
@@ -0,0 +1,42 @@
+/*
+ * 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.util.function.Supplier;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ChaCha20CipherFactory implements Supplier<Cipher> {
+
+    public static final ChaCha20CipherFactory INSTANCE = new 
ChaCha20CipherFactory();
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ChaCha20CipherFactory.class);
+
+    private ChaCha20CipherFactory() {
+        super();
+    }
+
+    @Override
+    public Cipher get() {
+        LOG.debug("Using Java 8 ChaCha20 factory");
+        return new ChaCha20Cipher();
+    }
+
+}
diff --git 
a/sshd-common/src/main/java11/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
 
b/sshd-common/src/main/java11/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
new file mode 100644
index 000000000..d7a3eb6c5
--- /dev/null
+++ 
b/sshd-common/src/main/java11/org/apache/sshd/common/cipher/ChaCha20CipherFactory.java
@@ -0,0 +1,202 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+import javax.crypto.AEADBadTagException;
+import javax.crypto.spec.ChaCha20ParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.sshd.common.mac.Mac;
+import org.apache.sshd.common.mac.Poly1305Mac;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class ChaCha20CipherFactory implements Supplier<Cipher> {
+
+    public static final ChaCha20CipherFactory INSTANCE = new 
ChaCha20CipherFactory();
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(ChaCha20CipherFactory.class);
+
+    private static final AtomicReference<Boolean> SUPPORTED = new 
AtomicReference<>();
+
+    private ChaCha20CipherFactory() {
+        super();
+    }
+
+    @Override
+    public Cipher get() {
+        if (hasChaCha20()) {
+            LOG.debug("Using SunJCE ChaCha20");
+            return ChaCha20Jdk.get();
+        }
+        // If there is no SunJCE provider, fall back to using own 
implementation.
+        LOG.debug("Using Java11 factory, but Java 8 ChaCha20.");
+        return new ChaCha20Cipher();
+    }
+
+    private boolean hasChaCha20() {
+        Boolean supported = SUPPORTED.get();
+        if (supported == null) {
+            try {
+                javax.crypto.Cipher cipher = 
javax.crypto.Cipher.getInstance("ChaCha20", "SunJCE");
+                supported = Boolean.valueOf(cipher != null);
+            } catch (GeneralSecurityException e) {
+                supported = Boolean.FALSE;
+            }
+            if (!SUPPORTED.compareAndSet(null, supported)) {
+                supported = SUPPORTED.get();
+            }
+        }
+        return supported.booleanValue();
+    }
+
+    private static class ChaCha20Jdk extends AbstractChaCha20Cipher {
+        protected final javax.crypto.Cipher headerEngine;
+        protected final javax.crypto.Cipher bodyEngine;
+        protected final Mac mac = new Poly1305Mac();
+        protected Mode mode;
+
+        private byte[] nonce;
+        private long initialNonce;
+        private SecretKeySpec k1, k2;
+
+        static Cipher get() {
+            try {
+                javax.crypto.Cipher header = 
javax.crypto.Cipher.getInstance("ChaCha20", "SunJCE");
+                javax.crypto.Cipher body = 
javax.crypto.Cipher.getInstance("ChaCha20", "SunJCE");
+                return new ChaCha20Jdk(header, body);
+            } catch (GeneralSecurityException e) {
+                // Should not happen; we check before we call get().
+                throw new IllegalStateException(e.getMessage(), e);
+            }
+        }
+
+        private ChaCha20Jdk(javax.crypto.Cipher header, javax.crypto.Cipher 
body) {
+            this.headerEngine = header;
+            this.bodyEngine = body;
+        }
+
+        @Override
+        public void init(Mode mode, byte[] key, byte[] iv) throws Exception {
+            this.mode = mode;
+
+            long hiBits = BufferUtils.getUInt(iv, 0, 4);
+            ValidateUtils.checkState(hiBits == 0, "ChaCha20 nonce is not a 
valid SSH packet sequence number");
+            initialNonce = BufferUtils.getUInt(iv, 4, 8);
+            // In: 64 bytes key (512bits) and 8 bytes (64bits) nonce (IV)
+            // JDK requires 32 bytes for the key (256 bits), and 12 bytes (96 
bits) for the nonce.
+            // JDK implements ChaCha20 as specified in RFC 8439. SSH uses the 
original version.
+            //
+            // JDK uses a 32bit counter plus the 96 bit nonce where SSH uses a 
64bit counter and
+            // a 64bit nonce. But in SSH, the hi 32 bits of the counter are 
always zero. Encryption
+            // happens at SSH packet level, and SSH packets have a maximum 
length that is way below
+            // 4GB. (More like a few kB, typically 32kB.) The packet sequence 
number goes into the
+            // nonce, and is incremented with each packet. The nonce in SSH 
thus always has only
+            // the last 32 bits set, the other bits are zero.
+            //
+            // Because of this behavior of SSH, we can simply provide a 96bit 
nonce by concatenating
+            // 32 zero bits with the given 64 IV bits, and then the RFC 8439 
algorithm can be used.
+            // This works because the ChaCha20 spec treat the counter as a 
little-endian integer,
+            // while the SSH nonce is the packet sequence number in big-endian 
format. So the middle
+            // 64 bits of the concatenation of counter and nonce are always 
zero, irrespective of
+            // whether the 64bit values are used for both, or a 32bit counter 
and a 96bit nonce.
+            //
+            // See also https://datatracker.ietf.org/doc/html/rfc8439 .
+            nonce = new byte[12];
+            System.arraycopy(iv, 4, nonce, 8, 4);
+            AlgorithmParameterSpec algorithmParameterSpec = new 
ChaCha20ParameterSpec(nonce, 0);
+            k1 = new SecretKeySpec(Arrays.copyOfRange(key, 0, 32), "ChaCha20");
+            bodyEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k1, 
algorithmParameterSpec);
+            mac.init(computePolyMacKey());
+
+            k2 = new SecretKeySpec(Arrays.copyOfRange(key, 32, 64), 
"ChaCha20");
+            headerEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k2, 
algorithmParameterSpec);
+        }
+
+        @Override
+        public void updateAAD(byte[] data, int offset, int length) throws 
Exception {
+            ValidateUtils.checkState(mode != null, "Cipher not initialized");
+            ValidateUtils.checkTrue(length == 4, "AAD only supported for 
encrypted packet length");
+
+            if (mode == Mode.Decrypt) {
+                mac.update(data, offset, length);
+            }
+
+            headerEngine.doFinal(data, offset, length, data, offset);
+
+            if (mode == Mode.Encrypt) {
+                mac.update(data, offset, length);
+            }
+        }
+
+        @Override
+        public void update(byte[] input, int inputOffset, int inputLen) throws 
Exception {
+            ValidateUtils.checkState(mode != null, "Cipher not initialized");
+
+            if (mode == Mode.Decrypt) {
+                mac.update(input, inputOffset, inputLen);
+                byte[] actual = mac.doFinal();
+                if (!Mac.equals(input, inputOffset + inputLen, actual, 0, 
actual.length)) {
+                    throw new AEADBadTagException("Tag mismatch");
+                }
+            }
+
+            bodyEngine.doFinal(input, inputOffset, inputLen, input, 
inputOffset);
+
+            if (mode == Mode.Encrypt) {
+                mac.update(input, inputOffset, inputLen);
+                mac.doFinal(input, inputOffset + inputLen);
+            }
+
+            // Prepare for the next round
+            // Increment the nonce (SSH sequence numbers wrap around on uint32 
overflow)
+            long counter = (BufferUtils.getUInt(nonce, 8, 4) + 1) & 
0xFFFF_FFFFL;
+            ValidateUtils.checkState(counter != initialNonce, "Packet sequence 
number cannot be reused with the same key");
+            BufferUtils.putUInt(counter, nonce, 8, 4);
+            AlgorithmParameterSpec algorithmParameterSpec = new 
ChaCha20ParameterSpec(nonce, 0);
+            bodyEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k1, 
algorithmParameterSpec);
+            mac.init(computePolyMacKey());
+            headerEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k2,
+                    algorithmParameterSpec);
+        }
+
+        private byte[] computePolyMacKey() throws GeneralSecurityException {
+            byte[] block = new byte[Poly1305Mac.KEY_BYTES];
+            bodyEngine.update(block, 0, block.length, block);
+            // JDK does not like re-initialization with same key and nonce, 
even if the counter is different.
+            // But it only checks the latest nonce. So trick it:
+            nonce[0] ^= 1;
+            AlgorithmParameterSpec next = new ChaCha20ParameterSpec(nonce, 1);
+            bodyEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k1, next);
+            nonce[0] ^= 1;
+            next = new ChaCha20ParameterSpec(nonce, 1);
+            bodyEngine.init(mode == Mode.Encrypt ? 
javax.crypto.Cipher.ENCRYPT_MODE : javax.crypto.Cipher.DECRYPT_MODE, k1, next);
+            return block;
+        }
+    }
+
+}
diff --git 
a/sshd-common/src/main/java11/org/apache/sshd/common/mac/Poly1305Mac.java 
b/sshd-common/src/main/java11/org/apache/sshd/common/mac/Poly1305Mac.java
new file mode 100644
index 000000000..6eadc25cb
--- /dev/null
+++ b/sshd-common/src/main/java11/org/apache/sshd/common/mac/Poly1305Mac.java
@@ -0,0 +1,295 @@
+/*
+ * 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.mac;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.IntBuffer;
+import java.security.InvalidKeyException;
+import java.util.Arrays;
+
+import org.apache.sshd.common.util.NumberUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+
+/**
+ * Poly1305 one-time message authentication code. This implementation is 
derived from the public domain C library
+ * <a href="https://github.com/floodyberry/poly1305-donna";>poly1305-donna</a>.
+ *
+ * @see <a href="http://cr.yp.to/mac/poly1305-20050329.pdf";>The Poly1305-AES 
message-authentication code</a>
+ */
+public class Poly1305Mac implements Mac {
+    public static final int KEY_BYTES = 32;
+    private static final int BLOCK_SIZE = 16;
+
+    private long r0;
+    private long r1;
+    private long r2;
+    private long r3;
+    private long r4;
+    private long s1;
+    private long s2;
+    private long s3;
+    private long s4;
+    private long k0;
+    private long k1;
+    private long k2;
+    private long k3;
+
+    private int h0;
+    private int h1;
+    private int h2;
+    private int h3;
+    private int h4;
+    private final byte[] currentBlock = new byte[BLOCK_SIZE];
+    private final IntBuffer currentInts = 
ByteBuffer.wrap(currentBlock).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
+    private int currentBlockOffset;
+
+    public Poly1305Mac() {
+        // empty
+    }
+
+    @Override
+    public String getAlgorithm() {
+        return "Poly1305";
+    }
+
+    @Override
+    public void init(byte[] key) throws Exception {
+        if (NumberUtils.length(key) != KEY_BYTES) {
+            throw new InvalidKeyException("Poly1305 key must be 32 bytes");
+        }
+
+        int t0 = unpackIntLE(key, 0);
+        int t1 = unpackIntLE(key, 4);
+        int t2 = unpackIntLE(key, 8);
+        int t3 = unpackIntLE(key, 12);
+
+        // NOTE: The masks perform the key "clamping" implicitly
+        r0 = t0 & 0x03FFFFFF;
+        r1 = (t0 >>> 26 | t1 << 6) & 0x03FFFF03;
+        r2 = (t1 >>> 20 | t2 << 12) & 0x03FFC0FF;
+        r3 = (t2 >>> 14 | t3 << 18) & 0x03F03FFF;
+        r4 = t3 >>> 8 & 0x000FFFFF;
+
+        // Precompute multipliers
+        s1 = r1 * 5;
+        s2 = r2 * 5;
+        s3 = r3 * 5;
+        s4 = r4 * 5;
+
+        k0 = unpackIntLE(key, 16) & 0xFFFF_FFFFL;
+        k1 = unpackIntLE(key, 20) & 0xFFFF_FFFFL;
+        k2 = unpackIntLE(key, 24) & 0xFFFF_FFFFL;
+        k3 = unpackIntLE(key, 28) & 0xFFFF_FFFFL;
+
+        currentBlockOffset = 0;
+    }
+
+    @Override
+    public void update(byte[] in, int offset, int length) {
+        if (currentBlockOffset > 0) {
+            // There is a partially filled block.
+            int toCopy = Math.min(length, BLOCK_SIZE - currentBlockOffset);
+            System.arraycopy(in, offset, currentBlock, currentBlockOffset, 
toCopy);
+            offset += toCopy;
+            length -= toCopy;
+            currentBlockOffset += toCopy;
+            if (currentBlockOffset == BLOCK_SIZE) {
+                currentInts.clear();
+                processBlock(currentInts, BLOCK_SIZE);
+                currentBlockOffset = 0;
+            }
+            if (length == 0) {
+                return;
+            }
+        }
+        if (length >= BLOCK_SIZE) {
+            int numBlocks = length / BLOCK_SIZE;
+            IntBuffer inB = ByteBuffer.wrap(in, offset, length & ~(BLOCK_SIZE 
- 1)).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer();
+            int bytesProcessed = numBlocks * BLOCK_SIZE;
+            while (numBlocks > 0) {
+                processBlock(inB, BLOCK_SIZE);
+                numBlocks--;
+            }
+            length -= bytesProcessed;
+            offset += bytesProcessed;
+        }
+        if (length > 0) {
+            // Put remaining bytes into internal buffer (length < BLOCK_SIZE 
here).
+            System.arraycopy(in, offset, currentBlock, 0, length);
+            currentBlockOffset = length;
+        }
+    }
+
+    @Override
+    public void updateUInt(long value) {
+        byte[] encoded = new byte[Integer.BYTES];
+        BufferUtils.putUInt(value, encoded);
+        update(encoded);
+    }
+
+    @Override
+    public void doFinal(byte[] out, int offset) throws Exception {
+        if (offset + BLOCK_SIZE > NumberUtils.length(out)) {
+            throw new BufferOverflowException();
+        }
+        if (currentBlockOffset > 0) {
+            if (currentBlockOffset < BLOCK_SIZE) {
+                // padding
+                currentBlock[currentBlockOffset] = 1;
+                for (int i = currentBlockOffset + 1; i < BLOCK_SIZE; i++) {
+                    currentBlock[i] = 0;
+                }
+            }
+            currentInts.clear();
+            processBlock(currentInts, currentBlockOffset);
+        }
+
+        h1 += h0 >>> 26;
+        h0 &= 0x3ffffff;
+        h2 += h1 >>> 26;
+        h1 &= 0x3ffffff;
+        h3 += h2 >>> 26;
+        h2 &= 0x3ffffff;
+        h4 += h3 >>> 26;
+        h3 &= 0x3ffffff;
+        h0 += (h4 >>> 26) * 5;
+        h4 &= 0x3ffffff;
+        h1 += h0 >>> 26;
+        h0 &= 0x3ffffff;
+
+        int g0 = h0 + 5;
+        int b = g0 >>> 26;
+        g0 &= 0x3ffffff;
+        int g1 = h1 + b;
+        b = g1 >>> 26;
+        g1 &= 0x3ffffff;
+        int g2 = h2 + b;
+        b = g2 >>> 26;
+        g2 &= 0x3ffffff;
+        int g3 = h3 + b;
+        b = g3 >>> 26;
+        g3 &= 0x3ffffff;
+        int g4 = h4 + b - (1 << 26);
+
+        b = (g4 >>> 31) - 1;
+        int nb = ~b;
+        h0 = h0 & nb | g0 & b;
+        h1 = h1 & nb | g1 & b;
+        h2 = h2 & nb | g2 & b;
+        h3 = h3 & nb | g3 & b;
+        h4 = h4 & nb | g4 & b;
+
+        long f0 = ((h0 | h1 << 26) & 0xFFFF_FFFFL) + k0;
+        long f1 = ((h1 >>> 6 | h2 << 20) & 0xFFFF_FFFFL) + k1;
+        long f2 = ((h2 >>> 12 | h3 << 14) & 0xFFFF_FFFFL) + k2;
+        long f3 = ((h3 >>> 18 | h4 << 8) & 0xFFFF_FFFFL) + k3;
+
+        packIntLE((int) f0, out, offset);
+        f1 += f0 >>> 32;
+        packIntLE((int) f1, out, offset + 4);
+        f2 += f1 >>> 32;
+        packIntLE((int) f2, out, offset + 8);
+        f3 += f2 >>> 32;
+        packIntLE((int) f3, out, offset + 12);
+
+        reset();
+    }
+
+    private void processBlock(IntBuffer block, int length) {
+
+        int t0 = block.get();
+        int t1 = block.get();
+        int t2 = block.get();
+        int t3 = block.get();
+
+        h0 += t0 & 0x3ffffff;
+        h1 += (t0 >>> 26 | t1 << 6) & 0x3ffffff;
+        h2 += (t1 >>> 20 | t2 << 12) & 0x3ffffff;
+        h3 += (t2 >>> 14 | t3 << 18) & 0x3ffffff;
+        h4 += t3 >>> 8;
+
+        if (length == BLOCK_SIZE) {
+            h4 += 1 << 24;
+        }
+
+        // The high bits of h0 to h4 are guaranteed to be zero, so we can just 
let the compiler extend the ints.
+        // No need to do a & 0xFFFF_FFFFL.
+        long l0 = h0;
+        long l1 = h1;
+        long l2 = h2;
+        long l3 = h3;
+        long l4 = h4;
+        long tp0 = l0 * r0 + l1 * s4 + l2 * s3 + l3 * s2 + l4 * s1;
+        long tp1 = l0 * r1 + l1 * r0 + l2 * s4 + l3 * s3 + l4 * s2;
+        long tp2 = l0 * r2 + l1 * r1 + l2 * r0 + l3 * s4 + l4 * s3;
+        long tp3 = l0 * r3 + l1 * r2 + l2 * r1 + l3 * r0 + l4 * s4;
+        long tp4 = l0 * r4 + l1 * r3 + l2 * r2 + l3 * r1 + l4 * r0;
+
+        h0 = (int) tp0 & 0x3ffffff;
+        tp1 += tp0 >>> 26;
+        h1 = (int) tp1 & 0x3ffffff;
+        tp2 += tp1 >>> 26;
+        h2 = (int) tp2 & 0x3ffffff;
+        tp3 += tp2 >>> 26;
+        h3 = (int) tp3 & 0x3ffffff;
+        tp4 += tp3 >>> 26;
+        h4 = (int) tp4 & 0x3ffffff;
+        h0 += (int) (tp4 >>> 26) * 5;
+        h1 += h0 >>> 26;
+        h0 &= 0x3ffffff;
+    }
+
+    private void reset() {
+        h0 = 0;
+        h1 = 0;
+        h2 = 0;
+        h3 = 0;
+        h4 = 0;
+        currentBlockOffset = 0;
+        Arrays.fill(currentBlock, (byte) 0);
+    }
+
+    @Override
+    public int getBlockSize() {
+        return BLOCK_SIZE;
+    }
+
+    @Override
+    public int getDefaultBlockSize() {
+        return BLOCK_SIZE;
+    }
+
+    public static int unpackIntLE(byte[] buf, int off) {
+        int ret = buf[off++] & 0xFF;
+        ret |= (buf[off++] & 0xFF) << 8;
+        ret |= (buf[off++] & 0xFF) << 16;
+        ret |= (buf[off] & 0xFF) << 24;
+        return ret;
+    }
+
+    public static void packIntLE(int value, byte[] dst, int off) {
+        dst[off++] = (byte) value;
+        dst[off++] = (byte) (value >> 8);
+        dst[off++] = (byte) (value >> 16);
+        dst[off] = (byte) (value >> 24);
+    }
+}
diff --git 
a/sshd-common/src/test/java/org/apache/sshd/common/cipher/ChaCha20CipherTest.java
 
b/sshd-common/src/test/java/org/apache/sshd/common/cipher/ChaCha20CipherTest.java
index e61ac4f9d..499b4d2fe 100644
--- 
a/sshd-common/src/test/java/org/apache/sshd/common/cipher/ChaCha20CipherTest.java
+++ 
b/sshd-common/src/test/java/org/apache/sshd/common/cipher/ChaCha20CipherTest.java
@@ -33,7 +33,7 @@ class ChaCha20CipherTest extends JUnitTestSupport {
 
     @Test
     void encryptDecrypt() throws Exception {
-        ChaCha20Cipher cipher = new ChaCha20Cipher();
+        Cipher cipher = ChaCha20CipherFactory.INSTANCE.get();
         byte[] key = new byte[cipher.getKdfSize()];
         for (int i = 0; i < key.length; i++) {
             key[i] = (byte) (i & 0xff);


Reply via email to