Skip to content

Support AesGcm cipher #1364

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

Closed
wants to merge 14 commits into from
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ the missing test once you figure things out. 🤓
* aes128-cbc
* aes192-cbc
* aes256-cbc
* aes128-gcm<span></span>@openssh.com (.NET Standard 2.1, .NET 6 and higher)
* aes256-gcm<span></span>@openssh.com (.NET Standard 2.1, .NET 6 and higher)
* blowfish-cbc
* twofish-cbc
* twofish192-cbc
Expand Down
12 changes: 11 additions & 1 deletion src/Renci.SshNet/CipherInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public class CipherInfo
/// </value>
public int KeySize { get; private set; }

/// <summary>
/// Gets a value indicating whether the cipher is AEAD (Authenticated Encryption with Associated data).
/// </summary>
/// <value>
/// <see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to incidicate the cipher is not AEAD.
/// </value>
public bool IsAead { get; private set; }

/// <summary>
/// Gets the cipher.
/// </summary>
Expand All @@ -27,10 +35,12 @@ public class CipherInfo
/// </summary>
/// <param name="keySize">Size of the key.</param>
/// <param name="cipher">The cipher.</param>
public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher)
/// <param name="isAead"><see langword="true"/> to indicate the cipher is AEAD, <see langword="false"/> to incidicate the cipher is not AEAD.</param>
public CipherInfo(int keySize, Func<byte[], byte[], Cipher> cipher, bool isAead = false)
{
KeySize = keySize;
Cipher = (key, iv) => cipher(key.Take(KeySize / 8), iv);
IsAead = isAead;
}
}
}
4 changes: 4 additions & 0 deletions src/Renci.SshNet/ConnectionInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy
{ "aes128-cbc", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
{ "aes192-cbc", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
{ "aes256-cbc", new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false)) },
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER
{ "aes128-gcm@openssh.com", new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
{ "aes256-gcm@openssh.com", new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv), isAead: true) },
#endif
{ "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "blowfish-cbc", new CipherInfo(128, (key, iv) => new BlowfishCipher(key, new CbcCipherMode(iv), padding: null)) },
{ "twofish-cbc", new CipherInfo(256, (key, iv) => new TwofishCipher(key, new CbcCipherMode(iv), padding: null)) },
Expand Down
10 changes: 5 additions & 5 deletions src/Renci.SshNet/Messages/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected override void WriteBytes(SshDataStream stream)
base.WriteBytes(stream);
}

internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool isEncryptThenMAC = false)
internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool excludePacketLengthFieldWhenPadding = false)
{
const int outboundPacketSequenceSize = 4;

Expand Down Expand Up @@ -78,9 +78,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
var packetLength = messageLength + 4 + 1;

// determine the padding length
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
// in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
// padding length calculation
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);

// add padding bytes
var paddingBytes = new byte[paddingLength];
Expand All @@ -106,9 +106,9 @@ internal byte[] GetPacket(byte paddingMultiplier, Compressor compressor, bool is
var packetLength = messageLength + 4 + 1;

// determine the padding length
// in Encrypt-then-MAC mode, the length field is not encrypted, so we should keep it out of the
// in Encrypt-then-MAC mode or AEAD, the length field is not encrypted, so we should keep it out of the
// padding length calculation
var paddingLength = GetPaddingLength(paddingMultiplier, isEncryptThenMAC ? packetLength - 4 : packetLength);
var paddingLength = GetPaddingLength(paddingMultiplier, excludePacketLengthFieldWhenPadding ? packetLength - 4 : packetLength);

var packetDataLength = GetPacketDataLength(messageLength, paddingLength);

Expand Down
38 changes: 26 additions & 12 deletions src/Renci.SshNet/Security/Cryptography/BlockCipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,18 +110,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
return output;
}

/// <summary>
/// Decrypts the specified data.
/// </summary>
/// <param name="input">The data.</param>
/// <returns>
/// The decrypted data.
/// </returns>
public override byte[] Decrypt(byte[] input)
{
return Decrypt(input, 0, input.Length);
}

/// <summary>
/// Decrypts the specified input.
/// </summary>
Expand Down Expand Up @@ -167,5 +155,31 @@ public override byte[] Decrypt(byte[] input, int offset, int length)

return output;
}

/// <summary>
/// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
/// </summary>
/// <param name="inputBuffer">The input data to encrypt.</param>
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
/// <param name="outputBuffer">The output to which to write encrypted data.</param>
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
/// <returns>
/// The number of bytes encrypted.
/// </returns>
public abstract int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);

/// <summary>
/// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
/// </summary>
/// <param name="inputBuffer">The input data to decrypt.</param>
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
/// <param name="outputBuffer">The output to which to write decrypted data.</param>
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
/// <returns>
/// The number of bytes decrypted.
/// </returns>
public abstract int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset);
}
}
13 changes: 12 additions & 1 deletion src/Renci.SshNet/Security/Cryptography/Cipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ public abstract class Cipher
/// </value>
public abstract byte MinimumSize { get; }

/// <summary>
/// Gets the tag (MAC) size.
/// </summary>
/// <value>
/// The tag (MAC) size.
/// </value>
public virtual int TagSize { get; }

/// <summary>
/// Encrypts the specified input.
/// </summary>
Expand Down Expand Up @@ -41,7 +49,10 @@ public byte[] Encrypt(byte[] input)
/// <returns>
/// The decrypted data.
/// </returns>
public abstract byte[] Decrypt(byte[] input);
public byte[] Decrypt(byte[] input)
{
return Decrypt(input, 0, input.Length);
}

/// <summary>
/// Decrypts the specified input.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@
/// </summary>
public enum AesCipherMode
{
/// <summary>CBC Mode.</summary>
/// <summary>Cipher Block Chain Mode.</summary>
CBC = 1,

/// <summary>ECB Mode.</summary>
/// <summary>Electronic Codebook Mode.</summary>
ECB = 2,

/// <summary>OFB Mode.</summary>
/// <summary>Output Feedback Mode.</summary>
OFB = 3,

/// <summary>CFB Mode.</summary>
/// <summary>Cipher Feedback Mode.</summary>
CFB = 4,

/// <summary>CTS Mode.</summary>
/// <summary>Cipher Text Stealing Mode.</summary>
CTS = 5,

/// <summary>CTR Mode.</summary>
/// <summary>Counter Mode.</summary>
CTR = 6
}
}
137 changes: 137 additions & 0 deletions src/Renci.SshNet/Security/Cryptography/Ciphers/AesGcmCipher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_0_OR_GREATER
using System;
using System.Buffers.Binary;
using System.Security.Cryptography;

using Renci.SshNet.Common;

namespace Renci.SshNet.Security.Cryptography.Ciphers
{
/// <summary>
/// AES GCM cipher implementation.
/// <see href="https://datatracker.ietf.org/doc/html/rfc5647"/>.
/// </summary>
public sealed class AesGcmCipher : SymmetricCipher, IDisposable
{
private readonly byte[] _nonce;
private readonly AesGcm _aesGcm;

/// <inheritdoc/>
public override byte MinimumSize
{
get
{
return 16;
}
}

/// <inheritdoc/>
public override int TagSize
{
get
{
return 16;
}
}

/// <summary>
/// Initializes a new instance of the <see cref="AesGcmCipher"/> class.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="iv">The IV.</param>
public AesGcmCipher(byte[] key, byte[] iv)
: base(key)
{
_nonce = iv.Take(12);
#if NET8_0_OR_GREATER
_aesGcm = new AesGcm(key, TagSize);
#else
_aesGcm = new AesGcm(key);
#endif
}

/// <summary>
/// Encrypts the specified input.
/// </summary>
/// <param name="input">The input.</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 packet length field + cipher text + tag.
/// </returns>
public override byte[] Encrypt(byte[] input, int offset, int length)
{
// [outbound sequence field][packet length field][padding length field sz][payload][random paddings]
// [----4 bytes----(offset)][------4 bytes------][----------------Plain Text---------------(length)]
var packetLengthField = new ReadOnlySpan<byte>(input, offset, 4);
var plainText = new ReadOnlySpan<byte>(input, offset + 4, length - 4);

var output = new byte[length + TagSize];
packetLengthField.CopyTo(output);
var cipherText = new Span<byte>(output, 4, length - 4);
var tag = new Span<byte>(output, length, TagSize);

_aesGcm.Encrypt(_nonce, plainText, cipherText, tag, packetLengthField);

IncrementCounter();

return output;
}

/// <summary>
/// Decrypts the specified input.
/// </summary>
/// <param name="input">The input.</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 packet length field + plain text.
/// </returns>
public override byte[] Decrypt(byte[] input, int offset, int length)
{
// [inbound sequence field][packet length field][padding length field sz][payload][random paddings][Authenticated TAG]
// [----4 bytes---(offset)][------4 bytes------][------------------Cipher Text--------------------][---TAG---(length)]
var packetLengthField = new ReadOnlySpan<byte>(input, offset, 4);
var cipherText = new ReadOnlySpan<byte>(input, offset + 4, length - 4 - TagSize);
var tag = new ReadOnlySpan<byte>(input, offset + length - TagSize, TagSize);

var output = new byte[length - TagSize];
packetLengthField.CopyTo(output);
var plainText = new Span<byte>(output, 4, length - 4 - TagSize);

_aesGcm.Decrypt(_nonce, cipherText, tag, plainText, packetLengthField);

IncrementCounter();

return output;
}

private void IncrementCounter()
{
var invocationCounter = new Span<byte>(_nonce, 4, 8);
var count = BinaryPrimitives.ReadUInt64BigEndian(invocationCounter);
BinaryPrimitives.WriteUInt64BigEndian(invocationCounter, count + 1);
}

/// <summary>
/// Dispose the instance.
/// </summary>
/// <param name="disposing">Set to True to dispose of resouces.</param>
public void Dispose(bool disposing)
{
if (disposing)
{
_aesGcm.Dispose();
}
}

/// <inheritdoc/>
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
#endif
44 changes: 0 additions & 44 deletions src/Renci.SshNet/Security/Cryptography/Ciphers/Arc4Cipher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,6 @@ public Arc4Cipher(byte[] key, bool dischargeFirstBytes)
}
}

/// <summary>
/// Encrypts the specified region of the input byte array and copies the encrypted data to the specified region of the output byte array.
/// </summary>
/// <param name="inputBuffer">The input data to encrypt.</param>
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
/// <param name="outputBuffer">The output to which to write encrypted data.</param>
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
/// <returns>
/// The number of bytes encrypted.
/// </returns>
public override int EncryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
}

/// <summary>
/// Decrypts the specified region of the input byte array and copies the decrypted data to the specified region of the output byte array.
/// </summary>
/// <param name="inputBuffer">The input data to decrypt.</param>
/// <param name="inputOffset">The offset into the input byte array from which to begin using data.</param>
/// <param name="inputCount">The number of bytes in the input byte array to use as data.</param>
/// <param name="outputBuffer">The output to which to write decrypted data.</param>
/// <param name="outputOffset">The offset into the output byte array from which to begin writing data.</param>
/// <returns>
/// The number of bytes decrypted.
/// </returns>
public override int DecryptBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
return ProcessBytes(inputBuffer, inputOffset, inputCount, outputBuffer, outputOffset);
}

/// <summary>
/// Encrypts the specified input.
/// </summary>
Expand All @@ -98,18 +66,6 @@ public override byte[] Encrypt(byte[] input, int offset, int length)
return output;
}

/// <summary>
/// Decrypts the specified input.
/// </summary>
/// <param name="input">The input.</param>
/// <returns>
/// The decrypted data.
/// </returns>
public override byte[] Decrypt(byte[] input)
{
return Decrypt(input, 0, input.Length);
}

/// <summary>
/// Decrypts the specified input.
/// </summary>
Expand Down
Loading