change pin to hash, use bcrypt for some sense of sensible security on passwords

This commit is contained in:
Ean Milligan
2026-04-23 02:39:50 -04:00
parent c6eab65f1d
commit 04074d19e5
4 changed files with 52 additions and 8 deletions

View File

@@ -18,7 +18,7 @@ await dbClient.execute(`
CREATE TABLE users ( CREATE TABLE users (
id varchar(20) NOT NULL, id varchar(20) NOT NULL,
name varchar(20) NOT NULL, name varchar(20) NOT NULL,
pin varchar(16) NOT NULL, hash varchar(60) NOT NULL,
email varchar(255) NULL, email varchar(255) NULL,
deleteCode varchar(20) NULL, deleteCode varchar(20) NULL,
PRIMARY KEY (id), PRIMARY KEY (id),

View File

@@ -22,6 +22,7 @@
}, },
"nodeModulesDir": "none", "nodeModulesDir": "none",
"imports": { "imports": {
"@bcrypt": "jsr:@felix/bcrypt",
"@mysql": "https://deno.land/x/mysql@v2.12.1/mod.ts", "@mysql": "https://deno.land/x/mysql@v2.12.1/mod.ts",
"@nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts", "@nanoid": "https://deno.land/x/nanoid@v3.0.0/mod.ts",
"@std/http": "jsr:@std/http@1.0.15", "@std/http": "jsr:@std/http@1.0.15",

46
deno.lock generated
View File

@@ -1,17 +1,40 @@
{ {
"version": "5", "version": "5",
"specifiers": { "specifiers": {
"jsr:@denosaurs/plug@^1.1.0": "1.1.0",
"jsr:@felix/bcrypt@*": "1.0.8",
"jsr:@std/cli@^1.0.17": "1.0.17", "jsr:@std/cli@^1.0.17": "1.0.17",
"jsr:@std/encoding@1": "1.0.10",
"jsr:@std/encoding@^1.0.10": "1.0.10", "jsr:@std/encoding@^1.0.10": "1.0.10",
"jsr:@std/fmt@1": "1.0.7",
"jsr:@std/fmt@^1.0.7": "1.0.7", "jsr:@std/fmt@^1.0.7": "1.0.7",
"jsr:@std/fs@1": "1.0.23",
"jsr:@std/html@^1.0.3": "1.0.3", "jsr:@std/html@^1.0.3": "1.0.3",
"jsr:@std/http@1.0.15": "1.0.15", "jsr:@std/http@1.0.15": "1.0.15",
"jsr:@std/internal@^1.0.12": "1.0.13",
"jsr:@std/media-types@^1.1.0": "1.1.0", "jsr:@std/media-types@^1.1.0": "1.1.0",
"jsr:@std/net@^1.0.4": "1.0.4", "jsr:@std/net@^1.0.4": "1.0.4",
"jsr:@std/path@1": "1.0.9",
"jsr:@std/path@^1.0.9": "1.0.9", "jsr:@std/path@^1.0.9": "1.0.9",
"jsr:@std/path@^1.1.4": "1.1.4",
"jsr:@std/streams@^1.0.9": "1.0.9" "jsr:@std/streams@^1.0.9": "1.0.9"
}, },
"jsr": { "jsr": {
"@denosaurs/plug@1.1.0": {
"integrity": "eb2f0b7546c7bca2000d8b0282c54d50d91cf6d75cb26a80df25a6de8c4bc044",
"dependencies": [
"jsr:@std/encoding@1",
"jsr:@std/fmt@1",
"jsr:@std/fs",
"jsr:@std/path@1"
]
},
"@felix/bcrypt@1.0.8": {
"integrity": "59c41160fc027882479c512db5d53792c4d91aadcd49467c85caa2f1679046f2",
"dependencies": [
"jsr:@denosaurs/plug"
]
},
"@std/cli@1.0.17": { "@std/cli@1.0.17": {
"integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b" "integrity": "e15b9abe629e17be90cc6216327f03a29eae613365f1353837fa749aad29ce7b"
}, },
@@ -21,6 +44,13 @@
"@std/fmt@1.0.7": { "@std/fmt@1.0.7": {
"integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb" "integrity": "2a727c043d8df62cd0b819b3fb709b64dd622e42c3b1bb817ea7e6cc606360fb"
}, },
"@std/fs@1.0.23": {
"integrity": "3ecbae4ce4fee03b180fa710caff36bb5adb66631c46a6460aaad49515565a37",
"dependencies": [
"jsr:@std/internal",
"jsr:@std/path@^1.1.4"
]
},
"@std/html@1.0.3": { "@std/html@1.0.3": {
"integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988" "integrity": "7a0ac35e050431fb49d44e61c8b8aac1ebd55937e0dc9ec6409aa4bab39a7988"
}, },
@@ -28,15 +58,18 @@
"integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159", "integrity": "435a4934b4e196e82a8233f724da525f7b7112f3566502f28815e94764c19159",
"dependencies": [ "dependencies": [
"jsr:@std/cli", "jsr:@std/cli",
"jsr:@std/encoding", "jsr:@std/encoding@^1.0.10",
"jsr:@std/fmt", "jsr:@std/fmt@^1.0.7",
"jsr:@std/html", "jsr:@std/html",
"jsr:@std/media-types", "jsr:@std/media-types",
"jsr:@std/net", "jsr:@std/net",
"jsr:@std/path", "jsr:@std/path@^1.0.9",
"jsr:@std/streams" "jsr:@std/streams"
] ]
}, },
"@std/internal@1.0.13": {
"integrity": "2f9546691d4ac2d32859c82dff284aaeac980ddeca38430d07941e7e288725c0"
},
"@std/media-types@1.1.0": { "@std/media-types@1.1.0": {
"integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4" "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
}, },
@@ -46,6 +79,12 @@
"@std/path@1.0.9": { "@std/path@1.0.9": {
"integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e" "integrity": "260a49f11edd3db93dd38350bf9cd1b4d1366afa98e81b86167b4e3dd750129e"
}, },
"@std/path@1.1.4": {
"integrity": "1d2d43f39efb1b42f0b1882a25486647cb851481862dc7313390b2bb044314b5",
"dependencies": [
"jsr:@std/internal"
]
},
"@std/streams@1.0.9": { "@std/streams@1.0.9": {
"integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035" "integrity": "a9d26b1988cdd7aa7b1f4b51e1c36c1557f3f252880fa6cc5b9f37078b1a5035"
} }
@@ -120,6 +159,7 @@
}, },
"workspace": { "workspace": {
"dependencies": [ "dependencies": [
"jsr:@felix/bcrypt@*",
"jsr:@std/http@1.0.15" "jsr:@std/http@1.0.15"
] ]
} }

11
mod.ts
View File

@@ -1,3 +1,4 @@
import { hash, verify } from '@bcrypt';
import { customAlphabet } from '@nanoid'; import { customAlphabet } from '@nanoid';
import { STATUS_CODE, STATUS_TEXT, StatusCode } from '@std/http/status'; import { STATUS_CODE, STATUS_TEXT, StatusCode } from '@std/http/status';
@@ -101,12 +102,13 @@ Deno.serve({ port: config.api.port }, async (req) => {
if (userNameMatches.length === 0) { if (userNameMatches.length === 0) {
if (body.name.length < 4 || body.name.length > 20) if (body.name.length < 4 || body.name.length > 20)
return genericResponse(STATUS_CODE.BadRequest, `Name too ${body.name.length < 4 ? 'short' : 'long'}.`); return genericResponse(STATUS_CODE.BadRequest, `Name too ${body.name.length < 4 ? 'short' : 'long'}.`);
if (body.pin.length < 4 || body.pin.length > 16) return genericResponse(STATUS_CODE.BadRequest, `PIN too ${body.pin.length < 4 ? 'short' : 'long'}.`); if (body.pin.length < 4) return genericResponse(STATUS_CODE.BadRequest, `PIN too ${body.pin.length < 4 ? 'short' : 'long'}.`);
if (body.email.length > 255) return genericResponse(STATUS_CODE.BadRequest, 'Email too long.'); if (body.email.length > 255) return genericResponse(STATUS_CODE.BadRequest, 'Email too long.');
const id = nanoid(); const id = nanoid();
const pinHash = await hash(body.pin);
await dbClient.execute('INSERT INTO users(id,name,pin,email) values(?,?,?,?)', [id, body.name, body.pin, body.email]).catch(() => { await dbClient.execute('INSERT INTO users(id,name,hash,email) values(?,?,?,?)', [id, body.name, pinHash, body.email]).catch(() => {
failed = true; failed = true;
}); });
@@ -121,11 +123,12 @@ Deno.serve({ port: config.api.port }, async (req) => {
} else { } else {
const body = await req.json(); const body = await req.json();
const loginMatch = await dbClient.query('SELECT id, email, deleteCode FROM users WHERE name = ? AND pin = ?', [body.name, body.pin]).catch(() => { const loginMatch = await dbClient.query('SELECT id, hash, email, deleteCode FROM users WHERE name = ?', [body.name]).catch(() => {
failed = true; failed = true;
}); });
if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB."); if (failed) return genericResponse(STATUS_CODE.InternalServerError, "Couldn't read DB.");
if (loginMatch.length === 0) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination.'); if (loginMatch.length === 0) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination. Remember name is case sensitive.');
if (!(await verify(body.pin, loginMatch[0].hash))) return genericResponse(STATUS_CODE.Forbidden, 'Invalid name/PIN combination.');
const id = loginMatch[0].id; const id = loginMatch[0].id;
const email = loginMatch[0].email; const email = loginMatch[0].email;
const hasEmail = email.length > 0; const hasEmail = email.length > 0;