mirror of
https://github.com/hierynomus/sshj.git
synced 2025-12-06 15:20:54 +03:00
Support cipher chacha20-poly1305@openssh.com (#682)
* Added cipher chacha20-poly1305@openssh.com * Small refactoring and remove mutable static buffer Co-authored-by: Jeroen van Erp <jeroen@hierynomus.com>
This commit is contained in:
@@ -64,7 +64,7 @@ In the `examples` directory, there is a separate Maven project that shows how th
|
||||
Implementations / adapters for the following algorithms are included:
|
||||
|
||||
ciphers::
|
||||
`aes{128,192,256}-{cbc,ctr}`, `aes{128,256}-gcm@openssh.com`, `blowfish-{cbc,ctr}`, `3des-{cbc,ctr}`, `twofish{128,192,256}-{cbc,ctr}`, `twofish-cbc`, `serpent{128,192,256}-{cbc,ctr}`, `idea-{cbc,ctr}`, `cast128-{cbc,ctr}`, `arcfour`, `arcfour{128,256}`
|
||||
`aes{128,192,256}-{cbc,ctr}`, `aes{128,256}-gcm@openssh.com`, `blowfish-{cbc,ctr}`, `chacha20-poly1305@openssh.com`, `3des-{cbc,ctr}`, `twofish{128,192,256}-{cbc,ctr}`, `twofish-cbc`, `serpent{128,192,256}-{cbc,ctr}`, `idea-{cbc,ctr}`, `cast128-{cbc,ctr}`, `arcfour`, `arcfour{128,256}`
|
||||
SSHJ also supports the following extended (non official) ciphers: `camellia{128,192,256}-{cbc,ctr}`, `camellia{128,192,256}-{cbc,ctr}@openssh.org`
|
||||
|
||||
key exchange::
|
||||
|
||||
@@ -133,4 +133,4 @@ macs umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.
|
||||
|
||||
TrustedUserCAKeys /etc/ssh/users_rsa_ca.pub
|
||||
|
||||
Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com
|
||||
Ciphers 3des-cbc,blowfish-cbc,aes128-cbc,aes192-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,chacha20-poly1305@openssh.com
|
||||
@@ -47,7 +47,8 @@ class CipherSpec extends IntegrationBaseSpec {
|
||||
BlockCiphers.AES256CBC(),
|
||||
BlockCiphers.AES256CTR(),
|
||||
GcmCiphers.AES128GCM(),
|
||||
GcmCiphers.AES256GCM()]
|
||||
GcmCiphers.AES256GCM(),
|
||||
ChachaPolyCiphers.CHACHA_POLY_OPENSSH()]
|
||||
cipher = cipherFactory.name
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.cipher;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
|
||||
import java.security.spec.AlgorithmParameterSpec;
|
||||
import java.util.Arrays;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
import net.schmizz.sshj.common.SecurityUtils;
|
||||
import net.schmizz.sshj.transport.cipher.BaseCipher;
|
||||
|
||||
public class ChachaPolyCipher extends BaseCipher {
|
||||
|
||||
private static final int CHACHA_KEY_SIZE = 32;
|
||||
private static final int AAD_LENGTH = 4;
|
||||
private static final int POLY_TAG_LENGTH = 16;
|
||||
|
||||
private static final String CIPHER_CHACHA = "CHACHA";
|
||||
private static final String MAC_POLY1305 = "POLY1305";
|
||||
|
||||
private static final byte[] POLY_KEY_INPUT = new byte[32];
|
||||
|
||||
private final int authSize;
|
||||
|
||||
private byte[] encryptedAad;
|
||||
|
||||
protected Mode mode;
|
||||
protected javax.crypto.Cipher aadCipher;
|
||||
protected javax.crypto.Mac mac;
|
||||
protected java.security.Key cipherKey;
|
||||
protected java.security.Key aadCipherKey;
|
||||
|
||||
public ChachaPolyCipher(int authSize, int bsize, String algorithm) {
|
||||
super(0, bsize, algorithm, CIPHER_CHACHA);
|
||||
this.authSize = authSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAuthenticationTagSize() {
|
||||
return authSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSequenceNumber(long seq) {
|
||||
byte[] seqAsBytes = longToBytes(seq);
|
||||
AlgorithmParameterSpec ivSpec = new IvParameterSpec(seqAsBytes);
|
||||
|
||||
try {
|
||||
cipher.init(getMode(mode), cipherKey, ivSpec);
|
||||
aadCipher.init(getMode(mode), aadCipherKey, ivSpec);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
}
|
||||
|
||||
byte[] polyKeyBytes = cipher.update(POLY_KEY_INPUT);
|
||||
cipher.update(POLY_KEY_INPUT); // this update is required to set the block counter of ChaCha to 1
|
||||
try {
|
||||
mac.init(getKeySpec(polyKeyBytes));
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new SSHRuntimeException(e);
|
||||
}
|
||||
|
||||
encryptedAad = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv)
|
||||
throws InvalidKeyException, InvalidAlgorithmParameterException {
|
||||
this.mode = mode;
|
||||
|
||||
cipherKey = getKeySpec(Arrays.copyOfRange(key, 0, CHACHA_KEY_SIZE));
|
||||
aadCipherKey = getKeySpec(Arrays.copyOfRange(key, CHACHA_KEY_SIZE, 2 * CHACHA_KEY_SIZE));
|
||||
|
||||
try {
|
||||
aadCipher = SecurityUtils.getCipher(CIPHER_CHACHA);
|
||||
mac = SecurityUtils.getMAC(MAC_POLY1305);
|
||||
} catch (GeneralSecurityException e) {
|
||||
cipher = null;
|
||||
aadCipher = null;
|
||||
mac = null;
|
||||
throw new SSHRuntimeException(e);
|
||||
}
|
||||
|
||||
setSequenceNumber(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAAD(byte[] data, int offset, int length) {
|
||||
if (offset != 0 || length != AAD_LENGTH) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("updateAAD called with offset %d and length %d", offset, length));
|
||||
}
|
||||
|
||||
if (mode == Mode.Decrypt) {
|
||||
encryptedAad = Arrays.copyOfRange(data, 0, AAD_LENGTH);
|
||||
}
|
||||
|
||||
try {
|
||||
aadCipher.update(data, 0, AAD_LENGTH, data, 0);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new SSHRuntimeException("Error updating data through cipher", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAAD(byte[] data) {
|
||||
updateAAD(data, 0, AAD_LENGTH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(byte[] input, int inputOffset, int inputLen) {
|
||||
if (inputOffset != AAD_LENGTH) {
|
||||
throw new IllegalArgumentException("updateAAD called with inputOffset " + inputOffset);
|
||||
}
|
||||
|
||||
final int macInputLength = AAD_LENGTH + inputLen;
|
||||
|
||||
if (mode == Mode.Decrypt) {
|
||||
byte[] macInput = new byte[macInputLength];
|
||||
System.arraycopy(encryptedAad, 0, macInput, 0, AAD_LENGTH);
|
||||
System.arraycopy(input, AAD_LENGTH, macInput, AAD_LENGTH, inputLen);
|
||||
|
||||
byte[] expectedPolyTag = mac.doFinal(macInput);
|
||||
byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH);
|
||||
if (!Arrays.equals(actualPolyTag, expectedPolyTag)) {
|
||||
throw new SSHRuntimeException("MAC Error");
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
cipher.update(input, AAD_LENGTH, inputLen, input, AAD_LENGTH);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new SSHRuntimeException("Error updating data through cipher", e);
|
||||
}
|
||||
|
||||
if (mode == Mode.Encrypt) {
|
||||
byte[] macInput = Arrays.copyOf(input, macInputLength);
|
||||
byte[] polyTag = mac.doFinal(macInput);
|
||||
System.arraycopy(polyTag, 0, input, macInputLength, POLY_TAG_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] longToBytes(long lng) {
|
||||
return new byte[] { (byte) (lng >> 56), (byte) (lng >> 48), (byte) (lng >> 40), (byte) (lng >> 32),
|
||||
(byte) (lng >> 24), (byte) (lng >> 16), (byte) (lng >> 8), (byte) lng };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hierynomus.sshj.transport.cipher;
|
||||
|
||||
import net.schmizz.sshj.transport.cipher.Cipher;
|
||||
|
||||
public class ChachaPolyCiphers {
|
||||
|
||||
public static Factory CHACHA_POLY_OPENSSH() {
|
||||
return new Factory(16, 512, "chacha20-poly1305@openssh.com", "ChaCha20");
|
||||
}
|
||||
|
||||
public static class Factory
|
||||
implements net.schmizz.sshj.common.Factory.Named<Cipher> {
|
||||
|
||||
private final int authSize;
|
||||
private final int keySize;
|
||||
private final String name;
|
||||
private final String cipher;
|
||||
|
||||
public Factory(int authSize, int keySize, String name, String cipher) {
|
||||
this.authSize = authSize;
|
||||
this.keySize = keySize;
|
||||
this.name = name;
|
||||
this.cipher = cipher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cipher create() {
|
||||
return new ChachaPolyCipher(authSize, keySize / 8, cipher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,7 @@ final class Decoder
|
||||
}
|
||||
|
||||
private int decryptLengthAAD() throws TransportException {
|
||||
cipher.setSequenceNumber(seq + 1 & 0xffffffffL);
|
||||
cipher.updateAAD(inputBuffer.array(), 0, 4);
|
||||
|
||||
final int len;
|
||||
@@ -185,10 +186,6 @@ final class Decoder
|
||||
}
|
||||
}
|
||||
|
||||
// private void decryptPayload(final byte[] data, int offset, int length) {
|
||||
// cipher.update(data, cipherSize, packetLength + 4 - cipherSize);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Adds {@code len} bytes from {@code b} to the decoder buffer. When a packet has been successfully decoded, hooks
|
||||
* in to {@link SSHPacketHandler#handle} of the {@link SSHPacketHandler} this decoder was initialized with.
|
||||
|
||||
@@ -57,8 +57,6 @@ final class Encoder
|
||||
* @param buffer the buffer to encode
|
||||
*
|
||||
* @return the sequence no. of encoded packet
|
||||
*
|
||||
* @throws TransportException
|
||||
*/
|
||||
long encode(SSHPacket buffer) {
|
||||
encodeLock.lock();
|
||||
@@ -140,11 +138,12 @@ final class Encoder
|
||||
}
|
||||
}
|
||||
|
||||
protected void aeadOutgoingBuffer(Buffer buf, int offset, int len) {
|
||||
protected void aeadOutgoingBuffer(Buffer<?> buf, int offset, int len) {
|
||||
if (cipher == null || cipher.getAuthenticationTagSize() == 0) {
|
||||
throw new IllegalArgumentException("AEAD mode requires an AEAD cipher");
|
||||
}
|
||||
byte[] data = buf.array();
|
||||
cipher.setSequenceNumber(seq);
|
||||
cipher.updateWithAAD(data, offset, 4, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -113,4 +113,9 @@ public abstract class BaseCipher
|
||||
updateAAD(input, offset, aadLen);
|
||||
update(input, offset + aadLen, inputLen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSequenceNumber(long seq) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,4 +78,6 @@ public interface Cipher {
|
||||
* @param inputLen The number of bytes to update - starting at offset + aadLen
|
||||
*/
|
||||
void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen);
|
||||
|
||||
void setSequenceNumber(long seq);
|
||||
}
|
||||
|
||||
@@ -73,4 +73,9 @@ public class NoneCipher
|
||||
public void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSequenceNumber(long seq) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Copyright (C)2009 - SSHJ Contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hierynomus.sshj.transport;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers;
|
||||
import net.schmizz.sshj.common.SSHRuntimeException;
|
||||
import net.schmizz.sshj.transport.cipher.Cipher;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class ChachaPolyCipherTest {
|
||||
|
||||
private static final int AAD_LENGTH = 4;
|
||||
private static final int POLY_TAG_LENGTH = 16;
|
||||
|
||||
private static final ChachaPolyCiphers.Factory FACTORY = ChachaPolyCiphers.CHACHA_POLY_OPENSSH();
|
||||
private static final String PLAINTEXT = "[Secret authenticated message using Chacha20Poly1305";
|
||||
|
||||
@Test
|
||||
public void testEncryptDecrypt() {
|
||||
Cipher enc = FACTORY.create();
|
||||
byte[] key = new byte[enc.getBlockSize()];
|
||||
Arrays.fill(key, (byte) 1);
|
||||
enc.init(Cipher.Mode.Encrypt, key, new byte[0]);
|
||||
|
||||
byte[] aad = new byte[AAD_LENGTH];
|
||||
byte[] ptBytes = PLAINTEXT.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] message = new byte[AAD_LENGTH + ptBytes.length + POLY_TAG_LENGTH];
|
||||
Arrays.fill(aad, (byte) 2);
|
||||
System.arraycopy(aad, 0, message, 0, AAD_LENGTH);
|
||||
System.arraycopy(ptBytes, 0, message, AAD_LENGTH, ptBytes.length);
|
||||
|
||||
enc.updateWithAAD(message, 0, AAD_LENGTH, ptBytes.length);
|
||||
byte[] corrupted = message.clone();
|
||||
|
||||
Cipher dec = FACTORY.create();
|
||||
dec.init(Cipher.Mode.Decrypt, key, new byte[0]);
|
||||
dec.updateWithAAD(message, 0, AAD_LENGTH, ptBytes.length);
|
||||
|
||||
assertArrayEquals(aad, Arrays.copyOf(message, AAD_LENGTH));
|
||||
String decodedString =
|
||||
new String(Arrays.copyOfRange(message, AAD_LENGTH, AAD_LENGTH + ptBytes.length), StandardCharsets.UTF_8);
|
||||
assertEquals(PLAINTEXT, decodedString);
|
||||
|
||||
corrupted[corrupted.length - 1] += 1;
|
||||
Cipher failingDec = FACTORY.create();
|
||||
failingDec.init(Cipher.Mode.Decrypt, key, new byte[0]);
|
||||
try {
|
||||
failingDec.updateWithAAD(corrupted, 0, AAD_LENGTH, ptBytes.length);
|
||||
fail("Modified authentication tag should not validate");
|
||||
} catch (SSHRuntimeException e) {
|
||||
assertEquals("MAC Error", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckOnUpdateParameters() {
|
||||
Cipher cipher = FACTORY.create();
|
||||
try {
|
||||
cipher.update(null, 8, 42);
|
||||
fail("Invalid inputOffset should trigger exception");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("updateAAD called with inputOffset 8", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckOnUpdateAADParameters() {
|
||||
Cipher cipher = FACTORY.create();
|
||||
try {
|
||||
cipher.updateAAD(null, 1, AAD_LENGTH);
|
||||
fail("Invalid offset should trigger exception");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("updateAAD called with offset 1 and length 4", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
cipher.updateAAD(null, 0, 5);
|
||||
fail("Invalid length should trigger exception");
|
||||
} catch (IllegalArgumentException e) {
|
||||
assertEquals("updateAAD called with offset 0 and length 5", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user