Skip to content

Commit

Permalink
Support decoding private keys that use the chacha20-poly1305@openssh.…
Browse files Browse the repository at this point in the history
…com cipher.
  • Loading branch information
tmds committed Sep 12, 2024
1 parent 3ea94c5 commit 53bdcab
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 11 deletions.
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("[email protected]")]
[InlineData("[email protected]")]
[InlineData("[email protected]")]
public async Task OpenSshRsaKey(string? cipher)
{
await RunWithKeyConversion(_sshServer.TestUserIdentityFile, async (string localKey) =>
Expand Down

0 comments on commit 53bdcab

Please sign in to comment.