diff --git a/src/main/java/com/hierynomus/sshj/transport/cipher/ChachaPolyCipher.java b/src/main/java/com/hierynomus/sshj/transport/cipher/ChachaPolyCipher.java index 42a296751..7a09083e4 100644 --- a/src/main/java/com/hierynomus/sshj/transport/cipher/ChachaPolyCipher.java +++ b/src/main/java/com/hierynomus/sshj/transport/cipher/ChachaPolyCipher.java @@ -16,9 +16,8 @@ package com.hierynomus.sshj.transport.cipher; import java.security.GeneralSecurityException; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; +import java.security.MessageDigest; import java.security.spec.AlgorithmParameterSpec; import java.util.Arrays; import javax.crypto.spec.IvParameterSpec; @@ -82,8 +81,7 @@ public void setSequenceNumber(long seq) { } @Override - protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) - throws InvalidKeyException, InvalidAlgorithmParameterException { + protected void initCipher(javax.crypto.Cipher cipher, Mode mode, byte[] key, byte[] iv) { this.mode = mode; cipherKey = getKeySpec(Arrays.copyOfRange(key, 0, CHACHA_KEY_SIZE)); @@ -127,31 +125,41 @@ public void updateAAD(byte[] data) { @Override public void update(byte[] input, int inputOffset, int inputLen) { - if (inputOffset != AAD_LENGTH) { + if (inputOffset != 0 && inputOffset != AAD_LENGTH) { throw new IllegalArgumentException("updateAAD called with inputOffset " + inputOffset); } - final int macInputLength = AAD_LENGTH + inputLen; - + final int macInputLength = inputOffset + 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); + final byte[] macInput = new byte[macInputLength]; + + if (inputOffset == 0) { + // Handle decryption without AAD + System.arraycopy(input, 0, macInput, 0, inputLen); + } else { + // Handle decryption with previous AAD from updateAAD() + 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)) { + final byte[] expectedPolyTag = mac.doFinal(macInput); + final byte[] actualPolyTag = Arrays.copyOfRange(input, macInputLength, macInputLength + POLY_TAG_LENGTH); + if (!MessageDigest.isEqual(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); - } + try { + cipher.update(input, inputOffset, inputLen, input, inputOffset); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException("ChaCha20 decryption failed", e); + } + } else { + try { + cipher.update(input, AAD_LENGTH, inputLen, input, AAD_LENGTH); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException("ChaCha20 encryption failed", 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); diff --git a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java index 889ce1136..9229fa4af 100644 --- a/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java +++ b/src/main/java/com/hierynomus/sshj/userauth/keyprovider/OpenSSHKeyV1KeyFile.java @@ -18,6 +18,7 @@ import com.hierynomus.sshj.common.KeyAlgorithm; import com.hierynomus.sshj.common.KeyDecryptionFailedException; import com.hierynomus.sshj.transport.cipher.BlockCiphers; +import com.hierynomus.sshj.transport.cipher.ChachaPolyCiphers; import com.hierynomus.sshj.transport.cipher.GcmCiphers; import net.i2p.crypto.eddsa.EdDSAPrivateKey; import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; @@ -83,6 +84,7 @@ public class OpenSSHKeyV1KeyFile extends BaseFileKeyProvider { SUPPORTED_CIPHERS.put(BlockCiphers.AES256CTR().getName(), BlockCiphers.AES256CTR()); SUPPORTED_CIPHERS.put(GcmCiphers.AES256GCM().getName(), GcmCiphers.AES256GCM()); SUPPORTED_CIPHERS.put(GcmCiphers.AES128GCM().getName(), GcmCiphers.AES128GCM()); + SUPPORTED_CIPHERS.put(ChachaPolyCiphers.CHACHA_POLY_OPENSSH().getName(), ChachaPolyCiphers.CHACHA_POLY_OPENSSH()); } private PublicKey pubKey; @@ -192,7 +194,7 @@ private byte[] readEncryptedPrivateKey(final byte[] privateKeyEncoded, final Pla if (bufferRemaining == 0) { encryptedPrivateKey = privateKeyEncoded; } else { - // Read Authentication Tag for AES-GCM + // Read Authentication Tag for AES-GCM or ChaCha20-Poly1305 final byte[] authenticationTag = new byte[bufferRemaining]; inputBuffer.readRawBytes(authenticationTag); @@ -314,7 +316,7 @@ private KeyPair readUnencrypted(final PlainBuffer keyBuffer, final PublicKey pub int checkInt1 = keyBuffer.readUInt32AsInt(); // uint32 checkint1 int checkInt2 = keyBuffer.readUInt32AsInt(); // uint32 checkint2 if (checkInt1 != checkInt2) { - throw new KeyDecryptionFailedException(); + throw new KeyDecryptionFailedException(new EncryptionException("OpenSSH Private Key integer comparison failed")); } // The private key section contains both the public key and the private key String keyType = keyBuffer.readString(); // string keytype diff --git a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java index e013e7c78..3e8cbae8d 100644 --- a/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java +++ b/src/test/java/net/schmizz/sshj/keyprovider/OpenSSHKeyFileTest.java @@ -265,6 +265,11 @@ public void shouldLoadProtectedEd25519PrivateKeyAES256GCM() throws IOException { checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_aes256-gcm", "sshjtest", true); } + @Test + public void shouldLoadProtectedEd25519PrivateKeyChaCha20Poly1305() throws IOException { + checkOpenSSHKeyV1("src/test/resources/keytypes/ed25519_chacha20-poly1305", "sshjtest", false); + } + @Test public void shouldFailOnIncorrectPassphraseAfterRetries() { assertThrows(KeyDecryptionFailedException.class, () -> { diff --git a/src/test/resources/keytypes/ed25519_chacha20-poly1305 b/src/test/resources/keytypes/ed25519_chacha20-poly1305 new file mode 100644 index 000000000..a5fb8d7c2 --- /dev/null +++ b/src/test/resources/keytypes/ed25519_chacha20-poly1305 @@ -0,0 +1,9 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm +JjcnlwdAAAABgAAAAQilqbRL4q3X9kEqWdTsD5/gAAABAAAAABAAAAMwAAAAtzc2gtZWQy +NTUxOQAAACCpvtXUZPONb1XDjLkHmP5mQrGryGaQsA68Nb+OAjaaEgAAAJh+Repmt76g31 +jlD1ITaJU298ZU3rFWgA/Hs3xnOTNPjhMMu9nzfoZAu0fraE1MBVaEgNKRpw7SG+2eDBOo +3fvN3lF15i7Q8YHZd9alfcUg3FrvBzjd0Edx4AQxbSueibPFaqnwmVk/YzDiQHwlyWfA1x +HbqxrbJf1S0i8Bt5OjLK6woGk0/lfWJmy82xIa1sa3ONkPVjaJncm/f2SKV7t2k1UP9/jx +dLA= +-----END OPENSSH PRIVATE KEY----- diff --git a/src/test/resources/keytypes/ed25519_chacha20-poly1305.pub b/src/test/resources/keytypes/ed25519_chacha20-poly1305.pub new file mode 100644 index 000000000..58f84dc38 --- /dev/null +++ b/src/test/resources/keytypes/ed25519_chacha20-poly1305.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKm+1dRk841vVcOMuQeY/mZCsavIZpCwDrw1v44CNpoS