Skip to Content
CoreEncryptionFrontend Decryption

Frontend Decryption

The @djangocfg/crypto package provides client-side decryption for Django-CFG encrypted API responses.

NPM Package: @djangocfg/crypto

Installation

pnpm add @djangocfg/crypto

Features

  • Web Crypto API - Native browser encryption, zero dependencies
  • TypeScript - Full type coverage
  • React Hooks - Declarative decryption patterns
  • Tree-Shakeable - Only imports what you use

Basic Usage

Create Decryption Client

import { createDecryptionClient } from '@djangocfg/crypto'; const crypto = await createDecryptionClient({ secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, userId: user.id, // Optional, for per-user keys iterations: 100_000, // Must match backend (default: 100,000) });

Decrypt API Response

const response = await fetch('/api/products/?encrypt=true'); const encrypted = await response.json(); // Recursively decrypts all encrypted fields const products = await crypto.decryptObject(encrypted); console.log(products[0].price); // Decrypted value

Decrypt Single Field

if (crypto.isEncryptedField(product.price)) { const price = await crypto.decryptField<number>(product.price); console.log(price); // 99.99 }

React Hooks

useDecrypt

Automatically decrypt data on mount:

import { useDecrypt } from '@djangocfg/crypto/react'; function ProductCard({ product }: { product: EncryptedProduct }) { const { data, isLoading, error } = useDecrypt(product, { secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, userId: user.id, }); if (isLoading) return <Skeleton />; if (error) return <Error message={error.message} />; return ( <div> <h2>{data.name}</h2> <span>${data.price}</span> </div> ); }

useLazyDecrypt

Decrypt on demand with manual trigger:

import { useLazyDecrypt } from '@djangocfg/crypto/react'; function ProductPrice({ product }: { product: EncryptedProduct }) { const { decrypt, data, isLoading } = useLazyDecrypt({ secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, }); return ( <div> <button onClick={() => decrypt(product)} disabled={isLoading}> {isLoading ? 'Decrypting...' : 'Show Price'} </button> {data && <span>${data.price}</span>} </div> ); }

useDecryptionClient

Create a memoized client for multiple decryptions:

import { useDecryptionClient } from '@djangocfg/crypto/react'; function ProductList({ products }: { products: EncryptedProduct[] }) { const { client, isReady, error } = useDecryptionClient({ secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, userId: user.id, }); if (!isReady) return <Loading />; return ( <ul> {products.map(product => ( <ProductItem key={product.id} product={product} client={client!} /> ))} </ul> ); }

useIsEncrypted

Check if a value is encrypted:

import { useIsEncrypted } from '@djangocfg/crypto/react'; function PriceDisplay({ value }: { value: unknown }) { const isEncrypted = useIsEncrypted(value); if (isEncrypted) { return <EncryptedBadge />; } return <span>{value}</span>; }

Integration Examples

With TanStack Query

import { useQuery } from '@tanstack/react-query'; import { useDecryptionClient } from '@djangocfg/crypto/react'; function useProducts() { const { client } = useDecryptionClient({ secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, }); return useQuery({ queryKey: ['products'], queryFn: async () => { const res = await fetch('/api/products/?encrypt=true'); const encrypted = await res.json(); return client?.decryptObject(encrypted); }, enabled: !!client, }); }

With SWR

import useSWR from 'swr'; import { createDecryptionClient } from '@djangocfg/crypto'; // Create client once at module level let cryptoClient: Awaited<ReturnType<typeof createDecryptionClient>> | null = null; async function getClient() { if (!cryptoClient) { cryptoClient = await createDecryptionClient({ secretKey: process.env.NEXT_PUBLIC_DECRYPT_KEY!, }); } return cryptoClient; } const fetcher = async (url: string) => { const client = await getClient(); const res = await fetch(url); const encrypted = await res.json(); return client.decryptObject(encrypted); }; function ProductList() { const { data } = useSWR('/api/products/?encrypt=true', fetcher); // ... }

TypeScript Types

// Encrypted field envelope interface EncryptedField { encrypted: true; field?: string; algorithm: 'AES-256-GCM' | 'AES-256-CBC'; iv: string; // base64 data: string; // base64 auth_tag: string; // base64 } // Configuration interface DecryptionConfig { secretKey: string; userId?: string | number; sessionId?: string; iterations?: number; // default: 100,000 keyPrefix?: string; // default: "djangocfg_encryption" } // Result type for safe decryption type DecryptionResult<T> = | { success: true; data: T } | { success: false; message: string; code: DecryptionErrorCode }; type DecryptionErrorCode = | 'INVALID_FORMAT' | 'KEY_ERROR' | 'AUTH_FAILED' | 'DECRYPTION_FAILED';

API Reference

Core Functions

FunctionDescription
createDecryptionClient(config)Create client with cached key
decryptField<T>(field, key)Decrypt single field
decryptObject<T>(data, key)Recursively decrypt object
isEncryptedField(value)Type guard for encrypted fields
safeDecrypt<T>(fn)Error-handled decryption

Key Derivation

FunctionDescription
deriveKeyFromConfig(config)Derive key from config (PBKDF2)
deriveKey(password, salt, iterations)Derive key from password + salt
buildSalt(keyPrefix, userId, sessionId)Generate salt from components

Error Handling

Use safeDecrypt for error handling:

import { safeDecrypt, createDecryptionClient } from '@djangocfg/crypto'; const crypto = await createDecryptionClient({ secretKey: '...' }); const result = await safeDecrypt(() => crypto.decryptObject(data)); if (result.success) { console.log(result.data); } else { console.error(result.code, result.message); // Handle: 'INVALID_FORMAT' | 'KEY_ERROR' | 'AUTH_FAILED' | 'DECRYPTION_FAILED' }

Browser Compatibility

BrowserVersion
Chrome37+
Firefox34+
Safari11+
Edge12+
Node.js15+ (with Web Crypto)

Security Notes

Important Security Considerations

  1. Environment Variables - Store secretKey in env variables, never hardcode
  2. Per-User Keys - Use userId for user isolation
  3. HTTPS Required - Key derivation happens in browser memory
  4. Iterations Match - iterations must match backend (default: 100,000)

See Also

Last updated on