owenrusk.dev

casebench

test cases straight onto the board.

git clone https://owenrusk.dev/casebench.git

commit d7077748bc1326bdc2a12408215fd70d1fe8271d
parent 1348d3064dbe12858c07a6ffbbd956b9c210b6ac
author Owen Rusk <owen@papermothgames.com>
date   2025-05-07 11:31:20 -0500
lock: salted scrypt, not bare sha256

a bare sha256 of a one-word answer is a lookup, not a lock.
casebench/cli.py+2-1
casebench/db.py+4-4
casebench/lock.py+11-4
diff --git a/casebench/cli.py b/casebench/cli.py
index a493488..7c7a768 100644
--- a/casebench/cli.py
+++ b/casebench/cli.py
@@ -82,5 +82,6 @@ def run_lock(conn: psycopg.Connection, args: argparse.Namespace) -> None:
     who = player(conn, args.player)
     if db.source(conn, who, args.case) is None:
         raise Fail(f"{args.player} doesn't have {args.case}")
-    db.lock(conn, who, args.case, args.text, lock.digest(args.answer))
+    salt, answer_hash = lock.make(args.answer)
+    db.lock(conn, who, args.case, args.text, salt, answer_hash)
     print(f"{args.player}: locked {args.case}")
diff --git a/casebench/db.py b/casebench/db.py
index 54293ab..6dc6b31 100644
--- a/casebench/db.py
+++ b/casebench/db.py
@@ -66,11 +66,11 @@ def note(conn: psycopg.Connection, player: str, case_id: str, item_id: str | Non
     conn.commit()
 
 
-def lock(conn: psycopg.Connection, player: str, case_id: str, text: str, answer_hash: str) -> None:
+def lock(conn: psycopg.Connection, player: str, case_id: str, text: str, salt: bytes, answer_hash: str) -> None:
     conn.execute(
-        "insert into board_locks (player_id, case_id, text, answer_hash) values (%s, %s, %s, %s)"
+        "insert into board_locks (player_id, case_id, text, salt, answer_hash) values (%s, %s, %s, %s, %s)"
         " on conflict (player_id, case_id) do update"
-        " set text = excluded.text, answer_hash = excluded.answer_hash, opened_at = null",
-        (player, case_id, text, answer_hash),
+        " set text = excluded.text, salt = excluded.salt, answer_hash = excluded.answer_hash, opened_at = null",
+        (player, case_id, text, salt, answer_hash),
     )
     conn.commit()
diff --git a/casebench/lock.py b/casebench/lock.py
index cce8057..c9e4ab9 100644
--- a/casebench/lock.py
+++ b/casebench/lock.py
@@ -1,14 +1,21 @@
 import hashlib
+import hmac
+import os
 
 
 def normalize(answer: str) -> str:
     return " ".join(answer.casefold().split())
 
 
-def digest(answer: str) -> str:
-    return hashlib.sha256(normalize(answer).encode()).hexdigest()
+def digest(answer: str, salt: bytes) -> str:
+    return hashlib.scrypt(normalize(answer).encode(), salt=salt, n=2**14, r=8, p=1, dklen=32).hex()
 
 
-def opens(stored: str, typed: str) -> bool:
+def make(answer: str) -> tuple[bytes, str]:
+    salt = os.urandom(16)
+    return salt, digest(answer, salt)
+
+
+def opens(salt: bytes, stored: str, typed: str) -> bool:
     # the board calls this when someone types into a lock.
-    return digest(typed) == stored
+    return hmac.compare_digest(digest(typed, salt), stored)