diff --git a/README.md b/README.md index aa55031..592ac11 100644 --- a/README.md +++ b/README.md @@ -447,6 +447,7 @@ Supported private key encryption cyphers: - OpenSSH Keys `OPENSSH PRIVATE KEY` (`openssh-key-v1`) - aes[128|192|256]-[cbc|ctr] - aes[128|256]-gcm@openssh.com + - chacha20-poly1305@openssh.com Supported client key algorithms: - ssh-ed25519 diff --git a/src/Tmds.Ssh/AesCtr.cs b/src/Tmds.Ssh/AesCtr.cs index 6116601..4838ed0 100644 --- a/src/Tmds.Ssh/AesCtr.cs +++ b/src/Tmds.Ssh/AesCtr.cs @@ -7,8 +7,11 @@ namespace Tmds.Ssh; static class AesCtr { - public static void DecryptCtr(ReadOnlySpan key, Span counter, ReadOnlySpan ciphertext, Span plaintext) + public static void DecryptCtr(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan ciphertext, Span plaintext) { + Span counter = stackalloc byte[iv.Length]; + iv.CopyTo(counter); + if (plaintext.Length < ciphertext.Length) { throw new ArgumentException("Plaintext buffer is too small."); diff --git a/src/Tmds.Ssh/OpenSshKeyCipher.cs b/src/Tmds.Ssh/OpenSshKeyCipher.cs index 42665e0..01c3342 100644 --- a/src/Tmds.Ssh/OpenSshKeyCipher.cs +++ b/src/Tmds.Ssh/OpenSshKeyCipher.cs @@ -3,12 +3,15 @@ using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; namespace Tmds.Ssh; sealed class OpenSshKeyCipher { - private delegate byte[] DecryptDelegate(ReadOnlySpan key, Span iv, ReadOnlySpan data, ReadOnlySpan tag); + private delegate byte[] DecryptDelegate(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data, ReadOnlySpan tag); private readonly DecryptDelegate _decryptData; @@ -28,7 +31,7 @@ private OpenSshKeyCipher( public int IVLength { get; } public int TagLength { get; } - public byte[] Decrypt(ReadOnlySpan key, Span iv, ReadOnlySpan data, ReadOnlySpan tag) + public byte[] Decrypt(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data, ReadOnlySpan tag) { if (KeyLength != key.Length) { @@ -59,16 +62,22 @@ public static bool TryGetCipher(Name name, [NotNullWhen(true)] out OpenSshKeyCip { AlgorithmNames.Aes256Ctr, CreateAesCtrCipher(32) }, { AlgorithmNames.Aes128Gcm, CreateAesGcmCipher(16) }, { AlgorithmNames.Aes256Gcm, CreateAesGcmCipher(32) }, + {AlgorithmNames.ChaCha20Poly1305, + new OpenSshKeyCipher( + keyLength: 64, + ivLength: 0, + DecryptChaCha20Poly1305, + tagLength: 16) }, }; private static OpenSshKeyCipher CreateAesCbcCipher(int keyLength) => new OpenSshKeyCipher(keyLength: keyLength, ivLength: 16, - (ReadOnlySpan key, Span iv, ReadOnlySpan data, ReadOnlySpan _) + (ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data, ReadOnlySpan _) => DecryptAesCbc(key, iv, data)); private static OpenSshKeyCipher CreateAesCtrCipher(int keyLength) => new OpenSshKeyCipher(keyLength: keyLength, ivLength: 16, - (ReadOnlySpan key, Span iv, ReadOnlySpan data, ReadOnlySpan _) + (ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data, ReadOnlySpan _) => DecryptAesCtr(key, iv, data)); private static OpenSshKeyCipher CreateAesGcmCipher(int keyLength) @@ -76,25 +85,57 @@ private static OpenSshKeyCipher CreateAesGcmCipher(int keyLength) DecryptAesGcm, tagLength: 16); - private static byte[] DecryptAesCbc(ReadOnlySpan key, Span iv, ReadOnlySpan data) + private static byte[] DecryptAesCbc(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data) { using Aes aes = Aes.Create(); aes.Key = key.ToArray(); return aes.DecryptCbc(data, iv, PaddingMode.None); } - private static byte[] DecryptAesCtr(ReadOnlySpan key, Span iv, ReadOnlySpan data) + private static byte[] DecryptAesCtr(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data) { byte[] plaintext = new byte[data.Length]; AesCtr.DecryptCtr(key, iv, data, plaintext); return plaintext; } - private static byte[] DecryptAesGcm(ReadOnlySpan key, Span iv, ReadOnlySpan data, ReadOnlySpan tag) + private static byte[] DecryptAesGcm(ReadOnlySpan key, ReadOnlySpan iv, ReadOnlySpan data, ReadOnlySpan tag) { using AesGcm aesGcm = new AesGcm(key, tag.Length); byte[] plaintext = new byte[data.Length]; aesGcm.Decrypt(iv, data, tag, plaintext, null); return plaintext; } + + private static byte[] DecryptChaCha20Poly1305(ReadOnlySpan key, ReadOnlySpan _, ReadOnlySpan ciphertext, ReadOnlySpan tag) + { + ReadOnlySpan iv = stackalloc byte[12]; + ReadOnlySpan K_1 = key[..32]; + + ChaCha7539Engine chacha = new(); + chacha.Init(forEncryption: false, new ParametersWithIV(new KeyParameter(K_1), iv)); + + // Calculate poly key + Span polyKey = stackalloc byte[64]; + chacha.ProcessBytes(input: polyKey, output: polyKey); + + // Calculate mac + Poly1305 poly = new(); + poly.Init(new KeyParameter(polyKey[..32])); + poly.BlockUpdate(ciphertext); + Span ciphertextTag = stackalloc byte[16]; + poly.DoFinal(ciphertextTag); + + // Check mac + if (!CryptographicOperations.FixedTimeEquals(ciphertextTag, tag)) + { + throw new CryptographicException(); + } + + // Decode plaintext + byte[] plaintext = new byte[ciphertext.Length]; + chacha.ProcessBytes(ciphertext, plaintext); + + return plaintext; + } } diff --git a/src/Tmds.Ssh/PacketEncryptionAlgorithm.cs b/src/Tmds.Ssh/PacketEncryptionAlgorithm.cs index a3c7c4c..4c1377b 100644 --- a/src/Tmds.Ssh/PacketEncryptionAlgorithm.cs +++ b/src/Tmds.Ssh/PacketEncryptionAlgorithm.cs @@ -16,7 +16,6 @@ private PacketEncryptionAlgorithm(int keyLength, int ivLength, { KeyLength = keyLength; IVLength = ivLength; - IsAuthenticated = isAuthenticated; TagLength = tagLength; _createPacketEncryptor = createPacketEncryptor; _createPacketDecryptor = createPacketDecryptor; @@ -24,8 +23,8 @@ private PacketEncryptionAlgorithm(int keyLength, int ivLength, public int KeyLength { get; } public int IVLength { get; } - public bool IsAuthenticated { get; } - public int TagLength { get; } // When IsAuthenticated == true + public bool IsAuthenticated => TagLength > 0; + private int TagLength { get; } public IPacketEncryptor CreatePacketEncryptor(byte[] key, byte[] iv, HMacAlgorithm? hmacAlgorithm, byte[] hmacKey) { diff --git a/test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs b/test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs index d1333ce..cfa91d0 100644 --- a/test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs +++ b/test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs @@ -101,6 +101,7 @@ await RunWithKeyConversion(_sshServer.TestUserIdentityFile, async (string localK [InlineData("aes256-ctr")] [InlineData("aes128-gcm@openssh.com")] [InlineData("aes256-gcm@openssh.com")] + [InlineData("chacha20-poly1305@openssh.com")] public async Task OpenSshRsaKey(string? cipher) { await RunWithKeyConversion(_sshServer.TestUserIdentityFile, async (string localKey) =>