Skip to content
This repository was archived by the owner on Sep 17, 2024. It is now read-only.

Commit b2cefc7

Browse files
committed
fix(tokens): Hash tokens in tokens module to resist ND2DB-style timing attack
1 parent 1317b2b commit b2cefc7

File tree

7 files changed

+28
-18
lines changed

7 files changed

+28
-18
lines changed

modules/tokens/db/migrations/20240307005945_init/migration.sql renamed to modules/tokens/db/migrations/20240501200641_init/migration.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
-- CreateTable
22
CREATE TABLE "Token" (
33
"id" UUID NOT NULL,
4-
"token" TEXT NOT NULL,
4+
"tokenHash" TEXT NOT NULL,
55
"type" TEXT NOT NULL,
66
"meta" JSONB NOT NULL,
77
"trace" JSONB NOT NULL,
@@ -13,4 +13,4 @@ CREATE TABLE "Token" (
1313
);
1414

1515
-- CreateIndex
16-
CREATE UNIQUE INDEX "Token_token_key" ON "Token"("token");
16+
CREATE UNIQUE INDEX "Token_tokenHash_key" ON "Token"("tokenHash");

modules/tokens/db/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ datasource db {
55

66
model Token {
77
id String @id @default(uuid()) @db.Uuid
8-
token String @unique
8+
tokenHash String @unique
99
type String
1010
meta Json
1111
trace Json

modules/tokens/scripts/create.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ScriptContext } from "../_gen/scripts/create.ts";
2-
import { TokenWithSecret } from "../utils/types.ts";
3-
import { tokenFromRow } from "../utils/types.ts";
2+
import { TokenWithSecret, tokenFromRow, hash } from "../utils/types.ts";
43

54
export interface Request {
65
type: string;
@@ -17,10 +16,9 @@ export async function run(
1716
req: Request,
1817
): Promise<Response> {
1918
const tokenStr = generateToken(req.type);
20-
2119
const token = await ctx.db.token.create({
2220
data: {
23-
token: tokenStr,
21+
tokenHash: await hash(tokenStr),
2422
type: req.type,
2523
meta: req.meta,
2624
trace: ctx.trace,
@@ -29,7 +27,7 @@ export async function run(
2927
});
3028

3129
return {
32-
token: tokenFromRow(token),
30+
token: tokenFromRow(token, () => tokenStr),
3331
};
3432
}
3533

modules/tokens/scripts/extend.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ScriptContext } from "../_gen/scripts/extend.ts";
2-
import { TokenWithSecret } from "../types/common.ts";
3-
import { tokenFromRow } from "../types/common.ts";
2+
import { TokenWithSecret, tokenFromRow } from "../utils/types.ts";
43

54
export interface Request {
65
token: string;
@@ -32,6 +31,6 @@ export async function run(
3231

3332
// Return the updated token
3433
return {
35-
token: tokenFromRow(newToken),
34+
token: tokenFromRow(newToken, () => req.token),
3635
};
3736
}

modules/tokens/scripts/get.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { ScriptContext } from "../_gen/scripts/get.ts";
2-
import { Token } from "../utils/types.ts";
3-
import { tokenFromRow } from "../utils/types.ts";
2+
import { Token, tokenFromRow } from "../utils/types.ts";
43

54
export interface Request {
65
tokenIds: string[];
@@ -23,7 +22,7 @@ export async function run(
2322
},
2423
});
2524

26-
const tokens = rows.map(tokenFromRow);
25+
const tokens = rows.map(row => tokenFromRow(row, () => ""));
2726

2827
return { tokens };
2928
}

modules/tokens/scripts/get_by_token.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ScriptContext } from "../_gen/scripts/get_by_token.ts";
2-
import { Token, tokenFromRow } from "../utils/types.ts";
2+
import { Token, tokenFromRow, hash } from "../utils/types.ts";
33

44
export interface Request {
55
tokens: string[];
@@ -13,18 +13,21 @@ export async function run(
1313
ctx: ScriptContext,
1414
req: Request,
1515
): Promise<Response> {
16+
const hashed = await Promise.all(req.tokens.map(hash));
1617
const rows = await ctx.db.token.findMany({
1718
where: {
18-
token: {
19-
in: req.tokens,
19+
tokenHash: {
20+
in: hashed,
2021
},
2122
},
2223
orderBy: {
2324
createdAt: "desc",
2425
},
2526
});
2627

27-
const tokens = rows.map(tokenFromRow);
28+
const hashMap = Object.fromEntries(req.tokens.map((token, i) => [hashed[i], token]));
29+
30+
const tokens = rows.map(row => tokenFromRow(row, h => hashMap[h]));
2831

2932
return { tokens };
3033
}

modules/tokens/utils/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,22 @@ export interface TokenWithSecret extends Token {
1515

1616
export function tokenFromRow(
1717
row: prisma.Prisma.TokenGetPayload<any>,
18+
hashToOrig: (hash: string) => string,
1819
): TokenWithSecret {
1920
return {
2021
...row,
2122
createdAt: row.createdAt.toISOString(),
2223
expireAt: row.expireAt?.toISOString() ?? null,
2324
revokedAt: row.revokedAt?.toISOString() ?? null,
25+
token: hashToOrig(row.tokenHash),
2426
};
2527
}
28+
29+
export async function hash(token: string): Promise<string> {
30+
const encoder = new TextEncoder();
31+
const data = encoder.encode(token);
32+
const hash = await crypto.subtle.digest("SHA-256", data);
33+
const digest = Array.from(new Uint8Array(hash));
34+
const strDigest = digest.map(b => b.toString(16).padStart(2, "0")).join("");
35+
return strDigest;
36+
}

0 commit comments

Comments
 (0)