diff --git a/.planning/ENVIRONMENTS.md b/.planning/ENVIRONMENTS.md new file mode 100644 index 0000000000..d5a4c007dc --- /dev/null +++ b/.planning/ENVIRONMENTS.md @@ -0,0 +1,907 @@ +# Environment Architecture + +**Created:** 2026-01-25 + +## Overview + +CipherBox requires isolated environments to prevent cross-environment interference, particularly with IPNS sequence numbers. This document defines the environment architecture and solves the Web3Auth key isolation problem. + +## The Problem + +**Current State:** All environments share the same Web3Auth project (SAPPHIRE_DEVNET) and client ID. This means: + +- Same user identity → same derived secp256k1 keypair → same Ed25519 IPNS keypair +- IPNS records use monotonically increasing sequence numbers +- If CI publishes seq=100, local dev (fresh DB) tries seq=1 → rejected by network/delegated routing +- Test repeatability is impossible without manual intervention + +## Environment Matrix + +| Environment | IPFS Mode | Web3Auth Network | IPNS Routing | Database | TEE Republishing | Isolation Level | +| -------------- | -------------- | ---------------- | ---------------------- | ------------------- | -------------------- | -------------------------- | +| **Local Dev** | Kubo (offline) | Sapphire Devnet | Mock service | Local Postgres | **Disabled** | Full | +| **CI E2E** | Kubo (offline) | Sapphire Devnet | Mock service (per-run) | Ephemeral Postgres | **Disabled** | Full per-run | +| **Staging** | Kubo (online) | Sapphire Devnet | delegated-ipfs.dev | Managed Postgres | **Active** (testnet) | Shared with Local/CI users | +| **Production** | Pinata | Sapphire Mainnet | delegated-ipfs.dev | Production Postgres | **Active** (mainnet) | Full | + +## Solution: Environment-Aware Key Derivation + +### Recommended Approach: Environment-Specific IPNS Key Derivation + +The IPNS keypair is derived from the user's vault key. By adding an environment-specific context (via the HKDF `info` parameter) to this derivation, we get different IPNS keys per environment while keeping the same Web3Auth identity. + +**Key Insight:** The problematic shared state is the **IPNS keypair**, not the user's encryption keypair. We can: + +1. Keep the same secp256k1 keypair from Web3Auth (same encryption keys) +2. Add environment context only to Ed25519 IPNS key derivation (different IPNS identities) + +This means: + +- Same user can encrypt/decrypt files across environments (if CIDs are known) + - **Security Note:** CID leakage can expose test or production data across environments. Use separate test accounts/credentials for CI vs production, and sanitize or isolate test data to avoid accidental cross-environment access. +- Different IPNS namespaces per environment → no sequence conflicts +- Test accounts work in all environments without conflicts + +### Implementation + +```typescript +// packages/crypto/src/ipns.ts + +// Fixed HKDF salt for domain separation (extract phase) +const HKDF_SALT = 'CipherBox-IPNS-v1'; + +// Environment context prefixes for HKDF info (expand phase) +const ENVIRONMENT_CONTEXT = { + local: 'env:local', + ci: 'env:ci', + staging: 'env:staging', + production: 'env:production', +} as const; + +type Environment = keyof typeof ENVIRONMENT_CONTEXT; + +export function deriveIpnsKeypair( + userSecp256k1PrivateKey: Uint8Array, + folderId: string, + environment: Environment +): { publicKey: Uint8Array; privateKey: Uint8Array } { + // HKDF-SHA256: Fixed salt for domain separation, environment/folder in info for context binding + // This follows RFC 5869 best practices and matches existing deriveKey patterns in the codebase + const info = `${ENVIRONMENT_CONTEXT[environment]}:folder:${folderId}`; + // ... derivation logic using HKDF(IKM=userSecp256k1PrivateKey, salt=HKDF_SALT, info=info) +} +``` + +**Configuration:** + +```bash +# .env +CIPHERBOX_ENVIRONMENT=local # local | ci | staging | production +``` + +### Alternative: Separate Web3Auth Projects + +If you prefer complete user isolation (different user databases per environment): + +| Environment | Web3Auth Project | Network | Client ID | +| ----------- | ----------------- | ---------------- | ---------- | +| Local/CI | cipherbox-dev | Sapphire Devnet | `BK...dev` | +| Staging | cipherbox-staging | Sapphire Devnet | `BK...stg` | +| Production | cipherbox-prod | Sapphire Mainnet | `BK...prd` | + +**Pros:** + +- Complete isolation including encryption keys +- No code changes needed (just config) +- Clear separation of concerns + +**Cons:** + +- Need separate test accounts per environment +- Can't share encrypted data between environments +- More Web3Auth dashboard management + +## Detailed Environment Specifications + +### 1. Local Development Environment + +**Purpose:** Isolated development with persistent state, no network dependencies + +**Infrastructure:** + +```yaml +# docker/docker-compose.local.yml +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: cipherbox_local + ports: + - '5432:5432' + volumes: + - postgres_local_data:/var/lib/postgresql/data + + ipfs: + image: ipfs/kubo:v0.34.0 + command: daemon --offline # KEY: Offline mode + ports: + - '127.0.0.1:5001:5001' # API + - '127.0.0.1:8080:8080' # Gateway + volumes: + - ipfs_local_data:/data/ipfs + + mock-ipns-routing: + build: ../tools/mock-ipns-routing + ports: + - '127.0.0.1:3001:3001' + volumes: + - mock_ipns_data:/data # Persistent mock IPNS records + +volumes: + postgres_local_data: + ipfs_local_data: + mock_ipns_data: +``` + +**Configuration:** + +```bash +# apps/web/.env.local +VITE_WEB3AUTH_CLIENT_ID=BK...dev # Devnet client ID +VITE_API_URL=http://localhost:3000 +VITE_PINATA_GATEWAY_URL=http://localhost:8080/ipfs +VITE_ENVIRONMENT=local + +# apps/api/.env.local +NODE_ENV=development +CIPHERBOX_ENVIRONMENT=local +DB_HOST=localhost +DB_PORT=5432 +DB_DATABASE=cipherbox_local +IPFS_PROVIDER=local +IPFS_LOCAL_API_URL=http://localhost:5001 +IPFS_LOCAL_GATEWAY_URL=http://localhost:8080 +DELEGATED_ROUTING_URL=http://localhost:3001 +JWT_SECRET=local-dev-jwt-secret-change-in-production +``` + +**Characteristics:** + +- Kubo runs with `--offline` flag (no DHT, no peer connections) +- Mock IPNS routing with persistent storage (survives restarts) +- Local Postgres with persistent volume +- Environment context: `local` + +### 2. CI E2E Testing Environment + +**Purpose:** Isolated, reproducible test runs with fresh state each time + +**GitHub Actions Configuration:** + +```yaml +# .github/workflows/e2e.yml +services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: cipherbox_ci + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 5 + + ipfs: + image: ipfs/kubo:v0.34.0 + # No --offline needed in CI (isolated network anyway) + # No volume mounts (ephemeral state) + options: >- + --health-cmd "ipfs id" + --health-interval 10s + --health-timeout 5s + --health-retries 10 + +env: + CIPHERBOX_ENVIRONMENT: ci + DELEGATED_ROUTING_URL: http://localhost:3001 + # Uses same Web3Auth client ID as local (Devnet) + VITE_WEB3AUTH_CLIENT_ID: ${{ secrets.VITE_WEB3AUTH_CLIENT_ID_DEV }} +``` + +**Key Difference from Local:** + +- No persistent volumes (fresh state each run) +- Mock IPNS routing resets via `/reset` endpoint before each test +- Environment context: `ci` +- Same Web3Auth project as local (Sapphire Devnet) + +**Test Setup Pattern:** + +```typescript +// tests/e2e/setup.ts +beforeAll(async () => { + // Reset mock IPNS routing for clean slate + await fetch('http://localhost:3001/reset', { method: 'POST' }); + + // Database is already fresh (CI service container) +}); +``` + +> **Security Note:** The `/reset` endpoint must be disabled or removed outside local development and CI environments (staging/production) to prevent accidental state resets. See Finding #4 in the security review. + +### 3. Staging Environment + +**Purpose:** Production-like environment for integration testing, real IPFS network + +**Infrastructure:** + +```yaml +# docker/docker-compose.staging.yml +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: cipherbox_staging + volumes: + - postgres_staging_data:/var/lib/postgresql/data + + ipfs: + image: ipfs/kubo:v0.34.0 + # NO --offline flag (publishes to real DHT) + ports: + - '4001:4001/tcp' # Swarm + - '4001:4001/udp' + - '127.0.0.1:5001:5001' # API + - '127.0.0.1:8080:8080' # Gateway + environment: + IPFS_PROFILE: server # Production-oriented config + volumes: + - ipfs_staging_data:/data/ipfs + +volumes: + postgres_staging_data: + ipfs_staging_data: +``` + +**Configuration:** + +```bash +# Staging environment +VITE_WEB3AUTH_CLIENT_ID=BK...dev # Same Devnet client ID +VITE_API_URL=https://staging-api.cipherbox.io +VITE_ENVIRONMENT=staging + +CIPHERBOX_ENVIRONMENT=staging +NODE_ENV=production +IPFS_PROVIDER=local +DELEGATED_ROUTING_URL=https://delegated-ipfs.dev # Real routing +JWT_SECRET=${{ secrets.JWT_SECRET_STAGING }} +``` + +**Characteristics:** + +- Kubo publishes to real DHT (content discoverable) +- Uses real delegated-ipfs.dev for IPNS +- Same Sapphire Devnet Web3Auth (shared identity with local/CI) +- Environment context: `staging` (different IPNS keys than local/CI) +- Can be used for user acceptance testing + +### 4. Production Environment + +**Purpose:** Live user-facing environment with production credentials + +**Infrastructure:** + +- Managed PostgreSQL (AWS RDS / Cloud SQL / etc.) +- Pinata for IPFS pinning (redundant, managed) +- Optional: Self-hosted Kubo cluster for reads + +**Configuration:** + +```bash +# Production environment +VITE_WEB3AUTH_CLIENT_ID=BK...prod # DIFFERENT - Production client ID +VITE_API_URL=https://api.cipherbox.io +VITE_ENVIRONMENT=production # Network is derived from this (see web3auth/config.ts) + +CIPHERBOX_ENVIRONMENT=production +NODE_ENV=production +IPFS_PROVIDER=pinata +PINATA_JWT=${{ secrets.PINATA_JWT }} +DELEGATED_ROUTING_URL=https://delegated-ipfs.dev +JWT_SECRET=${{ secrets.JWT_SECRET_PRODUCTION }} +``` + +**Critical Differences:** + +- **Separate Web3Auth Project** (Sapphire Mainnet) +- Different client ID (complete user isolation from dev/staging) +- Pinata for production-grade IPFS pinning +- Environment context: `production` + +## Web3Auth Project Setup + +### Required Projects + +1. **cipherbox-dev** (Sapphire Devnet) + - Used for: Local dev, CI, Staging + - Dashboard: Configure test accounts with static OTP + - Grouped connections: `cipherbox-grouped-connection` + - OAuth connections: `cipherbox-google-oauth-2`, `cb-email-testnet` + +2. **cipherbox-prod** (Sapphire Mainnet) + - Used for: Production only + - Dashboard: Real OAuth credentials + - Grouped connections: Same structure, production OAuth apps + +### Test Account Setup + +For Local/CI/Staging (Devnet project): + +1. Create test email in Web3Auth dashboard +2. Enable "Test User" mode (static OTP: 000000) +3. Store in GitHub Secrets: + ```bash + WEB3AUTH_TEST_EMAIL=test@cipherbox.dev + WEB3AUTH_TEST_OTP=000000 + ``` + +### Code Changes for Network Switching + +```typescript +// apps/web/src/lib/web3auth/config.ts +import { WEB3AUTH_NETWORK } from '@web3auth/modal'; + +const NETWORK_CONFIG = { + local: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, + ci: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, + staging: WEB3AUTH_NETWORK.SAPPHIRE_DEVNET, + production: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET, +} as const; + +export const web3AuthOptions: Web3AuthOptions = { + clientId: import.meta.env.VITE_WEB3AUTH_CLIENT_ID, + web3AuthNetwork: NETWORK_CONFIG[import.meta.env.VITE_ENVIRONMENT || 'local'], + // ... rest of config +}; +``` + +## Implementation Checklist + +### Phase 1: Environment Salt for IPNS Keys + +- [ ] Add `CIPHERBOX_ENVIRONMENT` env var to API +- [ ] Add `VITE_ENVIRONMENT` env var to web app +- [ ] Modify Ed25519 key derivation to include environment salt +- [ ] Update mock IPNS routing to support persistent storage mode + +### Phase 2: Docker Compose Profiles + +- [ ] Create `docker-compose.local.yml` with offline Kubo +- [ ] Create `docker-compose.staging.yml` with online Kubo +- [ ] Add npm scripts: `dev:local`, `dev:staging` +- [ ] Document volume management for data persistence + +### Phase 3: CI Updates + +- [ ] Update e2e.yml to pass `CIPHERBOX_ENVIRONMENT=ci` +- [ ] Add mock IPNS reset to test setup +- [ ] Verify test isolation + +### Phase 4: Production Web3Auth Setup + +- [ ] Create production Web3Auth project (Sapphire Mainnet) +- [ ] Configure production OAuth apps (Google, etc.) +- [ ] Update web app to switch networks based on environment +- [ ] Add `VITE_WEB3AUTH_CLIENT_ID_PROD` secret + +## Environment Variable Reference + +### Web App (Vite) + +| Variable | Local | CI | Staging | Production | +| ------------------------- | -------------------------- | -------------------------- | --------------------------------- | --------------------------------- | +| `VITE_WEB3AUTH_CLIENT_ID` | dev | dev | dev | **prod** | +| `VITE_API_URL` | http://localhost:3000 | http://localhost:3000 | https://staging-api.cipherbox.io | https://api.cipherbox.io | +| `VITE_PINATA_GATEWAY_URL` | http://localhost:8080/ipfs | http://localhost:8080/ipfs | https://gateway.pinata.cloud/ipfs | https://gateway.pinata.cloud/ipfs | +| `VITE_ENVIRONMENT` | local | ci | staging | production | + +### API (NestJS) + +| Variable | Local | CI | Staging | Production | +| ----------------------- | --------------- | -------------- | --------------------- | ------------------ | +| `NODE_ENV` | development | test | production | production | +| `CIPHERBOX_ENVIRONMENT` | local | ci | staging | production | +| `DB_DATABASE` | cipherbox_local | cipherbox_ci | cipherbox_staging | cipherbox_prod | +| `IPFS_PROVIDER` | local | local | local | pinata | +| `DELEGATED_ROUTING_URL` | localhost:3001 | localhost:3001 | delegated-ipfs.dev | delegated-ipfs.dev | +| `JWT_SECRET` | dev-secret | ci-secret | secrets.JWT_STAGING | secrets.JWT_PROD | +| `TEE_ENABLED` | **false** | **false** | true | true | +| `TEE_PROVIDER` | - | - | phala | phala | +| `PHALA_API_URL` | - | - | testnet.phala.network | api.phala.network | + +## TEE Infrastructure by Environment + +The TEE (Trusted Execution Environment) layer handles IPNS republishing to prevent record expiry (24-hour TTL). This section defines TEE requirements per environment. + +### TEE Environment Matrix + +| Environment | TEE Infrastructure | IPNS Republishing | Monitoring | Cleanup | +| -------------- | ---------------------- | ----------------- | ----------------- | -------------------------- | +| **Local Dev** | None | Not needed | None | User resets to clean slate | +| **CI E2E** | None | Not needed | None | Ephemeral per-run | +| **Staging** | Active (Phala testnet) | Every 3 hours | Integration tests | Periodic DHT cleanup | +| **Production** | Active (Phala mainnet) | Every 3 hours | Full alerting | Automated stale detection | + +### Local Development: No TEE + +**Rationale:** + +- IPNS records stored in mock routing service (persistent volume) +- No real DHT propagation = no expiry concerns +- Users working on non-TEE features don't need republishing complexity +- If IPNS state becomes corrupted, user can reset: `docker-compose down -v && docker-compose up` + +**Configuration:** + +```bash +# apps/api/.env.local +TEE_ENABLED=false +# No TEE_* environment variables needed +``` + +**When working on TEE features locally:** + +- Use staging environment for TEE development/testing +- Or run mock TEE service (future: `tools/mock-tee-service`) + +### CI E2E Testing: No TEE + +**Rationale:** + +- Test runs complete in minutes, well within 24-hour IPNS TTL +- No benefit to republishing during test execution +- Mock IPNS routing service resets per-run (no accumulated state) +- Simpler CI pipeline without TEE service dependencies + +**Configuration:** + +```yaml +# .github/workflows/e2e.yml +env: + TEE_ENABLED: 'false' + # No TEE service container needed +``` + +**E2E Test Considerations:** + +- Tests should NOT depend on IPNS records surviving between test runs +- Each test suite starts with fresh vault state +- IPNS sequence numbers start at 1 for each test user + +### Staging Environment: Active TEE with Testnet + +**Purpose:** Integration testing of full CipherBox + TEE + public IPFS stack + +**Infrastructure:** + +```yaml +# docker/docker-compose.staging.yml (additions) +services: + # ... existing services ... + + # Staging uses real Phala Cloud testnet + # TEE service is external - no local container + +# Backend cron jobs enabled +``` + +**Configuration:** + +```bash +# apps/api/.env.staging +TEE_ENABLED=true +TEE_PROVIDER=phala +PHALA_API_URL=https://testnet.phala.network/api/v1 +PHALA_CONTRACT_ID=${{ secrets.PHALA_CONTRACT_ID_STAGING }} +PHALA_API_KEY=${{ secrets.PHALA_API_KEY_STAGING }} + +# Republish schedule (production-like) +TEE_REPUBLISH_INTERVAL_HOURS=3 +TEE_KEY_EPOCH_ROTATION_WEEKS=4 +``` + +**Staging-Specific Challenges:** + +1. **Web3Auth Testnet Instability** + - Sapphire Devnet may reset or have unstable user IDs + - User keypairs could change unexpectedly + - This orphans IPNS records (old keys, no one to republish) + +2. **DHT Pollution** + - Staging publishes to real public DHT + - Old test IPNS records accumulate + - No automatic cleanup (IPNS records naturally expire after 24h without republish) + +3. **TEE Key Epochs** + - Staging TEE uses testnet keys (may be rotated more frequently) + - 4-week grace period still applies + +**Staging Cleanup Strategy:** + +```typescript +// tools/staging-cleanup/src/index.ts +// Periodic job to clean up orphaned staging data + +interface CleanupJob { + // 1. Find IPNS records that haven't been republished in >48 hours + findStaleIpnsRecords(): Promise; + + // 2. Check if owning user still exists and can republish + validateUserCanRepublish(record: FolderIpns): Promise; + + // 3. Mark orphaned records for cleanup (stop TEE republishing) + markOrphaned(recordIds: string[]): Promise; + + // 4. Unpin associated IPFS content after grace period + cleanupOrphanedContent(recordIds: string[], graceDays: number): Promise; +} + +// Run weekly in staging +// Cron: 0 0 * * 0 (Sundays at midnight) +``` + +**Staging Integration Tests:** + +```typescript +// tests/integration/tee-republish.test.ts +describe('TEE IPNS Republishing', () => { + it('should republish IPNS record via TEE within 3 hours', async () => { + // 1. Create folder with IPNS record + // 2. Note initial sequence number + // 3. Wait for TEE republish cycle (or trigger manually) + // 4. Verify sequence number incremented + // 5. Verify IPNS resolves to correct CID + }); + + it('should handle TEE key epoch rotation', async () => { + // 1. Create folder with old epoch key + // 2. Trigger epoch rotation + // 3. Verify republishing continues with new epoch + // 4. Verify old epoch still works during grace period + }); +}); +``` + +### Production Environment: Full TEE with Monitoring + +**Infrastructure:** + +- Phala Cloud mainnet contract +- Dedicated republishing cron (scales with user count) +- Full observability stack + +**Configuration:** + +```bash +# apps/api/.env.production +TEE_ENABLED=true +TEE_PROVIDER=phala +PHALA_API_URL=https://api.phala.network/v1 +PHALA_CONTRACT_ID=${{ secrets.PHALA_CONTRACT_ID_PROD }} +PHALA_API_KEY=${{ secrets.PHALA_API_KEY_PROD }} + +# Production republish settings +TEE_REPUBLISH_INTERVAL_HOURS=3 +TEE_KEY_EPOCH_ROTATION_WEEKS=4 +TEE_REPUBLISH_BATCH_SIZE=100 +TEE_REPUBLISH_CONCURRENCY=10 + +# Monitoring +TEE_METRICS_ENABLED=true +TEE_ALERT_STALE_THRESHOLD_HOURS=12 +``` + +**Production Monitoring Requirements:** + +1. **IPNS Staleness Detection** + + ```sql + -- Alert: Records not republished in >12 hours + SELECT folder_ipns.* + FROM folder_ipns + WHERE last_republish_at < NOW() - INTERVAL '12 hours' + AND is_active = true; + ``` + +2. **TEE Health Metrics** + - Republish success rate (target: >99.9%) + - Republish latency p50/p95/p99 + - TEE API error rate + - Key epoch rotation completion rate + +3. **Alerting Thresholds** + + | Metric | Warning | Critical | + | ---------------------------------- | ------- | -------- | + | Records not republished in X hours | 12h | 20h | + | TEE API error rate | >1% | >5% | + | Republish queue depth | >1000 | >5000 | + | Epoch rotation failures | Any | >10% | + +4. **Dashboard Panels** + - Active IPNS records by age since last republish + - TEE republish throughput (records/minute) + - Failed republish attempts (with error breakdown) + - Key epoch distribution (current vs grace period) + +**Production Incident Response:** + +```markdown +## IPNS Staleness Incident Runbook + +### Symptoms + +- Users report "folder not found" or stale data +- Monitoring shows records >20h since republish + +### Diagnosis + +1. Check TEE service health: `curl https://api.phala.network/health` +2. Check backend cron status: `systemctl status cipherbox-republish` +3. Query stale records: `SELECT COUNT(*) FROM folder_ipns WHERE last_republish_at < NOW() - INTERVAL '20 hours'` + +### Resolution + +1. If TEE service down: Wait for Phala recovery, records have 24h TTL buffer +2. If cron stopped: Restart cron, monitor catch-up +3. If specific records failing: Check encryptedIpnsPrivateKey validity, may need user to re-publish + +### User Communication + +- <20h stale: No user impact, monitor +- 20-24h stale: Prepare user comms, accelerate fix +- > 24h stale: IPNS records expired, users must re-publish from client +``` + +### TEE Key Encryption Architecture + +This section documents the security architecture for encrypting IPNS private keys before transmission to the TEE for republishing. + +#### Threat Model + +**Assets:** + +- IPNS private keys (Ed25519) - control over user's folder metadata +- User's ability to update IPNS records + +**Threats:** + +1. **Network interception:** Attacker intercepts IPNS private key in transit to TEE +2. **TEE key compromise:** Attacker obtains TEE private key, can decrypt all `encryptedIpnsPrivateKey` values +3. **Stale epoch attack:** Attacker replays old encrypted keys after epoch rotation + +**Security Goals:** + +- IPNS private keys are never transmitted in plaintext +- Only the current TEE can decrypt keys (forward secrecy via epoch rotation) +- Key compromise has bounded impact (4-week epoch window) + +#### Encryption Flow + +``` +User Client CipherBox API TEE (Phala Cloud) + | | | + | 1. Fetch TEE public key | | + |-------------------------------->| | + | | 2. Get current epoch key | + | |-------------------------------->| + | |<--------------------------------| + |<--------------------------------| {publicKey, epoch} | + | | | + | 3. ECIES encrypt IPNS key | | + | with TEE public key | | + | | | + | 4. POST /folders/:id/publish | | + | {encryptedIpnsPrivateKey, | | + | keyEpoch, ipnsValue, ...} | | + |-------------------------------->| | + | | 5. Store for republishing | + | | (cron every 3 hours) | + | | | + | | 6. Send encrypted key to TEE | + | |-------------------------------->| + | | | 7. Decrypt in hardware + | | | 8. Sign IPNS record + | | | 9. Discard key (never stored) + | |<--------------------------------| + | | {signedIpnsRecord} | +``` + +#### Encryption Primitive: ECIES over secp256k1 + +Per project security standards, IPNS private keys MUST be encrypted using ECIES (Elliptic Curve Integrated Encryption Scheme) with secp256k1: + +```typescript +// packages/crypto/src/tee/encrypt.ts +import { eciesEncrypt } from '../ecies'; + +export async function encryptIpnsKeyForTee( + ipnsPrivateKey: Uint8Array, // 32-byte Ed25519 private key + teePublicKey: Uint8Array, // secp256k1 public key (33 or 65 bytes) + keyEpoch: number // Current TEE key epoch +): Promise { + // ECIES encryption: ephemeral ECDH + AES-256-GCM + const ciphertext = await eciesEncrypt(ipnsPrivateKey, teePublicKey); + + return { + ciphertext, + keyEpoch, + algorithm: 'ECIES-secp256k1-AES256GCM', + }; +} +``` + +#### TEE Public Key Distribution + +The TEE public key is obtained via the CipherBox API, which caches and validates it from Phala Cloud: + +```typescript +// apps/api/src/tee/tee.service.ts +export class TeeService { + private cachedPublicKey: { key: Uint8Array; epoch: number; expiresAt: Date } | null = null; + + async getCurrentPublicKey(): Promise<{ publicKey: Uint8Array; epoch: number }> { + // 1. Check cache (valid for 1 hour) + if (this.cachedPublicKey && this.cachedPublicKey.expiresAt > new Date()) { + return { publicKey: this.cachedPublicKey.key, epoch: this.cachedPublicKey.epoch }; + } + + // 2. Fetch from Phala Cloud with attestation verification + const response = await this.phalaClient.getPublicKey(); + + // 3. Verify attestation quote (proves key is from genuine TEE) + await this.verifyAttestation(response.attestation); + + // 4. Cache with expiry + this.cachedPublicKey = { + key: response.publicKey, + epoch: response.epoch, + expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour + }; + + return { publicKey: response.publicKey, epoch: response.epoch }; + } +} +``` + +#### Key Epoch Rotation Scheme + +TEE keys rotate every 4 weeks (`TEE_KEY_EPOCH_ROTATION_WEEKS=4`) with a 1-week grace period: + +| Epoch State | Window | API Behavior | +| ---------------- | --------- | ------------------------------------------------ | +| Current | Weeks 0-4 | Normal processing | +| Previous (grace) | Weeks 4-5 | Accept, re-encrypt with new epoch, update record | +| Expired | >5 weeks | Reject with `EPOCH_EXPIRED` error | + +**Epoch Handling Logic:** + +```typescript +// apps/api/src/tee/epoch.service.ts +export class EpochService { + private readonly GRACE_PERIOD_WEEKS = 1; + + async validateEpoch(submittedEpoch: number): Promise { + const currentEpoch = await this.getCurrentEpoch(); + + if (submittedEpoch === currentEpoch) { + return { valid: true, action: 'PROCESS_NORMALLY' }; + } + + if (submittedEpoch === currentEpoch - 1) { + const gracePeriodEnd = this.getEpochEndDate(currentEpoch - 1).add( + this.GRACE_PERIOD_WEEKS, + 'weeks' + ); + + if (new Date() < gracePeriodEnd) { + return { valid: true, action: 'RE_ENCRYPT_AND_UPDATE' }; + } + } + + return { + valid: false, + action: 'REJECT', + error: { + code: 'EPOCH_EXPIRED', + message: 'TEE key epoch expired. Fetch new TEE public key and re-encrypt.', + currentEpoch, + }, + }; + } +} +``` + +**Grace Period Expiration:** +Records encrypted with an expired epoch that were not re-encrypted during the grace period become invalid. The TEE cannot decrypt them, and the republishing job will: + +1. Mark the record as `republish_failed` with reason `EPOCH_EXPIRED` +2. Send alert to monitoring (PagerDuty/Opsgenie) +3. User must re-publish from client to restore republishing + +#### Forward Secrecy Considerations + +- **Epoch rotation provides bounded compromise:** If a TEE private key is compromised, only records encrypted with that epoch's key are exposed +- **No retroactive decryption:** Old epoch keys are destroyed; past ciphertexts cannot be decrypted +- **Grace period tradeoff:** 1-week grace period balances usability (client update propagation) vs security (exposure window) + +### TEE Implementation Checklist + +#### Phase 1: Core TEE Infrastructure (Production + Staging) + +- [ ] Add `TEE_ENABLED` environment flag +- [ ] Create `TeeService` with Phala Cloud client +- [ ] Implement `ipns_republish_schedule` table +- [ ] Create republish cron job (3-hour interval) +- [ ] Add `encryptedIpnsPrivateKey` to folder publish flow (see encryption flow above) + +#### Phase 2: Monitoring & Alerting (Production) + +- [ ] Add Prometheus metrics for TEE operations +- [ ] Create Grafana dashboard for IPNS health +- [ ] Configure PagerDuty/Opsgenie alerts for staleness +- [ ] Write incident runbooks + +#### Phase 3: Staging Integration Testing + +- [ ] Deploy TEE to staging with testnet credentials +- [ ] Create integration test suite for republishing +- [ ] Implement staging cleanup job +- [ ] Add CI job for periodic staging health check + +#### Phase 4: Local TEE Development (Optional) + +- [ ] Create mock TEE service (`tools/mock-tee-service`) +- [ ] Add `docker-compose.tee-dev.yml` profile +- [ ] Document TEE feature development workflow + +## Appendix: Alternative Approaches Considered + +### A. Fully Separate Web3Auth Projects per Environment + +**Approach:** Create 4 separate Web3Auth projects (local, ci, staging, prod) + +**Rejected because:** + +- Overhead of managing 4 projects +- Need separate test accounts per environment +- Can't share test fixtures between local/CI/staging +- Environment context achieves same isolation with less complexity + +### B. Backend User Aggregation (Custom Auth) + +**Approach:** Backend issues environment-specific tokens, client presents to Web3Auth via Custom Auth + +**Rejected because:** + +- Significant implementation complexity +- Custom Auth provider setup required +- Adds latency to auth flow +- Over-engineered for the actual problem + +### C. IPNS Sequence Reset via Admin API + +**Approach:** Reset IPNS sequence numbers between test runs + +**Rejected because:** + +- IPNS sequence is network-wide, can't be reset +- Would require deleting/recreating IPNS keys +- Loses data in the process +- Doesn't solve the fundamental isolation problem + +--- + +_Document Status: Draft - Ready for implementation_ +_Last Updated: 2026-01-25 - Added TEE infrastructure analysis_ diff --git a/.planning/security/REVIEW-2026-01-25-environments.md b/.planning/security/REVIEW-2026-01-25-environments.md new file mode 100644 index 0000000000..7e6c9b5d6d --- /dev/null +++ b/.planning/security/REVIEW-2026-01-25-environments.md @@ -0,0 +1,544 @@ +# Security Review: Environment Architecture + +**Document Reviewed:** `.planning/ENVIRONMENTS.md` +**Review Date:** 2026-01-25 +**Reviewer:** Security Analysis Agent +**Overall Assessment:** **APPROVED WITH CONCERNS** + +--- + +## Executive Summary + +The environment architecture document proposes a well-considered approach to solving IPNS sequence number conflicts across environments. The core cryptographic design is sound, but there are several security concerns that should be addressed before implementation. + +**Key Strengths:** + +- Environment salt in IPNS key derivation is cryptographically correct +- TEE key epoch rotation with grace period is reasonable +- Separation of encryption keys from IPNS keys is a good architectural decision + +**Primary Concerns:** + +- Incomplete HKDF implementation details (salt vs info parameter usage) +- Cross-environment data accessibility creates unintentional attack surface +- Mock service security implications need explicit documentation + +--- + +## Findings + +### 1. [MEDIUM] HKDF Parameter Confusion: Salt vs Info String + +**Location:** `.planning/ENVIRONMENTS.md:61-64` + +**Code:** + +```typescript +const salt = ENVIRONMENT_SALTS[environment]; +// HKDF-SHA256 with environment-specific salt +const info = `${salt}:${folderId}`; +// ... derivation logic +``` + +**Issue:** +The document conflates HKDF terminology. The variable is named `salt` but is used to construct the `info` parameter. In HKDF (RFC 5869): + +- **Salt:** Should be random or fixed per application; provides domain separation at the extract phase +- **Info:** Provides context binding at the expand phase; appropriate for environment/folderId + +The current existing implementation at `packages/crypto/src/keys/derive.ts` uses a fixed salt (`CipherBox-v1`) and passes context via info, which is correct. + +**Impact:** +If implemented as written with the environment string as the actual HKDF salt (not info), security is not reduced, but it deviates from the established pattern in the codebase and RFC best practices. The primary risk is maintenance confusion. + +**Recommendation:** +Clarify the implementation to match existing patterns: + +```typescript +// packages/crypto/src/ipns.ts +const HKDF_SALT = new TextEncoder().encode('CipherBox-IPNS-v1'); + +const ENVIRONMENT_PREFIXES = { + local: 'env:local', + ci: 'env:ci', + staging: 'env:staging', + production: 'env:production', +} as const; + +export async function deriveIpnsKeypair( + userSecp256k1PrivateKey: Uint8Array, + folderId: string, + environment: Environment +): Promise { + // Info string provides context binding (environment + folder) + const info = new TextEncoder().encode(`${ENVIRONMENT_PREFIXES[environment]}:folder:${folderId}`); + + const seed = await deriveKey({ + inputKey: userSecp256k1PrivateKey, + salt: HKDF_SALT, // Fixed application salt + info: info, // Variable context + outputLength: 32, + }); + + // Use derived seed for Ed25519 keypair generation + return ed25519KeypairFromSeed(seed); +} +``` + +**References:** + +- RFC 5869 Section 3.1 (HKDF parameter recommendations) + +**Status:** Addressed in ENVIRONMENTS.md (2026-02-02) - Code example now uses fixed `HKDF_SALT` constant and `ENVIRONMENT_CONTEXT` for info parameter, matching RFC 5869 best practices. + +--- + +### 2. [MEDIUM] Architectural Change: Random vs Derived IPNS Keys + +**Location:** `.planning/ENVIRONMENTS.md:56-65` + +**Current Implementation:** `packages/crypto/src/ed25519/keygen.ts:30-38` + +```typescript +export function generateEd25519Keypair(): Ed25519Keypair { + const privateKey = ed.utils.randomPrivateKey(); // Random generation + const publicKey = ed.getPublicKey(privateKey); + return { publicKey, privateKey }; +} +``` + +**Proposed Change:** + +```typescript +// Derive from secp256k1 key + environment + folderId +const info = `${salt}:${folderId}`; +``` + +**Issue:** +The document proposes a significant change from random key generation to deterministic derivation from the user's secp256k1 key. This is a valid approach, but: + +1. **Key binding:** Derived IPNS keys become permanently bound to the user's Web3Auth identity. If Web3Auth ever changes the derived secp256k1 key (e.g., during devnet resets), all IPNS keys become orphaned with no recovery path. + +2. **Migration complexity:** Existing vaults use randomly generated IPNS keys stored encrypted. The migration path is not documented. + +3. **Cross-curve derivation:** Deriving Ed25519 keys from secp256k1 private keys is cryptographically sound when using HKDF, but the security margin depends on the entropy of the source key. + +**Impact:** + +- If Web3Auth devnet resets user identity (acknowledged in document), IPNS records become permanently inaccessible +- Existing vaults would need migration or parallel support + +**Recommendation:** + +1. Document the migration path for existing vaults +2. Add explicit error handling for Web3Auth identity changes +3. Consider storing a "key derivation version" flag to support future algorithm changes: + +```typescript +interface IpnsKeyDerivation { + version: 1; // Increment if algorithm changes + environment: Environment; + folderId: string; +} +``` + +**Status:** Open + +--- + +### 3. [LOW] Cross-Environment Data Accessibility + +**Location:** `.planning/ENVIRONMENTS.md:37-39` + +**Design:** + +```markdown +This means: + +- Same user can encrypt/decrypt files across environments (if CIDs are known) +- Different IPNS namespaces per environment → no sequence conflicts +``` + +**Issue:** +While this is presented as a feature, it creates an unintentional attack surface: + +1. **CID Leakage:** If an attacker obtains a CID from the CI environment (e.g., from logs, test output, or CI artifacts), they could potentially access that encrypted content in production if the user uses the same Web3Auth identity. + +2. **Test Data in Production:** Encrypted test data created in CI could be decrypted in production. If test data contains sensitive patterns or known plaintexts, this could aid cryptanalysis. + +**Impact:** +Low - requires multiple preconditions (CID knowledge + same user identity across environments), but worth documenting as a known property. + +**Recommendation:** + +1. Explicitly document this as a known security property in the architecture +2. Add guidance to use different test accounts for CI vs production +3. Consider adding an optional "environment binding" to encryption metadata in future versions: + +```typescript +// Future consideration: environment-bound encryption +interface EncryptionMetadata { + version: number; + environment?: string; // Optional environment binding for additional isolation +} +``` + +**Status:** Addressed in ENVIRONMENTS.md (2026-02-02) - Security note added warning about CID leakage risks and recommending separate test accounts. + +--- + +### 4. [LOW] Mock IPNS Routing Service Security + +**Location:** `.planning/ENVIRONMENTS.md:199-210` + +**Code:** + +```typescript +beforeAll(async () => { + // Reset mock IPNS routing for clean slate + await fetch('http://localhost:3001/reset', { method: 'POST' }); +}); +``` + +**Issue:** +The mock IPNS routing service exposes a `/reset` endpoint with no authentication. While this is appropriate for local/CI use, the document should explicitly state: + +1. This endpoint must never exist in staging/production +2. The mock service must validate it is not being used with real delegated routing + +**Impact:** +If mock service code is accidentally deployed or the reset endpoint is exposed, all IPNS records could be wiped. + +**Recommendation:** +Add explicit safeguards in the mock service: + +```typescript +// tools/mock-ipns-routing/src/index.ts +app.post('/reset', (req, res) => { + // Safety check: only allow reset in local/CI environments + const env = process.env.CIPHERBOX_ENVIRONMENT; + if (env !== 'local' && env !== 'ci') { + return res.status(403).json({ + error: 'Reset endpoint disabled outside local/CI environments', + }); + } + // ... reset logic +}); +``` + +**Status:** Addressed in ENVIRONMENTS.md (2026-02-02) - Security note added after `/reset` example warning it must be disabled outside local/CI environments. + +--- + +### 5. [INFO] TEE Key Epoch Rotation Design + +**Location:** `.planning/ENVIRONMENTS.md:474-476` + +**Configuration:** + +```bash +TEE_REPUBLISH_INTERVAL_HOURS=3 +TEE_KEY_EPOCH_ROTATION_WEEKS=4 +``` + +**Analysis:** +The 4-week epoch rotation with grace period is reasonable. Key observations: + +**Strengths:** + +- 3-hour republish interval is well within 24-hour IPNS TTL (8x safety margin) +- 4-week rotation balances security with operational complexity +- Grace period allows for migration without service disruption + +**Considerations:** + +- Document does not specify grace period duration (should be >= 1 republish cycle, ideally 1 week) +- Key epoch mismatch handling is not detailed (what happens if client sends old epoch?) + +**Recommendation:** +Add explicit handling for epoch transitions: + +```typescript +// API should accept: +// - Current epoch: normal processing +// - Previous epoch (within grace): re-encrypt with new epoch key, update record +// - Older epochs: reject with error instructing client to fetch new TEE public key +``` + +**Status:** Addressed in ENVIRONMENTS.md (2026-02-02) - Added "TEE Key Encryption Architecture" section with: + +- 1-week grace period duration specified +- Epoch validation logic with `PROCESS_NORMALLY`, `RE_ENCRYPT_AND_UPDATE`, and `REJECT` actions +- Grace period expiration handling (mark as `republish_failed`, alert, require user re-publish) +- Full encryption flow diagram and ECIES implementation details + +--- + +### 6. [INFO] Web3Auth Devnet Shared Identity Risk + +**Location:** `.planning/ENVIRONMENTS.md:303-308` + +**Design:** + +```markdown +1. **cipherbox-dev** (Sapphire Devnet) + - Used for: Local dev, CI, Staging +``` + +**Analysis:** +Sharing Sapphire Devnet across local/CI/staging means: + +- Test account actions in CI affect staging state +- Devnet resets could invalidate staging data +- IPNS key derivation isolation mitigates sequence conflicts but not identity issues + +The document acknowledges this risk but does not provide mitigation: + +```markdown +1. **Web3Auth Testnet Instability** + - Sapphire Devnet may reset or have unstable user IDs + - User keypairs could change unexpectedly + - This orphans IPNS records (old keys, no one to republish) +``` + +**Recommendation:** +Consider using separate Web3Auth projects for staging (if budget allows): + +```text +local/CI: cipherbox-dev (Sapphire Devnet) - ephemeral testing +staging: cipherbox-staging (Sapphire Devnet) - stable testing, separate project +production: cipherbox-prod (Sapphire Mainnet) - production +``` + +Alternatively, document the risk explicitly and add monitoring for identity stability. + +**Status:** Open - Evaluate budget for separate staging project + +--- + +### 7. [INFO] JWT_SECRET Placeholder in Local Config + +**Location:** `.planning/ENVIRONMENTS.md:153` + +**Code:** + +```bash +JWT_SECRET=local-dev-jwt-secret-change-in-production +``` + +**Analysis:** +Using a hardcoded JWT secret for local development is acceptable, but the placeholder could be stronger and the warning more prominent. + +**Recommendation:** +Use a format that is obviously invalid for production: + +```bash +JWT_SECRET=INSECURE_LOCAL_DEV_ONLY_DO_NOT_USE_IN_PRODUCTION_xxxxxxxxxxxxxxxx +``` + +**Status:** Open - Minor improvement + +--- + +## Test Case Recommendations + +Based on this review, the following security test cases should be implemented: + +```typescript +describe('Environment Key Derivation Security Tests', () => { + describe('IPNS Key Isolation', () => { + it('should derive different IPNS keys for same user in different environments', async () => { + const userKey = await generateTestUserKey(); + const folderId = 'test-folder-123'; + + const localKey = await deriveIpnsKeypair(userKey, folderId, 'local'); + const ciKey = await deriveIpnsKeypair(userKey, folderId, 'ci'); + const prodKey = await deriveIpnsKeypair(userKey, folderId, 'production'); + + expect(localKey.publicKey).not.toEqual(ciKey.publicKey); + expect(localKey.publicKey).not.toEqual(prodKey.publicKey); + expect(ciKey.publicKey).not.toEqual(prodKey.publicKey); + }); + + it('should derive same IPNS key for same inputs (deterministic)', async () => { + const userKey = await generateTestUserKey(); + const folderId = 'test-folder-123'; + + const key1 = await deriveIpnsKeypair(userKey, folderId, 'local'); + const key2 = await deriveIpnsKeypair(userKey, folderId, 'local'); + + expect(key1.privateKey).toEqual(key2.privateKey); + expect(key1.publicKey).toEqual(key2.publicKey); + }); + + it('should derive different keys for different folders', async () => { + const userKey = await generateTestUserKey(); + + const folder1 = await deriveIpnsKeypair(userKey, 'folder-1', 'local'); + const folder2 = await deriveIpnsKeypair(userKey, 'folder-2', 'local'); + + expect(folder1.publicKey).not.toEqual(folder2.publicKey); + }); + }); + + describe('Environment Validation', () => { + it('should reject invalid environment values', async () => { + const userKey = await generateTestUserKey(); + + await expect( + deriveIpnsKeypair(userKey, 'folder', 'invalid' as Environment) + ).rejects.toThrow(); + }); + + it('should not allow environment injection via folderId', async () => { + const userKey = await generateTestUserKey(); + + // Attempt to inject environment change via folderId + const maliciousFolderId = 'folder:production'; + const localKey = await deriveIpnsKeypair(userKey, maliciousFolderId, 'local'); + const prodKey = await deriveIpnsKeypair(userKey, 'folder', 'production'); + + // Keys should be different despite injection attempt + expect(localKey.publicKey).not.toEqual(prodKey.publicKey); + }); + }); + + describe('TEE Key Epoch', () => { + it('should reject encryptedIpnsPrivateKey with expired epoch', async () => { + // Test that old epochs beyond grace period are rejected + }); + + it('should accept encryptedIpnsPrivateKey with previous epoch during grace period', async () => { + // Test grace period handling + }); + }); + + describe('Private Key Security', () => { + it('should never log IPNS private keys', async () => { + const logSpy = vi.spyOn(console, 'log'); + const warnSpy = vi.spyOn(console, 'warn'); + const errorSpy = vi.spyOn(console, 'error'); + + const userKey = await generateTestUserKey(); + await deriveIpnsKeypair(userKey, 'folder-1', 'local'); + + // Verify no log output contains private key patterns + const allLogs = [ + ...logSpy.mock.calls.flat(), + ...warnSpy.mock.calls.flat(), + ...errorSpy.mock.calls.flat(), + ].map(String); + + for (const log of allLogs) { + expect(log).not.toMatch(/privateKey/i); + expect(log).not.toMatch(/0x04[0-9a-fA-F]{64}/); // secp256k1 pubkey pattern + expect(log).not.toMatch(/[0-9a-fA-F]{64}/); // 32-byte hex key + } + }); + + it('should not store IPNS private keys in localStorage or sessionStorage', async () => { + const userKey = await generateTestUserKey(); + await deriveIpnsKeypair(userKey, 'folder-1', 'local'); + + // Check localStorage + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i)!; + const value = localStorage.getItem(key)!; + expect(key.toLowerCase()).not.toContain('privatekey'); + expect(key.toLowerCase()).not.toContain('ipns'); + expect(key.toLowerCase()).not.toContain('ed25519'); + expect(value).not.toMatch(/[0-9a-fA-F]{64}/); + } + + // Check sessionStorage + for (let i = 0; i < sessionStorage.length; i++) { + const key = sessionStorage.key(i)!; + const value = sessionStorage.getItem(key)!; + expect(key.toLowerCase()).not.toContain('privatekey'); + expect(key.toLowerCase()).not.toContain('ipns'); + expect(key.toLowerCase()).not.toContain('ed25519'); + expect(value).not.toMatch(/[0-9a-fA-F]{64}/); + } + }); + + it('should only send encrypted IPNS private keys to TEE', async () => { + const mockTeeEndpoint = vi.fn(); + // Mock the TEE client + vi.spyOn(teeService, 'submitForRepublishing').mockImplementation(mockTeeEndpoint); + + const ipnsKey = await deriveIpnsKeypair(userKey, 'folder-1', 'local'); + const teePublicKey = await teeService.getCurrentPublicKey(); + + await folderService.publishFolder(folderId, ipnsKey, teePublicKey); + + // Verify the call was made with encrypted data + expect(mockTeeEndpoint).toHaveBeenCalledTimes(1); + const [payload] = mockTeeEndpoint.mock.calls[0]; + + // encryptedIpnsPrivateKey should be ECIES ciphertext (longer than raw key) + expect(payload.encryptedIpnsPrivateKey.length).toBeGreaterThan(32); + // Should NOT contain raw private key bytes + expect(payload.encryptedIpnsPrivateKey).not.toEqual(ipnsKey.privateKey); + // Should include key epoch + expect(payload.keyEpoch).toBeDefined(); + expect(typeof payload.keyEpoch).toBe('number'); + }); + + it('should verify IPNS signature before accepting record', async () => { + const validKeypair = await deriveIpnsKeypair(userKey, 'folder-1', 'local'); + const attackerKeypair = await generateEd25519Keypair(); // Random key + + // Create a valid IPNS record + const validRecord = await createIpnsRecord(validKeypair, 'QmValidCid', 1); + + // Forge a record with attacker's signature but claiming to be from validKeypair + const forgedRecord = await createIpnsRecord(attackerKeypair, 'QmMaliciousCid', 2); + forgedRecord.pubKey = validKeypair.publicKey; // Claim to be valid key + + // Valid record should be accepted + await expect(verifyIpnsRecord(validRecord)).resolves.toBe(true); + + // Forged record should be rejected (signature mismatch) + await expect(verifyIpnsRecord(forgedRecord)).resolves.toBe(false); + }); + }); +}); +``` + +--- + +## Summary + +| Severity | Count | Summary | +| -------- | ----- | ----------------------------------------------------------- | +| Critical | 0 | None found | +| High | 0 | None found | +| Medium | 2 | HKDF parameter naming; Random vs derived key migration | +| Low | 2 | Cross-environment accessibility; Mock service security | +| Info | 3 | TEE epoch design; Web3Auth shared identity; JWT placeholder | + +--- + +## Recommendations Priority + +1. **[Medium Priority]** Clarify HKDF implementation to match existing codebase patterns (salt vs info parameter usage) + +2. **[Medium Priority]** Document migration path for existing vaults using randomly generated IPNS keys + +3. **[Low Priority]** Add safeguards to mock IPNS routing service reset endpoint + +4. **[Low Priority]** Document cross-environment data accessibility as known security property + +5. **[Enhancement]** Consider separate Web3Auth project for staging environment if budget allows + +--- + +## Approval Conditions + +- Address Medium findings before implementation +- Document Low/Info findings in the final architecture + +--- + +_Review Status: Complete_ +_Next Review: After implementation begins_