Skip to content

Commit af477ba

Browse files
authored
Merge pull request #31 from fuzziecoder/codex/fix-issue-#26-by-migrating-to-db-aljyuy
Secure backend: scrypt password hashing and legacy migration (Issue #28)
2 parents 01446ee + 662313a commit af477ba

2 files changed

Lines changed: 49 additions & 7 deletions

File tree

backend/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ Server starts at `http://localhost:4000` by default.
1919
- On first start, seed data is inserted for users, spots, catalog items, and a sample order.
2020
- New orders are validated against DB data (known `spotId`, `userId`, `productId`) and item pricing is always derived from catalog prices in the database.
2121

22+
### Issue #28: Secure credential storage and verification
23+
24+
- Passwords are stored as salted `scrypt` hashes (not plaintext).
25+
- Legacy plaintext user passwords are auto-migrated to hashed values on successful login.
26+
2227
## Available endpoints
2328

2429
- `GET /api/health`

backend/db.js

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { existsSync, mkdirSync } from 'node:fs';
22
import { dirname, resolve } from 'node:path';
3-
import { randomUUID } from 'node:crypto';
3+
import { randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
44
import { DatabaseSync } from 'node:sqlite';
55

66
const defaultDbPath = resolve(process.cwd(), 'backend', 'data', 'brocode.sqlite');
@@ -15,6 +15,30 @@ const db = new DatabaseSync(dbPath);
1515
db.exec('PRAGMA journal_mode = WAL;');
1616
db.exec('PRAGMA foreign_keys = ON;');
1717

18+
const HASH_PREFIX = 'scrypt$';
19+
const SCRYPT_KEY_LENGTH = 64;
20+
21+
const hashPassword = (password, saltHex = randomBytes(16).toString('hex')) => {
22+
const derivedKey = scryptSync(password, Buffer.from(saltHex, 'hex'), SCRYPT_KEY_LENGTH);
23+
return `${HASH_PREFIX}${saltHex}$${derivedKey.toString('hex')}`;
24+
};
25+
26+
const verifyPassword = (password, storedPassword) => {
27+
if (!storedPassword?.startsWith(HASH_PREFIX)) {
28+
return password === storedPassword;
29+
}
30+
31+
const [, saltHex, expectedHashHex] = storedPassword.split('$');
32+
const candidateHash = scryptSync(password, Buffer.from(saltHex, 'hex'), SCRYPT_KEY_LENGTH);
33+
const expectedHash = Buffer.from(expectedHashHex, 'hex');
34+
35+
if (candidateHash.length !== expectedHash.length) {
36+
return false;
37+
}
38+
39+
return timingSafeEqual(candidateHash, expectedHash);
40+
};
41+
1842
db.exec(`
1943
CREATE TABLE IF NOT EXISTS users (
2044
id TEXT PRIMARY KEY,
@@ -65,8 +89,8 @@ const hasUsers = db.prepare('SELECT COUNT(*) AS count FROM users').get().count >
6589

6690
if (!hasUsers) {
6791
const insertUser = db.prepare('INSERT INTO users (id, username, password, name, role) VALUES (?, ?, ?, ?, ?)');
68-
insertUser.run('u-1', 'brocode', 'changeme', 'Ram', 'admin');
69-
insertUser.run('u-2', 'dhanush', 'changeme', 'Dhanush', 'user');
92+
insertUser.run('u-1', 'brocode', hashPassword('changeme'), 'Ram', 'admin');
93+
insertUser.run('u-2', 'dhanush', hashPassword('changeme'), 'Dhanush', 'user');
7094

7195
const insertSpot = db.prepare('INSERT INTO spots (id, location, date, host_user_id) VALUES (?, ?, ?, ?)');
7296
insertSpot.run('spot-2025-07-26', 'Attibele Toll Plaza', '2025-07-26T10:00:00.000Z', 'u-1');
@@ -134,14 +158,27 @@ const getCatalogItemByIdStatement = db.prepare(
134158

135159
const userExistsStatement = db.prepare('SELECT 1 AS found FROM users WHERE id = ? LIMIT 1');
136160
const spotExistsStatement = db.prepare('SELECT 1 AS found FROM spots WHERE id = ? LIMIT 1');
161+
const getUserByUsernameStatement = db.prepare('SELECT id, username, password, name, role FROM users WHERE username = ?');
162+
const updateUserPasswordStatement = db.prepare('UPDATE users SET password = ? WHERE id = ?');
137163

138164
export const database = {
139165
getUserByCredentials(username, password) {
140-
const user = db
141-
.prepare('SELECT id, username, name, role FROM users WHERE username = ? AND password = ?')
142-
.get(username, password);
166+
const user = getUserByUsernameStatement.get(username);
167+
168+
if (!user || !verifyPassword(password, user.password)) {
169+
return null;
170+
}
143171

144-
return user || null;
172+
if (!user.password.startsWith(HASH_PREFIX)) {
173+
updateUserPasswordStatement.run(hashPassword(password), user.id);
174+
}
175+
176+
return {
177+
id: user.id,
178+
username: user.username,
179+
name: user.name,
180+
role: user.role,
181+
};
145182
},
146183

147184
getCatalog() {

0 commit comments

Comments
 (0)