From 67446b36a15615bd32e8827d72e83cb6ae54697c Mon Sep 17 00:00:00 2001 From: clawbot Date: Tue, 10 Mar 2026 12:44:29 +0100 Subject: [PATCH] feat: store auth tokens as SHA-256 hashes instead of plaintext (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Hash client auth tokens with SHA-256 before storing in the database. When validating tokens, hash the incoming token and compare against the stored hash. This prevents token exposure if the database is compromised. Existing plaintext tokens are implicitly invalidated since they will not match the new hashed lookups — users will need to create new sessions. ## Changes - **`internal/db/queries.go`**: Added `hashToken()` helper using `crypto/sha256`. Updated `CreateSession` to store hashed token. Updated `GetSessionByToken` to hash the incoming token before querying. - **`internal/db/auth.go`**: Updated `RegisterUser` and `LoginUser` to store hashed tokens. ## Migration No schema changes needed. The `token` column remains `TEXT` but now stores 64-char hex SHA-256 digests instead of 64-char hex random tokens. Existing plaintext tokens are effectively invalidated. closes https://git.eeqj.de/sneak/chat/issues/34 Co-authored-by: user Reviewed-on: https://git.eeqj.de/sneak/chat/pulls/69 Co-authored-by: clawbot Co-committed-by: clawbot --- internal/db/auth.go | 8 ++++++-- internal/db/queries.go | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/internal/db/auth.go b/internal/db/auth.go index b27eed9..7bf18bd 100644 --- a/internal/db/auth.go +++ b/internal/db/auth.go @@ -64,12 +64,14 @@ func (database *Database) RegisterUser( sessionID, _ := res.LastInsertId() + tokenHash := hashToken(token) + clientRes, err := transaction.ExecContext(ctx, `INSERT INTO clients (uuid, session_id, token, created_at, last_seen) VALUES (?, ?, ?, ?, ?)`, - clientUUID, sessionID, token, now, now) + clientUUID, sessionID, tokenHash, now, now) if err != nil { _ = transaction.Rollback() @@ -137,12 +139,14 @@ func (database *Database) LoginUser( now := time.Now() + tokenHash := hashToken(token) + res, err := database.conn.ExecContext(ctx, `INSERT INTO clients (uuid, session_id, token, created_at, last_seen) VALUES (?, ?, ?, ?, ?)`, - clientUUID, sessionID, token, now, now) + clientUUID, sessionID, tokenHash, now, now) if err != nil { return 0, 0, "", fmt.Errorf( "create login client: %w", err, diff --git a/internal/db/queries.go b/internal/db/queries.go index 6ffff23..15a65c6 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -3,6 +3,7 @@ package db import ( "context" "crypto/rand" + "crypto/sha256" "database/sql" "encoding/hex" "encoding/json" @@ -31,6 +32,14 @@ func generateToken() (string, error) { return hex.EncodeToString(buf), nil } +// hashToken returns the lowercase hex-encoded SHA-256 +// digest of a plaintext token string. +func hashToken(token string) string { + sum := sha256.Sum256([]byte(token)) + + return hex.EncodeToString(sum[:]) +} + // IRCMessage is the IRC envelope for all messages. type IRCMessage struct { ID string `json:"id"` @@ -105,12 +114,14 @@ func (database *Database) CreateSession( sessionID, _ := res.LastInsertId() + tokenHash := hashToken(token) + clientRes, err := transaction.ExecContext(ctx, `INSERT INTO clients (uuid, session_id, token, created_at, last_seen) VALUES (?, ?, ?, ?, ?)`, - clientUUID, sessionID, token, now, now) + clientUUID, sessionID, tokenHash, now, now) if err != nil { _ = transaction.Rollback() @@ -143,6 +154,8 @@ func (database *Database) GetSessionByToken( nick string ) + tokenHash := hashToken(token) + err := database.conn.QueryRowContext( ctx, `SELECT s.id, c.id, s.nick @@ -150,7 +163,7 @@ func (database *Database) GetSessionByToken( INNER JOIN sessions s ON s.id = c.session_id WHERE c.token = ?`, - token, + tokenHash, ).Scan(&sessionID, &clientID, &nick) if err != nil { return 0, 0, "", fmt.Errorf(