Client Integration

The Capsule client is a lightweight browser library that handles key management and content decryption using the Web Crypto API.

Installation

npm install @sesamy/capsule

Basic Usage

import { CapsuleClient } from '@sesamy/capsule';

// Initialize client
const client = new CapsuleClient({
  keyId: 'user-keys',
  keySize: 2048, // or 4096
});

// Check if keys exist, generate if needed
const hasKeys = await client.hasKeyPair();
if(!hasKeys) {
  await client.generateKeyPair();
}

// Get public key to send to server
const publicKey = await client.getPublicKey(); // Base64 SPKI

// Decrypt content
const decrypted = await client.decryptArticle({
  encryptedContent: 'base64...',
  iv: 'base64...',
  encryptedDek: 'base64...'
});

API Reference

Constructor Options

interface CapsuleClientOptions {
  keySize?: 2048 | 4096;        // RSA key size(default: 2048)
  dbName?: string;              // IndexedDB database name
  storeName?: string;           // IndexedDB store name
  keyId?: string;               // Key identifier(default: 'default')
}

Methods

generateKeyPair()

Generates a new RSA-OAEP key pair and stores it in IndexedDB.

const publicKeyB64 = await client.generateKeyPair();
// Returns: Base64-encoded SPKI public key

hasKeyPair()

Checks if a key pair exists in storage.

const exists = await client.hasKeyPair();
// Returns: boolean

getPublicKey()

Retrieves the stored public key in Base64 SPKI format.

const publicKey = await client.getPublicKey();
// Returns: Base64-encoded SPKI public key

decryptArticle(payload)

Decrypts an encrypted article payload.

const content = await client.decryptArticle({
  encryptedContent: 'base64-ciphertext',
  iv: 'base64-iv',
  encryptedDek: 'base64-wrapped-dek'
});
// Returns: Decrypted content as string

decryptContent(payload)

Decrypts content and returns raw ArrayBuffer.

const buffer = await client.decryptContent(payload);
// Returns: ArrayBuffer

clearKeys()

Deletes all stored keys from IndexedDB.

await client.clearKeys();

React Integration

import { useState, useEffect } from 'react';
import { CapsuleClient } from '@sesamy/capsule';

export function useCapsule() {
  const [client, setClient] = useState(null);
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    async function init() {
      const capsule = new CapsuleClient({ keyId: 'demo-key' });
      
      const hasKeys = await capsule.hasKeyPair();
      if(!hasKeys) {
        await capsule.generateKeyPair();
      }
      
      setClient(capsule);
      setIsReady(true);
    }
    
    init();
  }, []);

  return { client, isReady };
}

// Usage
function Article({ encryptedData }) {
  const { client, isReady } = useCapsule();
  const [content, setContent] = useState(null);

  const unlock = async() => {
    const publicKey = await client.getPublicKey();
    
    // Get wrapped DEK from server
    const res = await fetch('/api/unlock', {
      method: 'POST',
      body: JSON.stringify({ tier: 'premium', publicKey })
    });
    const { encryptedDek } = await res.json();
    
    // Decrypt
    const decrypted = await client.decryptArticle({
      ...encryptedData,
      encryptedDek
    });
    
    setContent(decrypted);
  };

  return(
    <div>
      {content ? (
        <div>{content}</div>
      ) : (
        <button onClick={unlock} disabled={!isReady}>
          Unlock
        </button>
      )}
    </div>
  );
}

Advanced: DEK Caching

For subscription-based models, cache unwrapped DEKs to avoid repeated server requests:

const dekCache = new Map(); // tier → CryptoKey

async function unlockTier(tier, client) {
  // Check cache first
  if(dekCache.has(tier)) {
    return dekCache.get(tier);
  }
  
  // Get public key
  const publicKey = await client.getPublicKey();
  
  // Request wrapped DEK from server
  const res = await fetch('/api/unlock', {
    method: 'POST',
    body: JSON.stringify({ tier, publicKey })
  });
  const { encryptedDek } = await res.json();
  
  // Unwrap DEK
  const dek = await crypto.subtle.unwrapKey(
    'raw',
    base64ToArrayBuffer(encryptedDek),
    client.privateKey, // Access via client internals
    { name: 'RSA-OAEP' },
    { name: 'AES-GCM', length: 256 },
    false,
    ['decrypt']
  );
  
  // Cache for session
  dekCache.set(tier, dek);
  return dek;
}

Browser Compatibility

Capsule requires the Web Crypto API, which is available in:

Note: Web Crypto API is only available in secure contexts (HTTPS or localhost).