├── global.json ├── SSHServerNodeJS ├── SSHServerNodeJS │ ├── IAlgorithm.ts │ ├── Configuration.ts │ ├── Compressions │ │ ├── ICompression.ts │ │ └── NoCompression.ts │ ├── HostKeyAlgorithms │ │ ├── IHostKeyAlgorithm.ts │ │ └── SSHRSA.ts │ ├── KexAlgorithms │ │ ├── IKexAlgorithm.ts │ │ └── DiffieHellmanGroup14SHA1.ts │ ├── MACAlgorithms │ │ ├── IMACAlgorithm.ts │ │ └── HMACSHA1.ts │ ├── Ciphers │ │ ├── ICipher.ts │ │ ├── NoCipher.ts │ │ └── TripleDESCBC.ts │ ├── SSHServerException.ts │ ├── package.json │ ├── SSHLogger.ts │ ├── app.ts │ ├── Packets │ │ ├── NewKeys.ts │ │ ├── Unimplemented.ts │ │ ├── DisconnectReason.ts │ │ ├── Packet.ts │ │ ├── KexDHInit.ts │ │ ├── Disconnect.ts │ │ ├── KexDHReply.ts │ │ ├── PacketType.ts │ │ └── KexInit.ts │ ├── ExchangeContext.ts │ ├── sshserver.json │ ├── ByteReader.ts │ ├── ByteWriter.ts │ ├── Server.ts │ ├── SSHServerNodeJS.njsproj │ └── Client.ts └── SSHServerNodeJS.sln ├── test └── SSHServerTests │ ├── Tests.cs │ ├── project.json │ └── SSHServerTests.xproj ├── src └── SSHServer │ ├── IAlgorithm.cs │ ├── Compressions │ ├── ICompression.cs │ └── NoCompression.cs │ ├── KexAlgorithms │ ├── IKexAlgorithm.cs │ └── DiffieHellmanGroup14SHA1.cs │ ├── HostKeyAlgorithms │ ├── IHostKeyAlgorithm.cs │ └── SSHRSA.cs │ ├── Ciphers │ ├── ICipher.cs │ ├── NoCipher.cs │ └── TripleDESCBC.cs │ ├── MACAlgorithms │ ├── IMACAlgorithm.cs │ └── HMACSHA1.cs │ ├── SSHServerException.cs │ ├── Packets │ ├── NewKeys.cs │ ├── Unimplemented.cs │ ├── DisconnectReason.cs │ ├── KexDHInit.cs │ ├── Disconnect.cs │ ├── KexDHReply.cs │ ├── PacketType.cs │ ├── Packet.cs │ └── KexInit.cs │ ├── Program.cs │ ├── project.json │ ├── ExchangeContext.cs │ ├── SSHServer.xproj │ ├── sshserver.json │ ├── ByteWriter.cs │ ├── ByteReader.cs │ ├── Server.cs │ └── Client.cs ├── README.md ├── LICENSE ├── SSHServer.sln └── .gitignore /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ "src", "test" ] 3 | } 4 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/IAlgorithm.ts: -------------------------------------------------------------------------------- 1 | export interface IAlgorithm { 2 | getName(): string; 3 | } 4 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Configuration.ts: -------------------------------------------------------------------------------- 1 | export class Key { 2 | algorithm: string; 3 | key: any; 4 | } 5 | 6 | export class Configuration { 7 | port: number; 8 | keys: Key[]; 9 | } 10 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Compressions/ICompression.ts: -------------------------------------------------------------------------------- 1 | import { IAlgorithm } from "../IAlgorithm"; 2 | 3 | export interface ICompression extends IAlgorithm { 4 | compress(data: Buffer): Buffer; 5 | decompress(data: Buffer): Buffer; 6 | } 7 | -------------------------------------------------------------------------------- /test/SSHServerTests/Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Tests 5 | { 6 | public class Tests 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | Assert.True(true); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SSHServer/IAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer 7 | { 8 | public interface IAlgorithm 9 | { 10 | string Name { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/HostKeyAlgorithms/IHostKeyAlgorithm.ts: -------------------------------------------------------------------------------- 1 | import { IAlgorithm } from "../IAlgorithm"; 2 | 3 | export interface IHostKeyAlgorithm extends IAlgorithm { 4 | createKeyAndCertificatesData(): Buffer; 5 | createSignatureData(hash: Buffer): Buffer; 6 | } 7 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/KexAlgorithms/IKexAlgorithm.ts: -------------------------------------------------------------------------------- 1 | import { IAlgorithm } from "../IAlgorithm"; 2 | 3 | export interface IKexAlgorithm extends IAlgorithm { 4 | createKeyExchange(): Buffer; 5 | decryptKeyExchange(keyEx: Buffer): Buffer; 6 | computeHash(value: Buffer): Buffer; 7 | } 8 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/MACAlgorithms/IMACAlgorithm.ts: -------------------------------------------------------------------------------- 1 | import { IAlgorithm } from "../IAlgorithm"; 2 | 3 | export interface IMACAlgorithm extends IAlgorithm { 4 | getKeySize(): number; 5 | getDigestLength(): number; 6 | setKey(key: Buffer): void; 7 | computeHash(packetNumber: number, data: Buffer): Buffer; 8 | } 9 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Ciphers/ICipher.ts: -------------------------------------------------------------------------------- 1 | import { IAlgorithm } from "../IAlgorithm"; 2 | 3 | export interface ICipher extends IAlgorithm { 4 | getBlockSize(): number; 5 | getKeySize(): number; 6 | encrypt(data: Buffer): Buffer; 7 | decrypt(data: Buffer): Buffer; 8 | setKey(key: Buffer, iv: Buffer): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/SSHServer/Compressions/ICompression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Compressions 7 | { 8 | public interface ICompression : IAlgorithm 9 | { 10 | byte[] Compress(byte[] data); 11 | byte[] Decompress(byte[] data); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/SSHServerException.ts: -------------------------------------------------------------------------------- 1 | import { DisconnectReason } from "./Packets/DisconnectReason"; 2 | 3 | export class SSHServerException extends Error { 4 | public reason: DisconnectReason; 5 | 6 | constructor(reason: DisconnectReason, message: string) { 7 | super(message); 8 | 9 | this.reason = reason; 10 | } 11 | } 12 | 13 | export * from "./Packets/DisconnectReason"; 14 | -------------------------------------------------------------------------------- /src/SSHServer/KexAlgorithms/IKexAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.KexAlgorithms 7 | { 8 | public interface IKexAlgorithm : IAlgorithm 9 | { 10 | byte[] CreateKeyExchange(); 11 | byte[] DecryptKeyExchange(byte[] keyEx); 12 | byte[] ComputeHash(byte[] value); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Compressions/NoCompression.ts: -------------------------------------------------------------------------------- 1 | import { ICompression } from "./ICompression"; 2 | 3 | export class NoCompression implements ICompression { 4 | public getName(): string { 5 | return "none"; 6 | } 7 | 8 | public compress(data: Buffer): Buffer { 9 | return data; 10 | } 11 | 12 | public decompress(data: Buffer): Buffer { 13 | return data; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sshservernodejs", 3 | "version": "0.0.1", 4 | "description": "An SSH Server written in NodeJS", 5 | "main": "app.js", 6 | "author": { 7 | "name": "tyren" 8 | }, 9 | "dependencies": { 10 | "sleep2": "^1.0.0" 11 | }, 12 | "devDependencies": { 13 | "typescript": "^2.1.6", 14 | "@types/node": "^7.0.5" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SSHServer/HostKeyAlgorithms/IHostKeyAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.HostKeyAlgorithms 7 | { 8 | public interface IHostKeyAlgorithm : IAlgorithm 9 | { 10 | void ImportKey(string keyXml); 11 | byte[] CreateKeyAndCertificatesData(); 12 | byte[] CreateSignatureData(byte[] hash); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SSHServer/Ciphers/ICipher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Ciphers 7 | { 8 | public interface ICipher : IAlgorithm 9 | { 10 | uint BlockSize { get; } 11 | uint KeySize { get; } 12 | byte[] Encrypt(byte[] data); 13 | byte[] Decrypt(byte[] data); 14 | void SetKey(byte[] key, byte[] iv); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/SSHServer/MACAlgorithms/IMACAlgorithm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.MACAlgorithms 7 | { 8 | public interface IMACAlgorithm : IAlgorithm 9 | { 10 | uint KeySize { get; } 11 | uint DigestLength { get; } 12 | void SetKey(byte[] key); 13 | byte[] ComputeHash(uint packetNumber, byte[] data); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/SSHServer/SSHServerException.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Packets; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SSHServer 8 | { 9 | public class SSHServerException : Exception 10 | { 11 | public DisconnectReason Reason { get; set; } 12 | 13 | public SSHServerException(DisconnectReason reason, string message) : base(message) 14 | { 15 | Reason = reason; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/SSHLogger.ts: -------------------------------------------------------------------------------- 1 | export class SSHLogger { 2 | static logError(message: string): void { 3 | console.log("ERROR: " + message); 4 | } 5 | 6 | static logDebug(message: string): void { 7 | console.log("DEBUG: " + message); 8 | } 9 | 10 | static logWarning(message: string): void { 11 | console.log(" WARN: " + message); 12 | } 13 | 14 | static logInfo(message: string): void { 15 | console.log(" INFO: " + message); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Ciphers/NoCipher.ts: -------------------------------------------------------------------------------- 1 | import { ICipher } from "./ICipher"; 2 | 3 | export class NoCipher implements ICipher { 4 | public getName(): string { 5 | return "none"; 6 | } 7 | 8 | public getBlockSize(): number { 9 | return 8; 10 | } 11 | 12 | public getKeySize(): number { 13 | return 0; 14 | } 15 | 16 | public encrypt(data: Buffer): Buffer { 17 | return data; 18 | } 19 | 20 | public decrypt(data: Buffer): Buffer { 21 | return data; 22 | } 23 | 24 | public setKey(key: Buffer, iv: Buffer): void { 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/app.ts: -------------------------------------------------------------------------------- 1 | import { SSHLogger } from "./SSHLogger"; 2 | import { Server } from "./Server"; 3 | 4 | const sleep: any = require("sleep2"); 5 | 6 | let isRunning: boolean = true; 7 | process.on("SIGINT", function (): void { 8 | 9 | SSHLogger.logInfo("Gracefully shutting down from SIGINT (Ctrl+C)"); 10 | 11 | isRunning = false; 12 | }); 13 | 14 | let server: Server = new Server(); 15 | server.start(); 16 | 17 | (async () => { 18 | while (isRunning) { 19 | server.poll(); 20 | await sleep(25); 21 | } 22 | 23 | server.stop(); 24 | 25 | process.exit(0); 26 | })(); 27 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/NewKeys.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { PacketType } from "./PacketType"; 3 | import { ByteReader } from "../ByteReader"; 4 | import { ByteWriter } from "../ByteWriter"; 5 | 6 | export class NewKeys extends Packet { 7 | public clientValue: Buffer; 8 | 9 | public getPacketType(): PacketType { 10 | return PacketType.SSH_MSG_NEWKEYS; 11 | } 12 | 13 | protected internalGetBytes(writer: ByteWriter) { 14 | // no data, nothing to write 15 | } 16 | 17 | public load(reader: ByteReader) { 18 | // no data, nothing to load 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SSHServer/Compressions/NoCompression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Compressions 7 | { 8 | public class NoCompression : ICompression 9 | { 10 | public string Name 11 | { 12 | get 13 | { 14 | return "none"; 15 | } 16 | } 17 | 18 | public byte[] Compress(byte[] data) 19 | { 20 | return data; 21 | } 22 | 23 | public byte[] Decompress(byte[] data) 24 | { 25 | return data; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/SSHServerTests/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable" 5 | }, 6 | "dependencies": { 7 | "System.Runtime.Serialization.Primitives": "4.3.0", 8 | "xunit": "2.1.0", 9 | "dotnet-test-xunit": "2.2.0-preview2-build1029" 10 | }, 11 | "testRunner": "xunit", 12 | "frameworks": { 13 | "netcoreapp1.1": { 14 | "dependencies": { 15 | "Microsoft.NETCore.App": { 16 | "type": "platform", 17 | "version": "1.1.0" 18 | } 19 | }, 20 | "imports": [ 21 | "dotnet5.4", 22 | "portable-net451+win8" 23 | ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/Unimplemented.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { PacketType } from "./PacketType"; 3 | import { ByteReader } from "../ByteReader"; 4 | import { ByteWriter } from "../ByteWriter"; 5 | 6 | export class Unimplemented extends Packet { 7 | public rejectedPacketNumber: number; 8 | 9 | public getPacketType(): PacketType { 10 | return PacketType.SSH_MSG_UNIMPLEMENTED; 11 | } 12 | 13 | protected internalGetBytes(writer: ByteWriter) { 14 | writer.writeUInt32(this.rejectedPacketNumber); 15 | } 16 | 17 | public load(reader: ByteReader) { 18 | this.rejectedPacketNumber = reader.getUInt32(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/NewKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public class NewKeys : Packet 9 | { 10 | public override PacketType PacketType 11 | { 12 | get 13 | { 14 | return PacketType.SSH_MSG_NEWKEYS; 15 | } 16 | } 17 | 18 | protected override void InternalGetBytes(ByteWriter writer) 19 | { 20 | // No data, nothing to write 21 | } 22 | 23 | public override void Load(ByteReader reader) 24 | { 25 | // No data, nothing to load 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SSHServer/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SSHServer 4 | { 5 | public class Program 6 | { 7 | private static bool s_IsRunning = true; 8 | public static void Main(string[] args) 9 | { 10 | Console.CancelKeyPress += Console_CancelKeyPress; 11 | 12 | Server server = new Server(); 13 | server.Start(); 14 | 15 | while (s_IsRunning) 16 | { 17 | server.Poll(); 18 | System.Threading.Thread.Sleep(25); 19 | } 20 | 21 | server.Stop(); 22 | } 23 | 24 | private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) 25 | { 26 | e.Cancel = true; 27 | s_IsRunning = false; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/DisconnectReason.ts: -------------------------------------------------------------------------------- 1 | export enum DisconnectReason { 2 | None = 0, 3 | SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1, 4 | SSH_DISCONNECT_PROTOCOL_ERROR = 2, 5 | SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3, 6 | SSH_DISCONNECT_RESERVED = 4, 7 | SSH_DISCONNECT_MAC_ERROR = 5, 8 | SSH_DISCONNECT_COMPRESSION_ERROR = 6, 9 | SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7, 10 | SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8, 11 | SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9, 12 | SSH_DISCONNECT_CONNECTION_LOST = 10, 13 | SSH_DISCONNECT_BY_APPLICATION = 11, 14 | SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12, 15 | SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13, 16 | SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14, 17 | SSH_DISCONNECT_ILLEGAL_USER_NAME = 15, 18 | } 19 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/Packet.ts: -------------------------------------------------------------------------------- 1 | import { PacketType } from "./PacketType"; 2 | import { ByteReader } from "../ByteReader"; 3 | import { ByteWriter } from "../ByteWriter"; 4 | 5 | export abstract class Packet { 6 | // https://tools.ietf.org/html/rfc4253#section-6.1 7 | public static MaxPacketSize: number = 35000; 8 | public static PacketHeaderSize: number = 5; 9 | 10 | public packetSequence: number = 0; 11 | 12 | public abstract getPacketType(): PacketType; 13 | 14 | public getBytes(): Buffer { 15 | let writer: ByteWriter = new ByteWriter(); 16 | 17 | writer.writePacketType(this.getPacketType()); 18 | this.internalGetBytes(writer); 19 | return writer.toBuffer(); 20 | } 21 | 22 | public abstract load(reader: ByteReader): void; 23 | 24 | protected abstract internalGetBytes(writer: ByteWriter): void; 25 | } -------------------------------------------------------------------------------- /src/SSHServer/Packets/Unimplemented.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public class Unimplemented : Packet 9 | { 10 | public override PacketType PacketType 11 | { 12 | get 13 | { 14 | return PacketType.SSH_MSG_UNIMPLEMENTED; 15 | } 16 | } 17 | 18 | public uint RejectedPacketNumber { get; set; } 19 | 20 | public override void Load(ByteReader reader) 21 | { 22 | RejectedPacketNumber = reader.GetUInt32(); 23 | } 24 | 25 | protected override void InternalGetBytes(ByteWriter writer) 26 | { 27 | // uint32 packet sequence number of rejected message 28 | writer.WriteUInt32(RejectedPacketNumber); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/KexDHInit.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { PacketType } from "./PacketType"; 3 | import { ByteReader } from "../ByteReader"; 4 | import { ByteWriter } from "../ByteWriter"; 5 | import * as Exceptions from "../SSHServerException"; 6 | 7 | export class KexDHInit extends Packet { 8 | public clientValue: Buffer; 9 | 10 | public getPacketType(): PacketType { 11 | return PacketType.SSH_MSG_KEXDH_INIT; 12 | } 13 | 14 | protected internalGetBytes(writer: ByteWriter) { 15 | // Server never sends this 16 | throw new Exceptions.SSHServerException( 17 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 18 | "SSH Server should never send a SSH_MSG_KEXDH_INIT message"); 19 | } 20 | 21 | public load(reader: ByteReader) { 22 | this.clientValue = reader.getMPInt(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/SSHServer/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0-*", 3 | "buildOptions": { 4 | "debugType": "portable", 5 | "emitEntryPoint": true 6 | }, 7 | "dependencies": { 8 | "Microsoft.Extensions.Configuration": "1.1.0", 9 | "Microsoft.Extensions.Configuration.Binder": "1.1.0", 10 | "Microsoft.Extensions.Configuration.FileExtensions": "1.1.0", 11 | "Microsoft.Extensions.Configuration.Json": "1.1.0", 12 | "Microsoft.Extensions.Logging": "1.1.0", 13 | "Microsoft.Extensions.Logging.Console": "1.1.0", 14 | "System.Xml.XmlDocument": "4.0.1" 15 | }, 16 | "frameworks": { 17 | "netcoreapp1.1": { 18 | "dependencies": { 19 | "Microsoft.NETCore.App": { 20 | "type": "platform", 21 | "version": "1.1.0" 22 | } 23 | }, 24 | "imports": "dnxcore50" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSHServer 2 | This is a tutorial on how to build a basic SSH Server in C#. The wiki is a step by step process for setup with explanation of the various terms. 3 | 4 | Please view the [Wiki](https://github.com/TyrenDe/SSHServer/wiki) for a full walkthrough! 5 | 6 | For extra credit, I also ported the SSH Server to [NodeJS](https://github.com/TyrenDe/SSHServer/tree/master/SSHServerNodeJS). 7 | 8 | Both samples get as far as sending and receiving encrypted packets. It does not implement any SSH services such as user-auth. But, after finishing the tutorial, adding new handlers for those packets and responding to them should be simple. 9 | 10 | It also doesn't implement a variety of non-required algorithms. I recommend extending your service to include more algorithm options. 11 | 12 | - [C# SSHServer](https://github.com/TyrenDe/SSHServer/tree/master/src/SSHServer) 13 | - [NodeJS SSHServer](https://github.com/TyrenDe/SSHServer/tree/master/SSHServerNodeJS) 14 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/KexAlgorithms/DiffieHellmanGroup14SHA1.ts: -------------------------------------------------------------------------------- 1 | import { IKexAlgorithm } from "./IKexAlgorithm"; 2 | 3 | import crypto = require("crypto"); 4 | 5 | export class DiffieHellmanGroup14SHA1 implements IKexAlgorithm { 6 | private m_DiffieHellman: crypto.DiffieHellman; 7 | 8 | constructor() { 9 | this.m_DiffieHellman = crypto.getDiffieHellman("modp14"); 10 | this.m_DiffieHellman.generateKeys(); 11 | } 12 | 13 | public getName(): string { 14 | return "diffie-hellman-group14-sha1"; 15 | } 16 | 17 | public createKeyExchange(): Buffer { 18 | return this.m_DiffieHellman.getPublicKey(); 19 | } 20 | 21 | public decryptKeyExchange(keyEx: Buffer): Buffer { 22 | return this.m_DiffieHellman.computeSecret(keyEx); 23 | } 24 | 25 | public computeHash(value: Buffer): Buffer { 26 | let sha1: crypto.Hash = crypto.createHash("SHA1"); 27 | return sha1.update(value).digest(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/ExchangeContext.ts: -------------------------------------------------------------------------------- 1 | import { IKexAlgorithm } from "./KexAlgorithms/IKexAlgorithm"; 2 | import { IHostKeyAlgorithm } from "./HostKeyAlgorithms/IHostKeyAlgorithm"; 3 | import { ICipher } from "./Ciphers/ICipher"; 4 | import { NoCipher } from "./Ciphers/NoCipher"; 5 | import { IMACAlgorithm } from "./MACAlgorithms/IMACAlgorithm"; 6 | import { ICompression } from "./Compressions/ICompression"; 7 | import { NoCompression } from "./Compressions/NoCompression"; 8 | 9 | export class ExchangeContext { 10 | public kexAlgorithm: IKexAlgorithm = null; 11 | public hostKeyAlgorithm: IHostKeyAlgorithm = null; 12 | public cipherClientToServer: ICipher = new NoCipher(); 13 | public cipherServerToClient: ICipher = new NoCipher(); 14 | public macAlgorithmClientToServer: IMACAlgorithm = null; 15 | public macAlgorithmServerToClient: IMACAlgorithm = null; 16 | public compressionClientToServer: ICompression = new NoCompression(); 17 | public compressionServerToClient: ICompression = new NoCompression(); 18 | } 19 | -------------------------------------------------------------------------------- /src/SSHServer/ExchangeContext.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Ciphers; 2 | using SSHServer.Compressions; 3 | using SSHServer.HostKeyAlgorithms; 4 | using SSHServer.KexAlgorithms; 5 | using SSHServer.MACAlgorithms; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace SSHServer 12 | { 13 | public class ExchangeContext 14 | { 15 | public IKexAlgorithm KexAlgorithm { get; set; } = null; 16 | public IHostKeyAlgorithm HostKeyAlgorithm { get; set; } = null; 17 | public ICipher CipherClientToServer { get; set; } = new NoCipher(); 18 | public ICipher CipherServerToClient { get; set; } = new NoCipher(); 19 | public IMACAlgorithm MACAlgorithmClientToServer { get; set; } = null; 20 | public IMACAlgorithm MACAlgorithmServerToClient { get; set; } = null; 21 | public ICompression CompressionClientToServer { get; set; } = new NoCompression(); 22 | public ICompression CompressionServerToClient { get; set; } = new NoCompression(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/Disconnect.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { PacketType } from "./PacketType"; 3 | import { ByteReader } from "../ByteReader"; 4 | import { ByteWriter } from "../ByteWriter"; 5 | import { DisconnectReason } from "./DisconnectReason"; 6 | 7 | export class Disconnect extends Packet { 8 | public reason: DisconnectReason; 9 | public description: string; 10 | public language: string = "en"; 11 | 12 | public getPacketType(): PacketType { 13 | return PacketType.SSH_MSG_DISCONNECT; 14 | } 15 | 16 | protected internalGetBytes(writer: ByteWriter) { 17 | writer.writeUInt32(this.reason); 18 | writer.writeString(this.description, "UTF8"); 19 | writer.writeString(this.language); 20 | } 21 | 22 | public load(reader: ByteReader) { 23 | this.reason = reader.getUInt32(); 24 | this.description = reader.getString("UTF8"); 25 | if (!reader.isEOF()) 26 | this.language = reader.getString(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/SSHServer/Ciphers/NoCipher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Ciphers 7 | { 8 | public class NoCipher : ICipher 9 | { 10 | public uint BlockSize 11 | { 12 | get 13 | { 14 | return 8; 15 | } 16 | } 17 | 18 | public uint KeySize 19 | { 20 | get 21 | { 22 | return 0; 23 | } 24 | } 25 | 26 | public string Name 27 | { 28 | get 29 | { 30 | return "none"; 31 | } 32 | } 33 | 34 | public byte[] Decrypt(byte[] data) 35 | { 36 | return data; 37 | } 38 | 39 | public byte[] Encrypt(byte[] data) 40 | { 41 | return data; 42 | } 43 | 44 | public void SetKey(byte[] key, byte[] iv) 45 | { 46 | // No key for this cipher 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/DisconnectReason.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public enum DisconnectReason : uint 9 | { 10 | None = 0, 11 | SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1, 12 | SSH_DISCONNECT_PROTOCOL_ERROR = 2, 13 | SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3, 14 | SSH_DISCONNECT_RESERVED = 4, 15 | SSH_DISCONNECT_MAC_ERROR = 5, 16 | SSH_DISCONNECT_COMPRESSION_ERROR = 6, 17 | SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7, 18 | SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8, 19 | SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9, 20 | SSH_DISCONNECT_CONNECTION_LOST = 10, 21 | SSH_DISCONNECT_BY_APPLICATION = 11, 22 | SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12, 23 | SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13, 24 | SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14, 25 | SSH_DISCONNECT_ILLEGAL_USER_NAME = 15, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Ciphers/TripleDESCBC.ts: -------------------------------------------------------------------------------- 1 | import { ICipher } from "./ICipher"; 2 | 3 | import crypto = require("crypto"); 4 | 5 | export class TripleDESCBC implements ICipher { 6 | private m_Encryptor: crypto.Cipher; 7 | private m_Decryptor: crypto.Decipher; 8 | 9 | public getName(): string { 10 | return "3des-cbc"; 11 | } 12 | 13 | public getBlockSize(): number { 14 | return 8; 15 | } 16 | 17 | public getKeySize(): number { 18 | return 24; 19 | } 20 | 21 | public encrypt(data: Buffer): Buffer { 22 | return this.m_Encryptor.update(data); 23 | } 24 | 25 | public decrypt(data: Buffer): Buffer { 26 | return this.m_Decryptor.update(data); 27 | } 28 | 29 | public setKey(key: Buffer, iv: Buffer): void { 30 | this.m_Encryptor = crypto.createCipheriv("DES-EDE3-CBC", key, iv); 31 | this.m_Encryptor.setAutoPadding(false); 32 | 33 | this.m_Decryptor = crypto.createDecipheriv("DES-EDE3-CBC", key, iv); 34 | this.m_Decryptor.setAutoPadding(false); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "SSHServerNodeJS", "SSHServerNodeJS\SSHServerNodeJS.njsproj", "{B040EAAA-F43B-47C1-9BE2-2ED609B7DA96}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {B040EAAA-F43B-47C1-9BE2-2ED609B7DA96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {B040EAAA-F43B-47C1-9BE2-2ED609B7DA96}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {B040EAAA-F43B-47C1-9BE2-2ED609B7DA96}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {B040EAAA-F43B-47C1-9BE2-2ED609B7DA96}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/KexDHInit.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public class KexDHInit : Packet 9 | { 10 | public override PacketType PacketType 11 | { 12 | get 13 | { 14 | return PacketType.SSH_MSG_KEXDH_INIT; 15 | } 16 | } 17 | 18 | public byte[] ClientValue { get; set; } 19 | 20 | protected override void InternalGetBytes(ByteWriter writer) 21 | { 22 | // Server never sends this 23 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "SSH Server should never send a SSH_MSG_KEXDH_INIT message"); 24 | } 25 | 26 | public override void Load(ByteReader reader) 27 | { 28 | // First, the client sends the following: 29 | // byte SSH_MSG_KEXDH_INIT (handled by base class) 30 | // mpint e 31 | ClientValue = reader.GetMPInt(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Shane DeSeranno 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/SSHServer/SSHServer.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 8d384f15-ad75-4c22-8cc3-5c8bd5a417bb 10 | SSHServer 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/SSHServerTests/SSHServerTests.xproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | 7 | 8 | 9 | 27f29a1c-fc8e-425b-b91c-4ef1c8bf15e3 10 | SSHServerTests 11 | .\obj 12 | .\bin\ 13 | 14 | 15 | 16 | 2.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/Disconnect.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SSHServer.Packets 8 | { 9 | public class Disconnect : Packet 10 | { 11 | public override PacketType PacketType 12 | { 13 | get 14 | { 15 | return PacketType.SSH_MSG_DISCONNECT; 16 | } 17 | } 18 | 19 | public DisconnectReason Reason { get; set; } 20 | public string Description { get; set; } 21 | public string Language { get; set; } = "en"; 22 | 23 | public override void Load(ByteReader reader) 24 | { 25 | Reason = (DisconnectReason)reader.GetUInt32(); 26 | Description = reader.GetString(Encoding.UTF8); 27 | if (!reader.IsEOF) 28 | Language = reader.GetString(); 29 | } 30 | 31 | protected override void InternalGetBytes(ByteWriter writer) 32 | { 33 | writer.WriteUInt32((uint)Reason); 34 | writer.WriteString(Description, Encoding.UTF8); 35 | writer.WriteString(Language); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/KexDHReply.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { PacketType } from "./PacketType"; 3 | import { ByteReader } from "../ByteReader"; 4 | import { ByteWriter } from "../ByteWriter"; 5 | import * as Exceptions from "../SSHServerException"; 6 | 7 | export class KexDHReply extends Packet { 8 | public serverHostKey: Buffer; 9 | public serverValue: Buffer; 10 | public signature: Buffer; 11 | 12 | public getPacketType(): PacketType { 13 | return PacketType.SSH_MSG_KEXDH_REPLY; 14 | } 15 | 16 | protected internalGetBytes(writer: ByteWriter) { 17 | // string server public host key and certificates(K_S) 18 | // mpint f 19 | // string signature of H 20 | writer.writeBytes(this.serverHostKey); 21 | writer.writeMPInt(this.serverValue); 22 | writer.writeBytes(this.signature); 23 | } 24 | 25 | public load(reader: ByteReader) { 26 | // Client never sends this! 27 | throw new Exceptions.SSHServerException( 28 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 29 | "SSH Client should never send a SSH_MSG_KEXDH_REPLY message"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SSHServer/sshserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": true, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | }, 10 | "port": 22, 11 | "keys": { 12 | "ssh-rsa": "xXKzcIH/rzcfv2D7VcvLdxR5S5iw2TTsP65Aa82S4+9ZIqLPTNtuzr76Mz6Cx0yDOhawHlIujtalqaaQzaUvkudCtMcVMnj37OcCYz7XDAYejalCxf/vtJo7U4mnYdCM+nAOQKNIDKLGbLtGuEAGwdi560DOJY2plhnBf1oOI+k=AQAB

37kMr9YiU4cSqHqTJSjBJ/szG2O4n5xSIlPy4MZ4aAN5NALHxfsN0dq1y8NL6GTLMI5qoykvp4Bjrm2ZgU1cDQ==

4e84rF+UsFBfQEKJc2pbACWWJjttNW0hccdQZzA3IUxRmd/Z4yEMr1L70TP0XV7dw1RDs1JyU7xnXBIbGy5ETQ==vP0TbI6VnL3j0xMIrkFJOj8Ho0GQOrTQ5VLJP3wpRqR4hKk8nVBBEl+RZznpK73Jr5D/ICmwqezZSAYpwILbGQ==bHdzZtWwRYEgaXJIGL+7lnN1BT/MazTMNJpykEeGgBbqqgvcx/zq4RTezg26SEUuBANlSSbQukCeAoayurbYlQ==Irq9vR7CXIVR+r09caYIxIY8BOig+HShN1bXvATERJcjTW2jUgJrUttDGNEx70/hBd7m1NWCZz5YO3RH9Bdf5w==AqxsufxFcW9TDCAmQK4mwVdsoQjRp2jfcULmkM8fl9u40dtxTr6Csv5dz7qfKLWxHTGlDUDabCK2t/DCcZZoA3rsqwLADe4ZerDdg6xiq4MBzNprM8Y0IfNESEdFB9T0T73ONQCsMalUzEvUknC4Ed4Fya34LUHntgQtEhXpDJE=
" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/KexDHReply.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public class KexDHReply : Packet 9 | { 10 | public override PacketType PacketType 11 | { 12 | get 13 | { 14 | return PacketType.SSH_MSG_KEXDH_REPLY; 15 | } 16 | } 17 | 18 | public byte[] ServerHostKey { get; set; } 19 | public byte[] ServerValue { get; set; } 20 | public byte[] Signature { get; set; } 21 | 22 | protected override void InternalGetBytes(ByteWriter writer) 23 | { 24 | // string server public host key and certificates(K_S) 25 | // mpint f 26 | // string signature of H 27 | writer.WriteBytes(ServerHostKey); 28 | writer.WriteMPInt(ServerValue); 29 | writer.WriteBytes(Signature); 30 | } 31 | 32 | public override void Load(ByteReader reader) 33 | { 34 | // Client never sends this! 35 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "SSH Client should never send a SSH_MSG_KEXDH_REPLY message"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/PacketType.ts: -------------------------------------------------------------------------------- 1 | export enum PacketType { 2 | SSH_MSG_DISCONNECT = 1, 3 | SSH_MSG_IGNORE = 2, 4 | SSH_MSG_UNIMPLEMENTED = 3, 5 | SSH_MSG_DEBUG = 4, 6 | SSH_MSG_SERVICE_REQUEST = 5, 7 | SSH_MSG_SERVICE_ACCEPT = 6, 8 | SSH_MSG_KEXINIT = 20, 9 | SSH_MSG_NEWKEYS = 21, 10 | SSH_MSG_KEXDH_INIT = 30, 11 | SSH_MSG_KEXDH_REPLY = 31, 12 | SSH_MSG_USERAUTH_REQUEST = 50, 13 | SSH_MSG_USERAUTH_FAILURE = 51, 14 | SSH_MSG_USERAUTH_SUCCESS = 52, 15 | SSH_MSG_USERAUTH_BANNER = 53, 16 | SSH_MSG_GLOBAL_REQUEST = 80, 17 | SSH_MSG_REQUEST_SUCCESS = 81, 18 | SSH_MSG_REQUEST_FAILURE = 82, 19 | SSH_MSG_CHANNEL_OPEN = 90, 20 | SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91, 21 | SSH_MSG_CHANNEL_OPEN_FAILURE = 92, 22 | SSH_MSG_CHANNEL_WINDOW_ADJUST = 93, 23 | SSH_MSG_CHANNEL_DATA = 94, 24 | SSH_MSG_CHANNEL_EXTENDED_DATA = 95, 25 | SSH_MSG_CHANNEL_EOF = 96, 26 | SSH_MSG_CHANNEL_CLOSE = 97, 27 | SSH_MSG_CHANNEL_REQUEST = 98, 28 | SSH_MSG_CHANNEL_SUCCESS = 99, 29 | SSH_MSG_CHANNEL_FAILURE = 100, 30 | } 31 | 32 | export * from "./Packet"; 33 | export * from "./KexInit"; 34 | export * from "./KexDHInit"; 35 | export * from "./KexDHReply"; 36 | export * from "./NewKeys"; 37 | export * from "./Unimplemented"; 38 | export * from "./Disconnect"; 39 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/PacketType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace SSHServer.Packets 7 | { 8 | public enum PacketType : byte 9 | { 10 | SSH_MSG_DISCONNECT = 1, 11 | SSH_MSG_IGNORE = 2, 12 | SSH_MSG_UNIMPLEMENTED = 3, 13 | SSH_MSG_DEBUG = 4, 14 | SSH_MSG_SERVICE_REQUEST = 5, 15 | SSH_MSG_SERVICE_ACCEPT = 6, 16 | SSH_MSG_KEXINIT = 20, 17 | SSH_MSG_NEWKEYS = 21, 18 | SSH_MSG_KEXDH_INIT = 30, 19 | SSH_MSG_KEXDH_REPLY = 31, 20 | SSH_MSG_USERAUTH_REQUEST = 50, 21 | SSH_MSG_USERAUTH_FAILURE = 51, 22 | SSH_MSG_USERAUTH_SUCCESS = 52, 23 | SSH_MSG_USERAUTH_BANNER = 53, 24 | SSH_MSG_GLOBAL_REQUEST = 80, 25 | SSH_MSG_REQUEST_SUCCESS = 81, 26 | SSH_MSG_REQUEST_FAILURE = 82, 27 | SSH_MSG_CHANNEL_OPEN = 90, 28 | SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91, 29 | SSH_MSG_CHANNEL_OPEN_FAILURE = 92, 30 | SSH_MSG_CHANNEL_WINDOW_ADJUST = 93, 31 | SSH_MSG_CHANNEL_DATA = 94, 32 | SSH_MSG_CHANNEL_EXTENDED_DATA = 95, 33 | SSH_MSG_CHANNEL_EOF = 96, 34 | SSH_MSG_CHANNEL_CLOSE = 97, 35 | SSH_MSG_CHANNEL_REQUEST = 98, 36 | SSH_MSG_CHANNEL_SUCCESS = 99, 37 | SSH_MSG_CHANNEL_FAILURE = 100, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/sshserver.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 22, 3 | "keys": [ 4 | { 5 | "algorithm": "ssh-rsa", 6 | "key": { 7 | "modulus": "uJJN2jlBoe0haeq/qUvtYLwjJiSy1UIYgtyqYXDeKjLgUU2PRO4kkSyz8VQv2YHwCx7aPy998gGi2Nx9cuvNpRJP6rGW7pLSmxkG9eUn2y4VdU0ZXNtMmXTr7TQat+w3B7YQhgkSjDHiuZ6ymqFAeIx6Zr67GAC1zbYWBObKkiM=", 8 | "exponent": "AQAB", 9 | "pem": "-----BEGIN PRIVATE KEY-----\nMIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALiSTdo5QaHtIWnq\nv6lL7WC8IyYkstVCGILcqmFw3ioy4FFNj0TuJJEss/FUL9mB8Ase2j8vffIBotjc\nfXLrzaUST+qxlu6S0psZBvXlJ9suFXVNGVzbTJl06+00GrfsNwe2EIYJEowx4rme\nspqhQHiMema+uxgAtc22FgTmypIjAgMBAAECgYEArQ+xo+6P1c7Mx81vDMS+vTdr\nFbbPYBrrdLiHoXn9NkAiCNnafl11OBJcXun7O80UULkLFrfnNeXG1eRYVEs5s/cq\nBlPNWzEZw1nhwX6DPT1Lj8c2Yv7zKWG+gVA5dr0fcvEtLlgxa0dYkfiky/HD8reo\nHJDzCbKSvMsJLKsh3DECQQDqBfmQ1MocxoaVQSFmjGM1+/90SjhlrFlyGKW1xsww\n8uJvglT+lKbYFfTkGFX8UKGJuxuvOHc6fngqAoQDjZxVAkEAyed8LDdV/AecwJu2\n6jhV9tAgSntL/1pX50N7SlMgEg9DJKTOee7WiqrkvD2lviiA70ni6i9FsNN+pla1\n3pHslwJANH7Enb1t3QiXdfGXOXayZpCxm/duMTh5FAP9YApJEY3aR2M4B6d2ybAb\nL/NZjnDT255yNlr3O9LUx6+qx1VDxQJAb2zZEm3XfieVUpac89XzWyqxJ2m0H1B0\ngqSzPelyIYVawLZTXOd6bzywz1IWTkN8JJLaL/O2ukd99b6U0CgoMQJAJHRYWZYN\neJu/jfKhM5zzewGf4Ly2a62pl0iqN0eIqkt6Wfz8VjTMu0pwR6lR7WNfc/SPC0Zd\n9QTOzJvbAeTEjQ==\n-----END PRIVATE KEY-----" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/HostKeyAlgorithms/SSHRSA.ts: -------------------------------------------------------------------------------- 1 | import { IHostKeyAlgorithm } from "./IHostKeyAlgorithm"; 2 | import { ByteWriter } from "../ByteWriter"; 3 | 4 | import crypto = require("crypto"); 5 | 6 | export class SSHRSA implements IHostKeyAlgorithm { 7 | private m_PEM: string; 8 | private m_Modulus: Buffer; 9 | private m_Exponent: Buffer; 10 | 11 | constructor(pem: string, m: Buffer, e: Buffer) { 12 | this.m_PEM = pem; 13 | this.m_Modulus = m; 14 | this.m_Exponent = e; 15 | } 16 | 17 | public getName(): string { 18 | return "ssh-rsa"; 19 | } 20 | 21 | public createKeyAndCertificatesData(): Buffer { 22 | // The "ssh-rsa" key format has the following specific encoding: 23 | // string "ssh-rsa" 24 | // mpint e 25 | // mpint n 26 | let writer: ByteWriter = new ByteWriter(); 27 | writer.writeString(this.getName()); 28 | writer.writeMPInt(this.m_Exponent); 29 | writer.writeMPInt(this.m_Modulus); 30 | return writer.toBuffer(); 31 | } 32 | 33 | public createSignatureData(hash: Buffer): Buffer { 34 | let rsa: crypto.Signer = crypto.createSign("RSA-SHA1"); 35 | let signBuffer: Buffer = rsa.update(hash).sign(this.m_PEM); 36 | 37 | let writer: ByteWriter = new ByteWriter(); 38 | writer.writeString(this.getName()); 39 | writer.writeBytes(signBuffer); 40 | return writer.toBuffer(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/MACAlgorithms/HMACSHA1.ts: -------------------------------------------------------------------------------- 1 | import { IMACAlgorithm } from "./IMACAlgorithm"; 2 | import * as Exceptions from "../SSHServerException"; 3 | import { ByteWriter } from "../ByteWriter"; 4 | 5 | import crypto = require("crypto"); 6 | 7 | export class HMACSHA1 implements IMACAlgorithm { 8 | private m_MHAC: crypto.Hmac; 9 | private m_Key: Buffer; 10 | 11 | constructor() { 12 | } 13 | 14 | public getName(): string { 15 | return "hmac-sha1"; 16 | } 17 | 18 | public getKeySize(): number { 19 | // https://tools.ietf.org/html/rfc4253#section-6.4 20 | // according to this, the KeySize is 20 21 | return 20; 22 | } 23 | 24 | public getDigestLength(): number { 25 | // https://tools.ietf.org/html/rfc4253#section-6.4 26 | // according to this, the DigestLength is 20 27 | return 20; 28 | } 29 | 30 | public setKey(key: Buffer): void { 31 | this.m_Key = key; 32 | } 33 | 34 | public computeHash(packetNumber: number, data: Buffer): Buffer { 35 | if (this.m_Key === null) { 36 | throw new Exceptions.SSHServerException( 37 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 38 | "SetKey must be called before attempting to ComputeHash."); 39 | } 40 | 41 | let writer: ByteWriter = new ByteWriter(); 42 | writer.writeUInt32(packetNumber); 43 | writer.writeRawBytes(data); 44 | 45 | let hmac: crypto.Hmac = crypto.createHmac("sha1", this.m_Key); 46 | hmac.update(writer.toBuffer()); 47 | return hmac.digest(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/ByteReader.ts: -------------------------------------------------------------------------------- 1 | export class ByteReader { 2 | private m_Data: Buffer; 3 | private m_Offset: number = 0; 4 | 5 | constructor(data: Buffer) { 6 | this.m_Data = data; 7 | } 8 | 9 | public isEOF(): boolean { 10 | return (this.m_Offset >= this.m_Data.length); 11 | } 12 | 13 | public getBytes(length: number): Buffer { 14 | let results: Buffer = this.m_Data.slice(this.m_Offset, this.m_Offset + length); 15 | this.m_Offset += length; 16 | return results; 17 | } 18 | 19 | public getMPInt(): Buffer { 20 | let size: number = this.getUInt32(); 21 | 22 | if (size === 0) { 23 | return Buffer.alloc(1); 24 | } 25 | 26 | let data: Buffer = this.getBytes(size); 27 | if (data[0] === 0) { 28 | return data.slice(1); 29 | } 30 | 31 | return data; 32 | } 33 | 34 | public getUInt32(): number { 35 | let data: Buffer = this.getBytes(4); 36 | return new Buffer(data).readUInt32BE(0); 37 | } 38 | 39 | public getString(encoding?: string): string { 40 | if (encoding === null) { 41 | encoding = "ASCII"; 42 | } 43 | 44 | let length: number = this.getUInt32(); 45 | 46 | if (length === 0) { 47 | return ""; 48 | } 49 | 50 | return this.getBytes(length).toString(encoding); 51 | } 52 | 53 | public getNameList(): Array { 54 | return this.getString().split(","); 55 | } 56 | 57 | public getBoolean(): boolean { 58 | return (this.getByte() !== 0); 59 | } 60 | 61 | public getByte(): number { 62 | return this.getBytes(1)[0]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/Packet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Sockets; 5 | using System.Reflection; 6 | using System.Security.Cryptography; 7 | using System.Threading.Tasks; 8 | 9 | namespace SSHServer.Packets 10 | { 11 | public abstract class Packet 12 | { 13 | // https://tools.ietf.org/html/rfc4253#section-6.1 14 | public const int MaxPacketSize = 35000; 15 | 16 | public const int PacketHeaderSize = 5; 17 | 18 | public abstract PacketType PacketType { get; } 19 | 20 | public uint PacketSequence { get; set; } 21 | 22 | public static readonly Dictionary PacketTypes = new Dictionary(); 23 | 24 | static Packet() 25 | { 26 | var packets = Assembly.GetEntryAssembly().GetTypes().Where(t => typeof(Packet).IsAssignableFrom(t)); 27 | foreach(var packet in packets) 28 | { 29 | try 30 | { 31 | Packet packetInstance = Activator.CreateInstance(packet) as Packet; 32 | Packet.PacketTypes[packetInstance.PacketType] = packet; 33 | } 34 | catch { } 35 | } 36 | } 37 | 38 | public byte[] GetBytes() 39 | { 40 | using (ByteWriter writer = new ByteWriter()) 41 | { 42 | writer.WritePacketType(PacketType); 43 | InternalGetBytes(writer); 44 | return writer.ToByteArray(); 45 | } 46 | } 47 | 48 | public abstract void Load(ByteReader reader); 49 | protected abstract void InternalGetBytes(ByteWriter writer); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/SSHServer/MACAlgorithms/HMACSHA1.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Packets; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace SSHServer.MACAlgorithms 8 | { 9 | public class HMACSHA1 : IMACAlgorithm 10 | { 11 | System.Security.Cryptography.HMACSHA1 m_HMAC = null; 12 | 13 | public uint DigestLength 14 | { 15 | get 16 | { 17 | // https://tools.ietf.org/html/rfc4253#section-6.4 18 | // According to this, the DigestLength is 20 19 | return 20; 20 | } 21 | } 22 | 23 | public uint KeySize 24 | { 25 | get 26 | { 27 | // https://tools.ietf.org/html/rfc4253#section-6.4 28 | // According to this, the KeySize is 20 29 | return 20; 30 | } 31 | } 32 | 33 | public string Name 34 | { 35 | get 36 | { 37 | return "hmac-sha1"; 38 | } 39 | } 40 | 41 | public byte[] ComputeHash(uint packetNumber, byte[] data) 42 | { 43 | if (m_HMAC == null) 44 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "SetKey must be called before attempting to ComputeHash."); 45 | 46 | using (ByteWriter writer = new ByteWriter()) 47 | { 48 | writer.WriteUInt32(packetNumber); 49 | writer.WriteRawBytes(data); 50 | return m_HMAC.ComputeHash(writer.ToByteArray()); 51 | } 52 | } 53 | 54 | public void SetKey(byte[] key) 55 | { 56 | m_HMAC = new System.Security.Cryptography.HMACSHA1(key); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/ByteWriter.ts: -------------------------------------------------------------------------------- 1 | import { PacketType } from "./Packets/PacketType"; 2 | 3 | export class ByteWriter { 4 | private m_Data: Array = new Array(); 5 | 6 | public writePacketType(packetType: PacketType): void { 7 | this.m_Data.push(packetType); 8 | } 9 | 10 | public writeBytes(value: Buffer): void { 11 | this.writeUInt32(value.length); 12 | this.writeRawBytes(value); 13 | } 14 | 15 | public writeString(value: string, encoding?: string): void { 16 | if (encoding == null) { 17 | encoding = "ASCII"; 18 | } 19 | 20 | let buffer: Buffer = new Buffer(value, encoding); 21 | 22 | this.writeUInt32(buffer.length); 23 | this.writeRawBytes(buffer); 24 | } 25 | 26 | public writeStringList(list: Array): void { 27 | this.writeString(list.join(",")); 28 | } 29 | 30 | public writeUInt32(value: number): void { 31 | let buffer: Buffer = new Buffer(4); 32 | buffer.writeInt32BE(value, 0); 33 | this.writeRawBytes(buffer); 34 | } 35 | 36 | public writeMPInt(value: Buffer): void { 37 | if ((value.length === 1) && (value[0] === 0)) { 38 | this.writeUInt32(0); 39 | return; 40 | } 41 | 42 | let length: number = value.length; 43 | if ((value[0] & 0x80) !== 0) { 44 | this.writeUInt32(length + 1); 45 | this.writeByte(0x00); 46 | } else { 47 | this.writeUInt32(length); 48 | } 49 | 50 | this.writeRawBytes(value); 51 | } 52 | 53 | public writeRawBytes(value: Buffer): void { 54 | for (let v of value) { 55 | this.m_Data.push(v); 56 | } 57 | } 58 | 59 | public writeByte(value: number): void { 60 | this.m_Data.push(value); 61 | } 62 | 63 | public toBuffer(): Buffer { 64 | return Buffer.from(this.m_Data); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /SSHServer.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 14 4 | VisualStudioVersion = 14.0.25420.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SSHServer", "src\SSHServer\SSHServer.xproj", "{8D384F15-AD75-4C22-8CC3-5C8BD5A417BB}" 7 | EndProject 8 | Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SSHServerTests", "test\SSHServerTests\SSHServerTests.xproj", "{27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0FCD1EBF-795A-44BF-B688-47775CBFC9FA}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | global.json = global.json 14 | LICENSE = LICENSE 15 | README.md = README.md 16 | EndProjectSection 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{25914800-DC91-41E2-A9C3-52BD8D6238E8}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{42DCB943-272B-4D09-9BD6-7DB3D0B61B25}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {8D384F15-AD75-4C22-8CC3-5C8BD5A417BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8D384F15-AD75-4C22-8CC3-5C8BD5A417BB}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8D384F15-AD75-4C22-8CC3-5C8BD5A417BB}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {8D384F15-AD75-4C22-8CC3-5C8BD5A417BB}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {8D384F15-AD75-4C22-8CC3-5C8BD5A417BB} = {25914800-DC91-41E2-A9C3-52BD8D6238E8} 42 | {27F29A1C-FC8E-425B-B91C-4EF1C8BF15E3} = {42DCB943-272B-4D09-9BD6-7DB3D0B61B25} 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Server.ts: -------------------------------------------------------------------------------- 1 | import { SSHLogger } from "./SSHLogger"; 2 | import { Configuration } from "./Configuration"; 3 | import { Client } from "./Client"; 4 | import * as Exceptions from "./SSHServerException"; 5 | 6 | import net = require("net"); 7 | import util = require("util"); 8 | 9 | let config: Configuration = require("./sshserver.json"); 10 | 11 | export class Server { 12 | public static ProtocolVersionExchange: string = "SSH-2.0-SSHServer"; 13 | 14 | public static SupportedKexAlgorithms: Array = ["diffie-hellman-group14-sha1"]; 15 | public static SupportedHostKeyAlgorithms: Array = ["ssh-rsa"]; 16 | public static SupportedCiphers: Array = ["3des-cbc"]; 17 | public static SupportedMACAlgorithms: Array = ["hmac-sha1"]; 18 | public static SupportedCompressions: Array = ["none"]; 19 | 20 | private static DefaultPort: number = 22; 21 | 22 | private m_Server: net.Server; 23 | private m_Clients: Array = new Array(); 24 | 25 | public start(): void { 26 | // ensure we are stopped before we start listening 27 | this.stop(); 28 | 29 | SSHLogger.logInfo("Starting up..."); 30 | 31 | // create a listener on the required port 32 | let port: number = config.port; 33 | if (isNaN(port)) { 34 | port = Server.DefaultPort; 35 | } 36 | 37 | let server: net.Server = net.createServer(); 38 | this.m_Server = server.listen(port, null, 64); 39 | 40 | this.m_Server.on("connection", this.connectionReceived.bind(this)); 41 | 42 | SSHLogger.logInfo(util.format("Listening on port: %d", port)); 43 | } 44 | 45 | public poll(): void { 46 | 47 | // poll each client for activity 48 | this.m_Clients.forEach((client: Client) => client.poll()); 49 | 50 | // remove all disconnected clients 51 | this.m_Clients = this.m_Clients.filter((client: Client): boolean => { 52 | return client.getIsConnected(); 53 | }); 54 | } 55 | 56 | public stop(): void { 57 | if (this.m_Server != null) { 58 | SSHLogger.logInfo("Shutting down..."); 59 | 60 | // disconnect clients and clear clients 61 | for (let client of this.m_Clients) { 62 | client.disconnect( 63 | Exceptions.DisconnectReason.SSH_DISCONNECT_BY_APPLICATION, 64 | "The server is shutting down."); 65 | } 66 | 67 | this.m_Clients = []; 68 | 69 | this.m_Server.close(); 70 | this.m_Server = null; 71 | 72 | SSHLogger.logInfo("Stopped!"); 73 | } 74 | } 75 | 76 | private connectionReceived(socket: net.Socket): void { 77 | SSHLogger.logInfo("New Client: " + socket.remoteAddress); 78 | this.m_Clients.push(new Client(socket)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/SSHServer/HostKeyAlgorithms/SSHRSA.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Threading.Tasks; 6 | using System.Xml; 7 | 8 | namespace SSHServer.HostKeyAlgorithms 9 | { 10 | public class SSHRSA : IHostKeyAlgorithm 11 | { 12 | private readonly RSA m_RSA = RSA.Create(); 13 | 14 | public string Name 15 | { 16 | get 17 | { 18 | return "ssh-rsa"; 19 | } 20 | } 21 | 22 | public void ImportKey(string keyXml) 23 | { 24 | XmlDocument doc = new XmlDocument(); 25 | doc.LoadXml(keyXml); 26 | 27 | XmlElement root = doc["RSAKeyValue"]; 28 | 29 | RSAParameters p = new RSAParameters() 30 | { 31 | Modulus = Convert.FromBase64String(root["Modulus"].InnerText), 32 | Exponent = Convert.FromBase64String(root["Exponent"].InnerText), 33 | P = Convert.FromBase64String(root["P"].InnerText), 34 | Q = Convert.FromBase64String(root["Q"].InnerText), 35 | DP = Convert.FromBase64String(root["DP"].InnerText), 36 | DQ = Convert.FromBase64String(root["DQ"].InnerText), 37 | InverseQ = Convert.FromBase64String(root["InverseQ"].InnerText), 38 | D = Convert.FromBase64String(root["D"].InnerText) 39 | }; 40 | 41 | m_RSA.ImportParameters(p); 42 | } 43 | 44 | public byte[] CreateKeyAndCertificatesData() 45 | { 46 | // The "ssh-rsa" key format has the following specific encoding: 47 | // string "ssh-rsa" 48 | // mpint e 49 | // mpint n 50 | RSAParameters parameters = m_RSA.ExportParameters(false); 51 | 52 | using (ByteWriter writer = new ByteWriter()) 53 | { 54 | writer.WriteString(Name); 55 | writer.WriteMPInt(parameters.Exponent); 56 | writer.WriteMPInt(parameters.Modulus); 57 | return writer.ToByteArray(); 58 | } 59 | } 60 | 61 | public byte[] CreateSignatureData(byte[] value) 62 | { 63 | // Signing and verifying using this key format is performed according to 64 | // the RSASSA-PKCS1-v1_5 scheme in [RFC3447] using the SHA-1 hash. 65 | // The resulting signature is encoded as follows: 66 | // string "ssh-rsa" 67 | // string rsa_signature_blob 68 | using (ByteWriter writer = new ByteWriter()) 69 | { 70 | writer.WriteString(Name); 71 | writer.WriteBytes(m_RSA.SignData(value, HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1)); 72 | return writer.ToByteArray(); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/SSHServer/Ciphers/TripleDESCBC.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Packets; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Security.Cryptography; 7 | using System.Threading.Tasks; 8 | 9 | namespace SSHServer.Ciphers 10 | { 11 | public class TripleDESCBC : ICipher 12 | { 13 | private TripleDES m_3DES = TripleDES.Create(); 14 | private ICryptoTransform m_Decryptor; 15 | private ICryptoTransform m_Encryptor; 16 | 17 | public uint BlockSize 18 | { 19 | get 20 | { 21 | // According to https://msdn.microsoft.com/en-us/library/system.security.cryptography.symmetricalgorithm.blocksize(v=vs.110).aspx 22 | // TripleDES.BlockSize is the size of the block in bits, so we need to divide by 8 23 | // to convert from bits to bytes. 24 | return (uint)(m_3DES.BlockSize / 8); 25 | } 26 | } 27 | 28 | public uint KeySize 29 | { 30 | get 31 | { 32 | // https://msdn.microsoft.com/en-us/library/system.security.cryptography.symmetricalgorithm.keysize(v=vs.110).aspx 33 | // TripleDES.KeySize is the size of the key in bits, so we need to divide by 8 34 | // to convert from bits to bytes. 35 | return (uint)(m_3DES.KeySize / 8); 36 | } 37 | } 38 | 39 | public string Name 40 | { 41 | get 42 | { 43 | return "3des-cbc"; 44 | } 45 | } 46 | 47 | public TripleDESCBC() 48 | { 49 | m_3DES.KeySize = 192; 50 | m_3DES.Padding = PaddingMode.None; 51 | m_3DES.Mode = CipherMode.CBC; 52 | } 53 | 54 | public byte[] Decrypt(byte[] data) 55 | { 56 | return PerformTransform(m_Decryptor, data); 57 | } 58 | 59 | public byte[] Encrypt(byte[] data) 60 | { 61 | return PerformTransform(m_Encryptor, data); 62 | } 63 | 64 | public void SetKey(byte[] key, byte[] iv) 65 | { 66 | m_3DES.Key = key; 67 | m_3DES.IV = iv; 68 | 69 | m_Decryptor = m_3DES.CreateDecryptor(key, iv); 70 | m_Encryptor = m_3DES.CreateEncryptor(key, iv); 71 | } 72 | 73 | private byte[] PerformTransform(ICryptoTransform transform, byte[] data) 74 | { 75 | if (transform == null) 76 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "SetKey must be called before attempting to encrypt or decrypt data."); 77 | 78 | // I found a problem with using the CryptoStream here, but this works... 79 | var output = new byte[data.Length]; 80 | transform.TransformBlock(data, 0, data.Length, output, 0); 81 | return output; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/SSHServer/ByteWriter.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Packets; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace SSHServer 10 | { 11 | public class ByteWriter : IDisposable 12 | { 13 | private MemoryStream m_Stream = new MemoryStream(); 14 | 15 | public void WritePacketType(PacketType packetType) 16 | { 17 | WriteByte((byte)packetType); 18 | } 19 | 20 | public void WriteBytes(byte[] value) 21 | { 22 | WriteUInt32((uint)value.Count()); 23 | WriteRawBytes(value); 24 | } 25 | 26 | public void WriteString(string value) 27 | { 28 | WriteString(value, Encoding.ASCII); 29 | } 30 | 31 | public void WriteString(string value, Encoding encoding) 32 | { 33 | WriteBytes(encoding.GetBytes(value)); 34 | } 35 | 36 | public void WriteStringList(IEnumerable list) 37 | { 38 | WriteString(string.Join(",", list)); 39 | } 40 | 41 | public void WriteUInt32(uint value) 42 | { 43 | byte[] data = BitConverter.GetBytes(value); 44 | if (BitConverter.IsLittleEndian) 45 | data = data.Reverse().ToArray(); 46 | WriteRawBytes(data); 47 | } 48 | 49 | public void WriteMPInt(byte[] value) 50 | { 51 | if ((value.Length == 1) && (value[0] == 0)) 52 | { 53 | WriteUInt32(0); 54 | return; 55 | } 56 | 57 | uint length = (uint)value.Length; 58 | if (((value[0] & 0x80) != 0)) 59 | { 60 | WriteUInt32((uint)(length + 1)); 61 | WriteByte(0x00); 62 | } 63 | else 64 | { 65 | WriteUInt32((uint)length); 66 | } 67 | 68 | WriteRawBytes(value); 69 | } 70 | 71 | public void WriteRawBytes(byte[] value) 72 | { 73 | if (disposedValue) 74 | throw new ObjectDisposedException("ByteWriter"); 75 | m_Stream.Write(value, 0, value.Count()); 76 | } 77 | 78 | public void WriteByte(byte value) 79 | { 80 | if (disposedValue) 81 | throw new ObjectDisposedException("ByteWriter"); 82 | m_Stream.WriteByte(value); 83 | } 84 | 85 | public byte[] ToByteArray() 86 | { 87 | if (disposedValue) 88 | throw new ObjectDisposedException("ByteWriter"); 89 | return m_Stream.ToArray(); 90 | } 91 | 92 | #region IDisposable Support 93 | private bool disposedValue = false; // To detect redundant calls 94 | 95 | protected virtual void Dispose(bool disposing) 96 | { 97 | if (!disposedValue) 98 | { 99 | if (disposing) 100 | { 101 | m_Stream.Dispose(); 102 | m_Stream = null; 103 | } 104 | 105 | disposedValue = true; 106 | } 107 | } 108 | 109 | public void Dispose() 110 | { 111 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 112 | Dispose(true); 113 | } 114 | #endregion 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/SSHServer/ByteReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SSHServer 9 | { 10 | public class ByteReader : IDisposable 11 | { 12 | private readonly char[] ListSeparator = new char[] { ',' }; 13 | private MemoryStream m_Stream; 14 | 15 | public bool IsEOF 16 | { 17 | get 18 | { 19 | if (disposedValue) 20 | throw new ObjectDisposedException("ByteReader"); 21 | 22 | return m_Stream.Position == m_Stream.Length; 23 | } 24 | } 25 | 26 | public ByteReader(byte[] data) 27 | { 28 | m_Stream = new MemoryStream(data); 29 | } 30 | 31 | public byte[] GetBytes(int length) 32 | { 33 | if (disposedValue) 34 | throw new ObjectDisposedException("ByteReader"); 35 | 36 | byte[] data = new byte[length]; 37 | m_Stream.Read(data, 0, length); 38 | return data; 39 | } 40 | 41 | public byte[] GetMPInt() 42 | { 43 | uint size = GetUInt32(); 44 | 45 | if (size == 0) 46 | return new byte[1]; 47 | 48 | byte[] data = GetBytes((int)size); 49 | if (data[0] == 0) 50 | return data.Skip(1).ToArray(); 51 | 52 | return data; 53 | } 54 | 55 | public uint GetUInt32() 56 | { 57 | byte[] data = GetBytes(4); 58 | if (BitConverter.IsLittleEndian) 59 | data = data.Reverse().ToArray(); 60 | return BitConverter.ToUInt32(data, 0); 61 | } 62 | 63 | public string GetString() 64 | { 65 | return GetString(Encoding.ASCII); 66 | } 67 | 68 | public string GetString(Encoding encoding) 69 | { 70 | int length = (int)GetUInt32(); 71 | 72 | if (length == 0) 73 | return string.Empty; 74 | 75 | return encoding.GetString(GetBytes(length)); 76 | } 77 | 78 | public List GetNameList() 79 | { 80 | List data = new List(); 81 | 82 | return new List(GetString().Split(ListSeparator, StringSplitOptions.RemoveEmptyEntries)); 83 | } 84 | 85 | public bool GetBoolean() 86 | { 87 | return (GetByte() != 0); 88 | } 89 | 90 | 91 | public byte GetByte() 92 | { 93 | if (disposedValue) 94 | throw new ObjectDisposedException("ByteReader"); 95 | 96 | return (byte)m_Stream.ReadByte(); 97 | } 98 | 99 | #region IDisposable Support 100 | private bool disposedValue = false; // To detect redundant calls 101 | 102 | protected virtual void Dispose(bool disposing) 103 | { 104 | if (!disposedValue) 105 | { 106 | if (disposing) 107 | { 108 | m_Stream.Dispose(); 109 | m_Stream = null; 110 | } 111 | 112 | disposedValue = true; 113 | } 114 | } 115 | 116 | public void Dispose() 117 | { 118 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 119 | Dispose(true); 120 | } 121 | #endregion 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/SSHServer/KexAlgorithms/DiffieHellmanGroup14SHA1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Numerics; 6 | using System.Security.Cryptography; 7 | using System.Threading.Tasks; 8 | 9 | namespace SSHServer.KexAlgorithms 10 | { 11 | public class DiffieHellmanGroup14SHA1 : IKexAlgorithm 12 | { 13 | // http://tools.ietf.org/html/rfc3526 - 2048-bit MODP Group 14 | private const string MODPGroup2048 = "00FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"; 15 | private static readonly BigInteger s_P; 16 | private static readonly BigInteger s_G; 17 | 18 | private readonly BigInteger m_Y; 19 | 20 | private readonly SHA1 m_HashAlgorithm = SHA1.Create(); 21 | 22 | // The following steps are used to exchange a key. In this: 23 | // C is the client 24 | // S is the server 25 | // p is a large safe prime (RFC 3526) 26 | // g is a generator (RFC 3526) 27 | // For a subgroup of GF(p); q is the order of the subgroup; V_S is S's 28 | // identification string; V_C is C's identification string; K_S is S's 29 | // public host key; I_C is C's SSH_MSG_KEXINIT message and I_S is S's 30 | // SSH_MSG_KEXINIT message that have been exchanged before this part 31 | // begins. 32 | 33 | public string Name 34 | { 35 | get 36 | { 37 | return "diffie-hellman-group14-sha1"; 38 | } 39 | } 40 | 41 | static DiffieHellmanGroup14SHA1() 42 | { 43 | // p is a large safe prime (RFC 3526) 44 | s_P = BigInteger.Parse(MODPGroup2048, NumberStyles.HexNumber); 45 | 46 | // g is a generator (RFC 3526) 47 | s_G = new BigInteger(2); 48 | } 49 | 50 | public DiffieHellmanGroup14SHA1() 51 | { 52 | // 2. S generates a random number y (0 < y < q) 53 | var bytes = new byte[80]; // 80 * 8 = 640 bits 54 | RandomNumberGenerator.Create().GetBytes(bytes); 55 | m_Y = BigInteger.Abs(new BigInteger(bytes)); 56 | } 57 | 58 | public byte[] CreateKeyExchange() 59 | { 60 | // and computes: f = g ^ y mod p. 61 | BigInteger keyExchange = BigInteger.ModPow(s_G, m_Y, s_P); 62 | byte[] key = keyExchange.ToByteArray(); 63 | if (BitConverter.IsLittleEndian) 64 | key = key.Reverse().ToArray(); 65 | 66 | if ((key.Length > 1) && (key[0] == 0x00)) 67 | { 68 | key = key.Skip(1).ToArray(); 69 | } 70 | 71 | return key; 72 | } 73 | 74 | public byte[] DecryptKeyExchange(byte[] keyEx) 75 | { 76 | // https://tools.ietf.org/html/rfc4253#section-8 77 | // 1. C generates a random number x (1 < x < q) and computes 78 | // e = g ^ x mod p. C sends e to S. 79 | 80 | // S receives e. It computes K = e^y mod p, 81 | if (BitConverter.IsLittleEndian) 82 | keyEx = keyEx.Reverse().ToArray(); 83 | 84 | BigInteger e = new BigInteger(keyEx.Concat(new byte[] { 0 }).ToArray()); 85 | byte[] decrypted = BigInteger.ModPow(e, m_Y, s_P).ToByteArray(); 86 | if (BitConverter.IsLittleEndian) 87 | decrypted = decrypted.Reverse().ToArray(); 88 | 89 | if ((decrypted.Length > 1) && (decrypted[0] == 0x00)) 90 | { 91 | decrypted = decrypted.Skip(1).ToArray(); 92 | } 93 | 94 | return decrypted; 95 | } 96 | 97 | public byte[] ComputeHash(byte[] value) 98 | { 99 | // H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) 100 | return m_HashAlgorithm.ComputeHash(value); 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | 254 | SSHServerNodeJS/**/*.js 255 | SSHServerNodeJS/**/*.js.map -------------------------------------------------------------------------------- /src/SSHServer/Server.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Logging; 3 | using SSHServer.Ciphers; 4 | using SSHServer.Compressions; 5 | using SSHServer.HostKeyAlgorithms; 6 | using SSHServer.KexAlgorithms; 7 | using SSHServer.MACAlgorithms; 8 | using SSHServer.Packets; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Net; 14 | using System.Net.Sockets; 15 | using System.Threading.Tasks; 16 | 17 | namespace SSHServer 18 | { 19 | public class Server 20 | { 21 | public const string ProtocolVersionExchange = "SSH-2.0-SSHServer"; 22 | 23 | private const int DefaultPort = 22; 24 | private const int ConnectionBacklog = 64; 25 | 26 | private IConfigurationRoot m_Configuration; 27 | private LoggerFactory m_LoggerFactory; 28 | private ILogger m_Logger; 29 | 30 | private TcpListener m_Listener; 31 | private List m_Clients = new List(); 32 | 33 | private static Dictionary s_HostKeys = new Dictionary(); 34 | 35 | public static IReadOnlyList SupportedKexAlgorithms { get; private set; } = new List() 36 | { 37 | typeof(DiffieHellmanGroup14SHA1) 38 | }; 39 | 40 | public static IReadOnlyList SupportedHostKeyAlgorithms { get; private set; } = new List() 41 | { 42 | typeof(SSHRSA) 43 | }; 44 | 45 | public static IReadOnlyList SupportedCiphers { get; private set; } = new List() 46 | { 47 | typeof(TripleDESCBC) 48 | }; 49 | 50 | public static IReadOnlyList SupportedMACAlgorithms { get; private set; } = new List() 51 | { 52 | typeof(HMACSHA1) 53 | }; 54 | 55 | public static IReadOnlyList SupportedCompressions { get; private set; } = new List() 56 | { 57 | typeof(NoCompression) 58 | }; 59 | 60 | public Server() 61 | { 62 | m_Configuration = new ConfigurationBuilder() 63 | .SetBasePath(Directory.GetCurrentDirectory()) 64 | .AddJsonFile("sshserver.json", optional: false) 65 | .Build(); 66 | 67 | m_LoggerFactory = new LoggerFactory(); 68 | m_LoggerFactory.AddConsole(m_Configuration.GetSection("Logging")); 69 | m_Logger = m_LoggerFactory.CreateLogger("SSHServer"); 70 | 71 | IConfigurationSection keys = m_Configuration.GetSection("keys"); 72 | foreach (IConfigurationSection key in keys.GetChildren()) 73 | { 74 | s_HostKeys[key.Key] = key.Value; 75 | } 76 | } 77 | 78 | public void Start() 79 | { 80 | // Ensure we are stopped before we start listening 81 | Stop(); 82 | 83 | m_Logger.LogInformation("Starting up..."); 84 | 85 | // Create a listener on the required port 86 | int port = m_Configuration.GetValue("port", DefaultPort); 87 | m_Listener = new TcpListener(IPAddress.Any, port); 88 | m_Listener.Start(ConnectionBacklog); 89 | 90 | m_Logger.LogInformation($"Listening on port: {port}"); 91 | } 92 | 93 | public void Poll() 94 | { 95 | // Check for new connections 96 | while (m_Listener.Pending()) 97 | { 98 | Task acceptTask = m_Listener.AcceptSocketAsync(); 99 | acceptTask.Wait(); 100 | 101 | Socket socket = acceptTask.Result; 102 | m_Logger.LogDebug($"New Client: {socket.RemoteEndPoint.ToString()}"); 103 | 104 | // Create and add client list 105 | m_Clients.Add(new Client(socket, m_LoggerFactory.CreateLogger(socket.RemoteEndPoint.ToString()))); 106 | } 107 | 108 | // Poll each client for activity 109 | m_Clients.ForEach(c => c.Poll()); 110 | 111 | // Remove all disconnected clients 112 | m_Clients.RemoveAll(c => c.IsConnected == false); 113 | } 114 | 115 | public void Stop() 116 | { 117 | if (m_Listener != null) 118 | { 119 | m_Logger.LogInformation("Shutting down..."); 120 | 121 | // Disconnect clients and clear clients 122 | m_Clients.ForEach(c => c.Disconnect(DisconnectReason.SSH_DISCONNECT_BY_APPLICATION, "The server is shutting down.")); 123 | m_Clients.Clear(); 124 | 125 | m_Listener.Stop(); 126 | m_Listener = null; 127 | 128 | m_Logger.LogInformation("Stopped!"); 129 | } 130 | } 131 | 132 | public static T GetType(IReadOnlyList types, string selected) where T : class 133 | { 134 | foreach (Type type in types) 135 | { 136 | IAlgorithm algo = Activator.CreateInstance(type) as IAlgorithm; 137 | if (algo.Name.Equals(selected, StringComparison.OrdinalIgnoreCase)) 138 | { 139 | if (algo is IHostKeyAlgorithm) 140 | { 141 | ((IHostKeyAlgorithm)algo).ImportKey(s_HostKeys[algo.Name]); 142 | } 143 | 144 | return algo as T; 145 | } 146 | } 147 | 148 | return default(T); 149 | } 150 | 151 | public static IEnumerable GetNames(IReadOnlyList types) 152 | { 153 | foreach (Type type in types) 154 | { 155 | IAlgorithm algo = Activator.CreateInstance(type) as IAlgorithm; 156 | yield return algo.Name; 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/SSHServerNodeJS.njsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11.0 5 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 6 | SSHServerNodeJS 7 | NodejsConsoleApp1 8 | 9 | 10 | 11 | Debug 12 | 2.0 13 | b040eaaa-f43b-47c1-9be2-2ed609b7da96 14 | 15 | 16 | app.ts 17 | False 18 | 19 | 20 | . 21 | . 22 | v4.0 23 | {3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD} 24 | ProjectFiles 25 | true 26 | CommonJS 27 | true 28 | false 29 | 30 | 31 | true 32 | ES6 33 | None 34 | True 35 | False 36 | False 37 | 38 | 39 | 40 | False 41 | True 42 | 43 | 44 | CommonJS 45 | 46 | 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | False 103 | True 104 | 0 105 | / 106 | http://localhost:48022/ 107 | False 108 | True 109 | http://localhost:1337 110 | False 111 | 112 | 113 | 114 | 115 | 116 | 117 | CurrentPage 118 | True 119 | False 120 | False 121 | False 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | False 131 | False 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/SSHServer/Packets/KexInit.cs: -------------------------------------------------------------------------------- 1 | using SSHServer.Ciphers; 2 | using SSHServer.Compressions; 3 | using SSHServer.HostKeyAlgorithms; 4 | using SSHServer.KexAlgorithms; 5 | using SSHServer.MACAlgorithms; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Security.Cryptography; 10 | using System.Threading.Tasks; 11 | 12 | namespace SSHServer.Packets 13 | { 14 | public class KexInit : Packet 15 | { 16 | public override PacketType PacketType 17 | { 18 | get 19 | { 20 | return PacketType.SSH_MSG_KEXINIT; 21 | } 22 | } 23 | 24 | public byte[] Cookie { get; set; } = new byte[16]; 25 | public List KexAlgorithms { get; private set; } = new List(); 26 | public List ServerHostKeyAlgorithms { get; private set; } = new List(); 27 | public List EncryptionAlgorithmsClientToServer { get; private set; } = new List(); 28 | public List EncryptionAlgorithmsServerToClient { get; private set; } = new List(); 29 | public List MacAlgorithmsClientToServer { get; private set; } = new List(); 30 | public List MacAlgorithmsServerToClient { get; private set; } = new List(); 31 | public List CompressionAlgorithmsClientToServer { get; private set; } = new List(); 32 | public List CompressionAlgorithmsServerToClient { get; private set; } = new List(); 33 | public List LanguagesClientToServer { get; private set; } = new List(); 34 | public List LanguagesServerToClient { get; private set; } = new List(); 35 | public bool FirstKexPacketFollows { get; set; } 36 | 37 | public KexInit() 38 | { 39 | RandomNumberGenerator.Create().GetBytes(Cookie); 40 | } 41 | 42 | protected override void InternalGetBytes(ByteWriter writer) 43 | { 44 | writer.WriteRawBytes(Cookie); 45 | writer.WriteStringList(KexAlgorithms); 46 | writer.WriteStringList(ServerHostKeyAlgorithms); 47 | writer.WriteStringList(EncryptionAlgorithmsClientToServer); 48 | writer.WriteStringList(EncryptionAlgorithmsServerToClient); 49 | writer.WriteStringList(MacAlgorithmsClientToServer); 50 | writer.WriteStringList(MacAlgorithmsServerToClient); 51 | writer.WriteStringList(CompressionAlgorithmsClientToServer); 52 | writer.WriteStringList(CompressionAlgorithmsServerToClient); 53 | writer.WriteStringList(LanguagesClientToServer); 54 | writer.WriteStringList(LanguagesServerToClient); 55 | writer.WriteByte(FirstKexPacketFollows ? (byte)0x01 : (byte)0x00); 56 | writer.WriteUInt32(0); 57 | } 58 | 59 | public override void Load(ByteReader reader) 60 | { 61 | Cookie = reader.GetBytes(16); 62 | KexAlgorithms = reader.GetNameList(); 63 | ServerHostKeyAlgorithms = reader.GetNameList(); 64 | EncryptionAlgorithmsClientToServer = reader.GetNameList(); 65 | EncryptionAlgorithmsServerToClient = reader.GetNameList(); 66 | MacAlgorithmsClientToServer = reader.GetNameList(); 67 | MacAlgorithmsServerToClient = reader.GetNameList(); 68 | CompressionAlgorithmsClientToServer = reader.GetNameList(); 69 | CompressionAlgorithmsServerToClient = reader.GetNameList(); 70 | LanguagesClientToServer = reader.GetNameList(); 71 | LanguagesServerToClient = reader.GetNameList(); 72 | FirstKexPacketFollows = reader.GetBoolean(); 73 | /* 74 | uint32 0 (reserved for future extension) 75 | */ 76 | uint reserved = reader.GetUInt32(); 77 | } 78 | 79 | public IKexAlgorithm PickKexAlgorithm() 80 | { 81 | foreach (string algo in this.KexAlgorithms) 82 | { 83 | IKexAlgorithm selectedAlgo = Server.GetType(Server.SupportedKexAlgorithms, algo); 84 | if (selectedAlgo != null) 85 | { 86 | return selectedAlgo; 87 | } 88 | } 89 | 90 | // If no algorithm satisfying all these conditions can be found, the 91 | // connection fails, and both sides MUST disconnect. 92 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Kex Algorithm"); 93 | } 94 | 95 | public IHostKeyAlgorithm PickHostKeyAlgorithm() 96 | { 97 | foreach (string algo in this.ServerHostKeyAlgorithms) 98 | { 99 | IHostKeyAlgorithm selectedAlgo = Server.GetType(Server.SupportedHostKeyAlgorithms, algo); 100 | if (selectedAlgo != null) 101 | { 102 | return selectedAlgo; 103 | } 104 | } 105 | 106 | // If no algorithm satisfying all these conditions can be found, the 107 | // connection fails, and both sides MUST disconnect. 108 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Host Key Algorithm"); 109 | } 110 | 111 | public ICipher PickCipherClientToServer() 112 | { 113 | foreach (string algo in this.EncryptionAlgorithmsClientToServer) 114 | { 115 | ICipher selectedCipher = Server.GetType(Server.SupportedCiphers, algo); 116 | if (selectedCipher != null) 117 | { 118 | return selectedCipher; 119 | } 120 | } 121 | 122 | // If no algorithm satisfying all these conditions can be found, the 123 | // connection fails, and both sides MUST disconnect. 124 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Client-To-Server Cipher Algorithm"); 125 | } 126 | 127 | public ICipher PickCipherServerToClient() 128 | { 129 | foreach (string algo in this.EncryptionAlgorithmsServerToClient) 130 | { 131 | ICipher selectedCipher = Server.GetType(Server.SupportedCiphers, algo); 132 | if (selectedCipher != null) 133 | { 134 | return selectedCipher; 135 | } 136 | } 137 | 138 | // If no algorithm satisfying all these conditions can be found, the 139 | // connection fails, and both sides MUST disconnect. 140 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Server-To-Client Cipher Algorithm"); 141 | } 142 | 143 | public IMACAlgorithm PickMACAlgorithmClientToServer() 144 | { 145 | foreach (string algo in this.MacAlgorithmsClientToServer) 146 | { 147 | IMACAlgorithm selectedAlgo = Server.GetType(Server.SupportedMACAlgorithms, algo); 148 | if (selectedAlgo != null) 149 | { 150 | return selectedAlgo; 151 | } 152 | } 153 | 154 | // If no algorithm satisfying all these conditions can be found, the 155 | // connection fails, and both sides MUST disconnect. 156 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Client-To-Server MAC Algorithm"); 157 | } 158 | 159 | public IMACAlgorithm PickMACAlgorithmServerToClient() 160 | { 161 | foreach (string algo in this.MacAlgorithmsServerToClient) 162 | { 163 | IMACAlgorithm selectedAlgo = Server.GetType(Server.SupportedMACAlgorithms, algo); 164 | if (selectedAlgo != null) 165 | { 166 | return selectedAlgo; 167 | } 168 | } 169 | 170 | // If no algorithm satisfying all these conditions can be found, the 171 | // connection fails, and both sides MUST disconnect. 172 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Server-To-Client MAC Algorithm"); 173 | } 174 | 175 | public ICompression PickCompressionAlgorithmClientToServer() 176 | { 177 | foreach (string algo in this.CompressionAlgorithmsClientToServer) 178 | { 179 | ICompression selectedAlgo = Server.GetType(Server.SupportedCompressions, algo); 180 | if (selectedAlgo != null) 181 | { 182 | return selectedAlgo; 183 | } 184 | } 185 | 186 | // If no algorithm satisfying all these conditions can be found, the 187 | // connection fails, and both sides MUST disconnect. 188 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Client-To-Server Compression Algorithm"); 189 | } 190 | 191 | public ICompression PickCompressionAlgorithmServerToClient() 192 | { 193 | foreach (string algo in this.CompressionAlgorithmsServerToClient) 194 | { 195 | ICompression selectedAlgo = Server.GetType(Server.SupportedCompressions, algo); 196 | if (selectedAlgo != null) 197 | { 198 | return selectedAlgo; 199 | } 200 | } 201 | 202 | // If no algorithm satisfying all these conditions can be found, the 203 | // connection fails, and both sides MUST disconnect. 204 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, "Could not find a shared Server-To-Client Compresion Algorithm"); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Packets/KexInit.ts: -------------------------------------------------------------------------------- 1 | import { Packet } from "./Packet"; 2 | import { Server } from "../Server"; 3 | import { PacketType } from "./PacketType"; 4 | import { ByteReader } from "../ByteReader"; 5 | import { ByteWriter } from "../ByteWriter"; 6 | import * as Configurations from "../Configuration"; 7 | 8 | import { IKexAlgorithm } from "../KexAlgorithms/IKexAlgorithm"; 9 | import { DiffieHellmanGroup14SHA1 } from "../KexAlgorithms/DiffieHellmanGroup14SHA1"; 10 | 11 | import { IHostKeyAlgorithm } from "../HostKeyAlgorithms/IHostKeyAlgorithm"; 12 | import { SSHRSA } from "../HostKeyAlgorithms/SSHRSA"; 13 | 14 | import { ICipher } from "../Ciphers/ICipher"; 15 | import { TripleDESCBC } from "../Ciphers/TripleDESCBC"; 16 | 17 | import { IMACAlgorithm } from "../MACAlgorithms/IMACAlgorithm"; 18 | import { HMACSHA1 } from "../MACAlgorithms/HMACSHA1"; 19 | 20 | import { ICompression } from "../Compressions/ICompression"; 21 | import { NoCompression } from "../Compressions/NoCompression"; 22 | 23 | import * as Exceptions from "../SSHServerException"; 24 | 25 | import crypto = require('crypto'); 26 | 27 | let config: Configurations.Configuration = require("../sshserver.json"); 28 | 29 | export class KexInit extends Packet { 30 | public getPacketType(): PacketType { 31 | return PacketType.SSH_MSG_KEXINIT; 32 | } 33 | 34 | public cookie: Buffer = crypto.randomBytes(16); 35 | public kexAlgorithms: Array = new Array(); 36 | public serverHostKeyAlgorithms : Array = new Array(); 37 | public encryptionAlgorithmsClientToServer : Array = new Array(); 38 | public encryptionAlgorithmsServerToClient : Array = new Array(); 39 | public macAlgorithmsClientToServer : Array = new Array(); 40 | public macAlgorithmsServerToClient : Array = new Array(); 41 | public compressionAlgorithmsClientToServer : Array = new Array(); 42 | public compressionAlgorithmsServerToClient : Array = new Array(); 43 | public languagesClientToServer : Array = new Array(); 44 | public languagesServerToClient: Array = new Array(); 45 | public firstKexPacketFollows: boolean = false; 46 | 47 | protected internalGetBytes(writer: ByteWriter) { 48 | writer.writeRawBytes(this.cookie); 49 | writer.writeStringList(this.kexAlgorithms); 50 | writer.writeStringList(this.serverHostKeyAlgorithms); 51 | writer.writeStringList(this.encryptionAlgorithmsClientToServer); 52 | writer.writeStringList(this.encryptionAlgorithmsServerToClient); 53 | writer.writeStringList(this.macAlgorithmsClientToServer); 54 | writer.writeStringList(this.macAlgorithmsServerToClient); 55 | writer.writeStringList(this.compressionAlgorithmsClientToServer); 56 | writer.writeStringList(this.compressionAlgorithmsServerToClient); 57 | writer.writeStringList(this.languagesClientToServer); 58 | writer.writeStringList(this.languagesServerToClient); 59 | writer.writeByte(this.firstKexPacketFollows ? 0x01 : 0x00); 60 | writer.writeUInt32(0); 61 | } 62 | 63 | public load(reader: ByteReader) { 64 | this.cookie = reader.getBytes(16); 65 | this.kexAlgorithms = reader.getNameList(); 66 | this.serverHostKeyAlgorithms = reader.getNameList(); 67 | this.encryptionAlgorithmsClientToServer = reader.getNameList(); 68 | this.encryptionAlgorithmsServerToClient = reader.getNameList(); 69 | this.macAlgorithmsClientToServer = reader.getNameList(); 70 | this.macAlgorithmsServerToClient = reader.getNameList(); 71 | this.compressionAlgorithmsClientToServer = reader.getNameList(); 72 | this.compressionAlgorithmsServerToClient = reader.getNameList(); 73 | this.languagesClientToServer = reader.getNameList(); 74 | this.languagesServerToClient = reader.getNameList(); 75 | this.firstKexPacketFollows = reader.getBoolean(); 76 | 77 | // uint32 0 (reserved for future extension) 78 | reader.getUInt32(); 79 | } 80 | 81 | public pickKexAlgorithm(): IKexAlgorithm { 82 | for (let algo of this.kexAlgorithms) { 83 | if (Server.SupportedKexAlgorithms.find( 84 | (value: string, index: number, obj: Array): boolean => { 85 | return (value === algo); 86 | })) { 87 | 88 | switch (algo) { 89 | case "diffie-hellman-group14-sha1": 90 | return new DiffieHellmanGroup14SHA1(); 91 | } 92 | } 93 | } 94 | 95 | // If no algorithm satisfying all these conditions can be found, the 96 | // connection fails, and both sides MUST disconnect. 97 | throw new Exceptions.SSHServerException( 98 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 99 | "Could not find a shared Kex Algorithm"); 100 | } 101 | 102 | public pickHostKeyAlgorithm(): IHostKeyAlgorithm { 103 | for (let algo of this.serverHostKeyAlgorithms) { 104 | if (Server.SupportedHostKeyAlgorithms.find( 105 | (value: string, index: number, obj: Array): boolean => { 106 | return (value === algo); 107 | })) { 108 | 109 | switch (algo) { 110 | case "ssh-rsa": { 111 | let configKey: Configurations.Key = config.keys.find((value: Configurations.Key, index: number, object: Configurations.Key[]) => { return (value.algorithm === algo) }); 112 | if (configKey === null) { 113 | throw new Exceptions.SSHServerException( 114 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 115 | "Could not find a configuration for ssh-rsa."); 116 | } 117 | return new SSHRSA( 118 | configKey.key.pem, 119 | new Buffer(configKey.key.modulus, "base64"), 120 | new Buffer(configKey.key.exponent, "base64")); 121 | } 122 | } 123 | } 124 | } 125 | 126 | // If no algorithm satisfying all these conditions can be found, the 127 | // connection fails, and both sides MUST disconnect. 128 | throw new Exceptions.SSHServerException( 129 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 130 | "Could not find a shared Host Key Algorithm"); 131 | } 132 | 133 | public pickCipherClientToServer(): ICipher { 134 | for (let algo of this.encryptionAlgorithmsClientToServer) { 135 | if (Server.SupportedCiphers.find( 136 | (value: string, index: number, obj: Array): boolean => { 137 | return (value === algo); 138 | })) { 139 | 140 | switch (algo) { 141 | case "3des-cbc": 142 | return new TripleDESCBC(); 143 | } 144 | } 145 | } 146 | 147 | // If no algorithm satisfying all these conditions can be found, the 148 | // connection fails, and both sides MUST disconnect. 149 | throw new Exceptions.SSHServerException( 150 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 151 | "Could not find a shared Client-To-Server Cipher Algorithm"); 152 | } 153 | 154 | public pickCipherServerToClient(): ICipher { 155 | for (let algo of this.encryptionAlgorithmsServerToClient) { 156 | if (Server.SupportedCiphers.find( 157 | (value: string, index: number, obj: Array): boolean => { 158 | return (value === algo); 159 | })) { 160 | 161 | switch (algo) { 162 | case "3des-cbc": 163 | return new TripleDESCBC(); 164 | } 165 | } 166 | } 167 | 168 | // If no algorithm satisfying all these conditions can be found, the 169 | // connection fails, and both sides MUST disconnect. 170 | throw new Exceptions.SSHServerException( 171 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 172 | "Could not find a shared Server-To-Client Cipher Algorithm"); 173 | } 174 | 175 | public pickMACAlgorithmClientToServer(): IMACAlgorithm { 176 | for (let algo of this.macAlgorithmsClientToServer) { 177 | if (Server.SupportedMACAlgorithms.find( 178 | (value: string, index: number, obj: Array): boolean => { 179 | return (value === algo); 180 | })) { 181 | 182 | switch (algo) { 183 | case "hmac-sha1": 184 | return new HMACSHA1(); 185 | } 186 | } 187 | } 188 | 189 | // If no algorithm satisfying all these conditions can be found, the 190 | // connection fails, and both sides MUST disconnect. 191 | throw new Exceptions.SSHServerException( 192 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 193 | "Could not find a shared Client-To-Server MAC Algorithm"); 194 | } 195 | 196 | public pickMACAlgorithmServerToClient(): IMACAlgorithm { 197 | for (let algo of this.macAlgorithmsServerToClient) { 198 | if (Server.SupportedMACAlgorithms.find( 199 | (value: string, index: number, obj: Array): boolean => { 200 | return (value === algo); 201 | })) { 202 | 203 | switch (algo) { 204 | case "hmac-sha1": 205 | return new HMACSHA1(); 206 | } 207 | } 208 | } 209 | 210 | // If no algorithm satisfying all these conditions can be found, the 211 | // connection fails, and both sides MUST disconnect. 212 | throw new Exceptions.SSHServerException( 213 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 214 | "Could not find a shared Server-To-Client MAC Algorithm"); 215 | } 216 | 217 | public pickCompressionAlgorithmClientToServer(): ICompression { 218 | for (let algo of this.compressionAlgorithmsClientToServer) { 219 | if (Server.SupportedCompressions.find( 220 | (value: string, index: number, obj: Array): boolean => { 221 | return (value === algo); 222 | })) { 223 | 224 | switch (algo) { 225 | case "none": 226 | return new NoCompression(); 227 | } 228 | } 229 | } 230 | 231 | // If no algorithm satisfying all these conditions can be found, the 232 | // connection fails, and both sides MUST disconnect. 233 | throw new Exceptions.SSHServerException( 234 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 235 | "Could not find a shared Client-To-Server Compression Algorithm"); 236 | } 237 | 238 | public pickCompressionAlgorithmServerToClient(): ICompression { 239 | for (let algo of this.compressionAlgorithmsServerToClient) { 240 | if (Server.SupportedCompressions.find( 241 | (value: string, index: number, obj: Array): boolean => { 242 | return (value === algo); 243 | })) { 244 | 245 | switch (algo) { 246 | case "none": 247 | return new NoCompression(); 248 | } 249 | } 250 | } 251 | 252 | // If no algorithm satisfying all these conditions can be found, the 253 | // connection fails, and both sides MUST disconnect. 254 | throw new Exceptions.SSHServerException( 255 | Exceptions.DisconnectReason.SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 256 | "Could not find a shared Server-To-Client Compresion Algorithm"); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /SSHServerNodeJS/SSHServerNodeJS/Client.ts: -------------------------------------------------------------------------------- 1 | import { SSHLogger } from "./SSHLogger"; 2 | import { Server } from "./Server"; 3 | import { ByteReader } from "./ByteReader"; 4 | import { ByteWriter } from "./ByteWriter"; 5 | import { ExchangeContext } from "./ExchangeContext"; 6 | import * as Packets from "./Packets/PacketType"; 7 | import * as Exceptions from "./SSHServerException"; 8 | import { IKexAlgorithm } from "./KexAlgorithms/IKexAlgorithm"; 9 | 10 | import net = require("net"); 11 | import util = require("util"); 12 | import crypto = require("crypto"); 13 | 14 | export class Client { 15 | private m_Socket: net.Socket; 16 | private m_LastBytesRead: number = 0; 17 | 18 | private m_HasCompletedProtocolVersionExchange: boolean = false; 19 | private m_ProtocolVersionExchange: string = ""; 20 | 21 | private m_KexInitServerToClient: Packets.KexInit = new Packets.KexInit(); 22 | private m_KexInitClientToServer: Packets.KexInit = null; 23 | private m_SessionId: Buffer = null; 24 | 25 | private m_CurrentSentPacketNumber: number = 0; 26 | private m_CurrentReceivedPacketNumber: number = 0; 27 | 28 | private m_TotalBytesTransferred: number = 0; 29 | private m_KeyTimeout: NodeJS.Timer = null; 30 | 31 | private m_ActiveExchangeContext: ExchangeContext = new ExchangeContext(); 32 | private m_PendingExchangeContext: ExchangeContext = new ExchangeContext(); 33 | 34 | constructor(socket: net.Socket) { 35 | this.m_Socket = socket; 36 | 37 | this.resetKeyTimer(); 38 | 39 | this.m_KexInitServerToClient.kexAlgorithms = Server.SupportedKexAlgorithms; 40 | this.m_KexInitServerToClient.serverHostKeyAlgorithms = Server.SupportedHostKeyAlgorithms; 41 | this.m_KexInitServerToClient.encryptionAlgorithmsClientToServer = Server.SupportedCiphers; 42 | this.m_KexInitServerToClient.encryptionAlgorithmsServerToClient = Server.SupportedCiphers; 43 | this.m_KexInitServerToClient.macAlgorithmsClientToServer = Server.SupportedMACAlgorithms; 44 | this.m_KexInitServerToClient.macAlgorithmsServerToClient = Server.SupportedMACAlgorithms; 45 | this.m_KexInitServerToClient.compressionAlgorithmsClientToServer = Server.SupportedCompressions; 46 | this.m_KexInitServerToClient.compressionAlgorithmsServerToClient = Server.SupportedCompressions; 47 | this.m_KexInitServerToClient.firstKexPacketFollows = false; 48 | 49 | this.m_Socket.setNoDelay(true); 50 | 51 | this.m_Socket.on("close", this.closeReceived.bind(this)); 52 | 53 | // 4.2.Protocol Version Exchange - https://tools.ietf.org/html/rfc4253#section-4.2 54 | this.sendString(util.format("%s\r\n", Server.ProtocolVersionExchange)); 55 | 56 | // 7.1. Algorithm Negotiation - https://tools.ietf.org/html/rfc4253#section-7.1 57 | this.sendPacket(this.m_KexInitServerToClient); 58 | } 59 | 60 | public getIsConnected(): boolean { 61 | return (this.m_Socket != null); 62 | } 63 | 64 | public poll(): void { 65 | if (!this.getIsConnected()) { 66 | return; 67 | } 68 | 69 | if (this.getIsDataAvailable()) { 70 | if (!this.m_HasCompletedProtocolVersionExchange) { 71 | // wait for CRLF 72 | try { 73 | this.readProtocolVersionExchange(); 74 | if (this.m_HasCompletedProtocolVersionExchange) { 75 | SSHLogger.logDebug(util.format("Received ProtocolVersionExchange: %s", this.m_ProtocolVersionExchange)); 76 | this.validateProtocolVersionExchange(); 77 | } 78 | } catch (ex) { 79 | SSHLogger.logError(ex); 80 | this.disconnect( 81 | Exceptions.DisconnectReason.SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 82 | "Failed to get the protocol version exchange."); 83 | return; 84 | } 85 | } 86 | 87 | if (this.m_HasCompletedProtocolVersionExchange) { 88 | try { 89 | let packet: Packets.Packet = this.readPacket(); 90 | while (packet != null) { 91 | this.handlePacket(packet); 92 | packet = this.readPacket(); 93 | } 94 | 95 | this.considerReExchange(); 96 | } catch (ex) { 97 | if (ex instanceof Exceptions.SSHServerException) { 98 | let serverEx: Exceptions.SSHServerException = ex; 99 | SSHLogger.logError(ex); 100 | this.disconnect(serverEx.reason, serverEx.message); 101 | return; 102 | } else { 103 | SSHLogger.logError(ex); 104 | this.disconnect(Exceptions.DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, ex.message); 105 | return; 106 | } 107 | } 108 | } 109 | } 110 | } 111 | 112 | public sendString(message: string): void { 113 | if (!this.getIsConnected()) { 114 | return; 115 | } 116 | 117 | SSHLogger.logDebug(util.format("Sending raw string: %s", message.trim())); 118 | this.m_Socket.write(message, "UTF8"); 119 | } 120 | 121 | public sendPacket(packet: Packets.Packet): void { 122 | packet.packetSequence = this.m_CurrentSentPacketNumber; 123 | this.m_CurrentSentPacketNumber += 1; 124 | 125 | let payload: Buffer = this.m_ActiveExchangeContext.compressionServerToClient.compress(packet.getBytes()); 126 | 127 | let blockSize: number = this.m_ActiveExchangeContext.cipherServerToClient.getBlockSize(); 128 | 129 | let paddingLength: number = blockSize - (payload.length + 5) % blockSize; 130 | if (paddingLength < 4) { 131 | paddingLength += blockSize; 132 | } 133 | 134 | let padding: Buffer = crypto.randomBytes(paddingLength); 135 | let packetLength: number = payload.length + paddingLength + 1; 136 | 137 | let writer: ByteWriter = new ByteWriter(); 138 | writer.writeUInt32(packetLength); 139 | writer.writeByte(paddingLength); 140 | writer.writeRawBytes(payload); 141 | writer.writeRawBytes(padding); 142 | 143 | payload = writer.toBuffer(); 144 | 145 | let encryptedPayload: Buffer = this.m_ActiveExchangeContext.cipherServerToClient.encrypt(payload); 146 | if (this.m_ActiveExchangeContext.macAlgorithmServerToClient != null) { 147 | let mac: Buffer = this.m_ActiveExchangeContext.macAlgorithmServerToClient.computeHash(packet.packetSequence, payload); 148 | encryptedPayload = Buffer.concat([encryptedPayload, mac]); 149 | } 150 | 151 | this.sendRaw(payload); 152 | this.considerReExchange(); 153 | } 154 | 155 | public disconnect(reason: Exceptions.DisconnectReason, message: string): void { 156 | if (this.m_Socket != null) { 157 | SSHLogger.logInfo(util.format( 158 | "Disconnected from: %s - %s - %s", 159 | this.m_Socket.remoteAddress, 160 | Exceptions.DisconnectReason[reason], 161 | message)); 162 | 163 | if (reason !== Exceptions.DisconnectReason.None) { 164 | try { 165 | let disconnect: Packets.Disconnect = new Packets.Disconnect(); 166 | disconnect.reason = reason; 167 | disconnect.description = message; 168 | this.sendPacket(disconnect); 169 | } catch (ex) { } 170 | } 171 | 172 | try { 173 | this.m_Socket.destroy(); 174 | } catch (ex) { } 175 | this.m_Socket = null; 176 | } 177 | } 178 | 179 | private handlePacket(packet: Packets.Packet): void { 180 | switch (packet.getPacketType()) { 181 | case Packets.PacketType.SSH_MSG_KEXINIT: 182 | this.handleKexInit(packet); 183 | break; 184 | case Packets.PacketType.SSH_MSG_KEXDH_INIT: 185 | this.handleKexDHInit(packet); 186 | break; 187 | case Packets.PacketType.SSH_MSG_NEWKEYS: 188 | this.handleNewKeys(packet); 189 | break; 190 | case Packets.PacketType.SSH_MSG_DISCONNECT: 191 | this.handleDisconnect(packet); 192 | break; 193 | default: 194 | SSHLogger.logWarning(util.format("Unhandled packet type: %s", packet.getPacketType())); 195 | let unimplemented: Packets.Unimplemented = new Packets.Unimplemented(); 196 | unimplemented.rejectedPacketNumber = packet.packetSequence; 197 | this.sendPacket(unimplemented); 198 | break; 199 | } 200 | } 201 | 202 | private handleKexInit(packet: Packets.KexInit): void { 203 | SSHLogger.logDebug("Received KexInit"); 204 | 205 | if (this.m_PendingExchangeContext === null) { 206 | SSHLogger.logDebug("Trigger re-exchange from client"); 207 | this.m_PendingExchangeContext = new ExchangeContext(); 208 | this.sendPacket(this.m_KexInitServerToClient); 209 | } 210 | 211 | this.m_KexInitClientToServer = packet; 212 | 213 | this.m_PendingExchangeContext.kexAlgorithm = packet.pickKexAlgorithm(); 214 | this.m_PendingExchangeContext.hostKeyAlgorithm = packet.pickHostKeyAlgorithm(); 215 | this.m_PendingExchangeContext.cipherClientToServer = packet.pickCipherClientToServer(); 216 | this.m_PendingExchangeContext.cipherServerToClient = packet.pickCipherServerToClient(); 217 | this.m_PendingExchangeContext.macAlgorithmClientToServer = packet.pickMACAlgorithmClientToServer(); 218 | this.m_PendingExchangeContext.macAlgorithmServerToClient = packet.pickMACAlgorithmServerToClient(); 219 | this.m_PendingExchangeContext.compressionClientToServer = packet.pickCompressionAlgorithmClientToServer(); 220 | this.m_PendingExchangeContext.compressionServerToClient = packet.pickCompressionAlgorithmServerToClient(); 221 | 222 | SSHLogger.logDebug(util.format("Selected KexAlgorithm: %s", this.m_PendingExchangeContext.kexAlgorithm.getName())); 223 | SSHLogger.logDebug(util.format("Selected HostKeyAlgorithm: %s", this.m_PendingExchangeContext.hostKeyAlgorithm.getName())); 224 | SSHLogger.logDebug(util.format("Selected CipherClientToServer: %s", this.m_PendingExchangeContext.cipherClientToServer.getName())); 225 | SSHLogger.logDebug(util.format("Selected CipherServerToClient: %s", this.m_PendingExchangeContext.cipherServerToClient.getName())); 226 | SSHLogger.logDebug(util.format("Selected MACAlgorithmClientToServer: %s", this.m_PendingExchangeContext.macAlgorithmClientToServer.getName())); 227 | SSHLogger.logDebug(util.format("Selected MACAlgorithmServerToClient: %s", this.m_PendingExchangeContext.macAlgorithmServerToClient.getName())); 228 | SSHLogger.logDebug(util.format("Selected CompressionClientToServer: %s", this.m_PendingExchangeContext.compressionClientToServer.getName())); 229 | SSHLogger.logDebug(util.format("Selected CompressionServerToClient: %s", this.m_PendingExchangeContext.compressionServerToClient.getName())); 230 | } 231 | 232 | private handleKexDHInit(packet: Packets.KexDHInit): void { 233 | SSHLogger.logDebug("Received KexDHInit"); 234 | 235 | if ((this.m_PendingExchangeContext === null) || (this.m_PendingExchangeContext.kexAlgorithm === null)) { 236 | throw new Exceptions.SSHServerException( 237 | Exceptions.DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, 238 | "Server did not receive SSH_MSG_KEX_INIT as expected."); 239 | } 240 | 241 | // 1. C generates a random number x (1 < x < q) and computes e = g ^ x mod p. C sends e to S. 242 | // 2. S receives e. It computes K = e^y mod p 243 | let sharedSecret: Buffer = this.m_PendingExchangeContext.kexAlgorithm.decryptKeyExchange(packet.clientValue); 244 | 245 | // 2. S generates a random number y (0 < y < q) and computes f = g ^ y mod p. 246 | let serverKeyExchange: Buffer = this.m_PendingExchangeContext.kexAlgorithm.createKeyExchange(); 247 | 248 | let hostKey: Buffer = this.m_PendingExchangeContext.hostKeyAlgorithm.createKeyAndCertificatesData(); 249 | 250 | // h = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) 251 | let exchangeHash: Buffer = this.computeExchangeHash( 252 | this.m_PendingExchangeContext.kexAlgorithm, 253 | hostKey, 254 | packet.clientValue, 255 | serverKeyExchange, 256 | sharedSecret); 257 | 258 | if (this.m_SessionId === null) { 259 | this.m_SessionId = exchangeHash; 260 | } 261 | 262 | // initial IV client to server: HASH(K || H || "A" || session_id) 263 | // (Here K is encoded as mpint and "A" as byte and session_id as raw 264 | // data. "A" means the single character A, ASCII 65). 265 | let clientCipherIV: Buffer = this.computeEncryptionKey( 266 | this.m_PendingExchangeContext.kexAlgorithm, 267 | exchangeHash, 268 | this.m_PendingExchangeContext.cipherClientToServer.getBlockSize(), 269 | sharedSecret, "A"); 270 | 271 | // initial IV server to client: HASH(K || H || "B" || session_id) 272 | let serverCipherIV: Buffer = this.computeEncryptionKey( 273 | this.m_PendingExchangeContext.kexAlgorithm, 274 | exchangeHash, 275 | this.m_PendingExchangeContext.cipherServerToClient.getBlockSize(), 276 | sharedSecret, "B"); 277 | 278 | // encryption key client to server: HASH(K || H || "C" || session_id) 279 | let clientCipherKey: Buffer = this.computeEncryptionKey( 280 | this.m_PendingExchangeContext.kexAlgorithm, 281 | exchangeHash, 282 | this.m_PendingExchangeContext.cipherClientToServer.getKeySize(), 283 | sharedSecret, "C"); 284 | 285 | // encryption key server to client: HASH(K || H || "D" || session_id) 286 | let serverCipherKey: Buffer = this.computeEncryptionKey( 287 | this.m_PendingExchangeContext.kexAlgorithm, 288 | exchangeHash, 289 | this.m_PendingExchangeContext.cipherServerToClient.getKeySize(), 290 | sharedSecret, "D"); 291 | 292 | // integrity key client to server: HASH(K || H || "E" || session_id) 293 | let clientHmacKey: Buffer = this.computeEncryptionKey( 294 | this.m_PendingExchangeContext.kexAlgorithm, 295 | exchangeHash, 296 | this.m_PendingExchangeContext.macAlgorithmClientToServer.getKeySize(), 297 | sharedSecret, "E"); 298 | 299 | // integrity key server to client: HASH(K || H || "F" || session_id) 300 | let serverHmacKey: Buffer = this.computeEncryptionKey( 301 | this.m_PendingExchangeContext.kexAlgorithm, 302 | exchangeHash, 303 | this.m_PendingExchangeContext.macAlgorithmServerToClient.getKeySize(), 304 | sharedSecret, "F"); 305 | 306 | // set all keys we just generated 307 | this.m_PendingExchangeContext.cipherClientToServer.setKey(clientCipherKey, clientCipherIV); 308 | this.m_PendingExchangeContext.cipherServerToClient.setKey(serverCipherKey, serverCipherIV); 309 | this.m_PendingExchangeContext.macAlgorithmClientToServer.setKey(clientHmacKey); 310 | this.m_PendingExchangeContext.macAlgorithmServerToClient.setKey(serverHmacKey); 311 | 312 | let reply: Packets.KexDHReply = new Packets.KexDHReply(); 313 | reply.serverHostKey = hostKey; 314 | reply.serverValue = serverKeyExchange; 315 | reply.signature = this.m_PendingExchangeContext.hostKeyAlgorithm.createSignatureData(exchangeHash); 316 | 317 | this.sendPacket(reply); 318 | this.sendPacket(new Packets.NewKeys()); 319 | } 320 | 321 | private handleNewKeys(packet: Packets.NewKeys): void { 322 | SSHLogger.logDebug("Received NewKeys"); 323 | 324 | this.m_ActiveExchangeContext = this.m_PendingExchangeContext; 325 | this.m_PendingExchangeContext = null; 326 | 327 | this.m_TotalBytesTransferred = 0; 328 | this.resetKeyTimer(); 329 | } 330 | 331 | private handleDisconnect(packet: Packets.Disconnect): void { 332 | this.disconnect(packet.reason, packet.description); 333 | } 334 | 335 | private sendRaw(data: Buffer): void { 336 | if (!this.getIsConnected()) { 337 | return; 338 | } 339 | 340 | // increase bytes transferred 341 | this.m_TotalBytesTransferred += data.byteLength; 342 | 343 | this.m_Socket.write(new Buffer(data)); 344 | } 345 | 346 | private closeReceived(hadError: boolean): void { 347 | this.disconnect( 348 | Exceptions.DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, 349 | "The client disconnected."); 350 | } 351 | 352 | private getIsDataAvailable(): boolean { 353 | if (this.m_Socket == null) { 354 | return false; 355 | } 356 | 357 | return (this.m_Socket.bytesRead !== this.m_LastBytesRead); 358 | } 359 | 360 | private readBytes(size: number): Buffer { 361 | if (this.m_Socket == null) { 362 | return null; 363 | } 364 | 365 | let buffer: Buffer = this.m_Socket.read(size); 366 | 367 | if (buffer === null) { 368 | throw new Exceptions.SSHServerException( 369 | Exceptions.DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, 370 | "Failed to read from socket."); 371 | } 372 | 373 | if (buffer.byteLength !== size) { 374 | throw new Exceptions.SSHServerException( 375 | Exceptions.DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, 376 | "Failed to read from socket."); 377 | } 378 | 379 | this.m_LastBytesRead += size; 380 | 381 | return buffer; 382 | } 383 | 384 | private getDataAvailable(): number { 385 | if (this.m_Socket == null) { 386 | return 0; 387 | } 388 | 389 | return (this.m_Socket.bytesRead - this.m_LastBytesRead); 390 | } 391 | 392 | private readProtocolVersionExchange(): void { 393 | let data: Array = new Array(); 394 | 395 | let foundCR: boolean = false; 396 | let value: Buffer = this.readBytes(1); 397 | while (value != null) { 398 | if (foundCR && (value[0] === 10)) { 399 | // done 400 | this.m_HasCompletedProtocolVersionExchange = true; 401 | break; 402 | } 403 | 404 | if (value[0] === 13) { 405 | foundCR = true; 406 | } else { 407 | foundCR = false; 408 | data.push(value[0]); 409 | } 410 | 411 | value = this.readBytes(1); 412 | } 413 | 414 | this.m_ProtocolVersionExchange += new Buffer(data).toString("UTF8"); 415 | } 416 | 417 | private readPacket(): Packets.Packet { 418 | if (this.m_Socket == null) { 419 | return; 420 | } 421 | 422 | let blockSize: number = this.m_ActiveExchangeContext.cipherClientToServer.getBlockSize(); 423 | 424 | // we must have at least 1 block to read 425 | if (this.getDataAvailable() < blockSize) { 426 | return null; // packet not here 427 | } 428 | 429 | let firstBlock: Buffer = this.m_ActiveExchangeContext.cipherClientToServer.decrypt(this.readBytes(blockSize)); 430 | 431 | let reader: ByteReader = new ByteReader(firstBlock); 432 | 433 | // uint32 packet_length 434 | // packet_length 435 | // the length of the packet in bytes, not including 'mac' or the 436 | // 'packet_length' field itself. 437 | let packetLength: number = reader.getUInt32(); 438 | if (packetLength > Packets.Packet.MaxPacketSize) { 439 | throw new Exceptions.SSHServerException( 440 | Exceptions.DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, 441 | util.format( 442 | "Client tried to send a packet bigger than MaxPacketSize (%d bytes): %d bytes", 443 | Packets.Packet.MaxPacketSize, 444 | packetLength)); 445 | } 446 | 447 | // byte padding_length 448 | // padding_length 449 | // length of 'random padding' (bytes). 450 | let paddingLength: number = reader.getByte(); 451 | 452 | // byte[n1] payload; n1 = packet_length - padding_length - 1 453 | // payload 454 | // the useful contents of the packet. If compression has been 455 | // negotiated, this field is compressed. Initially, compression 456 | // must be "none". 457 | let bytesToRead: number = packetLength - blockSize + 4; 458 | 459 | let restOfPacket: Buffer = this.m_ActiveExchangeContext.cipherClientToServer.decrypt(this.readBytes(bytesToRead)); 460 | 461 | let payloadLength: number = packetLength - paddingLength - 1; 462 | let fullPacket: Buffer = Buffer.concat([ firstBlock, restOfPacket ]); 463 | 464 | // track total bytes read 465 | this.m_TotalBytesTransferred += fullPacket.byteLength; 466 | 467 | let payload: Buffer = fullPacket.slice( 468 | Packets.Packet.PacketHeaderSize, 469 | Packets.Packet.PacketHeaderSize + payloadLength); 470 | 471 | // byte[n2] random padding; n2 = padding_length 472 | // random padding 473 | // arbitrary-length padding, such that the total length of 474 | // (packet_length || padding_length || payload || random padding) 475 | // is a multiple of the cipher block size or 8, whichever is 476 | // larger. There MUST be at least four bytes of padding. The 477 | // padding SHOULD consist of random bytes. The maximum amount of 478 | // padding is 255 bytes. 479 | 480 | // byte[m] mac (Message Authentication Code - MAC); m = mac_length 481 | // mac 482 | // message Authentication Code. If message authentication has 483 | // been negotiated, this field contains the MAC bytes. Initially, 484 | // the MAC algorithm MUST be "none". 485 | 486 | let packetNumber: number = this.m_CurrentReceivedPacketNumber; 487 | this.m_CurrentReceivedPacketNumber += 1; 488 | 489 | if (this.m_ActiveExchangeContext.macAlgorithmClientToServer != null) { 490 | 491 | let clientMac: Buffer = this.readBytes(this.m_ActiveExchangeContext.macAlgorithmClientToServer.getDigestLength()); 492 | let mac: Buffer = this.m_ActiveExchangeContext.macAlgorithmClientToServer.computeHash(packetNumber, fullPacket); 493 | if (clientMac.compare(mac) !== 0) { 494 | throw new Exceptions.SSHServerException( 495 | Exceptions.DisconnectReason.SSH_DISCONNECT_MAC_ERROR, 496 | "MAC from client is invalid"); 497 | } 498 | } 499 | 500 | payload = this.m_ActiveExchangeContext.compressionClientToServer.decompress(payload); 501 | 502 | let packetReader: ByteReader = new ByteReader(payload); 503 | let packetType: Packets.PacketType = packetReader.getByte(); 504 | 505 | let packet: Packets.Packet = Client.createPacket(packetType); 506 | 507 | if (packet != null) { 508 | SSHLogger.logDebug(util.format("Received Packet: %s", Packets.PacketType[packetType])); 509 | packet.load(packetReader); 510 | } 511 | 512 | return packet; 513 | } 514 | 515 | private considerReExchange(): void { 516 | const OneGB: number = (1024 * 1024 * 1024); 517 | if (this.m_TotalBytesTransferred > OneGB) { 518 | this.reExchangeKeys(); 519 | } 520 | } 521 | 522 | private resetKeyTimer(): void { 523 | const MSInOneHour: number = 1000 * 60 * 60; 524 | 525 | if (this.m_KeyTimeout !== null) { 526 | clearTimeout(this.m_KeyTimeout); 527 | } 528 | 529 | this.m_KeyTimeout = setTimeout(this.reExchangeKeys, MSInOneHour); 530 | } 531 | 532 | private reExchangeKeys(): void { 533 | // time to get new keys! 534 | this.m_TotalBytesTransferred = 0; 535 | this.resetKeyTimer(); 536 | 537 | SSHLogger.logDebug("Trigger re-exchange from server"); 538 | this.m_PendingExchangeContext = new ExchangeContext(); 539 | this.sendPacket(this.m_KexInitServerToClient); 540 | } 541 | 542 | private computeExchangeHash( 543 | kexAlgorithm: IKexAlgorithm, 544 | hostKeyAndCerts: Buffer, 545 | clientExchangeValue: Buffer, 546 | serverExchangeValue: Buffer, 547 | sharedSecret: Buffer): Buffer { 548 | let writer: ByteWriter = new ByteWriter(); 549 | writer.writeString(this.m_ProtocolVersionExchange); 550 | writer.writeString(Server.ProtocolVersionExchange); 551 | 552 | writer.writeBytes(this.m_KexInitClientToServer.getBytes()); 553 | writer.writeBytes(this.m_KexInitServerToClient.getBytes()); 554 | writer.writeBytes(hostKeyAndCerts); 555 | 556 | writer.writeMPInt(clientExchangeValue); 557 | writer.writeMPInt(serverExchangeValue); 558 | writer.writeMPInt(sharedSecret); 559 | 560 | return kexAlgorithm.computeHash(writer.toBuffer()); 561 | } 562 | 563 | private computeEncryptionKey(kexAlgorithm: IKexAlgorithm, exchangeHash: Buffer, keySize: number, sharedSecret: Buffer, letter: string): Buffer { 564 | // k(X) = HASH(K || H || X || session_id) 565 | 566 | // prepare the buffer 567 | let keyBuffer: Buffer = new Buffer(keySize); 568 | let keyBufferIndex: number = 0; 569 | let currentHashLength: number = 0; 570 | let currentHash: Buffer = null; 571 | 572 | // we can stop once we fill the key buffer 573 | while (keyBufferIndex < keySize) { 574 | let writer: ByteWriter = new ByteWriter(); 575 | // write "K" 576 | writer.writeMPInt(sharedSecret); 577 | 578 | // write "H" 579 | writer.writeRawBytes(exchangeHash); 580 | 581 | if (currentHash === null) { 582 | // if we haven't done this yet, add the "X" and session_id 583 | writer.writeByte(letter.charCodeAt(0)); 584 | writer.writeRawBytes(this.m_SessionId); 585 | } else { 586 | // if the key isn't long enough after the first pass, we need to 587 | // write the current hash as described here: 588 | // k1 = HASH(K || H || X || session_id) (X is e.g., "A") 589 | // k2 = HASH(K || H || K1) 590 | // k3 = HASH(K || H || K1 || K2) 591 | // ... 592 | // key = K1 || K2 || K3 || ... 593 | writer.writeRawBytes(currentHash); 594 | } 595 | 596 | currentHash = kexAlgorithm.computeHash(writer.toBuffer()); 597 | 598 | currentHashLength = Math.min(currentHash.byteLength, (keySize - keyBufferIndex)); 599 | currentHash.copy(keyBuffer, keyBufferIndex, 0, currentHashLength); 600 | 601 | keyBufferIndex += currentHashLength; 602 | } 603 | 604 | return keyBuffer; 605 | } 606 | 607 | private validateProtocolVersionExchange(): void { 608 | // https://tools.ietf.org/html/rfc4253#section-4.2 609 | // - SSH-protoversion-softwareversion SP comments 610 | let pveParts: string[] = this.m_ProtocolVersionExchange.split(" "); 611 | if (pveParts.length == 0) { 612 | throw new Error("Invalid Protocol Version Exchange was received - No Data"); 613 | } 614 | 615 | let versionParts: string[] = pveParts[0].split("-"); 616 | if (versionParts.length < 3) { 617 | throw new Error(util.format("Invalid Protocol Version Exchange was received - Not enough dashes - %s", pveParts[0])); 618 | } 619 | 620 | if (versionParts[1] !== "2.0") { 621 | throw new Error(util.format("Invalid Protocol Version Exchange was received - Unsupported Version - %s", versionParts[1])); 622 | } 623 | 624 | // if we get here, all is well! 625 | } 626 | 627 | private static createPacket(packetType: Packets.PacketType): Packets.Packet { 628 | switch (packetType) { 629 | case Packets.PacketType.SSH_MSG_KEXINIT: 630 | return new Packets.KexInit(); 631 | case Packets.PacketType.SSH_MSG_KEXDH_INIT: 632 | return new Packets.KexDHInit(); 633 | case Packets.PacketType.SSH_MSG_NEWKEYS: 634 | return new Packets.NewKeys(); 635 | case Packets.PacketType.SSH_MSG_DISCONNECT: 636 | return new Packets.Disconnect(); 637 | } 638 | 639 | SSHLogger.logDebug(util.format("Unknown PacketType: %s", Packets.PacketType[packetType])); 640 | return null; 641 | } 642 | } 643 | -------------------------------------------------------------------------------- /src/SSHServer/Client.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CSharp.RuntimeBinder; 2 | using Microsoft.Extensions.Logging; 3 | using SSHServer.Packets; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Sockets; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using SSHServer.KexAlgorithms; 11 | using System.Threading; 12 | using System.Security.Cryptography; 13 | 14 | namespace SSHServer 15 | { 16 | public class Client 17 | { 18 | private Socket m_Socket; 19 | private ILogger m_Logger; 20 | 21 | private bool m_HasCompletedProtocolVersionExchange = false; 22 | private string m_ProtocolVersionExchange; 23 | 24 | private KexInit m_KexInitServerToClient = new KexInit(); 25 | private KexInit m_KexInitClientToServer = null; 26 | private byte[] m_SessionId = null; 27 | 28 | private int m_CurrentSentPacketNumber = -1; 29 | private int m_CurrentReceivedPacketNumber = -1; 30 | 31 | private long m_TotalBytesTransferred = 0; 32 | private DateTime m_KeyTimeout = DateTime.UtcNow.AddHours(1); 33 | 34 | private ExchangeContext m_ActiveExchangeContext = new ExchangeContext(); 35 | private ExchangeContext m_PendingExchangeContext = new ExchangeContext(); 36 | 37 | // We are considered connected if we have a valid socket object 38 | public bool IsConnected { get { return m_Socket != null; } } 39 | 40 | public Client(Socket socket, ILogger logger) 41 | { 42 | m_Socket = socket; 43 | m_Logger = logger; 44 | 45 | m_KexInitServerToClient.KexAlgorithms.AddRange(Server.GetNames(Server.SupportedKexAlgorithms)); 46 | m_KexInitServerToClient.ServerHostKeyAlgorithms.AddRange(Server.GetNames(Server.SupportedHostKeyAlgorithms)); 47 | m_KexInitServerToClient.EncryptionAlgorithmsClientToServer.AddRange(Server.GetNames(Server.SupportedCiphers)); 48 | m_KexInitServerToClient.EncryptionAlgorithmsServerToClient.AddRange(Server.GetNames(Server.SupportedCiphers)); 49 | m_KexInitServerToClient.MacAlgorithmsClientToServer.AddRange(Server.GetNames(Server.SupportedMACAlgorithms)); 50 | m_KexInitServerToClient.MacAlgorithmsServerToClient.AddRange(Server.GetNames(Server.SupportedMACAlgorithms)); 51 | m_KexInitServerToClient.CompressionAlgorithmsClientToServer.AddRange(Server.GetNames(Server.SupportedCompressions)); 52 | m_KexInitServerToClient.CompressionAlgorithmsServerToClient.AddRange(Server.GetNames(Server.SupportedCompressions)); 53 | 54 | const int socketBufferSize = 2 * Packet.MaxPacketSize; 55 | m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendBuffer, socketBufferSize); 56 | m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveBuffer, socketBufferSize); 57 | m_Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); 58 | m_Socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true); 59 | 60 | // 4.2.Protocol Version Exchange - https://tools.ietf.org/html/rfc4253#section-4.2 61 | Send($"{Server.ProtocolVersionExchange}\r\n"); 62 | 63 | // 7.1. Algorithm Negotiation - https://tools.ietf.org/html/rfc4253#section-7.1 64 | Send(m_KexInitServerToClient); 65 | } 66 | 67 | public void Poll() 68 | { 69 | if (!IsConnected) 70 | return; 71 | 72 | bool dataAvailable = m_Socket.Poll(0, SelectMode.SelectRead); 73 | if (dataAvailable) 74 | { 75 | int read = m_Socket.Available; 76 | if (read < 1) 77 | { 78 | Disconnect(DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, "The client disconnected."); 79 | return; 80 | } 81 | 82 | if (!m_HasCompletedProtocolVersionExchange) 83 | { 84 | // Wait for CRLF 85 | try 86 | { 87 | ReadProtocolVersionExchange(); 88 | if (m_HasCompletedProtocolVersionExchange) 89 | { 90 | m_Logger.LogDebug($"Received ProtocolVersionExchange: {m_ProtocolVersionExchange}"); 91 | ValidateProtocolVersionExchange(); 92 | } 93 | } 94 | catch (Exception ex) 95 | { 96 | m_Logger.LogError(ex.Message); 97 | Disconnect(DisconnectReason.SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, "Failed to get the protocol version exchange."); 98 | return; 99 | } 100 | } 101 | 102 | if (m_HasCompletedProtocolVersionExchange) 103 | { 104 | try 105 | { 106 | Packet packet = ReadPacket(); 107 | while (packet != null) 108 | { 109 | m_Logger.LogDebug($"Received Packet: {packet.PacketType}"); 110 | HandlePacket(packet); 111 | packet = ReadPacket(); 112 | } 113 | 114 | ConsiderReExchange(); 115 | } 116 | catch (SSHServerException ex) 117 | { 118 | m_Logger.LogError(ex.Message); 119 | Disconnect(ex.Reason, ex.Message); 120 | return; 121 | } 122 | catch (Exception ex) 123 | { 124 | m_Logger.LogError(ex.Message); 125 | Disconnect(DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, ex.Message); 126 | return; 127 | } 128 | } 129 | } 130 | } 131 | 132 | public void Disconnect(DisconnectReason reason, string message) 133 | { 134 | m_Logger.LogDebug($"Disconnected - {reason} - {message}"); 135 | if (m_Socket != null) 136 | { 137 | if (reason != DisconnectReason.None) 138 | { 139 | try 140 | { 141 | Disconnect disconnect = new Disconnect() 142 | { 143 | Reason = reason, 144 | Description = message 145 | }; 146 | Send(disconnect); 147 | } 148 | catch (Exception) { } 149 | } 150 | 151 | try 152 | { 153 | m_Socket.Shutdown(SocketShutdown.Both); 154 | } 155 | catch (Exception) { } 156 | 157 | m_Socket = null; 158 | } 159 | } 160 | 161 | private void HandlePacket(Packet packet) 162 | { 163 | try 164 | { 165 | HandleSpecificPacket((dynamic)packet); 166 | } 167 | catch (RuntimeBinderException) 168 | { 169 | m_Logger.LogWarning($"Unhandled packet type: {packet.PacketType}"); 170 | 171 | Unimplemented unimplemented = new Unimplemented() 172 | { 173 | RejectedPacketNumber = packet.PacketSequence 174 | }; 175 | Send(unimplemented); 176 | } 177 | } 178 | 179 | private void HandleSpecificPacket(KexInit packet) 180 | { 181 | m_Logger.LogDebug("Received KexInit"); 182 | 183 | if (m_PendingExchangeContext == null) 184 | { 185 | m_Logger.LogDebug("Trigger re-exchange from client"); 186 | m_PendingExchangeContext = new ExchangeContext(); 187 | Send(m_KexInitServerToClient); 188 | } 189 | 190 | m_KexInitClientToServer = packet; 191 | 192 | m_PendingExchangeContext.KexAlgorithm = packet.PickKexAlgorithm(); 193 | m_PendingExchangeContext.HostKeyAlgorithm = packet.PickHostKeyAlgorithm(); 194 | m_PendingExchangeContext.CipherClientToServer = packet.PickCipherClientToServer(); 195 | m_PendingExchangeContext.CipherServerToClient = packet.PickCipherServerToClient(); 196 | m_PendingExchangeContext.MACAlgorithmClientToServer = packet.PickMACAlgorithmClientToServer(); 197 | m_PendingExchangeContext.MACAlgorithmServerToClient = packet.PickMACAlgorithmServerToClient(); 198 | m_PendingExchangeContext.CompressionClientToServer = packet.PickCompressionAlgorithmClientToServer(); 199 | m_PendingExchangeContext.CompressionServerToClient = packet.PickCompressionAlgorithmServerToClient(); 200 | 201 | m_Logger.LogDebug($"Selected KexAlgorithm: {m_PendingExchangeContext.KexAlgorithm.Name}"); 202 | m_Logger.LogDebug($"Selected HostKeyAlgorithm: {m_PendingExchangeContext.HostKeyAlgorithm.Name}"); 203 | m_Logger.LogDebug($"Selected CipherClientToServer: {m_PendingExchangeContext.CipherClientToServer.Name}"); 204 | m_Logger.LogDebug($"Selected CipherServerToClient: {m_PendingExchangeContext.CipherServerToClient.Name}"); 205 | m_Logger.LogDebug($"Selected MACAlgorithmClientToServer: {m_PendingExchangeContext.MACAlgorithmClientToServer.Name}"); 206 | m_Logger.LogDebug($"Selected MACAlgorithmServerToClient: {m_PendingExchangeContext.MACAlgorithmServerToClient.Name}"); 207 | m_Logger.LogDebug($"Selected CompressionClientToServer: {m_PendingExchangeContext.CompressionClientToServer.Name}"); 208 | m_Logger.LogDebug($"Selected CompressionServerToClient: {m_PendingExchangeContext.CompressionServerToClient.Name}"); 209 | } 210 | 211 | private void HandleSpecificPacket(KexDHInit packet) 212 | { 213 | m_Logger.LogDebug("Received KexDHInit"); 214 | 215 | if ((m_PendingExchangeContext == null) || (m_PendingExchangeContext.KexAlgorithm == null)) 216 | { 217 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, "Server did not receive SSH_MSG_KEX_INIT as expected."); 218 | } 219 | 220 | // 1. C generates a random number x (1 < x < q) and computes e = g ^ x mod p. C sends e to S. 221 | // 2. S receives e. It computes K = e^y mod p 222 | byte[] sharedSecret = m_PendingExchangeContext.KexAlgorithm.DecryptKeyExchange(packet.ClientValue); 223 | 224 | // 2. S generates a random number y (0 < y < q) and computes f = g ^ y mod p. 225 | byte[] serverKeyExchange = m_PendingExchangeContext.KexAlgorithm.CreateKeyExchange(); 226 | 227 | byte[] hostKey = m_PendingExchangeContext.HostKeyAlgorithm.CreateKeyAndCertificatesData(); 228 | 229 | // H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) 230 | byte[] exchangeHash = ComputeExchangeHash( 231 | m_PendingExchangeContext.KexAlgorithm, 232 | hostKey, 233 | packet.ClientValue, 234 | serverKeyExchange, 235 | sharedSecret); 236 | 237 | if (m_SessionId == null) 238 | m_SessionId = exchangeHash; 239 | 240 | // https://tools.ietf.org/html/rfc4253#section-7.2 241 | 242 | // Initial IV client to server: HASH(K || H || "A" || session_id) 243 | // (Here K is encoded as mpint and "A" as byte and session_id as raw 244 | // data. "A" means the single character A, ASCII 65). 245 | byte[] clientCipherIV = ComputeEncryptionKey( 246 | m_PendingExchangeContext.KexAlgorithm, 247 | exchangeHash, 248 | m_PendingExchangeContext.CipherClientToServer.BlockSize, 249 | sharedSecret, 'A'); 250 | 251 | // Initial IV server to client: HASH(K || H || "B" || session_id) 252 | byte[] serverCipherIV = ComputeEncryptionKey( 253 | m_PendingExchangeContext.KexAlgorithm, 254 | exchangeHash, 255 | m_PendingExchangeContext.CipherServerToClient.BlockSize, 256 | sharedSecret, 'B'); 257 | 258 | // Encryption key client to server: HASH(K || H || "C" || session_id) 259 | byte[] clientCipherKey = ComputeEncryptionKey( 260 | m_PendingExchangeContext.KexAlgorithm, 261 | exchangeHash, 262 | m_PendingExchangeContext.CipherClientToServer.KeySize, 263 | sharedSecret, 'C'); 264 | 265 | // Encryption key server to client: HASH(K || H || "D" || session_id) 266 | byte[] serverCipherKey = ComputeEncryptionKey( 267 | m_PendingExchangeContext.KexAlgorithm, 268 | exchangeHash, 269 | m_PendingExchangeContext.CipherServerToClient.KeySize, 270 | sharedSecret, 'D'); 271 | 272 | // Integrity key client to server: HASH(K || H || "E" || session_id) 273 | byte[] clientHmacKey = ComputeEncryptionKey( 274 | m_PendingExchangeContext.KexAlgorithm, 275 | exchangeHash, 276 | m_PendingExchangeContext.MACAlgorithmClientToServer.KeySize, 277 | sharedSecret, 'E'); 278 | 279 | // Integrity key server to client: HASH(K || H || "F" || session_id) 280 | byte[] serverHmacKey = ComputeEncryptionKey( 281 | m_PendingExchangeContext.KexAlgorithm, 282 | exchangeHash, 283 | m_PendingExchangeContext.MACAlgorithmServerToClient.KeySize, 284 | sharedSecret, 'F'); 285 | 286 | // Set all keys we just generated 287 | m_PendingExchangeContext.CipherClientToServer.SetKey(clientCipherKey, clientCipherIV); 288 | m_PendingExchangeContext.CipherServerToClient.SetKey(serverCipherKey, serverCipherIV); 289 | m_PendingExchangeContext.MACAlgorithmClientToServer.SetKey(clientHmacKey); 290 | m_PendingExchangeContext.MACAlgorithmServerToClient.SetKey(serverHmacKey); 291 | 292 | // Send reply to client! 293 | KexDHReply reply = new KexDHReply() 294 | { 295 | ServerHostKey = hostKey, 296 | ServerValue = serverKeyExchange, 297 | Signature = m_PendingExchangeContext.HostKeyAlgorithm.CreateSignatureData(exchangeHash) 298 | }; 299 | 300 | Send(reply); 301 | Send(new NewKeys()); 302 | } 303 | 304 | private void HandleSpecificPacket(NewKeys packet) 305 | { 306 | m_Logger.LogDebug("Received NewKeys"); 307 | 308 | m_ActiveExchangeContext = m_PendingExchangeContext; 309 | m_PendingExchangeContext = null; 310 | 311 | // Reset re-exchange values 312 | m_TotalBytesTransferred = 0; 313 | m_KeyTimeout = DateTime.UtcNow.AddHours(1); 314 | } 315 | 316 | private void HandleSpecificPacket(Disconnect packet) 317 | { 318 | this.Disconnect(packet.Reason, packet.Description); 319 | } 320 | 321 | private byte[] ComputeExchangeHash(IKexAlgorithm kexAlgorithm, byte[] hostKeyAndCerts, byte[] clientExchangeValue, byte[] serverExchangeValue, byte[] sharedSecret) 322 | { 323 | // H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K) 324 | using (ByteWriter writer = new ByteWriter()) 325 | { 326 | writer.WriteString(m_ProtocolVersionExchange); 327 | writer.WriteString(Server.ProtocolVersionExchange); 328 | 329 | writer.WriteBytes(m_KexInitClientToServer.GetBytes()); 330 | writer.WriteBytes(m_KexInitServerToClient.GetBytes()); 331 | writer.WriteBytes(hostKeyAndCerts); 332 | 333 | writer.WriteMPInt(clientExchangeValue); 334 | writer.WriteMPInt(serverExchangeValue); 335 | writer.WriteMPInt(sharedSecret); 336 | 337 | return kexAlgorithm.ComputeHash(writer.ToByteArray()); 338 | } 339 | } 340 | 341 | private byte[] ComputeEncryptionKey(IKexAlgorithm kexAlgorithm, byte[] exchangeHash, uint keySize, byte[] sharedSecret, char letter) 342 | { 343 | // K(X) = HASH(K || H || X || session_id) 344 | 345 | // Prepare the buffer 346 | byte[] keyBuffer = new byte[keySize]; 347 | int keyBufferIndex = 0; 348 | int currentHashLength = 0; 349 | byte[] currentHash = null; 350 | 351 | // We can stop once we fill the key buffer 352 | while (keyBufferIndex < keySize) 353 | { 354 | using (ByteWriter writer = new ByteWriter()) 355 | { 356 | // Write "K" 357 | writer.WriteMPInt(sharedSecret); 358 | 359 | // Write "H" 360 | writer.WriteRawBytes(exchangeHash); 361 | 362 | if (currentHash == null) 363 | { 364 | // If we haven't done this yet, add the "X" and session_id 365 | writer.WriteByte((byte)letter); 366 | writer.WriteRawBytes(m_SessionId); 367 | } 368 | else 369 | { 370 | // If the key isn't long enough after the first pass, we need to 371 | // write the current hash as described here: 372 | // K1 = HASH(K || H || X || session_id) (X is e.g., "A") 373 | // K2 = HASH(K || H || K1) 374 | // K3 = HASH(K || H || K1 || K2) 375 | // ... 376 | // key = K1 || K2 || K3 || ... 377 | writer.WriteRawBytes(currentHash); 378 | } 379 | 380 | currentHash = kexAlgorithm.ComputeHash(writer.ToByteArray()); 381 | } 382 | 383 | currentHashLength = Math.Min(currentHash.Length, (int)(keySize - keyBufferIndex)); 384 | Array.Copy(currentHash, 0, keyBuffer, keyBufferIndex, currentHashLength); 385 | 386 | keyBufferIndex += currentHashLength; 387 | } 388 | 389 | return keyBuffer; 390 | } 391 | 392 | private void Send(string message) 393 | { 394 | m_Logger.LogDebug($"Sending raw string: {message.Trim()}"); 395 | Send(Encoding.UTF8.GetBytes(message)); 396 | } 397 | 398 | private void Send(byte[] data) 399 | { 400 | if (!IsConnected) 401 | return; 402 | 403 | // Increase bytes transferred 404 | m_TotalBytesTransferred += data.Length; 405 | 406 | m_Socket.Send(data); 407 | } 408 | 409 | public void Send(Packet packet) 410 | { 411 | packet.PacketSequence = GetSentPacketNumber(); 412 | 413 | byte[] payload = m_ActiveExchangeContext.CompressionServerToClient.Compress(packet.GetBytes()); 414 | 415 | uint blockSize = m_ActiveExchangeContext.CipherServerToClient.BlockSize; 416 | 417 | byte paddingLength = (byte)(blockSize - (payload.Length + 5) % blockSize); 418 | if (paddingLength < 4) 419 | paddingLength += (byte)blockSize; 420 | 421 | byte[] padding = new byte[paddingLength]; 422 | RandomNumberGenerator.Create().GetBytes(padding); 423 | 424 | uint packetLength = (uint)(payload.Length + paddingLength + 1); 425 | 426 | using (ByteWriter writer = new ByteWriter()) 427 | { 428 | writer.WriteUInt32(packetLength); 429 | writer.WriteByte(paddingLength); 430 | writer.WriteRawBytes(payload); 431 | writer.WriteRawBytes(padding); 432 | 433 | payload = writer.ToByteArray(); 434 | } 435 | 436 | byte[] encryptedPayload = m_ActiveExchangeContext.CipherServerToClient.Encrypt(payload); 437 | if (m_ActiveExchangeContext.MACAlgorithmServerToClient != null) 438 | { 439 | byte[] mac = m_ActiveExchangeContext.MACAlgorithmServerToClient.ComputeHash(packet.PacketSequence, payload); 440 | encryptedPayload = encryptedPayload.Concat(mac).ToArray(); 441 | } 442 | 443 | Send(encryptedPayload); 444 | this.ConsiderReExchange(); 445 | } 446 | 447 | private uint GetSentPacketNumber() 448 | { 449 | return (uint)Interlocked.Increment(ref m_CurrentSentPacketNumber); 450 | } 451 | 452 | private uint GetReceivedPacketNumber() 453 | { 454 | return (uint)Interlocked.Increment(ref m_CurrentReceivedPacketNumber); 455 | } 456 | 457 | // Read 1 byte from the socket until we find "\r\n" 458 | private void ReadProtocolVersionExchange() 459 | { 460 | NetworkStream stream = new NetworkStream(m_Socket, false); 461 | string result = null; 462 | 463 | List data = new List(); 464 | 465 | bool foundCR = false; 466 | int value = stream.ReadByte(); 467 | while (value != -1) 468 | { 469 | if (foundCR && (value == '\n')) 470 | { 471 | // DONE 472 | result = Encoding.UTF8.GetString(data.ToArray()); 473 | m_HasCompletedProtocolVersionExchange = true; 474 | break; 475 | } 476 | 477 | if (value == '\r') 478 | foundCR = true; 479 | else 480 | { 481 | foundCR = false; 482 | data.Add((byte)value); 483 | } 484 | 485 | value = stream.ReadByte(); 486 | } 487 | 488 | m_ProtocolVersionExchange += result; 489 | } 490 | 491 | public Packet ReadPacket() 492 | { 493 | if (m_Socket == null) 494 | return null; 495 | 496 | uint blockSize = m_ActiveExchangeContext.CipherClientToServer.BlockSize; 497 | 498 | // We must have at least 1 block to read 499 | if (m_Socket.Available < blockSize) 500 | return null; // Packet not here 501 | 502 | byte[] firstBlock = new byte[blockSize]; 503 | int bytesRead = m_Socket.Receive(firstBlock); 504 | if (bytesRead != blockSize) 505 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, "Failed to read from socket."); 506 | 507 | firstBlock = m_ActiveExchangeContext.CipherClientToServer.Decrypt(firstBlock); 508 | 509 | uint packetLength = 0; 510 | byte paddingLength = 0; 511 | using (ByteReader reader = new ByteReader(firstBlock)) 512 | { 513 | // uint32 packet_length 514 | // packet_length 515 | // The length of the packet in bytes, not including 'mac' or the 516 | // 'packet_length' field itself. 517 | packetLength = reader.GetUInt32(); 518 | if (packetLength > Packet.MaxPacketSize) 519 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_PROTOCOL_ERROR, $"Client tried to send a packet bigger than MaxPacketSize ({Packet.MaxPacketSize} bytes): {packetLength} bytes"); 520 | 521 | // byte padding_length 522 | // padding_length 523 | // Length of 'random padding' (bytes). 524 | paddingLength = reader.GetByte(); 525 | } 526 | 527 | // byte[n1] payload; n1 = packet_length - padding_length - 1 528 | // payload 529 | // The useful contents of the packet. If compression has been 530 | // negotiated, this field is compressed. Initially, compression 531 | // MUST be "none". 532 | uint bytesToRead = packetLength - blockSize + 4; 533 | 534 | byte[] restOfPacket = new byte[bytesToRead]; 535 | bytesRead = m_Socket.Receive(restOfPacket); 536 | if (bytesRead != bytesToRead) 537 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, "Failed to read from socket."); 538 | 539 | restOfPacket = m_ActiveExchangeContext.CipherClientToServer.Decrypt(restOfPacket); 540 | 541 | uint payloadLength = packetLength - paddingLength - 1; 542 | byte[] fullPacket = firstBlock.Concat(restOfPacket).ToArray(); 543 | 544 | // Track total bytes read 545 | m_TotalBytesTransferred += fullPacket.Length; 546 | 547 | byte[] payload = fullPacket.Skip(Packet.PacketHeaderSize).Take((int)(packetLength - paddingLength - 1)).ToArray(); 548 | 549 | // byte[n2] random padding; n2 = padding_length 550 | // random padding 551 | // Arbitrary-length padding, such that the total length of 552 | // (packet_length || padding_length || payload || random padding) 553 | // is a multiple of the cipher block size or 8, whichever is 554 | // larger. There MUST be at least four bytes of padding. The 555 | // padding SHOULD consist of random bytes. The maximum amount of 556 | // padding is 255 bytes. 557 | 558 | // byte[m] mac (Message Authentication Code - MAC); m = mac_length 559 | // mac 560 | // Message Authentication Code. If message authentication has 561 | // been negotiated, this field contains the MAC bytes. Initially, 562 | // the MAC algorithm MUST be "none". 563 | 564 | uint packetNumber = GetReceivedPacketNumber(); 565 | if (m_ActiveExchangeContext.MACAlgorithmClientToServer != null) 566 | { 567 | byte[] clientMac = new byte[m_ActiveExchangeContext.MACAlgorithmClientToServer.DigestLength]; 568 | bytesRead = m_Socket.Receive(clientMac); 569 | if (bytesRead != m_ActiveExchangeContext.MACAlgorithmClientToServer.DigestLength) 570 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_CONNECTION_LOST, "Failed to read from socket."); 571 | 572 | var mac = m_ActiveExchangeContext.MACAlgorithmClientToServer.ComputeHash(packetNumber, fullPacket); 573 | if (!clientMac.SequenceEqual(mac)) 574 | { 575 | throw new SSHServerException(DisconnectReason.SSH_DISCONNECT_MAC_ERROR, "MAC from client is invalid"); 576 | } 577 | } 578 | 579 | payload = m_ActiveExchangeContext.CompressionClientToServer.Decompress(payload); 580 | 581 | using (ByteReader packetReader = new ByteReader(payload)) 582 | { 583 | PacketType type = (PacketType)packetReader.GetByte(); 584 | 585 | if (Packet.PacketTypes.ContainsKey(type)) 586 | { 587 | 588 | Packet packet = Activator.CreateInstance(Packet.PacketTypes[type]) as Packet; 589 | packet.Load(packetReader); 590 | packet.PacketSequence = packetNumber; 591 | return packet; 592 | } 593 | 594 | m_Logger.LogWarning($"Unimplemented packet type: {type}"); 595 | 596 | Unimplemented unimplemented = new Unimplemented() 597 | { 598 | RejectedPacketNumber = packetNumber 599 | }; 600 | Send(unimplemented); 601 | } 602 | 603 | return null; 604 | } 605 | 606 | private void ConsiderReExchange() 607 | { 608 | const long OneGB = (1024 * 1024 * 1024); 609 | if ((m_TotalBytesTransferred > OneGB) || (m_KeyTimeout < DateTime.UtcNow)) 610 | { 611 | // Time to get new keys! 612 | m_TotalBytesTransferred = 0; 613 | m_KeyTimeout = DateTime.UtcNow.AddHours(1); 614 | 615 | m_Logger.LogDebug("Trigger re-exchange from server"); 616 | m_PendingExchangeContext = new ExchangeContext(); 617 | Send(m_KexInitServerToClient); 618 | } 619 | } 620 | 621 | private void ValidateProtocolVersionExchange() 622 | { 623 | // https://tools.ietf.org/html/rfc4253#section-4.2 624 | //SSH-protoversion-softwareversion SP comments 625 | 626 | string[] pveParts = m_ProtocolVersionExchange.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); 627 | if (pveParts.Length == 0) 628 | throw new UnauthorizedAccessException("Invalid Protocol Version Exchange was received - No Data"); 629 | 630 | string[] versionParts = pveParts[0].Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries); 631 | if (versionParts.Length < 3) 632 | throw new UnauthorizedAccessException($"Invalid Protocol Version Exchange was received - Not enough dashes - {pveParts[0]}"); 633 | 634 | if (versionParts[1] != "2.0") 635 | throw new UnauthorizedAccessException($"Invalid Protocol Version Exchange was received - Unsupported Version - {versionParts[1]}"); 636 | 637 | // If we get here, all is well! 638 | } 639 | } 640 | } 641 | --------------------------------------------------------------------------------