From 26f5d4ecc63f26794c3cdb440abff49088545ca4 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 21 Mar 2026 00:10:47 -0500 Subject: [PATCH] fix android shit + fix clients not showing if 2 clients have the same username and pc name as another client --- .../cmd/agent/config/identity_android.go | 33 +++++++++- Overlord-Server/src/db.ts | 5 ++ Overlord-Server/src/purgatory.test.ts | 64 +++++++++++++++++++ .../routes/websocket-lifecycle-routes.ts | 8 ++- 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/Overlord-Client/cmd/agent/config/identity_android.go b/Overlord-Client/cmd/agent/config/identity_android.go index e23374b..693fa90 100644 --- a/Overlord-Client/cmd/agent/config/identity_android.go +++ b/Overlord-Client/cmd/agent/config/identity_android.go @@ -2,6 +2,37 @@ package config +import ( + "crypto/rand" + "encoding/hex" + "log" + "os" + "strings" +) + +const androidMachineIDFile = "config/android_machine_id" + func platformMachineID() string { - return "" + data, err := os.ReadFile(androidMachineIDFile) + if err == nil { + if id := strings.TrimSpace(string(data)); id != "" { + return id + } + } + + b := make([]byte, 16) + if _, err := rand.Read(b); err != nil { + log.Printf("[identity] WARNING: failed to generate android machine ID: %v", err) + return "" + } + id := hex.EncodeToString(b) + + if err := os.MkdirAll("config", 0700); err != nil { + log.Printf("[identity] WARNING: failed to create config dir for android machine ID: %v", err) + return id + } + if err := os.WriteFile(androidMachineIDFile, []byte(id), 0600); err != nil { + log.Printf("[identity] WARNING: failed to persist android machine ID: %v", err) + } + return id } diff --git a/Overlord-Server/src/db.ts b/Overlord-Server/src/db.ts index 4ad090b..9585da2 100644 --- a/Overlord-Server/src/db.ts +++ b/Overlord-Server/src/db.ts @@ -662,6 +662,11 @@ export function clientExists(id: string): boolean { return !!row?.id; } +export function getClientPublicKeyById(id: string): string | null { + const row = db.query<{ public_key: string | null }>(`SELECT public_key FROM clients WHERE id=? LIMIT 1`).get(id); + return row?.public_key ?? null; +} + export function listDistinctCountries(): { code: string; count: number }[] { const rows = db .query<{ code: string; count: number }>( diff --git a/Overlord-Server/src/purgatory.test.ts b/Overlord-Server/src/purgatory.test.ts index 533cbc2..4850986 100644 --- a/Overlord-Server/src/purgatory.test.ts +++ b/Overlord-Server/src/purgatory.test.ts @@ -549,3 +549,67 @@ describe("purgatory — IP banning", () => { expect(truncated).toBe("A".repeat(200)); }); }); + +describe("purgatory — HWID collision prevention", () => { + test("two different public keys produce different key fingerprints", async () => { + const kpA = await generateKeypair(); + const kpB = await generateKeypair(); + const fpA = computeKeyFingerprint(kpA.publicKeyBase64); + const fpB = computeKeyFingerprint(kpB.publicKeyBase64); + expect(fpA).not.toBe(fpB); + // fingerprints are 64-char hex + expect(fpA).toMatch(/^[a-f0-9]{64}$/); + expect(fpB).toMatch(/^[a-f0-9]{64}$/); + }); + + test("key fingerprint is deterministic for same public key", async () => { + const kp = await generateKeypair(); + const fp1 = computeKeyFingerprint(kp.publicKeyBase64); + const fp2 = computeKeyFingerprint(kp.publicKeyBase64); + expect(fp1).toBe(fp2); + }); + + test("collision detection assigns different IDs to same-HWID machines", async () => { + // Simulate: Machine A and Machine B share HWID "colliding-hwid" + // but have different key pairs. After collision detection, + // Machine B should get the key fingerprint as its ID. + const kpA = await generateKeypair(); + const kpB = await generateKeypair(); + const sharedHwid = "colliding-hwid"; + + const fpA = computeKeyFingerprint(kpA.publicKeyBase64); + const fpB = computeKeyFingerprint(kpB.publicKeyBase64); + + // Machine A: lookupClientByPublicKey → null, no existing client at ID → keeps HWID + const wsA = createMockWs({ clientId: sharedHwid }); + // Simulate server logic: no existing key at this ID → keep URL ID + const existingPkAtId: string | null = null; // DB empty + if (existingPkAtId && existingPkAtId !== kpA.publicKeyBase64) { + wsA.data.clientId = fpA; + } + expect(wsA.data.clientId).toBe(sharedHwid); // Machine A keeps HWID + + // Machine B: lookupClientByPublicKey → null, but ID already taken by PK_A + const wsB = createMockWs({ clientId: sharedHwid }); + const existingPkAtIdForB: string | null = kpA.publicKeyBase64; // Machine A's key is stored at this ID + if (existingPkAtIdForB && existingPkAtIdForB !== kpB.publicKeyBase64) { + wsB.data.clientId = fpB; + } + expect(wsB.data.clientId).toBe(fpB); // Machine B gets reassigned + expect(wsB.data.clientId).not.toBe(sharedHwid); + }); + + test("no collision when same machine reconnects with same public key", async () => { + const kp = await generateKeypair(); + const hwid = "same-machine-hwid"; + const fp = computeKeyFingerprint(kp.publicKeyBase64); + + const ws = createMockWs({ clientId: hwid }); + // Same key already at this ID → no collision + const existingPkAtId: string | null = kp.publicKeyBase64; + if (existingPkAtId && existingPkAtId !== kp.publicKeyBase64) { + ws.data.clientId = fp; + } + expect(ws.data.clientId).toBe(hwid); // keeps original ID + }); +}); diff --git a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts index dccc0ea..cccc5bc 100644 --- a/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts +++ b/Overlord-Server/src/server/routes/websocket-lifecycle-routes.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from "uuid"; import geoip from "geoip-lite"; import { logAudit, AuditAction } from "../../auditLog"; import * as clientManager from "../../clientManager"; -import { clientExists, setOnlineState, upsertClientRow, getClientEnrollmentStatus, setClientEnrollmentStatus, lookupClientByPublicKey } from "../../db"; +import { clientExists, setOnlineState, upsertClientRow, getClientEnrollmentStatus, setClientEnrollmentStatus, lookupClientByPublicKey, getClientPublicKeyById } from "../../db"; import { getConfig } from "../../config"; import { logger } from "../../logger"; import { metrics } from "../../metrics"; @@ -251,6 +251,12 @@ export async function handleWebSocketMessage( ws.data.clientId = existing.id; } else { enrollmentStatus = requireApproval ? "pending" : "approved"; + + const existingPk = getClientPublicKeyById(ws.data.clientId); + if (existingPk && existingPk !== publicKey) { + ws.data.clientId = keyFingerprint; + logger.info(`[purgatory] ID collision detected — reassigned to ${keyFingerprint}`); + } } const resolvedId = ws.data.clientId;