From f32613f3a4ae5e81be0bf78dfdc5f12ee1b7b1e5 Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 22 Apr 2026 04:40:07 +0100 Subject: [PATCH 1/2] Use UUIDv5 for offline players --- mapsync-server/package.json | 1 + mapsync-server/pnpm-lock.yaml | 9 +++++++++ mapsync-server/src/auth.ts | 5 +++++ mapsync-server/src/constants.ts | 6 ++++++ mapsync-server/src/deps/uuid.ts | 13 +++++++++++++ mapsync-server/src/main.ts | 5 +++-- mapsync-server/src/metadata.ts | 8 ++------ 7 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 mapsync-server/src/deps/uuid.ts diff --git a/mapsync-server/package.json b/mapsync-server/package.json index 910b623..48b0f31 100644 --- a/mapsync-server/package.json +++ b/mapsync-server/package.json @@ -19,6 +19,7 @@ "better-sqlite3": "12.8.0", "kysely": "0.28.14", "source-map-support": "0.5.21", + "uuid": "^14.0.0", "ws": "8.20.0", "zod": "3.25.76", "zod-validation-error": "1.5.0" diff --git a/mapsync-server/pnpm-lock.yaml b/mapsync-server/pnpm-lock.yaml index e6b649b..cf4f82f 100644 --- a/mapsync-server/pnpm-lock.yaml +++ b/mapsync-server/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: source-map-support: specifier: 0.5.21 version: 0.5.21 + uuid: + specifier: ^14.0.0 + version: 14.0.0 ws: specifier: 8.20.0 version: 8.20.0(bufferutil@4.1.0) @@ -229,6 +232,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -436,6 +443,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@14.0.0: {} + wrappy@1.0.2: {} ws@8.20.0(bufferutil@4.1.0): diff --git a/mapsync-server/src/auth.ts b/mapsync-server/src/auth.ts index 9ddca02..8d86df2 100644 --- a/mapsync-server/src/auth.ts +++ b/mapsync-server/src/auth.ts @@ -1,3 +1,5 @@ +import { isValidUuid } from "./deps/uuid.ts"; + export abstract class AuthState { protected constructor(public readonly logName: string | null) {} } @@ -20,6 +22,9 @@ export class Welcomed extends AuthState { public readonly uuid: string, public readonly authed: boolean, ) { + if (!isValidUuid(uuid)) { + throw new Error(`Invalid UUID: ${uuid}`); + } super(name + (authed ? "" : "?")); } } diff --git a/mapsync-server/src/constants.ts b/mapsync-server/src/constants.ts index 852aae2..490f4d5 100644 --- a/mapsync-server/src/constants.ts +++ b/mapsync-server/src/constants.ts @@ -1,5 +1,11 @@ +import { nameUuidFromBytes } from "./deps/uuid.ts"; + export const SUPPORTED_VERSIONS = new Set(["2.2.0-SNAPSHOT-1.21.11"]); // SHA1 produces 160-bit (20-byte) hashes // https://en.wikipedia.org/wiki/SHA-1 export const SHA1_HASH_LENGTH = 20; + +export const UUID_NAMESPACE = nameUuidFromBytes( + Buffer.from("mapsync:server", "utf8"), +); diff --git a/mapsync-server/src/deps/uuid.ts b/mapsync-server/src/deps/uuid.ts new file mode 100644 index 0000000..70d8aa1 --- /dev/null +++ b/mapsync-server/src/deps/uuid.ts @@ -0,0 +1,13 @@ +import node_crypto from "crypto"; +import { stringify as parseUuidBytes } from "uuid"; +export { v5 as uuidv5, validate as isValidUuid } from "uuid"; + +// https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/UUID.html#nameUUIDFromBytes(byte[]) +export function nameUuidFromBytes(bytes: Buffer): string { + const hash = node_crypto.createHash("md5").update(bytes).digest(); + hash[6] &= 0x0f; // Clears the version + hash[6] |= 0x30; // Sets version to 3 + hash[8] &= 0x3f; // Clears the variant + hash[8] |= 0x80; // Sets variant to IETF + return parseUuidBytes(hash); +} diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index 8bb7294..f3e2360 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -23,7 +23,8 @@ import { AwaitingIdentityResponse, Welcomed, } from "./auth.ts"; -import { SUPPORTED_VERSIONS } from "./constants.ts"; +import { SUPPORTED_VERSIONS, UUID_NAMESPACE } from "./constants.ts"; +import { uuidv5 } from "./deps/uuid.ts"; let config: metadata.Config = null!; let main: ProtocolHandler = null!; @@ -150,7 +151,7 @@ export class ProtocolHandler { } client.auth = new Welcomed( packet.claimedUsername, - `AUTH-DISABLED-${packet.claimedUsername}`, + uuidv5(`Offline:${packet.claimedUsername}`, UUID_NAMESPACE), false, ); } diff --git a/mapsync-server/src/metadata.ts b/mapsync-server/src/metadata.ts index 5dc5ea4..5b36211 100644 --- a/mapsync-server/src/metadata.ts +++ b/mapsync-server/src/metadata.ts @@ -96,9 +96,7 @@ export function getConfig(): Config { const WHITELIST_FILE = "whitelist.json"; const WHITELIST_MUTEX = new Mutex(); -const WHITELIST_SCHEMA = z.array( - z.union([z.string().uuid(), z.string().regex(/^AUTH-DISABLED-.+/)]), -); +const WHITELIST_SCHEMA = z.array(z.string().uuid()); export const whitelist = new Set(); export async function loadWhitelist() { @@ -130,9 +128,7 @@ export async function saveWhitelist() { const UUID_CACHE_FILE = "uuid_cache.json"; const UUID_CACHE_MUTEX = new Mutex(); -const UUID_CACHE_SCHEMA = z.record( - z.union([z.string().uuid(), z.string().regex(/^AUTH-DISABLED-.+/)]), -); +const UUID_CACHE_SCHEMA = z.record(z.string().uuid()); // IGN UUID const uuid_cache = new Map(); From fd439bc42a13d49673e282a719d524a85966cd1a Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 29 Apr 2026 07:40:09 +0100 Subject: [PATCH 2/2] Create migratory uuid zod type I figured that, given the recent PRs, it would be rude to make their instances start throwing. --- mapsync-server/src/constants.ts | 6 ------ mapsync-server/src/deps/uuid.ts | 13 ++++++++++++- mapsync-server/src/deps/zod.ts | 18 ++++++++++++++++++ mapsync-server/src/main.ts | 6 +++--- mapsync-server/src/metadata.ts | 9 ++++----- 5 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 mapsync-server/src/deps/zod.ts diff --git a/mapsync-server/src/constants.ts b/mapsync-server/src/constants.ts index 490f4d5..852aae2 100644 --- a/mapsync-server/src/constants.ts +++ b/mapsync-server/src/constants.ts @@ -1,11 +1,5 @@ -import { nameUuidFromBytes } from "./deps/uuid.ts"; - export const SUPPORTED_VERSIONS = new Set(["2.2.0-SNAPSHOT-1.21.11"]); // SHA1 produces 160-bit (20-byte) hashes // https://en.wikipedia.org/wiki/SHA-1 export const SHA1_HASH_LENGTH = 20; - -export const UUID_NAMESPACE = nameUuidFromBytes( - Buffer.from("mapsync:server", "utf8"), -); diff --git a/mapsync-server/src/deps/uuid.ts b/mapsync-server/src/deps/uuid.ts index 70d8aa1..ff1eaa9 100644 --- a/mapsync-server/src/deps/uuid.ts +++ b/mapsync-server/src/deps/uuid.ts @@ -1,6 +1,9 @@ import node_crypto from "crypto"; import { stringify as parseUuidBytes } from "uuid"; -export { v5 as uuidv5, validate as isValidUuid } from "uuid"; +import { v5 as _uuidv5 } from "uuid"; +export { validate as isValidUuid } from "uuid"; + +export const uuidv5 = _uuidv5; // https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/UUID.html#nameUUIDFromBytes(byte[]) export function nameUuidFromBytes(bytes: Buffer): string { @@ -11,3 +14,11 @@ export function nameUuidFromBytes(bytes: Buffer): string { hash[8] |= 0x80; // Sets variant to IETF return parseUuidBytes(hash); } + +export const UUID_NAMESPACE = nameUuidFromBytes( + Buffer.from("mapsync:server", "utf8"), +); + +export function createOfflineUuid(name: string) { + return uuidv5(`Offline:${name}`, UUID_NAMESPACE); +} diff --git a/mapsync-server/src/deps/zod.ts b/mapsync-server/src/deps/zod.ts new file mode 100644 index 0000000..203eb87 --- /dev/null +++ b/mapsync-server/src/deps/zod.ts @@ -0,0 +1,18 @@ +export * from "zod"; +export { fromZodError } from "zod-validation-error"; + +import * as z from "zod"; +import { createOfflineUuid } from "./uuid.ts"; + +export function offlineUuid() { + return z.preprocess((val) => { + if (typeof val !== "string") { + return val; + } + const match = /^AUTH-DISABLED-(.+)/.exec(val) ?? null; + if (match === null) { + return val; + } + return createOfflineUuid(match[1]); + }, z.string().uuid()); +} diff --git a/mapsync-server/src/main.ts b/mapsync-server/src/main.ts index f3e2360..a567eaf 100644 --- a/mapsync-server/src/main.ts +++ b/mapsync-server/src/main.ts @@ -23,8 +23,8 @@ import { AwaitingIdentityResponse, Welcomed, } from "./auth.ts"; -import { SUPPORTED_VERSIONS, UUID_NAMESPACE } from "./constants.ts"; -import { uuidv5 } from "./deps/uuid.ts"; +import { SUPPORTED_VERSIONS } from "./constants.ts"; +import { createOfflineUuid } from "./deps/uuid.ts"; let config: metadata.Config = null!; let main: ProtocolHandler = null!; @@ -151,7 +151,7 @@ export class ProtocolHandler { } client.auth = new Welcomed( packet.claimedUsername, - uuidv5(`Offline:${packet.claimedUsername}`, UUID_NAMESPACE), + createOfflineUuid(packet.claimedUsername), false, ); } diff --git a/mapsync-server/src/metadata.ts b/mapsync-server/src/metadata.ts index 5b36211..6a7331e 100644 --- a/mapsync-server/src/metadata.ts +++ b/mapsync-server/src/metadata.ts @@ -3,8 +3,7 @@ import node_path from "node:path"; import { Mutex } from "async-mutex"; import * as errors from "./deps/errors.ts"; import * as json from "./deps/json.ts"; -import * as z from "zod"; -import { fromZodError } from "zod-validation-error"; +import * as z from "./deps/zod.ts"; export const DATA_FOLDER = process.env["MAPSYNC_DATA_DIR"] ?? "./mapsync"; try { @@ -49,7 +48,7 @@ function parseConfigFile( return parser(json.parse(fileContents)); } catch (e) { if (e instanceof z.ZodError) { - throw "Could not parse " + file + ": " + fromZodError(e); + throw "Could not parse " + file + ": " + z.fromZodError(e); } throw e; } @@ -96,7 +95,7 @@ export function getConfig(): Config { const WHITELIST_FILE = "whitelist.json"; const WHITELIST_MUTEX = new Mutex(); -const WHITELIST_SCHEMA = z.array(z.string().uuid()); +const WHITELIST_SCHEMA = z.array(z.offlineUuid()); export const whitelist = new Set(); export async function loadWhitelist() { @@ -128,7 +127,7 @@ export async function saveWhitelist() { const UUID_CACHE_FILE = "uuid_cache.json"; const UUID_CACHE_MUTEX = new Mutex(); -const UUID_CACHE_SCHEMA = z.record(z.string().uuid()); +const UUID_CACHE_SCHEMA = z.record(z.offlineUuid()); // IGN UUID const uuid_cache = new Map();