Skip to content

Commit e8a138f

Browse files
committed
fix: add timeSafe checks for signed tokens and api keys
1 parent 7ced044 commit e8a138f

3 files changed

Lines changed: 26 additions & 6 deletions

File tree

‎packages/shared/signedTokens.test.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,19 @@ describe("signed tokens", () => {
6161
const decoded: SignedTokenPayload = JSON.parse(
6262
Buffer.from(token, "base64").toString(),
6363
);
64-
decoded.signature = "tampered" + decoded.signature;
64+
decoded.signature = `${decoded.signature[0] === "0" ? "1" : "0"}${decoded.signature.slice(1)}`;
65+
const tampered = Buffer.from(JSON.stringify(decoded)).toString("base64");
66+
const verified = verifySignedToken(tampered, SECRET, testSchema);
67+
expect(verified).toBeNull();
68+
});
69+
70+
it("should return null for a malformed signature", () => {
71+
const token = createSignedToken(testPayload, SECRET);
72+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
73+
const decoded: SignedTokenPayload = JSON.parse(
74+
Buffer.from(token, "base64").toString(),
75+
);
76+
decoded.signature = "not-a-hex-signature";
6577
const tampered = Buffer.from(JSON.stringify(decoded)).toString("base64");
6678
const verified = verifySignedToken(tampered, SECRET, testSchema);
6779
expect(verified).toBeNull();

‎packages/shared/signedTokens.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,13 @@ export function verifySignedToken<T>(
8888
const expectedSignature = crypto
8989
.createHmac("sha256", secret)
9090
.update(JSON.stringify(payload))
91-
.digest("hex");
91+
.digest();
92+
const actualSignature = Buffer.from(signature, "hex");
9293

93-
if (signature !== expectedSignature) {
94+
if (
95+
actualSignature.length !== expectedSignature.length ||
96+
!crypto.timingSafeEqual(actualSignature, expectedSignature)
97+
) {
9498
return null;
9599
}
96100
// Check expiry

‎packages/trpc/auth.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createHash, randomBytes } from "crypto";
1+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
22
import * as bcrypt from "bcryptjs";
33
import { and, eq } from "drizzle-orm";
44

@@ -124,10 +124,14 @@ export async function authenticateApiKey(key: string, database: Context["db"]) {
124124
case 1:
125125
validation = await bcrypt.compare(keySecret, hash);
126126
break;
127-
case 2:
127+
case 2: {
128+
const candidateHash = createHash("sha256").update(keySecret).digest();
129+
const expectedHash = Buffer.from(hash, "base64");
128130
validation =
129-
createHash("sha256").update(keySecret).digest("base64") == hash;
131+
candidateHash.length === expectedHash.length &&
132+
timingSafeEqual(candidateHash, expectedHash);
130133
break;
134+
}
131135
default:
132136
throw new Error("Invalid API Key");
133137
}

0 commit comments

Comments
 (0)