fix: format keys as XXXX-XXXX-...-XXXX (8 groups), normalize before hashing
Keys are now human-readable grouped hex instead of raw 64-char blobs. resolveKey() strips dashes before sha256/bcrypt so both formats work. All key creation paths (register, reset-key, sub-keys) hash the normalized form. Login placeholder and maxlength updated to match.
This commit is contained in:
parent
b8ac4e7b1f
commit
43a1abc2ed
|
|
@ -21,9 +21,9 @@
|
|||
<form id="login-form" action="/account/login" method="POST">
|
||||
<input type="hidden" name="_form" value="1">
|
||||
<label class="block text-xs text-gray-500 uppercase tracking-wider mb-2">Account Key</label>
|
||||
<input id="key-input" name="key" type="text" placeholder="Paste your account key" autocomplete="off" spellcheck="false"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm font-mono"
|
||||
maxlength="64">
|
||||
<input id="key-input" name="key" type="text" placeholder="XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX" autocomplete="off" spellcheck="false"
|
||||
class="w-full bg-gray-800 border border-gray-700 rounded-lg px-4 py-3 text-gray-100 placeholder-gray-600 focus:outline-none focus:border-blue-500 text-sm font-mono tracking-wider"
|
||||
maxlength="71">
|
||||
<button type="submit"
|
||||
class="w-full mt-3 bg-blue-600 hover:bg-blue-500 text-white font-medium py-3 rounded-lg transition-colors">
|
||||
Sign In
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ import { randomBytes, createHash } from "crypto";
|
|||
import sql from "../db";
|
||||
|
||||
function generateKey(): string {
|
||||
return randomBytes(32).toString("hex");
|
||||
const hex = randomBytes(32).toString("hex"); // 64 hex chars
|
||||
// Format as 8 groups of 4: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX
|
||||
return (hex.match(/.{4}/g) as string[]).join("-").toUpperCase();
|
||||
}
|
||||
|
||||
function normalizeKey(raw: string): string {
|
||||
// Strip dashes/spaces so both formatted and raw hex keys resolve correctly
|
||||
return raw.replace(/[-\s]/g, "").toLowerCase();
|
||||
}
|
||||
|
||||
function sha256(data: string): string {
|
||||
|
|
@ -21,12 +28,13 @@ function hashEmail(email: string): string {
|
|||
* 3. Verify with bcrypt for extra security
|
||||
*/
|
||||
async function resolveKey(rawKey: string): Promise<{ accountId: string; keyId: string | null } | null> {
|
||||
const lookup = sha256(rawKey);
|
||||
const normalized = normalizeKey(rawKey);
|
||||
const lookup = sha256(normalized);
|
||||
|
||||
// Check primary account key
|
||||
const [account] = await sql`SELECT id, key_hash FROM accounts WHERE key_lookup = ${lookup}`;
|
||||
if (account) {
|
||||
const valid = await Bun.password.verify(rawKey, account.key_hash);
|
||||
const valid = await Bun.password.verify(normalized, account.key_hash);
|
||||
if (!valid) return null;
|
||||
return { accountId: account.id, keyId: null };
|
||||
}
|
||||
|
|
@ -34,7 +42,7 @@ async function resolveKey(rawKey: string): Promise<{ accountId: string; keyId: s
|
|||
// Check API sub-keys
|
||||
const [apiKey] = await sql`SELECT id, account_id, key_hash FROM api_keys WHERE key_lookup = ${lookup}`;
|
||||
if (apiKey) {
|
||||
const valid = await Bun.password.verify(rawKey, apiKey.key_hash);
|
||||
const valid = await Bun.password.verify(normalized, apiKey.key_hash);
|
||||
if (!valid) return null;
|
||||
sql`UPDATE api_keys SET last_used_at = now() WHERE id = ${apiKey.id}`.catch(() => {});
|
||||
return { accountId: apiKey.account_id, keyId: apiKey.id };
|
||||
|
|
@ -115,8 +123,9 @@ export const account = new Elysia({ prefix: "/account" })
|
|||
// ── Register ────────────────────────────────────────────────────────
|
||||
.post("/register", async ({ body, cookie }) => {
|
||||
const rawKey = generateKey();
|
||||
const keyLookup = sha256(rawKey);
|
||||
const keyHash = await Bun.password.hash(rawKey, { algorithm: "bcrypt", cost: 10 });
|
||||
const norm = normalizeKey(rawKey);
|
||||
const keyLookup = sha256(norm);
|
||||
const keyHash = await Bun.password.hash(norm, { algorithm: "bcrypt", cost: 10 });
|
||||
const emailHash = body.email ? hashEmail(body.email) : null;
|
||||
|
||||
await sql`INSERT INTO accounts (key_lookup, key_hash, email_hash) VALUES (${keyLookup}, ${keyHash}, ${emailHash})`;
|
||||
|
|
@ -163,8 +172,9 @@ export const account = new Elysia({ prefix: "/account" })
|
|||
// Reset primary key — generates a new one, old one immediately invalid
|
||||
.post("/reset-key", async ({ accountId, cookie }) => {
|
||||
const rawKey = generateKey();
|
||||
const keyLookup = sha256(rawKey);
|
||||
const keyHash = await Bun.password.hash(rawKey, { algorithm: "bcrypt", cost: 10 });
|
||||
const norm = normalizeKey(rawKey);
|
||||
const keyLookup = sha256(norm);
|
||||
const keyHash = await Bun.password.hash(norm, { algorithm: "bcrypt", cost: 10 });
|
||||
|
||||
await sql`UPDATE accounts SET key_lookup = ${keyLookup}, key_hash = ${keyHash} WHERE id = ${accountId}`;
|
||||
|
||||
|
|
@ -180,8 +190,9 @@ export const account = new Elysia({ prefix: "/account" })
|
|||
// Create a sub-key (for different apps or shared access)
|
||||
.post("/keys", async ({ accountId, body }) => {
|
||||
const rawKey = generateKey();
|
||||
const keyLookup = sha256(rawKey);
|
||||
const keyHash = await Bun.password.hash(rawKey, { algorithm: "bcrypt", cost: 10 });
|
||||
const norm = normalizeKey(rawKey);
|
||||
const keyLookup = sha256(norm);
|
||||
const keyHash = await Bun.password.hash(norm, { algorithm: "bcrypt", cost: 10 });
|
||||
|
||||
await sql`INSERT INTO api_keys (key_lookup, key_hash, account_id, label) VALUES (${keyLookup}, ${keyHash}, ${accountId}, ${body.label})`;
|
||||
return { key: rawKey, label: body.label };
|
||||
|
|
|
|||
Loading…
Reference in New Issue