Browse Source

fix: use Deno native crypto.subtle for HMAC calculation #96

- Replaced Node.js crypto compatibility layer with native Deno API
- Using crypto.subtle.importKey and crypto.subtle.sign as per Deno docs
- This matches the exact pattern from Deno documentation for HMAC-SHA256
Claude 5 months ago
parent
commit
894bcd6e35
1 changed files with 38 additions and 30 deletions
  1. 38 30
      supabase/functions/oauth-shoprenter-callback/index.ts

+ 38 - 30
supabase/functions/oauth-shoprenter-callback/index.ts

@@ -1,7 +1,30 @@
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
 import { wrapHandler, logError } from '../_shared/error-handler.ts'
-import { createHmac, timingSafeEqual } from 'https://deno.land/std@0.168.0/node/crypto.ts'
+
+// Helper function to convert ArrayBuffer to hex string
+function bufferToHex(buffer: ArrayBuffer): string {
+  const byteArray = new Uint8Array(buffer);
+  return Array.from(byteArray)
+    .map((byte) => byte.toString(16).padStart(2, "0"))
+    .join("");
+}
+
+// Calculate HMAC-SHA256 using Deno's native crypto.subtle API
+async function calculateHmacSha256(secret: string, message: string): Promise<string> {
+  const encoder = new TextEncoder();
+  const keyData = encoder.encode(secret);
+  const key = await crypto.subtle.importKey(
+    "raw",
+    keyData,
+    { name: "HMAC", hash: { name: "SHA-256" } },
+    false,
+    ["sign"]
+  );
+  const messageData = encoder.encode(message);
+  const signature = await crypto.subtle.sign("HMAC", key, messageData);
+  return bufferToHex(signature);
+}
 
 
 const corsHeaders = {
 const corsHeaders = {
   'Access-Control-Allow-Origin': '*',
   'Access-Control-Allow-Origin': '*',
@@ -10,7 +33,7 @@ const corsHeaders = {
 
 
 // Validate HMAC signature from ShopRenter
 // Validate HMAC signature from ShopRenter
 // Per ShopRenter documentation, HMAC is calculated from code, shopname, timestamp only
 // Per ShopRenter documentation, HMAC is calculated from code, shopname, timestamp only
-function validateHMAC(params: URLSearchParams, clientSecret: string, clientId: string): boolean {
+async function validateHMAC(params: URLSearchParams, clientSecret: string, clientId: string): Promise<boolean> {
   if (!clientSecret) {
   if (!clientSecret) {
     console.error('[ShopRenter] Client secret is empty or undefined')
     console.error('[ShopRenter] Client secret is empty or undefined')
     return false
     return false
@@ -35,43 +58,28 @@ function validateHMAC(params: URLSearchParams, clientSecret: string, clientId: s
   console.log(`[ShopRenter] HMAC validation - client secret length: ${clientSecret.length}`)
   console.log(`[ShopRenter] HMAC validation - client secret length: ${clientSecret.length}`)
   console.log(`[ShopRenter] HMAC validation - client id length: ${clientId.length}`)
   console.log(`[ShopRenter] HMAC validation - client id length: ${clientId.length}`)
 
 
-  // Calculate HMAC using sha256 with Client Secret
-  const calculatedHmacWithSecret = createHmac('sha256', clientSecret)
-    .update(sortedParams)
-    .digest('hex')
+  // Calculate HMAC using Deno's native crypto.subtle API (SHA-256)
+  const calculatedHmacWithSecret = await calculateHmacSha256(clientSecret, sortedParams)
 
 
   // Also try with Client ID (in case of documentation confusion)
   // Also try with Client ID (in case of documentation confusion)
-  const calculatedHmacWithId = createHmac('sha256', clientId)
-    .update(sortedParams)
-    .digest('hex')
+  const calculatedHmacWithId = await calculateHmacSha256(clientId, sortedParams)
 
 
   console.log(`[ShopRenter] HMAC validation - received hmac: ${hmacValue}`)
   console.log(`[ShopRenter] HMAC validation - received hmac: ${hmacValue}`)
   console.log(`[ShopRenter] HMAC validation - calculated hmac (with secret): ${calculatedHmacWithSecret}`)
   console.log(`[ShopRenter] HMAC validation - calculated hmac (with secret): ${calculatedHmacWithSecret}`)
   console.log(`[ShopRenter] HMAC validation - calculated hmac (with id): ${calculatedHmacWithId}`)
   console.log(`[ShopRenter] HMAC validation - calculated hmac (with id): ${calculatedHmacWithId}`)
 
 
-  // Timing-safe comparison - try both
-  try {
-    const resultWithSecret = timingSafeEqual(
-      new TextEncoder().encode(calculatedHmacWithSecret),
-      new TextEncoder().encode(hmacValue)
-    )
-    console.log(`[ShopRenter] HMAC validation result (with secret): ${resultWithSecret}`)
+  // Compare HMACs
+  const resultWithSecret = calculatedHmacWithSecret === hmacValue
+  console.log(`[ShopRenter] HMAC validation result (with secret): ${resultWithSecret}`)
 
 
-    if (resultWithSecret) {
-      return true
-    }
+  if (resultWithSecret) {
+    return true
+  }
 
 
-    const resultWithId = timingSafeEqual(
-      new TextEncoder().encode(calculatedHmacWithId),
-      new TextEncoder().encode(hmacValue)
-    )
-    console.log(`[ShopRenter] HMAC validation result (with id): ${resultWithId}`)
+  const resultWithId = calculatedHmacWithId === hmacValue
+  console.log(`[ShopRenter] HMAC validation result (with id): ${resultWithId}`)
 
 
-    return resultWithId
-  } catch (error) {
-    console.error('[ShopRenter] HMAC comparison error:', error)
-    return false
-  }
+  return resultWithId
 }
 }
 
 
 // Validate timestamp to prevent replay attacks
 // Validate timestamp to prevent replay attacks
@@ -191,7 +199,7 @@ serve(wrapHandler('oauth-shoprenter-callback', async (req) => {
     }
     }
 
 
     // Validate HMAC using decoded parameter values (per ShopRenter docs)
     // Validate HMAC using decoded parameter values (per ShopRenter docs)
-    if (!validateHMAC(url.searchParams, shoprenterClientSecret, shoprenterClientId)) {
+    if (!(await validateHMAC(url.searchParams, shoprenterClientSecret, shoprenterClientId))) {
       return new Response(
       return new Response(
         JSON.stringify({ error: 'HMAC validation failed' }),
         JSON.stringify({ error: 'HMAC validation failed' }),
         { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
         { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }