--- description: E2E encrypted records and group encryption for the Syntropy Institute portal match: - encrypt - decrypt - record - NIP-44 - group key - channel - community --- # Encrypted Records Skill ## When to Use When working with E2E encrypted client records, community posts, or group key management. ## NIP-44 Encryption (Practitioner <-> Client) ```typescript import { nip44 } from 'nostr-tools'; // Encrypt record for a specific client function encryptRecord( practitionerSecretKey: Uint8Array, clientPubKey: string, recordData: object ): string { const plaintext = JSON.stringify(recordData); const conversationKey = nip44.v2.utils.getConversationKey(practitionerSecretKey, clientPubKey); return nip44.v2.encrypt(plaintext, conversationKey); } // Decrypt record (client side) function decryptRecord( clientSecretKey: Uint8Array, practitionerPubKey: string, encryptedData: string ): object { const conversationKey = nip44.v2.utils.getConversationKey(clientSecretKey, practitionerPubKey); const plaintext = nip44.v2.decrypt(encryptedData, conversationKey); return JSON.parse(plaintext); } ``` ## Group Encryption (Community Channels) ### Creating a Channel with Group Key ```typescript // Generate a symmetric group key for the channel function generateGroupKey(): Uint8Array { return crypto.getRandomValues(new Uint8Array(32)); } // Encrypt group key for a specific member using NIP-44 function wrapGroupKeyForMember( adminSecretKey: Uint8Array, memberPubKey: string, groupKey: Uint8Array ): string { const conversationKey = nip44.v2.utils.getConversationKey(adminSecretKey, memberPubKey); return nip44.v2.encrypt( btoa(String.fromCharCode(...groupKey)), conversationKey ); } // Member unwraps their group key function unwrapGroupKey( memberSecretKey: Uint8Array, adminPubKey: string, wrappedKey: string ): Uint8Array { const conversationKey = nip44.v2.utils.getConversationKey(memberSecretKey, adminPubKey); const decoded = nip44.v2.decrypt(wrappedKey, conversationKey); return Uint8Array.from(atob(decoded), c => c.charCodeAt(0)); } ``` ### Encrypting Community Posts with Group Key ```typescript // Encrypt post content with the channel's symmetric group key async function encryptPostWithGroupKey( groupKey: Uint8Array, content: string ): Promise { const key = await crypto.subtle.importKey( 'raw', groupKey, 'AES-GCM', false, ['encrypt'] ); const iv = crypto.getRandomValues(new Uint8Array(12)); const encoded = new TextEncoder().encode(content); const ciphertext = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, key, encoded ); const combined = new Uint8Array([...iv, ...new Uint8Array(ciphertext)]); return btoa(String.fromCharCode(...combined)); } // Decrypt post content async function decryptPostWithGroupKey( groupKey: Uint8Array, encrypted: string ): Promise { const combined = Uint8Array.from(atob(encrypted), c => c.charCodeAt(0)); const iv = combined.slice(0, 12); const ciphertext = combined.slice(12); const key = await crypto.subtle.importKey( 'raw', groupKey, 'AES-GCM', false, ['decrypt'] ); const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv }, key, ciphertext ); return new TextDecoder().decode(decrypted); } ``` ### Key Rotation (when member removed) ```typescript // When a member is removed from a channel: // 1. Generate new group key // 2. Re-wrap for all remaining members // 3. Update ChannelMember.encryptedGroupKey for each // 4. New posts use new key; old posts remain readable with old key // (store key version/epoch on each post) ``` ## Record Types ```typescript interface ClientRecord { type: 'session_notes' | 'assessment' | 'treatment_plan'; title: string; content: string; // Rich text or structured data attachments?: { name: string; mimeType: string; encryptedData: string; // Each file encrypted separately }[]; createdAt: string; // ISO timestamp } ``` ## Rules - ALL encryption happens client-side — server stores only encrypted blobs - Use NIP-44 v2 (NOT NIP-04 — it's deprecated and has known weaknesses) - Group keys: AES-256-GCM with random IVs - Include key version/epoch on group-encrypted content for rotation support - Never log or expose plaintext content on the server - Media files are encrypted individually before upload