Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0880a6a
Implements ChaCha20 cipher algorithm.
scott-xu May 25, 2024
5471a32
Implements [email protected]
scott-xu May 26, 2024
da9bc08
Update Cipher.cs
scott-xu May 26, 2024
aaf49a1
Update ChaCha20Poly1305Cipher.cs
scott-xu May 26, 2024
7780ff8
Note that the length of the concatenation of 'packet_length',
scott-xu May 26, 2024
d29d8de
Use Chaos.Nacl Poly1305Donna
scott-xu May 26, 2024
9983a31
Fix build. Fix typo. Update README
scott-xu May 26, 2024
920d681
Update README.md
scott-xu May 26, 2024
bea3b48
Fix build
scott-xu May 26, 2024
ec84664
Remove trailing whitespace
scott-xu May 27, 2024
eecffe6
Fix build
scott-xu May 27, 2024
b67024c
Merge branch 'chacha20-poly1305' of https://github.com/scott-xu/SSH.N…
scott-xu Jul 16, 2024
1bbca33
Merge branch 'chacha20-poly1305' of https://github.com/scott-xu/SSH.N…
scott-xu Jul 16, 2024
898cf08
Change to BouncyCastle
scott-xu Jul 17, 2024
7a057de
Merge branch 'develop' of https://github.com/scott-xu/SSH.NET into ch…
scott-xu Jul 17, 2024
b600dd2
Inherit from SymmetricCipher instead of StreamCipher since StreamCiph…
scott-xu Jul 17, 2024
5dbb8d7
Resolve conflicts
scott-xu Jul 17, 2024
15be6bc
Move field to local variable
scott-xu Jul 17, 2024
fa69068
Compute poly key stream once
scott-xu Jul 21, 2024
90826d3
Update test/Renci.SshNet.IntegrationTests/CipherTests.cs
scott-xu Jul 23, 2024
4ff14c6
Fix build; Add net48 integration test in CI
scott-xu Jul 23, 2024
379ef1f
Merge branch 'develop' into chacha20-poly1305
scott-xu Jul 23, 2024
9046899
Merge branch 'develop' into chacha20-poly1305
scott-xu Jul 23, 2024
9f3eafd
Merge branch 'develop' into chacha20-poly1305
WojciechNagorski Jul 24, 2024
566fa98
Merge branch 'develop' into chacha20-poly1305
scott-xu Jul 25, 2024
6106068
Merge branch 'develop' into chacha20-poly1305
scott-xu Jul 25, 2024
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 @@ -72,6 +72,7 @@ The main types provided by this library are:
* aes256-ctr
* aes128-gcm<span></span>@openssh.com (.NET 6 and higher)
* aes256-gcm<span></span>@openssh.com (.NET 6 and higher)
* chacha20-poly1305<span></span>@openssh.com
* aes128-cbc
* aes192-cbc
* aes256-cbc
Expand Down
1 change: 1 addition & 0 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
Encryptions.Add("[email protected]", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true));
}
#endif
Encryptions.Add("[email protected]", new CipherInfo(512, (key, iv) => new ChaCha20Poly1305Cipher(key), isAead: true));
Encryptions.Add("aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
Encryptions.Add("aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
Encryptions.Add("aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)));
Expand Down
12 changes: 10 additions & 2 deletions src/Renci.SshNet/Security/Cryptography/Cipher.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Renci.SshNet.Security.Cryptography
namespace Renci.SshNet.Security.Cryptography
{
/// <summary>
/// Base class for cipher implementation.
Expand All @@ -22,6 +22,14 @@ public abstract class Cipher
/// </value>
public virtual int TagSize { get; }

/// <summary>
/// Sets the sequence number.
/// </summary>
/// <param name="sequenceNumber">The sequence number.</param>
internal virtual void SetSequenceNumber(uint sequenceNumber)
{
}

/// <summary>
/// Encrypts the specified input.
/// </summary>
Expand Down Expand Up @@ -50,7 +58,7 @@ public byte[] Encrypt(byte[] input)
/// <returns>
/// The decrypted data.
/// </returns>
public byte[] Decrypt(byte[] input)
public virtual byte[] Decrypt(byte[] input)
{
return Decrypt(input, 0, input.Length);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
using System;
using System.Buffers.Binary;
using System.Diagnostics;

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Macs;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Utilities;

using Renci.SshNet.Common;
using Renci.SshNet.Messages.Transport;

namespace Renci.SshNet.Security.Cryptography.Ciphers
{
/// <summary>
/// ChaCha20Poly1305 cipher implementation.
/// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00"/>.
/// </summary>
internal sealed class ChaCha20Poly1305Cipher : SymmetricCipher
{
private readonly ChaCha7539Engine _aadCipher = new ChaCha7539Engine();
private readonly ChaCha7539Engine _cipher = new ChaCha7539Engine();
private readonly Poly1305 _mac = new Poly1305();

/// <summary>
/// Gets the minimun block size.
/// </summary>
public override byte MinimumSize
{
get
{
return 16;
}
}

/// <summary>
/// Gets the tag size in bytes.
/// Poly1305 [Poly1305], also by Daniel Bernstein, is a one-time Carter-
/// Wegman MAC that computes a 128 bit integrity tag given a message
/// <see href="https://datatracker.ietf.org/doc/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00#section-1"/>.
/// </summary>
public override int TagSize
{
get
{
return 16;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ChaCha20Poly1305Cipher"/> class.
/// </summary>
/// <param name="key">The key.</param>
public ChaCha20Poly1305Cipher(byte[] key)
: base(key)
{
}

/// <summary>
/// Encrypts the specified input.
/// </summary>
/// <param name="input">
/// The input data with below format:
/// <code>
/// [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
/// [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
/// </code>
/// </param>
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin encrypting.</param>
/// <param name="length">The number of bytes to encrypt from <paramref name="input"/>.</param>
/// <returns>
/// The encrypted data with below format:
/// <code>
/// [packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
/// [------4 bytes------][------------------Cipher Text--------------------][-------TAG-------]
/// </code>
/// </returns>
public override byte[] Encrypt(byte[] input, int offset, int length)
{
var output = new byte[length + TagSize];

_aadCipher.ProcessBytes(input, offset, 4, output, 0);
_cipher.ProcessBytes(input, offset + 4, length - 4, output, 4);

_mac.BlockUpdate(output, 0, length);
_ = _mac.DoFinal(output, length);

return output;
}

/// <summary>
/// Decrypts the first block which is packet length field.
/// </summary>
/// <param name="input">The encrypted packet length field.</param>
/// <returns>The decrypted packet length field.</returns>
public override byte[] Decrypt(byte[] input)
{
var output = new byte[input.Length];
_aadCipher.ProcessBytes(input, 0, input.Length, output, 0);

return output;
}

/// <summary>
/// Decrypts the specified input.
/// </summary>
/// <param name="input">
/// The input data with below format:
/// <code>
/// [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
/// [--------4 bytes-------][--4 bytes--(offset)][--------------Cipher Text----------------(length)][-------TAG-------]
/// </code>
/// </param>
/// <param name="offset">The zero-based offset in <paramref name="input"/> at which to begin decrypting and authenticating.</param>
/// <param name="length">The number of bytes to decrypt and authenticate from <paramref name="input"/>.</param>
/// <returns>
/// The decrypted data with below format:
/// <code>
/// [padding length field sz][payload][random paddings]
/// [--------------------Plain Text-------------------]
/// </code>
/// </returns>
public override byte[] Decrypt(byte[] input, int offset, int length)
{
Debug.Assert(offset == 8, "The offset must be 8");

var tag = new byte[TagSize];
_mac.BlockUpdate(input, offset - 4, length + 4);
_ = _mac.DoFinal(tag, 0);
if (!Arrays.FixedTimeEquals(TagSize, tag, 0, input, offset + length))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}

var output = new byte[length];
_cipher.ProcessBytes(input, offset, length, output, 0);

return output;
}

internal override void SetSequenceNumber(uint sequenceNumber)
{
var iv = new byte[12];
BinaryPrimitives.WriteUInt64BigEndian(iv.AsSpan(4), sequenceNumber);

// ChaCha20 encryption and decryption is completely
// symmetrical, so the 'forEncryption' is
// irrelevant. (Like 90% of stream ciphers)
_aadCipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 32, 32), iv));
_cipher.Init(forEncryption: true, new ParametersWithIV(new KeyParameter(Key, 0, 32), iv));

var keyStream = new byte[64];
_cipher.ProcessBytes(keyStream, 0, keyStream.Length, keyStream, 0);
_mac.Init(new KeyParameter(keyStream, 0, 32));
}
}
}
47 changes: 32 additions & 15 deletions src/Renci.SshNet/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using System.Threading;
using System.Threading.Tasks;

using Org.BouncyCastle.Utilities;

using Renci.SshNet.Abstractions;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
Expand Down Expand Up @@ -1059,6 +1061,7 @@ internal void SendMessage(Message message)
// Encrypt packet data
if (_clientCipher != null)
{
_clientCipher.SetSequenceNumber(_outboundPacketSequence);
if (_clientEtm)
{
// The length of the "packet length" field in bytes
Expand Down Expand Up @@ -1246,15 +1249,28 @@ private Message ReceiveMessage(Socket socket)
return null;
}

if (_serverCipher != null && !_serverAead && (_serverMac == null || !_serverEtm))
var plainFirstBlock = firstBlock;

// First block is not encrypted in AES GCM mode.
if (_serverCipher is not null
#if NET6_0_OR_GREATER
and not Security.Cryptography.Ciphers.AesGcmCipher
#endif
)
{
firstBlock = _serverCipher.Decrypt(firstBlock);
_serverCipher.SetSequenceNumber(_inboundPacketSequence);

// First block is not encrypted in ETM mode.
if (_serverMac == null || !_serverEtm)
{
plainFirstBlock = _serverCipher.Decrypt(firstBlock);
}
Comment on lines +1252 to +1265
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole method makes me shed a tear. Maybe one day we can design something easier to comprehend. I have a bit of an idea

Copy link
Collaborator Author

@scott-xu scott-xu Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's becoming "nobody knows why but it just works" 🤣
Looking forward your idea.

}

packetLength = BinaryPrimitives.ReadUInt32BigEndian(firstBlock);
packetLength = BinaryPrimitives.ReadUInt32BigEndian(plainFirstBlock);

// Test packet minimum and maximum boundaries
if (packetLength < Math.Max((byte)16, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
if (packetLength < Math.Max((byte)8, blockSize) - 4 || packetLength > MaximumSshPacketSize - 4)
{
throw new SshConnectionException(string.Format(CultureInfo.CurrentCulture, "Bad packet length: {0}.", packetLength),
DisconnectReason.ProtocolError);
Expand All @@ -1277,7 +1293,16 @@ private Message ReceiveMessage(Socket socket)
// to read the packet including server MAC in a single pass (except for the initial block).
data = new byte[bytesToRead + blockSize + inboundPacketSequenceLength];
BinaryPrimitives.WriteUInt32BigEndian(data, _inboundPacketSequence);
Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, firstBlock.Length);

// Use raw packet length field to calculate the mac in AEAD mode.
if (_serverAead)
{
Buffer.BlockCopy(firstBlock, 0, data, inboundPacketSequenceLength, blockSize);
}
else
{
Buffer.BlockCopy(plainFirstBlock, 0, data, inboundPacketSequenceLength, blockSize);
}

if (bytesToRead > 0)
{
Expand All @@ -1292,11 +1317,7 @@ private Message ReceiveMessage(Socket socket)
if (_serverMac != null && _serverEtm)
{
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
if (!CryptographicOperations.FixedTimeEquals(clientHash, new ReadOnlySpan<byte>(data, data.Length - serverMacLength, serverMacLength)))
#else
if (!Security.Chaos.NaCl.CryptoBytes.ConstantTimeEquals(clientHash, 0, data, data.Length - serverMacLength, serverMacLength))
#endif
if (!Arrays.FixedTimeEquals(serverMacLength, clientHash, 0, data, data.Length - serverMacLength))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}
Expand All @@ -1320,11 +1341,7 @@ private Message ReceiveMessage(Socket socket)
if (_serverMac != null && !_serverEtm)
{
var clientHash = _serverMac.ComputeHash(data, 0, data.Length - serverMacLength);
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP2_1_OR_GREATER
if (!CryptographicOperations.FixedTimeEquals(clientHash, new ReadOnlySpan<byte>(data, data.Length - serverMacLength, serverMacLength)))
#else
if (!Security.Chaos.NaCl.CryptoBytes.ConstantTimeEquals(clientHash, 0, data, data.Length - serverMacLength, serverMacLength))
#endif
if (!Arrays.FixedTimeEquals(serverMacLength, clientHash, 0, data, data.Length - serverMacLength))
{
throw new SshConnectionException("MAC error", DisconnectReason.MacError);
}
Expand Down
6 changes: 6 additions & 0 deletions test/Renci.SshNet.IntegrationTests/CipherTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ public void Aes256Gcm()
{
DoTest(Cipher.Aes256Gcm);
}

[TestMethod]
public void ChaCha20Poly1305()
{
DoTest(Cipher.Chacha20Poly1305);
}
#endif
private void DoTest(Cipher cipher)
{
Expand Down