@khoadue/react-crypto
v0.1.0
Published
React field-level E2E encryption library with X25519 + AES-256-GCM envelopes, blind indexes, and optional Supabase key registry
Maintainers
Readme
@khoadue/react-crypto
React field-level E2E encryption library. Sensitive fields are encrypted in the browser before they reach your API or database. Only the intended recipient can decrypt.
Algorithms: X25519 (ECDH) + HKDF-SHA-256 + AES-256-GCM
Install
npm install @khoadue/react-cryptoOptional peer for Supabase public-key registry:
npm install @supabase/supabase-jsQuick start
import {
BrowserKeyStore,
EncryptionProvider,
useCryptoKeySetup,
useDecrypt,
useEncrypt,
publicKeyToBase64,
} from '@khoadue/react-crypto';
const keyStore = new BrowserKeyStore();
export function App({ userId }: { userId: string }) {
return (
<EncryptionProvider keyStore={keyStore} keyId={`user:${userId}`}>
<PatientForm />
</EncryptionProvider>
);
}
function PatientForm({ userId, doctorId }: { userId: string; doctorId: string }) {
const { initializeKeys } = useCryptoKeySetup(userId);
const { encrypt, encrypting } = useEncrypt();
const { decrypt, decrypting } = useDecrypt();
async function onSubmit(form: { ssn: string; name: string }) {
await initializeKeys();
const doctorPublicKeyB64 = await fetchDoctorPublicKey(doctorId);
const payload = await encrypt(form, {
ssn: { recipientId: doctorId, publicKeyB64: doctorPublicKeyB64 },
});
await fetch('/api/patients', {
method: 'POST',
body: JSON.stringify(payload),
});
}
async function onLoad(record: Record<string, unknown>) {
return decrypt(record, ['ssn']);
}
// render form using encrypting/decrypting flags for UX
}Envelope format
Encrypted fields are stored as compact strings:
enc:v1:x25519-aes256gcm:{iv}:{ct}:{eph}:{kid}| Part | Meaning |
|------|---------|
| iv | Base64 12-byte AES-GCM IV |
| ct | Base64 ciphertext + auth tag |
| eph | Base64 ephemeral X25519 public key |
| kid | Recipient key id, e.g. user:uuid |
API
Core exports
| Export | Purpose |
|--------|---------|
| EncryptionProvider | Injects KeyStore + active keyId |
| useEncrypt() | Encrypt selected fields before submit |
| useDecrypt() | Decrypt encrypted fields after fetch |
| useCryptoKeySetup(userId) | Generate/load user keypair in key store |
| encryptField / encryptFields | Low-level encrypt helpers |
| decryptField / decryptFields | Low-level decrypt helpers |
| generateBlindIndex | HMAC blind index for searchable encrypted fields |
| BrowserKeyStore | IndexedDB-backed key storage (browser) |
| MemoryKeyStore | In-memory key storage (tests/SSR injection) |
| isEncryptedField | Detect envelope strings |
Supabase adapter
import { createSupabaseKeyRegistry } from '@khoadue/react-crypto/supabase';
import { supabase } from './supabase-client';
const registry = createSupabaseKeyRegistry({
supabase,
userId: currentUserId,
keyStore,
});
await registry.registerPublicKey();
const doctorKey = await registry.fetchPublicKey(doctorUserId);Expects a user_public_keys table (see e2e-field-encryption skill schema): user_id, public_key, key_type, is_active.
Security notes
- Private keys never leave the client key store.
- Do not log plaintext, private keys, or envelope contents.
- Validate inputs before encrypting; treat decrypted output as sensitive.
- Blind indexes enable equality search but are not full-text search.
Development
npm install
npm run build --workspace=@khoadue/react-crypto
npm run test --workspace=@khoadue/react-cryptoLicense
MIT
