diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 147f61d7c..08afef046 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,4 +1,10 @@ -import { randomBytes, scryptSync, subtle } from "node:crypto"; +import { + type BinaryLike, + randomBytes, + scrypt, + subtle, + timingSafeEqual, +} from "node:crypto"; import { customId } from "@/common/id"; export const createHash = async (key: string) => { @@ -25,18 +31,38 @@ export const initializeAccessToken = ({ }; }; -export const createSecureHash = (secret: string) => { +const scryptAsync = ( + password: BinaryLike, + salt: BinaryLike, + keylen: number, +): Promise => { + return new Promise((resolve, reject) => { + scrypt(password, salt, keylen, (err, derivedKey) => { + if (err) reject(err); + else resolve(derivedKey); + }); + }); +}; + +export const createSecureHash = async (secret: string) => { const data = new TextEncoder().encode(secret); const salt = randomBytes(32).toString("hex"); - const derivedKey = scryptSync(data, salt, 64); + const derivedKey = await scryptAsync(data, salt, 64); return `${salt}:${derivedKey.toString("hex")}`; }; -export const verifySecureHash = (secret: string, hash: string) => { - const data = new TextEncoder().encode(secret); - const [salt, storedHash] = hash.split(":"); - const derivedKey = scryptSync(data, String(salt), 64); +export const verifySecureHash = async (secret: string, hash: string) => { + try { + const data = new TextEncoder().encode(secret); + const [salt, storedHash] = hash.split(":"); + + const derivedKey = await scryptAsync(data, salt ?? "", 64); - return storedHash === derivedKey.toString("hex"); + const derivedKeyBuffer = Buffer.from(derivedKey); + const storedHashBuffer = Buffer.from(storedHash ?? "", "hex"); + return timingSafeEqual(derivedKeyBuffer, storedHashBuffer); + } catch (_error) { + return false; + } }; diff --git a/src/server/api/middlewares/bearer-token.ts b/src/server/api/middlewares/bearer-token.ts index 625576ba8..428d28878 100644 --- a/src/server/api/middlewares/bearer-token.ts +++ b/src/server/api/middlewares/bearer-token.ts @@ -1,6 +1,7 @@ import { verifySecureHash } from "@/lib/crypto"; import type { Context } from "hono"; import { createMiddleware } from "hono/factory"; +import { nanoid } from "nanoid"; import { ApiError } from "../error"; export type accessTokenAuthMiddlewareOptions = @@ -46,21 +47,16 @@ async function authenticateWithAccessToken( }); } - const accessToken = await findAccessToken(clientId, c); + const randomId = nanoid(); - if (!accessToken) { - throw new ApiError({ - code: "UNAUTHORIZED", - message: "Bearer token is invalid", - }); - } + const accessToken = await findAccessToken(clientId, c); const isAccessTokenValid = await verifySecureHash( clientSecret, - accessToken.clientSecret, + accessToken?.clientSecret ?? randomId, ); - if (!isAccessTokenValid) { + if (!isAccessTokenValid || !accessToken) { throw new ApiError({ code: "UNAUTHORIZED", message: "Bearer token is invalid",