Skip to main content

useRelationshipEncryption

Location: src/hooks/useRelationshipEncryption.js

Overview

Manages client-side encryption for relationship data (labels and notes on relationship edges). Uses the Web Crypto API for key derivation and encryption. The derived key is kept in memory only (never persisted to disk), while the user's secret is stored in sessionStorage for session continuity. Supports initialization, unlock/lock, encrypt/decrypt operations, and migration of existing plaintext data to encrypted storage.

Signature

function useRelationshipEncryption(): {
// State
isSupported: boolean,
isInitialized: boolean,
isUnlocked: boolean,
loading: boolean,
error: string | null,
salt: string | null,
// Actions
initializeEncryption: (secret: string) => Promise<{ success?: boolean, error?: string }>,
unlockEncryption: (secret: string) => Promise<{ success?: boolean, error?: string }>,
lockEncryption: () => void,
restoreFromSession: () => Promise<boolean>,
// Encryption helpers
encrypt: (data: { label?: string, notes?: string }) => Promise<{ label_encrypted?, notes_encrypted?, label?, notes? }>,
decrypt: (data: object) => Promise<object>,
decryptMany: (edges: object[]) => Promise<object[]>,
// Migration
getUnencryptedEdges: () => Promise<{ data: object[], error: string | null }>,
migrateEdge: (edge: object) => Promise<{ success?: boolean, error?: string }>,
migrateAllEdges: () => Promise<{ migrated: number, total?: number, errors?: object[] | null, error?: string }>
}

Parameters

This hook takes no parameters. It determines the current user internally.

Return Value

PropertyTypeDescription
isSupportedbooleanWhether the browser supports the Web Crypto API
isInitializedbooleanWhether encryption has been initialized (salt exists in DB)
isUnlockedbooleanWhether the encryption key is currently in memory
loadingbooleanWhether the salt is being fetched
errorstring | nullError message if initialization or fetch failed
saltstring | nullThe user's encryption salt (stored in DB)
initializeEncryption(secret) => Promise<Result>First-time setup: generates a salt, saves it to DB, derives the key
unlockEncryption(secret) => Promise<Result>Derive the encryption key from the user's secret and salt
lockEncryption() => voidClear the key from memory and session storage
restoreFromSession() => Promise<boolean>Attempt to restore the key from session storage
encrypt(data) => Promise<object>Encrypt label and notes fields. Returns plaintext if not unlocked.
decrypt(data) => Promise<object>Decrypt encrypted fields on a relationship object. Returns as-is if not unlocked.
decryptMany(edges) => Promise<object[]>Decrypt an array of relationship edge objects
getUnencryptedEdges() => Promise<Result>Fetch relationship edges that have not been encrypted yet
migrateEdge(edge) => Promise<Result>Encrypt and update a single relationship edge
migrateAllEdges() => Promise<Result>Migrate all unencrypted edges. Returns count of migrated, total, and any errors.

Usage

import { useRelationshipEncryption } from '../hooks/useRelationshipEncryption';

function EncryptionSettings() {
const {
isSupported,
isInitialized,
isUnlocked,
initializeEncryption,
unlockEncryption,
lockEncryption,
migrateAllEdges
} = useRelationshipEncryption();

if (!isSupported) return <p>Encryption is not supported in this browser.</p>;

if (!isInitialized) {
return (
<button onClick={() => initializeEncryption('user-secret')}>
Enable Encryption
</button>
);
}

if (!isUnlocked) {
return (
<button onClick={() => unlockEncryption('user-secret')}>
Unlock Encryption
</button>
);
}

return (
<div>
<p>Encryption is active</p>
<button onClick={lockEncryption}>Lock</button>
<button onClick={migrateAllEdges}>Migrate existing data</button>
</div>
);
}

Notes

  • The encryption key is derived using the user's ID, a secret, and a salt via the getRelationshipKey function from src/lib/encryption.js.
  • The key is stored in a useRef (memory only) and never written to localStorage or the database.
  • The user's secret is stored in sessionStorage (cleared when the browser is closed) for session continuity.
  • If encryption is not unlocked, encrypt returns plaintext data and decrypt returns data as-is (graceful fallback).
  • Migration converts plaintext label and notes fields to encrypted versions using the migrate_relationship_to_encrypted RPC.

Last updated: 2026-02-07