Skip to content

Commit 542bb35

Browse files
Add ChaCha20-Poly1305 Support for OpenSSH Keys (#904)
* Add ChaCha20-Poly1305 Support for OpenSSH Keys - Updated ChachaPolyCipher to support decryption without Additional Authenticated Data * Added test for ChachaPolyCipher without AAD * Streamlined ChachaPolyCipher.update() method
1 parent 3b67d2b commit 542bb35

File tree

6 files changed

+60
-17
lines changed

6 files changed

+60
-17
lines changed

src/main/java/com/hierynomus/sshj/transport/cipher/ChachaPolyCipher.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@
1616
package com.hierynomus.sshj.transport.cipher;
1717

1818
import java.security.GeneralSecurityException;
19-
import java.security.InvalidAlgorithmParameterException;
20-
import java.security.InvalidKeyException;
2119

20+
import java.security.MessageDigest;
2221
import java.security.spec.AlgorithmParameterSpec;
2322
import java.util.Arrays;
2423
import javax.crypto.spec.IvParameterSpec;
@@ -82,8 +81,7 @@ public void setSequenceNumber(long seq) {
8281
}
8382

8483
@Override
85-
protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv)
86-
throws InvalidKeyException, InvalidAlgorithmParameterException {
84+
protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) {
8785
this.mode = mode;
8886

8987
cipherKey = getKeySpec(Arrays.copyOfRange(key, 0, CHACHA_KEY_SIZE));
@@ -127,28 +125,34 @@ public void updateAAD(byte[] data) {
127125

128126
@Override
129127
public void update(byte[] input, int inputOffset, int inputLen) {
130-
if (inputOffset != AAD_LENGTH) {
128+
if (inputOffset != 0 && inputOffset != AAD_LENGTH) {
131129
throw new IllegalArgumentException("updateAAD called with inputOffset " + inputOffset);
132130
}
133131

134-
final int macInputLength = AAD_LENGTH + inputLen;
135-
132+
final int macInputLength = inputOffset + inputLen;
136133
if (mode == Mode.Decrypt) {
137-
byte[] macInput = new byte[macInputLength];
138-
System.arraycopy(encryptedAad, 0, macInput, 0, AAD_LENGTH);
139-
System.arraycopy(input, AAD_LENGTH, macInput, AAD_LENGTH, inputLen);
134+
final byte[] macInput = new byte[macInputLength];
135+
136+
if (inputOffset == 0) {
137+
// Handle decryption without AAD
138+
System.arraycopy(input, 0, macInput, 0, inputLen);
139+
} else {
140+
// Handle decryption with previous AAD from updateAAD()
141+
System.arraycopy(encryptedAad, 0, macInput, 0, AAD_LENGTH);
142+
System.arraycopy(input, AAD_LENGTH, macInput, AAD_LENGTH, inputLen);
143+
}
140144

141-
byte[] expectedPolyTag = mac.doFinal(macInput);
142-
byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH);
143-
if (!Arrays.equals(actualPolyTag, expectedPolyTag)) {
145+
final byte[] expectedPolyTag = mac.doFinal(macInput);
146+
final byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH);
147+
if (!MessageDigest.isEqual(actualPolyTag, expectedPolyTag)) {
144148
throw new SSHRuntimeException("MAC Error");
145149
}
146150
}
147151

148152
try {
149-
cipher.update(input, AAD_LENGTH, inputLen, input, AAD_LENGTH);
153+
cipher.update(input, inputOffset, inputLen, input, inputOffset);
150154
} catch (GeneralSecurityException e) {
151-
throw new SSHRuntimeException("Error updating data through cipher", e);
155+
throw new SSHRuntimeException("ChaCha20 cipher processing failed", e);
152156
}
153157

154158
if (mode == Mode.Encrypt) {

src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import com.hierynomus.sshj.common.KeyAlgorithm;
1919
import com.hierynomus.sshj.common.KeyDecryptionFailedException;
2020
import com.hierynomus.sshj.transport.cipher.BlockCiphers;
21+
import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers;
2122
import com.hierynomus.sshj.transport.cipher.GcmCiphers;
2223
import net.i2p.crypto.eddsa.EdDSAPrivateKey;
2324
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable;
@@ -83,6 +84,7 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider {
8384
SUPPORTED_CIPHERS.put(BlockCiphers.AES256CTR().getName(), BlockCiphers.AES256CTR());
8485
SUPPORTED_CIPHERS.put(GcmCiphers.AES256GCM().getName(), GcmCiphers.AES256GCM());
8586
SUPPORTED_CIPHERS.put(GcmCiphers.AES128GCM().getName(), GcmCiphers.AES128GCM());
87+
SUPPORTED_CIPHERS.put(ChachaPolyCiphers.CHACHA_POLY_OPENSSH().getName(), ChachaPolyCiphers.CHACHA_POLY_OPENSSH());
8688
}
8789

8890
private PublicKey pubKey;
@@ -192,7 +194,7 @@ private byte[] readEncryptedPrivateKey(final byte[] privateKeyEncoded, final Pla
192194
if (bufferRemaining == 0) {
193195
encryptedPrivateKey = privateKeyEncoded;
194196
} else {
195-
// Read Authentication Tag for AES-GCM
197+
// Read Authentication Tag for AES-GCM or ChaCha20-Poly1305
196198
final byte[] authenticationTag = new byte[bufferRemaining];
197199
inputBuffer.readRawBytes(authenticationTag);
198200

@@ -314,7 +316,7 @@ private KeyPair readUnencrypted(final PlainBuffer keyBuffer, final PublicKey pub
314316
int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1
315317
int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2
316318
if (checkInt1 != checkInt2) {
317-
throw new KeyDecryptionFailedException();
319+
throw new KeyDecryptionFailedException(new EncryptionException("OpenSSH Private Key integer comparison failed"));
318320
}
319321
// The private key section contains both the public key and the private key
320322
String keyType = keyBuffer.readString(); // string keytype

src/test/java/com/hierynomus/sshj/transport/ChachaPolyCipherTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,28 @@ public void testEncryptDecrypt() {
7070
}
7171
}
7272

73+
@Test
74+
public void testEncryptDecryptWithoutAAD() {
75+
final Cipher encryptionCipher = FACTORY.create();
76+
final byte[] key = new byte[encryptionCipher.getBlockSize()];
77+
Arrays.fill(key, (byte) 1);
78+
encryptionCipher.init(Cipher.Mode.Encrypt, key, new byte[0]);
79+
80+
final byte[] plaintextBytes = PLAINTEXT.getBytes(StandardCharsets.UTF_8);
81+
final byte[] message = new byte[plaintextBytes.length + POLY_TAG_LENGTH];
82+
System.arraycopy(plaintextBytes, 0, message, 0, plaintextBytes.length);
83+
84+
encryptionCipher.update(message, 0, plaintextBytes.length);
85+
86+
final Cipher decryptionCipher = FACTORY.create();
87+
decryptionCipher.init(Cipher.Mode.Decrypt, key, new byte[0]);
88+
decryptionCipher.update(message, 0, plaintextBytes.length);
89+
90+
final byte[] decrypted = Arrays.copyOfRange(message, 0, plaintextBytes.length);
91+
final String decoded = new String(decrypted, StandardCharsets.UTF_8);
92+
assertEquals(PLAINTEXT, decoded);
93+
}
94+
7395
@Test
7496
public void testCheckOnUpdateParameters() {
7597
Cipher cipher = FACTORY.create();

src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,11 @@ public void shouldLoadProtectedEd25519PrivateKeyAES256GCM() throws IOException {
265265
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256-gcm", "sshjtest", true);
266266
}
267267

268+
@Test
269+
public void shouldLoadProtectedEd25519PrivateKeyChaCha20Poly1305() throws IOException {
270+
checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_chacha20-poly1305", "sshjtest", false);
271+
}
272+
268273
@Test
269274
public void shouldFailOnIncorrectPassphraseAfterRetries() {
270275
assertThrows(KeyDecryptionFailedException.class, () -> {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm
3+
JjcnlwdAAAABgAAAAQilqbRL4q3X9kEqWdTsD5/gAAABAAAAABAAAAMwAAAAtzc2gtZWQy
4+
NTUxOQAAACCpvtXUZPONb1XDjLkHmP5mQrGryGaQsA68Nb+OAjaaEgAAAJh+Repmt76g31
5+
jlD1ITaJU298ZU3rFWgA/Hs3xnOTNPjhMMu9nzfoZAu0fraE1MBVaEgNKRpw7SG+2eDBOo
6+
3fvN3lF15i7Q8YHZd9alfcUg3FrvBzjd0Edx4AQxbSueibPFaqnwmVk/YzDiQHwlyWfA1x
7+
HbqxrbJf1S0i8Bt5OjLK6woGk0/lfWJmy82xIa1sa3ONkPVjaJncm/f2SKV7t2k1UP9/jx
8+
dLA=
9+
-----END OPENSSH PRIVATE KEY-----
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKm+1dRk841vVcOMuQeY/mZCsavIZpCwDrw1v44CNpoS

0 commit comments

Comments
 (0)