- Add queue-based matchmaking with Elo-proximity and 10s timeout - Procedural sound engine (SFX, voice announcer, 4-track music) - Sprite system refactored into 6 archetypes (standard, lobster, sheep, cyborg, blob, tank) - 42+ fight choreographies with themed/generic/wild card selection - 4 KO finish styles, super-speed mode, hyperdetail close-ups - Auth routes, JoinBout page, bot profile with stats - 7-tier ranking system (Baby through Legend) - Arena and challenge system expansions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
4.3 KiB
4.3 KiB
description, match
| description | match | |||||||
|---|---|---|---|---|---|---|---|---|
| E2E encrypted records and group encryption for the Syntropy Institute portal |
|
Encrypted Records Skill
When to Use
When working with E2E encrypted client records, community posts, or group key management.
NIP-44 Encryption (Practitioner <-> Client)
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
// 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
// Encrypt post content with the channel's symmetric group key
async function encryptPostWithGroupKey(
groupKey: Uint8Array,
content: string
): Promise<string> {
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<string> {
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)
// 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
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