feat: v2 — queue matchmaking, procedural audio, sprite archetypes, auth
- 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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
335c148866
commit
47d20fbe66
@@ -0,0 +1,147 @@
|
||||
---
|
||||
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<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)
|
||||
```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
|
||||
Reference in New Issue
Block a user