SSH.NET v2025.1.0 Release Notes

Release Date: 2025-10-27 // 9 months ago
  • Highlights

    • ➕ Add DownloadFileAsync and UploadFileAsync to SftpClient (#1634)
    • 🐎 Much improved performance of SftpFileStream in consecutive read (e.g. SftpFileStream.CopyTo) scenarios (#1705)

    💥 Breaking changes:

    • 🔀 SftpFileStream previously had some incomplete synchronisation for multi-threaded access, but was not advertised nor fully functioning as thread safe. This synchronisation was removed in #1705. When accessing an SftpFileStream instance from multiple threads simultaneously, ensure there exists appropriate synchronisation.
    • SftpClient.CreateText and WriteAll{Bytes/Text/Lines} were changed in #1686 to truncate the file before writing if it exists, to align with the equivalent methods on System.IO.File. Given that the prior behaviour was 14 years old, the change treads the line between breaking change and bug fix.
    • ⚡️ IEnumerable<string> ReadLines on SftpClient was updated in #1681 to download and yield lines during enumeration rather than reading them all up front and returning the result. This means that the connection must be active during enumeration. When storing the result of ReadLines for later use, consider using string[] ReadAllLines instead.

    What's Changed

    🆕 New Contributors

    Full Changelog : 2025.0.0...2025.1.0

    API diff

    namespace Renci.SshNet
      {
          public class ConnectionInfo
          {+ public Microsoft.Extensions.Logging.ILoggerFactory? LoggerFactory { get; set; }}
          public interface ISftpClient : Renci.SshNet.IBaseClient
          {+ System.Threading.Tasks.Task DownloadFileAsync(string path, System.IO.Stream output, System.Threading.CancellationToken cancellationToken = default);+ System.Threading.Tasks.Task\<Renci.SshNet.Sftp.SftpFileAttributes\> GetAttributesAsync(string path, System.Threading.CancellationToken cancellationToken);+ System.Threading.Tasks.Task UploadFileAsync(System.IO.Stream input, string path, System.Threading.CancellationToken cancellationToken = default);}
          public class SftpClient : Renci.SshNet.BaseClient, Renci.SshNet.ISftpClient, Renci.SshNet.IBaseClient
          {+ public System.Threading.Tasks.Task DownloadFileAsync(string path, System.IO.Stream output, System.Threading.CancellationToken cancellationToken = default);+ public System.Threading.Tasks.Task\<Renci.SshNet.Sftp.SftpFileAttributes\> GetAttributesAsync(string path, System.Threading.CancellationToken cancellationToken);+ public System.Threading.Tasks.Task UploadFileAsync(System.IO.Stream input, string path, System.Threading.CancellationToken cancellationToken = default);}
          public sealed class ShellStream
          {+ public void ChangeWindowSize(uint columns, uint rows, uint width, uint height);}
      }
      namespace Renci.SshNet.Common
      {
          public class SftpPathNotFoundException : Renci.SshNet.Common.SftpException
          {+ public SftpPathNotFoundException(string? message, string? path, System.Exception? innerException);+ public SftpPathNotFoundException(string? message, string? path);+ public string? Path { get; }}+ public class SftpException : Renci.SshNet.Common.SshException+ {+ public SftpException(Renci.SshNet.Sftp.StatusCode statusCode, string? message, System.Exception? innerException);+ public SftpException(Renci.SshNet.Sftp.StatusCode statusCode, string? message);+ public SftpException(Renci.SshNet.Sftp.StatusCode statusCode);+ public Renci.SshNet.Sftp.StatusCode StatusCode { get; }+ }}
      namespace Renci.SshNet.Security
      {+ public class KeyExchangeDiffieHellman : Renci.SshNet.Security.KeyExchange+ {+ public KeyExchangeDiffieHellman(string name, Org.BouncyCastle.Crypto.Parameters.DHParameters parameters, System.Security.Cryptography.HashAlgorithmName hashAlgorithm);+ public override void Start(Renci.SshNet.Session session, Renci.SshNet.Messages.Transport.KeyExchangeInitMessage message, bool sendClientInitMessage);+ public override string Name { get; }+ }+ public class KeyExchangeDiffieHellmanGroupExchange : Renci.SshNet.Security.KeyExchange+ {+ public KeyExchangeDiffieHellmanGroupExchange(string name, System.Security.Cryptography.HashAlgorithmName hashAlgorithm, uint minimumGroupSize, uint preferredGroupSize, uint maximumGroupSize);+ public KeyExchangeDiffieHellmanGroupExchange(string name, System.Security.Cryptography.HashAlgorithmName hashAlgorithm);+ public override void Start(Renci.SshNet.Session session, Renci.SshNet.Messages.Transport.KeyExchangeInitMessa...
    

Previous changes from v2025.0.0

  • Highlights

    • ➕ Add support for post-quantum key exchange methods sntrup761x25519-sha512 and mlkem768x25519-sha256
    • ➕ Add support for PuTTY private key files
    • ➕ Add logging capability via Microsoft.Extensions.Logging

    💥 Breaking changes

    • 👌 Support for DSA was dropped in #1558
    • CipherPadding was deleted in #1546 and uses replaced with Org.BouncyCastle.Crypto.Paddings.IBlockCipherPadding
    • 👀 See the full API diff at the end

    What's Changed

    • ⬆️ Bump version by @Rob-Hague in #1533
    • 🛠 fix Package Downgrade Warning with .NET 9 SDK by @mus65 in #1538
    • ⚡️ Update PrivateKeyFile.cs by @scott-xu in #1541
    • 🖐 Handle lower-case hex in private key's salt field by @RiJo in #179
    • Split PrivateKeyFile into different implementations. by @scott-xu in #1542
    • Migrate from AppVeyor to GitHub Actions by @mus65 in #1539
    • ➕ Add .NET 9 target by @mus65 in #1480
    • ✂ Remove appveyor.yml by @mus65 in #1547
    • 🌲 Replace DiagnosticAbstration with Microsoft.Extensions.Logging.Abstractions by @mus65 in #1509
    • 👍 [Private Key] Add support for PuTTY private key file format (V3 and V2) by @scott-xu in #1543
    • 🛠 fix newline characters in comment by @Varorbc in #1550
    • 🐧 Pin Alpine Linux image to 3.20 by @Rob-Hague in #1554
    • 🛠 Fix consumption of NBGV version properties in build by @Rob-Hague in #1544
    • ⬇️ Drop net7.0 target by @Rob-Hague in #1468
    • ➕ Add padding when encrypt and remove padding when decrypt by @scott-xu in #1545
    • 🔒 Use System.Security.Cryptography for TripleDesCipher by @scott-xu in #1546
    • 👍 [PQC] Add support for sntrup761x25519-sha512 key exchange method by @scott-xu in #1562
    • ⬇️ Drop DSA by @Rob-Hague in #1558
    • 🐳 Create dependabot.yml for docker image by @Rob-Hague in #1559
    • 🛠 Fix hang/unhandled exception in SshCommand upon disconnect by @Rob-Hague in #1565
    • ⬆️ Bump alpine from 3.20 to 3.21 in /test/Renci.SshNet.IntegrationTests by @dependabot in #1567
    • 👍 [PQC] Add support for mlkem768x25519-sha256 key exchange method by @scott-xu in #1563
    • 🚚 Move IDisposable implementation declaration from inheritees to parent by @bad-samaritan in #746
    • ⚡️ Update bound port on ForwardedPortDynamic after connection (in case original was passed as zero) by @timyhac in #1577
    • ⚡️ Configure Dependabot to also update NuGet and GitHub Actions by @mus65 in #1581
    • ⬆️ Bump Microsoft.NET.Test.Sdk from 17.11.1 to 17.12.0 by @dependabot in #1585
    • ⬆️ Bump actions/configure-pages from 4 to 5 by @dependabot in #1582
    • 🛠 fix: UnhandledException: System.ObjectDisposedException. by @SoftStoneDevelop in #1590
    • ⬆️ Bump test dependencies by @dependabot in #1583
    • 🌲 Tweak logging.md by @Rob-Hague in #1601
    • Reply to global requests when want_reply is true by @Rob-Hague in #1600
    • Don't dispose channel when completing SshCommand by @Rob-Hague in #1596
    • ⬇️ Drop net6.0 target by @mus65 in #1580
    • ✂ remove Reverse extension to avoid source-breaking change with .NET 10 by @mus65 in #1606
    • ⚡️ Bump the dependencies group with 7 updates by @dependabot in #1607
    • 🏁 CI: run .NET Framework Integration Tests on Windows by @mus65 in #1615
    • 🛠 Fix API break on KeyExchange by @Rob-Hague in #1609
    • ➕ Add a Stream buffer validation helper by @Rob-Hague in #1605
    • ➕ Add an OrderedDictionary implementation for algorithm priorities by @Rob-Hague in #1611
    • 🔒 lock SendData to fix random connection failures by @mus65 in #1623
    • 🚀 Only enable TreatWarningsAsErrors in Release by @mus65 in #1624
    • ✂ Remove unused bcrypt password hashing code by @mus65 in #1626
    • ⚡️ Bump the dependencies group with 5 updates by @dependabot in #1625
    • ➕ Added ExistsAsync and GetAsync to ISftpClient by @Maarty in #1628

    🆕 New Contributors

    Full Changelog : 2024.2.0...2025.0.0

    API diff

    namespace Renci.SshNet
      {
          public abstract class AuthenticationMethod
          {+ public void Dispose();+ protected virtual void Dispose(bool disposing);}
          public class ConnectionInfo
          {- public System.Collections.Generic.IDictionary\<string, System.Func\<Renci.SshNet.Compression.Compressor\>\> CompressionAlgorithms { get; }+ public Renci.SshNet.IOrderedDictionary\<string, System.Func\<Renci.SshNet.Compression.Compressor\>\> CompressionAlgorithms { get; }- public System.Collections.Generic.IDictionary\<string, Renci.SshNet.CipherInfo\> Encryptions { get; }+ public Renci.SshNet.IOrderedDictionary\<string, Renci.SshNet.CipherInfo\> Encryptions { get; }- public System.Collections.Generic.IDictionary\<string, Renci.SshNet.HashInfo\> HmacAlgorithms { get; }+ public Renci.SshNet.IOrderedDictionary\<string, Renci.SshNet.HashInfo\> HmacAlgorithms { get; }- public System.Collections.Generic.IDictionary\<string, System.Func\<byte[], Renci.SshNet.Security.KeyHostAlgorithm\>\> HostKeyAlgorithms { get; }+ public Renci.SshNet.IOrderedDictionary\<string, System.Func\<byte[], Renci.SshNet.Security.KeyHostAlgorithm\>\> HostKeyAlgorithms { get; }- public System.Collections.Generic.IDictionary\<string, System.Func\<Renci.SshNet.Security.IKeyExchange\>\> KeyExchangeAlgorithms { get; }+ public Renci.SshNet.IOrderedDictionary\<string, System.Func\<Renci.SshNet.Security.IKeyExchange\>\> KeyExchangeAlgorithms { get; }}
          public interface ISftpClient : Renci.SshNet.IBaseClient
          {+ System.Threading.Tasks.Task\<bool\> ExistsAsync(string path, System.Threading.CancellationToken cancellationToken = null);+ System.Threading.Tasks.Task\<Renci.SshNet.Sftp.ISftpFile\> GetAsync(string path, System.Threading.CancellationToken cancellationToken);}
          public class KeyboardInteractiveAuthenticationMethod : Renci.SshNet.AuthenticationMethod
          {- public void Dispose();- protected virtual void Dispose(bool disposing);+ protected override void Dispose(bool disposing);}
          public class NoneAuthenticationMethod : Renci.SshNet.AuthenticationMethod
          {- public void Dispose();- protected virtual void Dispose(bool disposing);+ protected override void Dispose(bool disposing);}
          public class PasswordAuthenticationMethod : Renci.SshNet.AuthenticationMethod
          {- public void Dispose();- protected virtual void Dispose(bool disposing);+ protected override void Dispose(bool disposing);}
          public class PrivateKeyAuthenticationMethod : Renci.SshNet.AuthenticationMethod
          {- public void Dispose();- protected virtual void Dispose(bool disposing);+ protected override void Dispose(bool disposing);}+ public interface IOrderedDictionary\<TKey, TValue\> : IDictionary\<TKey, TValue\>, IReadOnlyDictionary\<TKey, TValue\>+ {+ bool ContainsKey(TKey key);+ bool ContainsValue(TValue value);+ System.Collections.Generic.KeyValuePair\<TKey, TValue\> GetAt(int index);+ int IndexOf(TKey key);+ void Insert(int index, TKey key, TValue value);+ bool Remove(TKey key, out TValue value);+ void RemoveAt(int index);+ void SetAt(int index, TKey key, TValue value);+ void SetAt(int index, TValue value);+ void SetPosition(TKey key, int newIndex);+ void SetPosition(int index, int newIndex);+ bool TryAdd(TKey key, TValue value, out int index);+ bool TryAdd(TKey key, TValue value);+ bool TryGetValue(TKey key, out TValue value, out int index);+ bool TryGetValue(TKey key, out TValue value);+ int Count { get; }+ TValue this[TKey key] { get; set; }+ ...