From d1da42b7ec8e59c5141843518000913e62d90a90 Mon Sep 17 00:00:00 2001 From: greysoh Date: Thu, 9 May 2024 19:39:16 -0400 Subject: [PATCH] feature: Adds (semi-broken) public key authentication. --- lom/src/copyID.ts | 38 +++++++++++++++++++++++ lom/src/index.ts | 78 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 lom/src/copyID.ts diff --git a/lom/src/copyID.ts b/lom/src/copyID.ts new file mode 100644 index 0000000..e7efc85 --- /dev/null +++ b/lom/src/copyID.ts @@ -0,0 +1,38 @@ +import { writeFile } from "node:fs/promises"; +import ssh2 from "ssh2"; + +import { readFromKeyboard } from "./libs/readFromKeyboard.js"; +import type { ClientKeys } from "./index.js"; + +export async function runCopyID(username: string, password: string, keys: ClientKeys, stream: ssh2.ServerChannel) { + stream.write("Hey there! I think you're using ssh-copy-id. If this is an error, you may close this terminal.\n"); + stream.write("Please wait...\n"); + + const keyData = await readFromKeyboard(stream, true); + stream.write("Parsing key...\n"); + + const parsedKey = ssh2.utils.parseKey(keyData); + + if (parsedKey instanceof Error) { + stream.write(parsedKey.message + "\n"); + return stream.close(); + } + + stream.write("Passed checks. Writing changes...\n"); + + keys.push({ + username, + password, + publicKey: keyData + }); + + try { + await writeFile("../keys/clients.json", JSON.stringify(keys, null, 2)); + } catch (e) { + console.log(e); + return stream.write("ERROR: Failed to save changes! If you're the administrator, view the console for details.\n"); + } + + stream.write("Success!\n"); + return stream.close(); +} \ No newline at end of file diff --git a/lom/src/index.ts b/lom/src/index.ts index da044ad..6d18fe5 100644 --- a/lom/src/index.ts +++ b/lom/src/index.ts @@ -7,8 +7,16 @@ import ssh2 from "ssh2"; import { readFromKeyboard } from "./libs/readFromKeyboard.js"; import { commands } from "./commands.js"; +import { runCopyID } from "./copyID.js"; -let keyFile: Buffer | string | undefined; +export type ClientKeys = { + publicKey: string, + username: string, + password: string, +}[]; + +let serverKeyFile: Buffer | string | undefined; +let clientKeys: ClientKeys = []; const serverBaseURL: string = process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/"; @@ -19,9 +27,15 @@ const axios = baseAxios.create({ }); try { - keyFile = await readFile("../keys/host.key"); + clientKeys = JSON.parse(await readFile("../keys/clients.json", "utf8")); } catch (e) { - console.log("Error reading host key file! Creating new keypair..."); + console.log("INFO: We don't have the client key file."); +} + +try { + serverKeyFile = await readFile("../keys/host.key"); +} catch (e) { + console.log("ERROR: Failed to read the host key file! Creating new keypair..."); await mkdir("../keys").catch(() => null); const keyPair: { private: string; public: string } = await new Promise( @@ -32,13 +46,16 @@ try { await writeFile("../keys/host.key", keyPair.private); await writeFile("../keys/host.pub", keyPair.public); - keyFile = keyPair.private; + serverKeyFile = keyPair.private; } -if (!keyFile) throw new Error("Somehow failed to fetch the key file!"); +if (!serverKeyFile) throw new Error("Somehow failed to fetch the key file!"); const server: ssh2.Server = new ssh2.Server({ - hostKeys: [keyFile], + hostKeys: [ + serverKeyFile + ], + banner: "NextNet-LOM (c) NextNet project et al." }); @@ -56,7 +73,7 @@ server.on("connection", client => { }); if (response.status == 403) { - return auth.reject(["password"]); + return auth.reject(["password", "publickey"]); } token = response.data.token; @@ -66,8 +83,47 @@ server.on("connection", client => { auth.accept(); } else if (auth.method == "publickey") { - return auth.reject(); - // todo + const userData = { + username: "", + password: "" + }; + + for (const rawKey of clientKeys) { + const key = ssh2.utils.parseKey(rawKey.publicKey); + + if (key instanceof Error) { + console.log(key); + continue; + } + + console.log(auth.signature, auth.blob); + + if ( + rawKey.username == auth.username && + auth.key.algo == key.type && + auth.key.data == key.getPublicSSH() && + auth.signature && key.verify(auth.blob as Buffer, auth.signature, auth.key.algo) + ) { + console.log(" -- VERIFIED PUBLIC KEY --"); + userData.username = rawKey.username; + userData.password = rawKey.password; + }; + } + + if (!userData.username || !userData.password) return auth.reject(["password", "publickey"]); + + const response = await axios.post("/api/v1/users/login", userData); + + if (response.status == 403) { + return auth.reject(["password", "publickey"]); + } + + token = response.data.token; + + username = userData.username; + password = userData.password; + + auth.accept(); } else { return auth.reject(["password", "publickey"]); } @@ -80,6 +136,10 @@ server.on("connection", client => { conn.on("exec", async (accept, reject, info) => { const stream = accept(); + if (info.command.includes(".ssh/authorized_keys") && info.command.startsWith("exec sh -c")) { + return await runCopyID(username, password, clientKeys, stream); + } + // Matches on ; and && const commandsRecv = info.command.split(/;|&&/).map((i) => i.trim());