diff --git a/lom/src/copyID.ts b/lom/src/copyID.ts new file mode 100644 index 0000000..1b992b5 --- /dev/null +++ b/lom/src/copyID.ts @@ -0,0 +1,48 @@ +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(); +} diff --git a/lom/src/index.ts b/lom/src/index.ts index 82361e1..b4f0f45 100644 --- a/lom/src/index.ts +++ b/lom/src/index.ts @@ -1,5 +1,6 @@ import { readFile, writeFile, mkdir } from "node:fs/promises"; import { format } from "node:util"; +import { timingSafeEqual } from "node:crypto"; import parseArgsStringToArgv from "string-argv"; import baseAxios from "axios"; @@ -7,8 +8,23 @@ 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; +}[]; + +function checkValue(input: Buffer, allowed: Buffer): boolean { + const autoReject = input.length !== allowed.length; + if (autoReject) allowed = input; + const isMatch = timingSafeEqual(input, allowed); + return !autoReject && isMatch; +} + +let serverKeyFile: Buffer | string | undefined; +let clientKeys: ClientKeys = []; const serverBaseURL: string = process.env.SERVER_BASE_URL ?? "http://127.0.0.1:3000/"; @@ -19,9 +35,17 @@ 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 +56,13 @@ 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.", }); @@ -58,7 +82,7 @@ server.on("connection", client => { }); if (response.status == 403) { - return auth.reject(["password"]); + return auth.reject(["password", "publickey"]); } token = response.data.token; @@ -68,8 +92,49 @@ 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 && + checkValue(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"]); } @@ -82,6 +147,13 @@ 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());