Browse Source

refactor(security): replace timingSafeEqual with XOR loop for edge runtime compat

ding113 2 weeks ago
parent
commit
e2c5b5af77
1 changed files with 23 additions and 12 deletions
  1. 23 12
      src/lib/security/constant-time-compare.ts

+ 23 - 12
src/lib/security/constant-time-compare.ts

@@ -1,27 +1,38 @@
-import { timingSafeEqual } from "node:crypto";
+const encoder = new TextEncoder();
 
 /**
  * Constant-time string comparison to prevent timing attacks.
  *
- * Uses crypto.timingSafeEqual internally. When lengths differ, a dummy
+ * Uses bitwise XOR accumulation instead of node:crypto.timingSafeEqual
+ * to remain compatible with Edge Runtime. When lengths differ, a dummy
  * comparison is still performed so the total CPU time does not leak
  * length information.
  */
 export function constantTimeEqual(a: string, b: string): boolean {
-  const bufA = Buffer.from(a, "utf-8");
-  const bufB = Buffer.from(b, "utf-8");
+  const bufA = encoder.encode(a);
+  const bufB = encoder.encode(b);
 
-  if (bufA.length !== bufB.length) {
+  const lenA = bufA.byteLength;
+  const lenB = bufB.byteLength;
+
+  if (lenA !== lenB) {
     // Pad both to the same length so the dummy comparison time does not
     // leak which side is shorter (attacker may control either one).
-    const padLen = Math.max(bufA.length, bufB.length);
-    const padA = Buffer.alloc(padLen);
-    const padB = Buffer.alloc(padLen);
-    bufA.copy(padA);
-    bufB.copy(padB);
-    timingSafeEqual(padA, padB);
+    const padLen = Math.max(lenA, lenB);
+    const padA = new Uint8Array(padLen);
+    const padB = new Uint8Array(padLen);
+    padA.set(bufA);
+    padB.set(bufB);
+    let dummy = 0;
+    for (let i = 0; i < padLen; i++) {
+      dummy |= padA[i] ^ padB[i];
+    }
     return false;
   }
 
-  return timingSafeEqual(bufA, bufB);
+  let result = 0;
+  for (let i = 0; i < lenA; i++) {
+    result |= bufA[i] ^ bufB[i];
+  }
+  return result === 0;
 }