Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support decoding private keys that use the chacha20-poly1305@openssh.com cipher. #225

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion src/Tmds.Ssh/AesCtr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ namespace Tmds.Ssh;

static class AesCtr
{
public static void DecryptCtr(ReadOnlySpan<byte> key, Span<byte> counter, ReadOnlySpan<byte> ciphertext, Span<byte> plaintext)
public static void DecryptCtr(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> ciphertext, Span<byte> plaintext)
{
Span<byte> counter = stackalloc byte[iv.Length];
iv.CopyTo(counter);

if (plaintext.Length < ciphertext.Length)
{
throw new ArgumentException("Plaintext buffer is too small.");
Expand Down
55 changes: 48 additions & 7 deletions src/Tmds.Ssh/OpenSshKeyCipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag);
private delegate byte[] DecryptDelegate(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag);

private readonly DecryptDelegate _decryptData;

Expand All @@ -28,7 +31,7 @@ private OpenSshKeyCipher(
public int IVLength { get; }
public int TagLength { get; }

public byte[] Decrypt(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag)
public byte[] Decrypt(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag)
{
if (KeyLength != key.Length)
{
Expand Down Expand Up @@ -59,42 +62,80 @@ 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<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
=> DecryptAesCbc(key, iv, data));

private static OpenSshKeyCipher CreateAesCtrCipher(int keyLength)
=> new OpenSshKeyCipher(keyLength: keyLength, ivLength: 16,
(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> _)
=> DecryptAesCtr(key, iv, data));

private static OpenSshKeyCipher CreateAesGcmCipher(int keyLength)
=> new OpenSshKeyCipher(keyLength: keyLength, ivLength: 12,
DecryptAesGcm,
tagLength: 16);

private static byte[] DecryptAesCbc(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data)
private static byte[] DecryptAesCbc(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data)
{
using Aes aes = Aes.Create();
aes.Key = key.ToArray();
return aes.DecryptCbc(data, iv, PaddingMode.None);
}

private static byte[] DecryptAesCtr(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data)
private static byte[] DecryptAesCtr(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data)
{
byte[] plaintext = new byte[data.Length];
AesCtr.DecryptCtr(key, iv, data, plaintext);
return plaintext;
}

private static byte[] DecryptAesGcm(ReadOnlySpan<byte> key, Span<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> tag)
private static byte[] DecryptAesGcm(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> data, ReadOnlySpan<byte> 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<byte> key, ReadOnlySpan<byte> _, ReadOnlySpan<byte> ciphertext, ReadOnlySpan<byte> tag)
{
ReadOnlySpan<byte> iv = stackalloc byte[12];
ReadOnlySpan<byte> K_1 = key[..32];

ChaCha7539Engine chacha = new();
chacha.Init(forEncryption: false, new ParametersWithIV(new KeyParameter(K_1), iv));

// Calculate poly key
Span<byte> 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<byte> 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;
}
}
5 changes: 2 additions & 3 deletions src/Tmds.Ssh/PacketEncryptionAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,15 @@ private PacketEncryptionAlgorithm(int keyLength, int ivLength,
{
KeyLength = keyLength;
IVLength = ivLength;
IsAuthenticated = isAuthenticated;
TagLength = tagLength;
_createPacketEncryptor = createPacketEncryptor;
_createPacketDecryptor = createPacketDecryptor;
}

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)
{
Expand Down
1 change: 1 addition & 0 deletions test/Tmds.Ssh.Tests/PrivateKeyCredentialTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Loading