Hash seat tokens at rest (security review finding)

Raw seat tokens no longer touch disk or long-lived memory: rooms store
sha-256 hashes, joins compare timing-safely, sessions keep the raw
token they authenticated with only for minting transfer phrases, and
legacy plaintext room files still load (hashed on read). Verified:
rejoin and transfer both work, and the room file contains only
hostTokenHash — no raw token anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Eric Wagoner
2026-08-16 00:03:36 -04:00
co-authored by Claude Fable 5
parent ce8b6a315b
commit 0221e91ba3
3 changed files with 48 additions and 17 deletions
+6 -2
View File
@@ -36,6 +36,8 @@ interface Session {
socket: WebSocket;
playerId: PlayerId | null;
roomId: string | null;
/** The raw seat token this connection authenticated with (memory only). */
token: string | null;
claimFails: number;
}
@@ -71,7 +73,7 @@ function broadcastRoomState(room: Room): void {
}
wss.on("connection", (socket) => {
const session: Session = { socket, playerId: null, roomId: null, claimFails: 0 };
const session: Session = { socket, playerId: null, roomId: null, token: null, claimFails: 0 };
sessions.add(session);
send(socket, { type: "welcome", game: "wizwar" });
@@ -93,6 +95,7 @@ wss.on("connection", (socket) => {
const { room, token } = createRoom(name);
session.playerId = name;
session.roomId = room.id;
session.token = token;
send(socket, { type: "seat", playerId: name, token });
broadcastRoomState(room);
break;
@@ -107,6 +110,7 @@ wss.on("connection", (socket) => {
if ("error" in result) return send(socket, { type: "error", message: result.error });
session.playerId = name;
session.roomId = room.id;
session.token = result.token;
send(socket, { type: "seat", playerId: name, token: result.token });
// Rejoining a running game: replay the chronicle so far.
if (room.state) {
@@ -137,7 +141,7 @@ wss.on("connection", (socket) => {
case "makeTransfer": {
const room = session.roomId ? getRoom(session.roomId) : undefined;
if (!room || !session.playerId) return send(socket, { type: "error", message: "not in a room" });
const result = makeTransferCode(room, session.playerId);
const result = makeTransferCode(room, session.playerId, session.token);
if ("error" in result) return send(socket, { type: "error", message: result.error });
send(socket, { type: "transferCode", code: result.code, expiresAt: result.expiresAt });
break;