From a55daf4ef3133a8f498fd6b3f545dafb41c489e3 Mon Sep 17 00:00:00 2001 From: Johannes Date: Fri, 13 Mar 2026 00:31:33 +0100 Subject: [PATCH] Add password protection to server saves (SHA-256 client hash, lock icons) --- game.v2025-07-31.bundle.js | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/game.v2025-07-31.bundle.js b/game.v2025-07-31.bundle.js index 806d0f3..0b334b7 100644 --- a/game.v2025-07-31.bundle.js +++ b/game.v2025-07-31.bundle.js @@ -1056,6 +1056,10 @@ !e.hasOwnProperty("primaryMapData") || !e.hasOwnProperty("subMapData") ); } + static async sha256_hex(str) { + const buf = await window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(str)); + return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, "0")).join(""); + } static compressJSObjectToString(e) { return btoa( n.arraybufferToString( @@ -5625,7 +5629,7 @@ () => { E.exportSavegamePrivateBin(); }, - )}"> ${e.T("settings-share-save-button", "Export save without file")}\n
\n --\x3e\n \n
\n
Server Saves
\n
\n \n
\n \n
\n
\n Upload a complete export to the server under a custom name.\n
\n \n
\n \n
\n
\n Load a previously saved game from the server. This will overwrite your current progress.\n
\n \n `; + )}"> ${e.T("settings-share-save-button", "Export save without file")}\n
\n --\x3e\n \n
\n
Server Saves
\n
\n \n
\n \n
\n
\n \n Upload a complete export to the server under a custom name.\n
\n \n
\n \n
\n
\n \n Load a previously saved game from the server. This will overwrite your current progress.\n
\n \n `; } static mapManagementFormView() { const e = s.getInstance(); @@ -5875,16 +5879,21 @@ const orig_compress = n.getSetting("compressExports"); n.setSetting("completeExports", true); n.setSetting("compressExports", true); + const password = $("#named-save-password").val(); try { const data = await _.getSavegameData(error_obj); const compressed = n.compressJSObjectToString(data); const blob = new Blob([compressed], { type: "application/octet-stream" }); + const headers = { "Content-Type": "application/octet-stream" }; + if (password) headers["X-Password-Hash"] = await n.sha256_hex(password); await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, { method: "POST", mode: "cors", + headers, body: blob, }); - n.showToast("Save stored", `"${save_name}" was saved to the server.`, "success"); + const label = password ? " (protected)" : ""; + n.showToast("Save stored", `"${save_name}"${label} was saved to the server.`, "success"); } catch (e) { n.showToast("Save failed", "Could not store the save on the server.", "danger"); } @@ -5894,14 +5903,17 @@ static async populateServerSaves() { try { const res = await fetch("https://pu-saves.burrson.de/saves", { mode: "cors" }); - const saves = await res.json(); + const data = await res.json(); const select = $("#server-saves-select"); if (!select.length) return; select.empty(); - if (saves.length === 0) { + if (data.saves.length === 0) { select.append(''); } else { - saves.forEach(name => select.append(``)); + data.saves.forEach(name => { + const is_protected = data.protected.includes(name); + select.append(``); + }); } } catch (e) { $("#server-saves-select").empty().append(''); @@ -5910,13 +5922,21 @@ static async importNamedSave() { const save_name = $("#server-saves-select").val(); if (!save_name) return; + const password = $("#named-load-password").val(); try { - const res = await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, { mode: "cors" }); + const headers = {}; + if (password) headers["X-Password-Hash"] = await n.sha256_hex(password); + const res = await fetch(`https://pu-saves.burrson.de/saves/${encodeURIComponent(save_name)}`, { mode: "cors", headers }); + if (res.status === 401) { + const msg = await res.text(); + n.showToast("Access denied", msg.includes("required") ? "This save is password protected." : "Wrong password.", "danger"); + return; + } if (!res.ok) throw "Not found"; const text = await res.text(); if (n.hasScript(text)) throw "Script found!"; - const data = JSON.parse(n.decompressJsonFromString(text)); - E.importSaveGame(data); + const parsed = JSON.parse(n.decompressJsonFromString(text)); + E.importSaveGame(parsed); } catch (e) { n.showToast("Load failed", "Could not load the save from the server.", "danger"); }