From 87628d765c87d436119c6832ec521f38bae7ba1d Mon Sep 17 00:00:00 2001 From: Manaf941 Date: Tue, 23 Jul 2024 00:24:46 +0200 Subject: [PATCH] feat: Add GLOBAL_REQUEST, DEBUG, IGNORE, aes256-ctr, aes192-ctr --- src/Client.ts | 92 ++++++++++++++++++++++--- src/algorithms.ts | 16 +++-- src/algorithms/encryption/aes128-ctr.ts | 21 +----- src/algorithms/encryption/aes192-ctr.ts | 17 +++++ src/algorithms/encryption/aes256-ctr.ts | 17 +++++ src/algorithms/encryption/aesN-ctr.ts | 27 ++++++++ src/constants.ts | 2 + src/index.ts | 5 +- src/packet.ts | 17 +++++ src/packets/Debug.ts | 60 ++++++++++++++++ src/packets/GlobalRequest.ts | 56 +++++++++++++++ src/packets/Ignore.ts | 42 +++++++++++ src/packets/RequestFailure.ts | 33 +++++++++ src/packets/RequestSuccess.ts | 35 ++++++++++ 14 files changed, 404 insertions(+), 36 deletions(-) create mode 100644 src/algorithms/encryption/aes192-ctr.ts create mode 100644 src/algorithms/encryption/aes256-ctr.ts create mode 100644 src/algorithms/encryption/aesN-ctr.ts create mode 100644 src/packets/Debug.ts create mode 100644 src/packets/GlobalRequest.ts create mode 100644 src/packets/Ignore.ts create mode 100644 src/packets/RequestFailure.ts create mode 100644 src/packets/RequestSuccess.ts diff --git a/src/Client.ts b/src/Client.ts index a8b0696..4e310b0 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -35,6 +35,10 @@ import ServiceRequest from "./packets/ServiceRequest.js" import ServiceAccept from "./packets/ServiceAccept.js" import Agent from "./publickey/Agent.js" import NoneAgent from "./publickey/NoneAgent.js" +import GlobalRequest from "./packets/GlobalRequest.js" +import RequestFailure from "./packets/RequestFailure.js" +import Debug from "./packets/Debug.js" +import { readNextBuffer } from "./utils/Buffer.js" export interface ClientOptions { hostname: string @@ -156,6 +160,7 @@ export default class Client extends (EventEmitter as new () => TypedEmitter TypedEmitter { + if (!(packet instanceof GlobalRequest)) return + + this.debug(`Received global request packet:`, packet) + + switch (packet.data.request_name) { + case "hostkeys-00@openssh.com": { + const hostkeys = [] + let raw = packet.data.args + while (raw.length != 0) { + let arg: Buffer + ;[arg, raw] = readNextBuffer(raw) + + try { + hostkeys.push(PublicKey.parse(arg)) + } catch (err) { + // unsupported host key algorithm + // or parse error + // either way don't care and silently fail. + this.debug(`Error while trying to parse host key:`, err) + } + } + + this.debug(`Received ${hostkeys.length} valid host keys`) + + // Do we care ? + // at this point, most usage will be + // from people ignoring host keys + // TODO: need to implement verifying host keys + + // https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD + // section 2.5 (ctrl + f search for "hostkeys-00@openssh.com") + break + } + default: { + this.debug(`Unknown global request name: ${packet.data.request_name}`) + if (packet.data.want_reply) { + // this might be a keep alive lol + // shitty spec + // either way, send a failure response. + this.sendPacket(new RequestFailure({})) + } + } + } + }) } waitEvent( @@ -622,33 +677,48 @@ export default class Client extends (EventEmitter as new () => TypedEmitter 0) { diff --git a/src/algorithms.ts b/src/algorithms.ts index 3343f20..a6889d6 100644 --- a/src/algorithms.ts +++ b/src/algorithms.ts @@ -10,6 +10,8 @@ import DiffieHellmanGroup15SHA512 from "./algorithms/kex/diffie-hellman-group15- import DiffieHellmanGroup17SHA512 from "./algorithms/kex/diffie-hellman-group17-sha512.js" import AES128CTR from "./algorithms/encryption/aes128-ctr.js" +import AES192CTR from "./algorithms/encryption/aes192-ctr.js" +import AES256CTR from "./algorithms/encryption/aes256-ctr.js" //import HMACSHA2256 from "./algorithms/mac/hmac-sha2-256.js"; import HMACSHA1 from "./algorithms/mac/hmac-sha1.js" @@ -24,12 +26,10 @@ export abstract class KexAlgorithm { static requires_encryption: boolean static requires_signature: boolean - constructor() { throw new Error("Not implemented") } - static instantiate(): KexAlgorithm { throw new Error("Not implemented") } @@ -80,6 +80,8 @@ export abstract class EncryptionAlgorithm { } } export const encryption_algorithms = new Map([ + ["aes256-ctr", AES256CTR], + ["aes192-ctr", AES192CTR], ["aes128-ctr", AES128CTR], ]) @@ -189,7 +191,11 @@ export function chooseAlgorithms(client: Client | ServerClient) { assert(client.hostKeyAlgorithm, "No host key algorithm found") } - for (const alg of client.clientKexInit.data.encryption_algorithms_client_to_server) { + // TODO: Figure out why this needs a reverse + // I will rewrite this to be cleaner later. + for (const alg of [ + ...client.clientKexInit.data.encryption_algorithms_client_to_server, + ].reverse()) { if (!client.serverKexInit.data.encryption_algorithms_client_to_server.includes(alg)) { continue } @@ -200,7 +206,9 @@ export function chooseAlgorithms(client: Client | ServerClient) { client.clientEncryptionAlgorithm = algorithm } assert(client.clientEncryptionAlgorithm, "No client to server encryption algorithm found") - for (const alg of client.clientKexInit.data.encryption_algorithms_server_to_client) { + for (const alg of [ + ...client.clientKexInit.data.encryption_algorithms_server_to_client, + ].reverse()) { if (!client.serverKexInit.data.encryption_algorithms_server_to_client.includes(alg)) { continue } diff --git a/src/algorithms/encryption/aes128-ctr.ts b/src/algorithms/encryption/aes128-ctr.ts index b5d7b81..8595f7b 100644 --- a/src/algorithms/encryption/aes128-ctr.ts +++ b/src/algorithms/encryption/aes128-ctr.ts @@ -1,7 +1,7 @@ -import crypto from "crypto" import { EncryptionAlgorithm } from "../../algorithms.js" +import AESNCTR from "./aesN-ctr.js" -export default class AES128CTR implements EncryptionAlgorithm { +export default class AES128CTR extends AESNCTR { static alg_name = "aes128-ctr" static key_length = 16 static iv_length = 16 @@ -11,22 +11,7 @@ export default class AES128CTR implements EncryptionAlgorithm { return new AES128CTR(key, iv) } - key: Buffer - iv: Buffer - encrypt_instance: crypto.Cipher - decrypt_instance: crypto.Cipher constructor(key: Buffer, iv: Buffer) { - this.key = key - this.iv = iv - this.encrypt_instance = crypto.createCipheriv("aes-128-ctr", this.key, this.iv) - this.decrypt_instance = crypto.createDecipheriv("aes-128-ctr", this.key, this.iv) - } - - encrypt(plaintext: Buffer): Buffer { - return this.encrypt_instance.update(plaintext) - } - - decrypt(ciphertext: Buffer): Buffer { - return this.decrypt_instance.update(ciphertext) + super("aes-128-ctr", key, iv) } } diff --git a/src/algorithms/encryption/aes192-ctr.ts b/src/algorithms/encryption/aes192-ctr.ts new file mode 100644 index 0000000..9469488 --- /dev/null +++ b/src/algorithms/encryption/aes192-ctr.ts @@ -0,0 +1,17 @@ +import { EncryptionAlgorithm } from "../../algorithms.js" +import AESNCTR from "./aesN-ctr.js" + +export default class AES192CTR extends AESNCTR { + static alg_name = "aes192-ctr" + static key_length = 24 + static iv_length = 16 + static block_size = 16 + + static instantiate(key: Buffer, iv: Buffer): EncryptionAlgorithm { + return new AES192CTR(key, iv) + } + + constructor(key: Buffer, iv: Buffer) { + super("aes-192-ctr", key, iv) + } +} diff --git a/src/algorithms/encryption/aes256-ctr.ts b/src/algorithms/encryption/aes256-ctr.ts new file mode 100644 index 0000000..f8d301c --- /dev/null +++ b/src/algorithms/encryption/aes256-ctr.ts @@ -0,0 +1,17 @@ +import { EncryptionAlgorithm } from "../../algorithms.js" +import AESNCTR from "./aesN-ctr.js" + +export default class AES256CTR extends AESNCTR { + static alg_name = "aes256-ctr" + static key_length = 32 + static iv_length = 16 + static block_size = 16 + + static instantiate(key: Buffer, iv: Buffer): EncryptionAlgorithm { + return new AES256CTR(key, iv) + } + + constructor(key: Buffer, iv: Buffer) { + super("aes-256-ctr", key, iv) + } +} diff --git a/src/algorithms/encryption/aesN-ctr.ts b/src/algorithms/encryption/aesN-ctr.ts new file mode 100644 index 0000000..4efb229 --- /dev/null +++ b/src/algorithms/encryption/aesN-ctr.ts @@ -0,0 +1,27 @@ +import crypto from "crypto" +import { EncryptionAlgorithm } from "../../algorithms.js" + +export default class AESNCTR implements EncryptionAlgorithm { + static key_length: number + static iv_length: number + static block_size: number + + key: Buffer + iv: Buffer + encrypt_instance: crypto.Cipher + decrypt_instance: crypto.Cipher + constructor(algorithm: string, key: Buffer, iv: Buffer) { + this.key = key + this.iv = iv + this.encrypt_instance = crypto.createCipheriv(algorithm, this.key, this.iv) + this.decrypt_instance = crypto.createDecipheriv(algorithm, this.key, this.iv) + } + + encrypt(plaintext: Buffer): Buffer { + return this.encrypt_instance.update(plaintext) + } + + decrypt(ciphertext: Buffer): Buffer { + return this.decrypt_instance.update(ciphertext) + } +} diff --git a/src/constants.ts b/src/constants.ts index ef7b312..6befdee 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,6 +24,8 @@ export enum SSHPacketType { SSH_MSG_USERAUTH_REQUEST = 50, SSH_MSG_USERAUTH_FAILURE = 51, SSH_MSG_USERAUTH_SUCCESS = 52, + // TODO: Support SSH_MSG_USERAUTH_BANNER + // Currently, if a server sends it, the connection will crash. SSH_MSG_USERAUTH_BANNER = 53, // This is messed up in the spec diff --git a/src/index.ts b/src/index.ts index 71a9681..571504c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,10 +3,9 @@ import Client from "./Client.js" import DiskAgent from "./publickey/DiskAgent.js" const client = new Client({ - hostname: "127.0.0.1", - port: 1022, + hostname: "VPS1", + port: 22, username: "debian", - password: "debian", agent: new DiskAgent(), }) client.on("debug", (...args) => console.debug(...args)) diff --git a/src/packet.ts b/src/packet.ts index a770709..947418f 100644 --- a/src/packet.ts +++ b/src/packet.ts @@ -1,9 +1,14 @@ import { SSHPacketType } from "./constants.js" +import Debug from "./packets/Debug.js" import Disconnect from "./packets/Disconnect.js" +import GlobalRequest from "./packets/GlobalRequest.js" +import Ignore from "./packets/Ignore.js" import KexDHInit from "./packets/KexDHInit.js" import KexDHReply from "./packets/KexDHReply.js" import KexInit from "./packets/KexInit.js" import NewKeys from "./packets/NewKeys.js" +import RequestFailure from "./packets/RequestFailure.js" +import RequestSuccess from "./packets/RequestSuccess.js" import ServiceAccept from "./packets/ServiceAccept.js" import ServiceRequest from "./packets/ServiceRequest.js" import Unimplemented from "./packets/Unimplemented.js" @@ -34,7 +39,9 @@ export default abstract class Packet { export const packets = new Map([ [SSHPacketType.SSH_MSG_DISCONNECT, Disconnect], + [SSHPacketType.SSH_MSG_IGNORE, Ignore], [SSHPacketType.SSH_MSG_UNIMPLEMENTED, Unimplemented], + [SSHPacketType.SSH_MSG_DEBUG, Debug], [SSHPacketType.SSH_MSG_SERVICE_REQUEST, ServiceRequest], [SSHPacketType.SSH_MSG_SERVICE_ACCEPT, ServiceAccept], @@ -49,10 +56,16 @@ export const packets = new Map([ [SSHPacketType.SSH_MSG_USERAUTH_SUCCESS, UserAuthSuccess], [SSHPacketType.SSH_MSG_USERAUTH_PK_OK, UserAuthPKOK], + + [SSHPacketType.SSH_MSG_GLOBAL_REQUEST, GlobalRequest], + [SSHPacketType.SSH_MSG_REQUEST_FAILURE, RequestFailure], + [SSHPacketType.SSH_MSG_REQUEST_SUCCESS, RequestSuccess], ]) export interface PacketTypes { [SSHPacketType.SSH_MSG_DISCONNECT]: Disconnect + [SSHPacketType.SSH_MSG_IGNORE]: Ignore [SSHPacketType.SSH_MSG_UNIMPLEMENTED]: Unimplemented + [SSHPacketType.SSH_MSG_DEBUG]: Debug [SSHPacketType.SSH_MSG_SERVICE_REQUEST]: ServiceRequest [SSHPacketType.SSH_MSG_SERVICE_ACCEPT]: ServiceAccept @@ -67,4 +80,8 @@ export interface PacketTypes { [SSHPacketType.SSH_MSG_USERAUTH_SUCCESS]: UserAuthSuccess [SSHPacketType.SSH_MSG_USERAUTH_PK_OK]: UserAuthPKOK + + [SSHPacketType.SSH_MSG_GLOBAL_REQUEST]: GlobalRequest + [SSHPacketType.SSH_MSG_REQUEST_FAILURE]: RequestFailure + [SSHPacketType.SSH_MSG_REQUEST_SUCCESS]: RequestSuccess } diff --git a/src/packets/Debug.ts b/src/packets/Debug.ts new file mode 100644 index 0000000..5adcb10 --- /dev/null +++ b/src/packets/Debug.ts @@ -0,0 +1,60 @@ +import assert from "assert" +import { SSHPacketType } from "../constants.js" +import Packet from "../packet.js" +import { + readNextBinaryBoolean, + readNextBuffer, + readNextUint8, + serializeBuffer, +} from "../utils/Buffer.js" +import { serializeBinaryBoolean } from "../utils/BinaryBoolean.js" + +export interface DebugData { + always_display: boolean + message: string + language_tag: string +} + +export default class Debug implements Packet { + static type = SSHPacketType.SSH_MSG_DEBUG + + data: DebugData + constructor(data: DebugData) { + this.data = data + } + + serialize(): Buffer { + const buffers = [] + + buffers.push(Buffer.from([Debug.type])) + + buffers.push(serializeBinaryBoolean(this.data.always_display)) + buffers.push(serializeBuffer(Buffer.from(this.data.message, "utf8"))) + buffers.push(serializeBuffer(Buffer.from(this.data.language_tag, "utf8"))) + + return Buffer.concat(buffers) + } + + static parse(raw: Buffer): Debug { + let packetType: number + ;[packetType, raw] = readNextUint8(raw) + assert(packetType === Debug.type) + + let always_display: boolean + ;[always_display, raw] = readNextBinaryBoolean(raw) + + let message: Buffer + ;[message, raw] = readNextBuffer(raw) + + let language_tag: Buffer + ;[language_tag, raw] = readNextBuffer(raw) + + assert(raw.length === 0) + + return new Debug({ + always_display: always_display, + message: message.toString("utf8"), + language_tag: language_tag.toString("utf8"), + }) + } +} diff --git a/src/packets/GlobalRequest.ts b/src/packets/GlobalRequest.ts new file mode 100644 index 0000000..b92bc80 --- /dev/null +++ b/src/packets/GlobalRequest.ts @@ -0,0 +1,56 @@ +import assert from "assert" +import { SSHPacketType } from "../constants.js" +import Packet from "../packet.js" +import { + readNextBinaryBoolean, + readNextBuffer, + readNextUint8, + serializeBuffer, +} from "../utils/Buffer.js" +import { serializeBinaryBoolean } from "../utils/BinaryBoolean.js" + +export interface GlobalRequestData { + request_name: string + want_reply: boolean + args: Buffer +} + +export default class GlobalRequest implements Packet { + static type = SSHPacketType.SSH_MSG_GLOBAL_REQUEST + + data: GlobalRequestData + constructor(data: GlobalRequestData) { + this.data = data + } + + serialize(): Buffer { + const buffers = [] + + buffers.push(Buffer.from([GlobalRequest.type])) + + buffers.push(serializeBuffer(Buffer.from(this.data.request_name, "ascii"))) + buffers.push(serializeBinaryBoolean(this.data.want_reply)) + + buffers.push(serializeBuffer(this.data.args)) + + return Buffer.concat(buffers) + } + + static parse(raw: Buffer): GlobalRequest { + let packetType: number + ;[packetType, raw] = readNextUint8(raw) + assert(packetType === GlobalRequest.type) + + let request_name: Buffer + ;[request_name, raw] = readNextBuffer(raw) + + let want_reply: boolean + ;[want_reply, raw] = readNextBinaryBoolean(raw) + + return new GlobalRequest({ + request_name: request_name.toString("ascii"), + want_reply: want_reply, + args: raw, + }) + } +} diff --git a/src/packets/Ignore.ts b/src/packets/Ignore.ts new file mode 100644 index 0000000..47be3f5 --- /dev/null +++ b/src/packets/Ignore.ts @@ -0,0 +1,42 @@ +import assert from "assert" +import { SSHPacketType } from "../constants.js" +import Packet from "../packet.js" +import { readNextBuffer, readNextUint8, serializeBuffer } from "../utils/Buffer.js" + +export interface IgnoreData { + data: Buffer +} + +export default class Ignore implements Packet { + static type = SSHPacketType.SSH_MSG_IGNORE + + data: IgnoreData + constructor(data: IgnoreData) { + this.data = data + } + + serialize(): Buffer { + const buffers = [] + + buffers.push(Buffer.from([Ignore.type])) + + buffers.push(serializeBuffer(this.data.data)) + + return Buffer.concat(buffers) + } + + static parse(raw: Buffer): Ignore { + let packetType: number + ;[packetType, raw] = readNextUint8(raw) + assert(packetType === Ignore.type) + + let data: Buffer + ;[data, raw] = readNextBuffer(raw) + + assert(raw.length === 0) + + return new Ignore({ + data: data, + }) + } +} diff --git a/src/packets/RequestFailure.ts b/src/packets/RequestFailure.ts new file mode 100644 index 0000000..a8d6223 --- /dev/null +++ b/src/packets/RequestFailure.ts @@ -0,0 +1,33 @@ +import assert from "assert" +import { SSHPacketType } from "../constants.js" +import Packet from "../packet.js" +import { readNextUint8 } from "../utils/Buffer.js" + +export interface RequestFailureData {} + +export default class RequestFailure implements Packet { + static type = SSHPacketType.SSH_MSG_REQUEST_FAILURE + + data: RequestFailureData + constructor(data: RequestFailureData) { + this.data = data + } + + serialize(): Buffer { + const buffers = [] + + buffers.push(Buffer.from([RequestFailure.type])) + + return Buffer.concat(buffers) + } + + static parse(raw: Buffer): RequestFailure { + let packetType: number + ;[packetType, raw] = readNextUint8(raw) + assert(packetType === RequestFailure.type) + + assert(raw.length === 0) + + return new RequestFailure({}) + } +} diff --git a/src/packets/RequestSuccess.ts b/src/packets/RequestSuccess.ts new file mode 100644 index 0000000..668247e --- /dev/null +++ b/src/packets/RequestSuccess.ts @@ -0,0 +1,35 @@ +import assert from "assert" +import { SSHPacketType } from "../constants.js" +import Packet from "../packet.js" +import { readNextUint8 } from "../utils/Buffer.js" + +// TODO: Request success might hold data, depending on the request. +// need to impl this. +export interface RequestSuccessData {} + +export default class RequestSuccess implements Packet { + static type = SSHPacketType.SSH_MSG_REQUEST_SUCCESS + + data: RequestSuccessData + constructor(data: RequestSuccessData) { + this.data = data + } + + serialize(): Buffer { + const buffers = [] + + buffers.push(Buffer.from([RequestSuccess.type])) + + return Buffer.concat(buffers) + } + + static parse(raw: Buffer): RequestSuccess { + let packetType: number + ;[packetType, raw] = readNextUint8(raw) + assert(packetType === RequestSuccess.type) + + assert(raw.length === 0) + + return new RequestSuccess({}) + } +}