Decrypt Webhook Payload

Securely decrypt and verify webhook payloads

All OZZOBiT webhooks are encrypted using AES-256-GCM encryption to ensure data integrity and prevent tampering. You must decrypt payloads using your webhook secret key before processing them.

Get Your Webhook Secret

  1. Navigate to your Partner Dashboard
  2. Go to Settings → Webhooks
  3. Copy your Webhook Secret Key
  4. Store it securely in your environment variables

Decryption Implementation

OZZOBiT-webhook.tstypescript
// lib/OZZOBiT-webhook.ts
import crypto from 'crypto'

const ALGORITHM = 'aes-256-gcm'
const AUTH_TAG_LENGTH = 16
const IV_LENGTH = 12

interface DecryptedPayload {
  eventName: string
  data: Record<string, any>
  timestamp: string
}

export function decryptWebhookPayload(
  encryptedPayload: string,
  webhookSecret: string
): DecryptedPayload {
  // The payload is base64 encoded
  const encryptedBuffer = Buffer.from(encryptedPayload, 'base64')
  
  // Extract components (OZZOBiT format: iv + authTag + ciphertext)
  const iv = encryptedBuffer.subarray(0, IV_LENGTH)
  const authTag = encryptedBuffer.subarray(
    IV_LENGTH,
    IV_LENGTH + AUTH_TAG_LENGTH
  )
  const ciphertext = encryptedBuffer.subarray(IV_LENGTH + AUTH_TAG_LENGTH)
  
  // Create decipher
  const decipher = crypto.createDecipheriv(
    ALGORITHM,
    Buffer.from(webkeySecret.padStart(32, '0').slice(0, 32), 'utf-8'),
    iv
  )
  decipher.setAuthTag(authTag)
  
  // Decrypt
  let decrypted: string
  try {
    decrypted = decipher.update(ciphertext, undefined, 'utf8')
    decrypted += decipher.final('utf8')
  } catch (error) {
    throw new Error(`Decryption failed: ${error.message}`)
  }
  
  return JSON.parse(decrypted)
}

// Verify signature (additional security layer)
export function verifyWebhookSignature(
  payload: string,
  signature: string,
  webhookSecret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex')
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  )
}

// Next.js Route Handler example:
// app/api/webhooks/OZZOBiT/route.ts
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  // Get raw body for signature verification
  const payload = await request.text()
  const signature = request.headers.get('x-OZZOBiT-signature')!
  
  // Verify signature first
  if (!verifyWebhookSignature(payload, signature, process.env.OZZOBiT_WEBHOOK_SECRET!)) {
    return new Response('Invalid signature', { status: 401 })
  }
  
  // Decrypt payload
  const event = decryptWebhookPayload(
    payload,
    process.env.OZZOBiT_WEBHOOK_SECRET!
  )
  
  console.log('Received event:', event.eventName)
  
  // Handle event...
  switch (event.eventName) {
    case 'ORDER_COMPLETED':
      await handleOrderCompleted(event.data)
      break
    case 'ORDER_FAILED':
      await handleOrderFailed(event.data)
      break
  }
  
  return Response.json({ success: true })
}
OZZOBiTWebhookDecryptor.javajava
// OZZOBiTWebhookDecryptor.java
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class OZZOBiTWebhookDecryptor {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;

    public static String decrypt(String encryptedPayload, String webhookSecret) throws Exception {
        byte[] decoded = Base64.getDecoder().decode(encryptedPayload);
        
        // Extract IV (first 12 bytes)
        byte[] iv = new byte[GCM_IV_LENGTH];
        System.arraycopy(decoded, 0, iv, 0, GCM_IV_LENGTH);
        
        // Prepare key (pad or truncate to 32 bytes for AES-256)
        byte[] keyBytes = Arrays.copyOf(
            webhookSecret.getBytes(StandardCharsets.UTF_8), 
            32
        );
        
        // Initialize cipher
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
        
        // Decrypt (remaining bytes after IV)
        byte[] ciphertext = Arrays.copyOfRange(decoded, GCM_IV_LENGTH, decoded.length);
        byte[] decrypted = cipher.doFinal(ciphertext);
        
        return new String(decrypted, StandardCharsets.UTF_8);
    }

    public static boolean verifySignature(
        String payload, 
        String receivedSignature, 
        String webhookSecret
    ) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        SecretKeySpec keySpec = new SecretKeySpec(
            webhookSecret.getBytes(StandardCharsets.UTF_8), 
            "HmacSHA256"
        );
        mac.init(keySpec);
        byte[] expectedHash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
        String expectedSig = Base64.getEncoder().encodeToString(expectedHash);
        
        return MessageDigest.isEqual(
            expectedSig.getBytes(StandardCharsets.UTF_8),
            receivedSignature.getBytes(StandardCharsets.UTF_8)
        );
    }
}

// Spring Boot Controller example:
@RestController
@RequestMapping("/api/webhooks")
public class OZZOBiTWebhookController {

    @Value("${OZZOBiT.webhook.secret}")
    private String webhookSecret;

    @PostMapping("/OZZOBiT")
    public ResponseEntity<?> handleWebhook(
        @RequestBody String payload,
        @RequestHeader("X-OZZOBiT-Signature") String signature
    ) {
        try {
            // Verify signature
            if (!OZZOBiTWebhookDecryptor.verifySignature(payload, signature, webhookSecret)) {
                return ResponseEntity.status(401).body("Invalid signature");
            }
            
            // Decrypt payload
            String decrypted = OZZOBiTWebhookDecryptor.decrypt(payload, webhookSecret);
            JsonObject event = JsonParser.parseString(decrypted).getAsJsonObject();
            
            String eventName = event.get("eventName").getAsString();
            
            switch (eventName) {
                case "ORDER_COMPLETED":
                    handleOrderCompleted(event.getAsJsonObject("data"));
                    break;
                case "ORDER_FAILED":
                    handleOrderFailed(event.getAsJsonObject("data"));
                    break;
            }
            
            return ResponseEntity.ok(Map.of("success", true));
            
        } catch (Exception e) {
            log.error("Webhook processing error", e);
            return ResponseEntity.status(500).body("Processing error");
        }
    }
}
OZZOBiT_webhook.gogo
// OZZOBiT_webhook.go
package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "errors"
    "io"
)

const (
    algorithm     = "aes-256-gcm"
    ivLength      = 12
    tagLength     = 16
)

type WebhookEvent struct {
    EventName string                 `json:"eventName"`
    Data      map[string]interface{} `json:"data"`
    Timestamp string                 `json:"timestamp"`
}

func DecryptWebhookPayload(encryptedPayload, webhookSecret string) (*WebhookEvent, error) {
    // Decode base64
    ciphertext, err := base64.StdEncoding.DecodeString(encryptedPayload)
    if err != nil {
        return nil, err
    }
    
    // Extract IV (first 12 bytes)
    if len(ciphertext) < ivLength+tagLength {
        return nil, errors.New("payload too short")
    }
    
    iv := ciphertext[:ivLength]
    tag := ciphertext[ivLength : ivLength+tagLength]
    encData := ciphertext[ivLength+tagLength:]
    
    // Prepare key
    key := []byte(webhookSecret)
    if len(key) > 32 {
        key = key[:32]
    }
    for len(key) < 32 {
        key = append(key, 0)
    }
    
    // Create block cipher
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }
    
    // Create GCM mode with tag appended
    gcm, err := cipher.NewGCMWithTagSize(block, tagLength*8)
    if err != nil {
        return nil, err
    }
    
    // Decrypt (ciphertext includes tag)
    plaintext, err := gcm.Open(nil, iv, append(encData, tag...), nil)
    if err != nil {
        return nil, err
    }
    
    var event WebhookEvent
    if err := json.Unmarshal(plaintext, &event); err != nil {
        return nil, err
    }
    
    return &event, nil
}

func VerifyWebhookSignature(payload, signature, webhookSecret string) bool {
    mac := hmac.New(sha256.New, []byte(webhookSecret))
    io.WriteString(mac, payload)
    expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expectedSig))
}

// HTTP handler example (using net/http):
func OZZOBiTWebhookHandler(w http.ResponseWriter, r *http.Request) {
    // Read body
    body, _ := io.ReadAll(r.Body)
    defer r.Body.Close()
    
    signature := r.Header.Get("X-OZZOBiT-Signature")
    
    // Verify signature
    if !VerifyWebhookSignature(string(body), signature, webhookSecret) {
        w.WriteHeader(http.StatusUnauthorized)
        w.Write([]byte("Invalid signature"))
        return
    }
    
    // Decrypt
    event, err := DecryptWebhookPayload(string(body), webhookSecret)
    if err != nil {
        w.WriteHeader(http.StatusBadRequest)
        w.Write([]byte("Decryption failed"))
        return
    }
    
    // Handle event
    switch event.EventName {
    case "ORDER_COMPLETED":
        handleOrderCompleted(event.Data)
    case "ORDER_FAILED":
        handleOrderFailed(event.Data)
    }
    
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]bool{"success": true})
}
⚠️
Important Security Notes
  • Always verify the signature before decrypting
  • Use constant-time comparison (crypto.timingSafeEqual) to prevent timing attacks
  • Never log the raw encrypted payload or your webhook secret
  • Return HTTP 200 quickly even if processing fails to avoid unnecessary retries

Testing Your Webhook Handler

Use a Tunneling Service

Use ngrok, cloudflared tunnel, or similar to expose your local endpoint:

bashbash
ngrok http 3000

Register the URL

Add your tunnel URL (e.g., https://abc123.ngrok.io/api/webhooks/OZZOBiT) in your dashboard under Settings → Webhooks.

Trigger a Test Webhook

Create a sandbox order through the widget or API to trigger a test webhook delivery.