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