* 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:
Henning Poettker
2021-04-20 16:22:11 +02:00
committed by GitHub
parent e283880e49
commit 16db0365d3
11 changed files with 343 additions and 10 deletions

View File

@@ -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: Implementations / adapters for the following algorithms are included:
ciphers:: 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` 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:: key exchange::

View File

@@ -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 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

View File

@@ -47,7 +47,8 @@ class CipherSpec extends IntegrationBaseSpec {
BlockCiphers.AES256CBC(), BlockCiphers.AES256CBC(),
BlockCiphers.AES256CTR(), BlockCiphers.AES256CTR(),
GcmCiphers.AES128GCM(), GcmCiphers.AES128GCM(),
GcmCiphers.AES256GCM()] GcmCiphers.AES256GCM(),
ChachaPolyCiphers.CHACHA_POLY_OPENSSH()]
cipher = cipherFactory.name cipher = cipherFactory.name
} }

View File

@@ -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 };
}
}

View File

@@ -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();
}
}
}

View File

@@ -145,6 +145,7 @@ final class Decoder
} }
private int decryptLengthAAD() throws TransportException { private int decryptLengthAAD() throws TransportException {
cipher.setSequenceNumber(seq + 1 & 0xffffffffL);
cipher.updateAAD(inputBuffer.array(), 0, 4); cipher.updateAAD(inputBuffer.array(), 0, 4);
final int len; 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 * 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. * in to {@link SSHPacketHandler#handle} of the {@link SSHPacketHandler} this decoder was initialized with.

View File

@@ -57,8 +57,6 @@ final class Encoder
* @param buffer the buffer to encode * @param buffer the buffer to encode
* *
* @return the sequence no. of encoded packet * @return the sequence no. of encoded packet
*
* @throws TransportException
*/ */
long encode(SSHPacket buffer) { long encode(SSHPacket buffer) {
encodeLock.lock(); 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) { if (cipher == null || cipher.getAuthenticationTagSize() == 0) {
throw new IllegalArgumentException("AEAD mode requires an AEAD cipher"); throw new IllegalArgumentException("AEAD mode requires an AEAD cipher");
} }
byte[] data = buf.array(); byte[] data = buf.array();
cipher.setSequenceNumber(seq);
cipher.updateWithAAD(data, offset, 4, len); cipher.updateWithAAD(data, offset, 4, len);
} }

View File

@@ -113,4 +113,9 @@ public abstract class BaseCipher
updateAAD(input, offset, aadLen); updateAAD(input, offset, aadLen);
update(input, offset + aadLen, inputLen); update(input, offset + aadLen, inputLen);
} }
@Override
public void setSequenceNumber(long seq) {
}
} }

View File

@@ -78,4 +78,6 @@ public interface Cipher {
* @param inputLen The number of bytes to update - starting at offset + aadLen * @param inputLen The number of bytes to update - starting at offset + aadLen
*/ */
void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen); void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen);
void setSequenceNumber(long seq);
} }

View File

@@ -73,4 +73,9 @@ public class NoneCipher
public void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) { public void updateWithAAD(byte[] input, int offset, int aadLen, int inputLen) {
} }
@Override
public void setSequenceNumber(long seq) {
}
} }

View File

@@ -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());
}
}
}