chalawa-web
v1.0.1
Published
Encryption-in-transit for React / Next.js — powered by the Web Crypto API (no Node.js required)
Downloads
264
Maintainers
Readme
chalawa-web
Encryption-in-transit for React / Next.js — powered by the Web Crypto API (zero dependencies, browser-native).
Fully interoperable with chalawa-node — encrypt on the frontend, decrypt on the backend (and vice versa).
Features
| Feature | Detail |
|---|---|
| AES-256-GCM | Authenticated encryption with built-in tamper detection |
| ECDH P-256 | Modern elliptic-curve key exchange (replaces classic finite-field DH) |
| Zero dependencies | Uses window.crypto.subtle — no Node.js, no polyfills needed |
| Fully async | All functions return Promise — composable with async/await |
| TypeScript | Full type definitions included |
| Interoperable | Wire format identical to chalawa-node |
Installation
npm install chalawa-webRequirements: Any modern browser (Chrome 37+, Firefox 34+, Safari 11+, Edge 79+) or Next.js (client components).
Quick Start
Method 1 — Password-based encryption
import { encrypt, decrypt } from 'chalawa-web';
const password = 'your-shared-secret';
const data = { userId: 123, token: 'abc' };
// Encrypt (e.g. before sending to API)
const cipherText = await encrypt({
plainText: JSON.stringify(data),
password,
});
// Decrypt (e.g. response from server)
const decrypted = await decrypt({
encryptedText: cipherText,
password,
});Method 2 — ECDH key exchange (recommended)
import { generateDHKeyPair, computeSharedSecret, dhEncrypt, dhDecrypt } from 'chalawa-web';
// ── Step 1: Generate your key pair ──────────────────────────────────────────
const clientKeys = await generateDHKeyPair();
// ── Step 2: Exchange public keys with server ─────────────────────────────────
// Send : clientKeys.publicKey → server (130 hex chars)
// Receive: serverPublicKey ← server (130 hex chars)
const res = await fetch('/api/key-exchange', {
method: 'POST',
body: JSON.stringify({ publicKey: clientKeys.publicKey }),
headers: { 'Content-Type': 'application/json' },
});
const { publicKey: serverPublicKey } = await res.json();
// ── Step 3: Derive shared secret (never leaves the device) ───────────────────
const sharedSecret = await computeSharedSecret({
privateKey: clientKeys.privateKey,
otherPublicKey: serverPublicKey,
});
// ── Step 4: Encrypt data before sending ─────────────────────────────────────
const sensitiveData = { creditCard: '4532-xxxx-xxxx-9012' };
const encrypted = await dhEncrypt({
plainText: JSON.stringify(sensitiveData),
sharedSecret,
});
await fetch('/api/secure-data', {
method: 'POST',
body: JSON.stringify({ payload: encrypted }),
headers: { 'Content-Type': 'application/json' },
});
// ── Step 5: Decrypt a response ───────────────────────────────────────────────
const decrypted = await dhDecrypt({ encryptedText: encrypted, sharedSecret });Method 3 — Enhanced security (ECDH + password)
const password = 'additional-layer';
const clientKeys = await generateDHKeyPair({ password });
const sharedSecret = await computeSharedSecret({
privateKey: clientKeys.privateKey,
otherPublicKey: serverPublicKey,
password, // mixed into the secret derivation
});React Hook Example
// hooks/useEncryption.ts
import { useState, useEffect, useCallback } from 'react';
import { generateDHKeyPair, computeSharedSecret, dhEncrypt, dhDecrypt, DHKeyPair } from 'chalawa-web';
export function useEncryption(apiBaseUrl: string) {
const [keys, setKeys] = useState<DHKeyPair | null>(null);
const [sharedSecret, setSharedSecret] = useState<string | null>(null);
// Perform key exchange on mount
useEffect(() => {
(async () => {
const clientKeys = await generateDHKeyPair();
setKeys(clientKeys);
const res = await fetch(`${apiBaseUrl}/key-exchange`, {
method: 'POST',
body: JSON.stringify({ publicKey: clientKeys.publicKey }),
headers: { 'Content-Type': 'application/json' },
});
const { publicKey: serverPub } = await res.json();
const secret = await computeSharedSecret({
privateKey: clientKeys.privateKey,
otherPublicKey: serverPub,
});
setSharedSecret(secret);
})();
}, [apiBaseUrl]);
const sendSecure = useCallback(async (data: object) => {
if (!sharedSecret) throw new Error('Key exchange not complete');
return dhEncrypt({ plainText: JSON.stringify(data), sharedSecret });
}, [sharedSecret]);
const receiveSecure = useCallback(async (encryptedText: string) => {
if (!sharedSecret) throw new Error('Key exchange not complete');
return dhDecrypt({ encryptedText, sharedSecret });
}, [sharedSecret]);
return { ready: !!sharedSecret, sendSecure, receiveSecure };
}Next.js Usage
Use inside Client Components only (Web Crypto requires browser context):
'use client';
import { encrypt, decrypt } from 'chalawa-web';For Server Components / API Routes, use chalawa-node instead.
API Reference
encrypt(options): Promise<string>
| Param | Type | Description |
|---|---|---|
| plainText | string | Data to encrypt |
| password | string | Shared password |
decrypt(options): Promise<any>
| Param | Type | Description |
|---|---|---|
| encryptedText | string | Wire-format string from encrypt() |
| password | string | Same password used to encrypt |
generateDHKeyPair(options?): Promise<DHKeyPair>
Returns { privateKey: string (JWK), publicKey: string (hex uncompressed point) }
computeSharedSecret(options): Promise<string>
Returns a hex string — both parties independently derive the same value.
dhEncrypt(options): Promise<string>
Encrypts using the shared secret. Returns wire-format string.
dhDecrypt(options): Promise<any>
Decrypts using the shared secret. Returns the original parsed value.
validatePublicKey(publicKey: string): boolean
Returns true if the public key is a valid uncompressed P-256 point.
Wire Format
All encrypted payloads use the format:
<base64(ciphertext)>:<base64(iv)>:<base64(authTag)>This format is identical between chalawa-web and chalawa-node.
Security Notes
- Never store the private key in localStorage — use
sessionStorageor keep it in memory - Always use HTTPS for public key exchange
- Generate fresh key pairs per session for perfect forward secrecy
- Validate received public keys with
validatePublicKey()before use
License
MIT
