public SshClient(SshClientSettings settings, ILogger?logger = null) { ValidateSettings(settings); _settings = settings ?? throw new ArgumentNullException(nameof(settings)); _logger = logger ?? NullLogger.Instance; _abortCts = new CancellationTokenSource(); }
private static void ValidateSettings(SshClientSettings settings) { // TODO: extend this... if (settings.Host == null) { throw new ArgumentNullException(nameof(settings.Host)); } }
private static async Task PerformDefaultExchange(SshConnection connection, SshConnectionInfo connectionInfo, ILogger logger, SshClientSettings settings, CancellationToken ct) { // Protocol Version Exchange: https://tools.ietf.org/html/rfc4253#section-4.2. // The maximum length of the string is 255 characters, including the Carriage Return and Line Feed. const int MaxLineLength = 255 - 2; // The server MAY send other lines of data before sending the version string. const int MaxLineReads = 20; AssemblyInformationalVersionAttribute?versionAttribute = typeof(SshClient).Assembly.GetCustomAttribute <AssemblyInformationalVersionAttribute>(); string version = versionAttribute?.InformationalVersion ?? "0.0"; version = version.Replace('-', '_'); string identificationString = $"SSH-2.0-TmdsSsh_{version}"; connectionInfo.ClientIdentificationString = identificationString; // Send our identification string. logger.LogInformation("Local version string {identificationString}", identificationString); await connection.WriteLineAsync(identificationString, ct); // Receive peer identification string. for (int i = 0; i < MaxLineReads; i++) { string line = await connection.ReceiveLineAsync(MaxLineLength, ct); if (line.StartsWith("SSH-", StringComparison.Ordinal)) { connectionInfo.ServerIdentificationString = line; if (line.StartsWith("SSH-2.0-", StringComparison.Ordinal)) { logger.LogInformation("Remote version string {identificationString}", connectionInfo.ServerIdentificationString); return; } ThrowHelper.ThrowProtocolUnsupportedVersion(line); } } ThrowHelper.ThrowProtocolNoVersionIdentificationString(); }
private async static Task PerformDefaultAuthentication(SshConnection connection, ILogger logger, SshClientSettings settings, SshConnectionInfo connectionInfo, CancellationToken ct) { // TODO: handle SSH_MSG_USERAUTH_BANNER. // Request ssh-userauth service { using var serviceRequestMsg = CreateServiceRequestMessage(connection.SequencePool); await connection.SendPacketAsync(serviceRequestMsg, ct); } { using Packet serviceAcceptMsg = await connection.ReceivePacketAsync(ct); ParseServiceAccept(serviceAcceptMsg); } // Try credentials. foreach (var credential in settings.Credentials) { if (credential is PasswordCredential passwordCredential) { logger.AuthenticationMethod("password"); using var userAuthMsg = CreatePasswordRequestMessage(connection.SequencePool, settings.UserName !, passwordCredential.Password); await connection.SendPacketAsync(userAuthMsg, ct); } else if (credential is IdentityFileCredential ifCredential) { if (IdentityFileCredential.TryParseFile(ifCredential.Filename, out PrivateKey? pk)) { using (pk) { logger.AuthenticationMethodPublicKey(ifCredential.Filename); using var userAuthMsg = CreatePublicKeyRequestMessage(connection.SequencePool, settings.UserName !, connectionInfo.SessionId !, pk !); await connection.SendPacketAsync(userAuthMsg, ct); } } else { continue; } } else { throw new NotImplementedException("Unsupported credential type: " + credential.GetType().FullName); } // TODO... using Packet response = await connection.ReceivePacketAsync(ct); if (IsAuthSuccesfull(response)) { logger.AuthenticationSucceeded(); return; } } throw new AuthenticationFailedException(); }
private async static Task PerformDefaultExchange(SshConnection connection, Packet clientKexInitMsg, Packet serverKexInitMsg, ILogger logger, SshClientSettings settings, SshConnectionInfo connectionInfo, CancellationToken ct) { // Key Exchange: https://tools.ietf.org/html/rfc4253#section-7. SequencePool sequencePool = connection.SequencePool; var remoteInit = ParseKeyExchangeInitMessage(serverKexInitMsg); // The chosen algorithm MUST be the first algorithm on the client's name-list // that is also on the server's name-list. Name encC2S = ChooseAlgorithm(settings.EncryptionAlgorithmsClientToServer, remoteInit.encryption_algorithms_client_to_server); Name encS2C = ChooseAlgorithm(settings.EncryptionAlgorithmsServerToClient, remoteInit.encryption_algorithms_server_to_client); Name macC2S = ChooseAlgorithm(settings.MacAlgorithmsClientToServer, remoteInit.mac_algorithms_client_to_server); Name macS2C = ChooseAlgorithm(settings.MacAlgorithmsServerToClient, remoteInit.mac_algorithms_server_to_client); Name comC2S = ChooseAlgorithm(settings.CompressionAlgorithmsClientToServer, remoteInit.compression_algorithms_client_to_server); Name comS2C = ChooseAlgorithm(settings.CompressionAlgorithmsServerToClient, remoteInit.compression_algorithms_server_to_client); if (encC2S.IsEmpty || encS2C.IsEmpty || macC2S.IsEmpty || macS2C.IsEmpty || comC2S.IsEmpty || comS2C.IsEmpty) { ThrowHelper.ThrowKeyExchangeFailed("No common encryption/integrity/compression algorithm."); } // Make an ordered list of host key algorithms. The key exchange algorithm will pick a compatible one. List <Name> hostKeyAlgorithms = new List <Name>(capacity: settings.ServerHostKeyAlgorithms.Count); foreach (var hostKeyAlgorithm in settings.ServerHostKeyAlgorithms) { if (remoteInit.server_host_key_algorithms.Contains(hostKeyAlgorithm)) { hostKeyAlgorithms.Add(hostKeyAlgorithm); } } // The first algorithm MUST be the preferred (and guessed) algorithm. If // both sides make the same guess, that algorithm MUST be used. Name matchingKex = settings.KeyExchangeAlgorithms.Count == 0 || remoteInit.kex_algorithms.Length == 0 || settings.KeyExchangeAlgorithms[0] != remoteInit.kex_algorithms[0] ? default(Name) : settings.KeyExchangeAlgorithms[0]; KeyExchangeOutput?keyExchangeOutput = null; Packet exchangeInitMsg = default; try { if (remoteInit.first_kex_packet_follows) { exchangeInitMsg = await connection.ReceivePacketAsync(ct); if (matchingKex.IsEmpty || settings.ServerHostKeyAlgorithms.Count == 0 || remoteInit.server_host_key_algorithms.Length == 0 || settings.ServerHostKeyAlgorithms[0] != remoteInit.server_host_key_algorithms[0]) { // Silently ignore if guessed wrong. exchangeInitMsg.Dispose(); exchangeInitMsg = default; } else { // Only accept the first hostKeyAlgorithm. if (hostKeyAlgorithms.Count > 1) { hostKeyAlgorithms.RemoveRange(1, hostKeyAlgorithms.Count - 1); } } } EncryptionFactory.Default.GetKeyAndIVLength(encC2S, out int encryptionKeyC2SLength, out int initialIVC2SLength); EncryptionFactory.Default.GetKeyAndIVLength(encS2C, out int encryptionKeyS2CLength, out int initialIVS2CLength); int integrityKeyC2SLength = HMacFactory.Default.GetKeyLength(macC2S); int integrityKeyS2CLength = HMacFactory.Default.GetKeyLength(macS2C); var keyExchangeInput = new KeyExchangeInput(hostKeyAlgorithms, exchangeInitMsg, clientKexInitMsg, serverKexInitMsg, connectionInfo, initialIVC2SLength, initialIVS2CLength, encryptionKeyC2SLength, encryptionKeyS2CLength, integrityKeyC2SLength, integrityKeyS2CLength); foreach (var keyAlgorithm in settings.KeyExchangeAlgorithms) { if (remoteInit.kex_algorithms.Contains(keyAlgorithm)) { logger.KeyExchangeAlgorithm(keyAlgorithm); using (var algorithm = KeyExchangeAlgorithmFactory.Default.Create(keyAlgorithm)) { keyExchangeOutput = await algorithm.TryExchangeAsync(connection, keyExchangeInput, logger, ct); } if (keyExchangeOutput != null) { connectionInfo.SessionId ??= keyExchangeOutput.ExchangeHash; break; } // Preferred algorithm must be used. if (!matchingKex.IsEmpty) { ThrowHelper.ThrowKeyExchangeFailed("Preferred key exchange algorithm failed."); } } } // If no algorithm satisfying all these conditions can be found, the // connection fails, and both sides MUST disconnect. if (keyExchangeOutput == null) { ThrowHelper.ThrowKeyExchangeFailed("Key exchange failed"); } } finally { exchangeInitMsg.Dispose(); } logger.AlgorithmsServerToClient(encS2C, macS2C, comS2C); logger.AlgorithmsClientToServer(encC2S, macC2S, comC2S); // Send SSH_MSG_NEWKEYS. { using Packet newKeysMsg = CreateNewKeysMessage(sequencePool); await connection.SendPacketAsync(newKeysMsg, ct); } // Receive SSH_MSG_NEWKEYS. using Packet newKeysReceivedMsg = await connection.ReceivePacketAsync(ct); ParseNewKeysMessage(newKeysReceivedMsg); var encrypt = EncryptionFactory.Default.CreateEncryptor(encC2S, keyExchangeOutput.EncryptionKeyC2S, keyExchangeOutput.InitialIVC2S); var macForEncoder = HMacFactory.Default.Create(macC2S, keyExchangeOutput.IntegrityKeyC2S); var decrypt = EncryptionFactory.Default.CreateDecryptor(encS2C, keyExchangeOutput.EncryptionKeyS2C, keyExchangeOutput.InitialIVS2C); var macForDecoder = HMacFactory.Default.Create(macS2C, keyExchangeOutput.IntegrityKeyS2C); connection.SetEncoderDecoder(new PacketEncoder(encrypt, macForEncoder), new PacketDecoder(sequencePool, decrypt, macForDecoder));
internal static async Task <SshConnection> EstablishConnectionAsync(ILogger logger, SequencePool sequencePool, SshClientSettings settings, CancellationToken ct) { Socket?socket = null; try { socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); // Connect to the remote host logger.Connecting(settings.Host !, settings.Port); await socket.ConnectAsync(settings.Host !, settings.Port, ct); logger.ConnectionEstablished(); socket.NoDelay = true; return(new SocketSshConnection(logger, sequencePool, socket)); } catch { socket?.Dispose(); throw; } }