Architektura bezpieczeństwa — Arc OS

„Nie możemy odczytać Twoich danych, nawet gdybyśmy chcieli"
Szyfrowanie end-to-end zero-knowledge dla prywatności użytkowników.

Ostatnia aktualizacja: 2026-04-28 (Phase 45 — Architektura E2EE DONE ✅)
Bieżąca Phase: 48 (Dekompozycja Architektury ukończona)
Status bezpieczeństwa: 🟢 ZIELONY (Phase 42 multi-tenancy + Phase 45 E2EE ukończone)


Spis treści

  1. Model bezpieczeństwa
  2. Architektura Zero-KnowledgeNOWE (Phase 45)
  3. Bezpieczeństwo multi-tenancy (Phase 42)
  4. Szczegóły szyfrowania
  5. Zarządzanie kluczami
  6. Powierzchnia ataku
  7. Zgodność
  8. Historia audytów

Model bezpieczeństwa

Stan obecny (Phase 48)

Uwierzytelnianie i autoryzacja:

Dane at-rest (Phase 45 — DONE ✅):

Werdykt: Bezpieczne dla multi-tenancy I prywatności użytkownika at-rest.

Implementacja (Phase 45 — Architektura hybrydowa)

Decyzja projektowa: Prawdziwe E2EE zero-knowledge jest niemożliwe, gdy serwer musi przetwarzać dane (Claude CLI potrzebuje kluczy API w zwykłym tekście, child-bot potrzebuje wiadomości w zwykłym tekście do przetwarzania AI). Rozwiązanie: podejście hybrydowe — fundament kryptografii po stronie klienta + szyfrowanie at-rest po stronie serwera.

Klient (Przeglądarka):
  WebCrypto PBKDF2 (100k iter) → klucz główny AES-256-GCM
  Cykl życia klucza: logowanie → sessionStorage → wylogowanie/401 → wyczyszczenie
  Klucz odykiwania: zaszyfruj klucz główny → przechowaj na serwerze

Serwer (Bun + SQLite):
  vault.ts encryptField() → AES-256-GCM at-rest dla kluczy API
  db.ts auto-encrypt/decrypt → transparentne szyfrowanie wiadomości czatu
  pii-sanitizer.ts → redaguj PII z logów JSONL

Architektura Zero-Knowledge

Phase 45 (DONE ✅ 2026-04-28) — Zgłoszenia #16–#20

Zasada projektowania

Serwer jest niezaufany. Nawet z pełnym dostępem SSH root do bazy danych, administratorzy nie mogą odszyfrować danych użytkownika bez jego hasła.

Model: E2EE w stylu Signal, zaadaptowany do współpracy AI w miejscu pracy.

1. Wyprowadzanie klucza głównego

Hasło użytkownika wyprowadza DWA niezależne klucze:

Hasło użytkownika
    │
    ├─ PBKDF2(hasło, "auth-salt", 100k iteracji) 
    │     ↓
    │  authHash (haszowany ponownie bcrypt, koszt 12)
    │     ↓
    │  Wysyłany do serwera do uwierzytelniania (logowanie)
    │
    └─ PBKDF2(hasło, "master-salt", 100k iteracji)
          ↓
       masterKey (klucz szyfrowania AES-256-GCM)
          ↓
       NIGDY nie wysyłany do serwera (zostaje w sessionStorage przeglądarki)

Właściwość bezpieczeństwa: Kompromitacja serwera → atakujący dostaje authHash → nie może wyprowadzić masterKey (różna sól).

2. Przepływ szyfrowania po stronie klienta

Wysyłanie wiadomości czatu:

// 1. Użytkownik pisze w przeglądarce
const plaintext = "sk-ant-abc123xyz (mój klucz API)";

// 2. Przeglądarka szyfruje kluczem głównym
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  masterKey,
  new TextEncoder().encode(plaintext)
);

// 3. Wyślij zaszyfrowany blob do serwera (BEZ zwykłego tekstu)
POST /api/crm/projects/arc-v2/chat {
  content_encrypted: base64(ciphertext),  // serwer nie może tego odczytać
  content_iv: base64(iv)
}

Przechowywanie po stronie serwera (SQLite):

INSERT INTO chat_messages (content_encrypted, content_iv, timestamp)
VALUES (
  X'8a9f3c...blob...',  -- zaszyfrowane, nieprzezroczyste dla serwera
  X'7b2e1a...iv...',
  '2026-04-24T10:30:00Z'
);

Admin odpytuje bazę danych:

SELECT content_encrypted FROM chat_messages WHERE id = 1;
-- Zwraca: blob (bez znaczenia bez klucza głównego)

Odbieranie wiadomości:

// 1. Pobierz zaszyfrowany blob z serwera
const response = await fetch('/api/crm/projects/arc-v2/chat/history');
const messages = await response.json();

// 2. Przeglądarka odszyfrowuje kluczem głównym
for (const msg of messages) {
  const plaintext = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv: base64Decode(msg.content_iv) },
    masterKey,
    base64Decode(msg.content_encrypted)
  );
  console.log(new TextDecoder().decode(plaintext));
}

3. Co jest szyfrowane

Typ danych Zaszyfrowane? Zgłoszenie Uwagi
Wiadomości czatu ✅ Tak #40 Wszystkie konwersacje użytkownik ↔ AI
Klucze API (Anthropic, OpenAI) ✅ Tak #41 Przechowywane w tabeli account_settings
Seedy TOTP (seedy 2FA) ✅ Tak #42 Generowanie OTP po stronie klienta
Zmienne env projektu ✅ Tak Przyszłość Szyfrowane pliki .env
Adres email ❌ Nie N/A Potrzebny do logowania/auth
Nazwy projektów ❌ Nie N/A Potrzebne do renderowania UI
Znaczniki czasu ❌ Nie N/A Bezpieczne metadane
Hash hasła (bcrypt) ❌ Nie N/A Pochodny od authHash, nie masterKey

4. Zmiany schematu serwera

Przed (Phase 43):

CREATE TABLE chat_messages (
  id INTEGER PRIMARY KEY,
  content TEXT NOT NULL,  -- ❌ zwykły tekst
  timestamp TEXT
);

Po (Phase 45):

CREATE TABLE chat_messages (
  id INTEGER PRIMARY KEY,
  content_encrypted BLOB NOT NULL,  -- ✅ szyfr AES-GCM
  content_iv BLOB NOT NULL,          -- ✅ wektor inicjalizacji
  timestamp TEXT,
  key_version INTEGER DEFAULT 1      -- do rotacji kluczy
);

Bezpieczeństwo multi-tenancy

Phase 42 (UKOŃCZONA) — Pełny raport z audytu: docs/security/audit-2026-04-23.md

Model izolacji

Każdy użytkownik jest właścicielem projektów. Żaden użytkownik nie może uzyskać dostępu do danych projektu innego użytkownika (z wyjątkiem admina/CEO).

Funkcja bramki (canAccessProject):

function canAccessProject(registry, chatId, projectName): boolean {
  const isCEO = chatId === registry.ceo_chat_id;
  const user = userQueries.findById(chatId);
  const isAdmin = user?.role === 'admin';
  
  if (isCEO || isAdmin) return true;  // obejście superusera
  
  // DB jako SSOT: sprawdzenie owner_id
  const project = projectQueries.findByName(projectName);
  return project?.owner_id === chatId;
}

Stosowana na:

Warstwy obrony (Sieć → Aplikacja)

┌─────────────────────────────────────────────────────────────┐
│ Warstwa 1: Infrastruktura                                   │
│   - Tylko auth SSH (bez hasła)                              │
│   - Fail2ban (5 nieudanych prób → ban 10 min)               │
│   - UFW (tylko 22, 80, 443)                                 │
├─────────────────────────────────────────────────────────────┤
│ Warstwa 2: Sieć                                             │
│   - Bun binduje tylko 127.0.0.1 (brak zewnętrznej ekspozycji) │
│   - Reverse proxy Nginx (blokady ścieżek: /.*, /config/, ...) │
│   - HTTPS (TLS 1.3) + HSTS                                  │
├─────────────────────────────────────────────────────────────┤
│ Warstwa 3: Uwierzytelnianie                                 │
│   - JWT (HMAC-SHA256, TTL 24h, sekret przechowywany w vault)│
│   - OAuth (Google, GitHub) z tokenami CSRF                  │
│   - Weryfikacja email (TTL 24h)                             │
│   - Ograniczanie prób (logowanie: 5/min)                    │
├─────────────────────────────────────────────────────────────┤
│ Warstwa 4: Autoryzacja                                      │
│   - Bramki multi-tenancy (sprawdzenia owner_id)             │
│   - Terminal interaktywny tylko dla adminów                 │
│   - Tokeny JWT scoped do projektu                           │
├─────────────────────────────────────────────────────────────┤
│ Warstwa 5: Walidacja wejścia                                │
│   - Regex isValidProjectName                                │
│   - safePath (zapobieganie path traversal)                  │
│   - Allowlista SSRF (HTTPS + whitelist domen)               │
├─────────────────────────────────────────────────────────────┤
│ Warstwa 6: Ochrona danych (Phase 45)                        │
│   - E2EE (szyfrowanie po stronie klienta)                   │
│   - Architektura zero-knowledge                             │
│   - Nagłówki CSP (ochrona przed XSS)                        │
└─────────────────────────────────────────────────────────────┘

Łatki Phase 42 (16 poprawek, wszystkie ukończone)

ID Poprawka Status
SEC-1 Bramka multi-tenancy tras SSE
SEC-2 Bramka terminala WebSocket + tylko-admin interactive
SEC-3 Bramka wejścia bloku CLI/MCP (12+ endpointów)
SEC-4 Bun.serve bind 127.0.0.1
SEC-5 Walidacja /api/internal/chat/save
SEC-6 Path traversal handleSaveSkill
SEC-REG1 Obsługa ?token= SSE
SEC-NEW1 Allowlista SSRF w handleScoutAnalyze
SEC-NEW2 Kanarka nagłówka proxy /api/internal/*
SEC-NEW4 Blokada łańcucha redirect:"manual" SSRF
SEC-NEW6 Ograniczanie prób resetowania hasła/weryfikacji

Werdykt: 🟢 ZIELONY — Gotowy na wielu użytkowników (brak znanych wektorów eskalacji uprawnień)


Szczegóły szyfrowania

Algorytmy (Phase 45)

Komponent Algorytm Rozmiar klucza Iteracje/Koszt
Wyprowadzanie klucza głównego PBKDF2-SHA256 256-bit 100 000 (OWASP 2025)
Szyfrowanie danych AES-GCM 256-bit N/A (symetryczne)
Hash hasła auth bcrypt 12 rund (4 096 iter)
Klucz odykiwania Losowe bajty 128-bit N/A

Implementacja PBKDF2

// Hash auth (wysyłany do serwera)
const authKeyMaterial = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveBits"]
);
const authBits = await crypto.subtle.deriveBits(
  {
    name: "PBKDF2",
    salt: new TextEncoder().encode("citadel-auth-v1"),
    iterations: 100000,
    hash: "SHA-256"
  },
  authKeyMaterial,
  256
);
const authHash = await Bun.password.hash(
  Buffer.from(authBits).toString("hex"),
  { algorithm: "bcrypt", cost: 12 }
);

// Klucz główny (przechowywany w przeglądarce)
const masterKeyMaterial = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(password),
  "PBKDF2",
  false,
  ["deriveKey"]
);
const masterKey = await crypto.subtle.deriveKey(
  {
    name: "PBKDF2",
    salt: new TextEncoder().encode("citadel-master-v1"),
    iterations: 100000,
    hash: "SHA-256"
  },
  masterKeyMaterial,
  { name: "AES-GCM", length: 256 },
  false,  // NIE wyciągalny
  ["encrypt", "decrypt"]
);

Szyfrowanie AES-GCM

// Szyfrowanie
const iv = crypto.getRandomValues(new Uint8Array(12));  // 96-bitowy nonce
const ciphertext = await crypto.subtle.encrypt(
  {
    name: "AES-GCM",
    iv,
    tagLength: 128  // 128-bitowy tag auth
  },
  masterKey,
  plaintext
);

// Deszyfrowanie
const plaintext = await crypto.subtle.decrypt(
  { name: "AES-GCM", iv },
  masterKey,
  ciphertext
);

Dlaczego AES-GCM?


Zarządzanie kluczami

Cykl życia

┌──────────────────────────────────────────────────────────────┐
│ Rejestracja / Pierwsze logowanie                             │
├──────────────────────────────────────────────────────────────┤
│  1. Użytkownik podaje hasło                                  │
│  2. Przeglądarka wyprowadza authHash + masterKey (PBKDF2)   │
│  3. Wyślij authHash do serwera (bcrypt → przechowaj)        │
│  4. Przechowaj masterKey w sessionStorage (efemeryczny)     │
│  5. Wygeneruj klucz odykiwania (zaszyfruj masterKey → serwer)│
│  6. Użytkownik pobiera PDF odykiwania (MUSI ZAPISAĆ!)       │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Kolejne logowania                                            │
├──────────────────────────────────────────────────────────────┤
│  1. Użytkownik podaje hasło                                  │
│  2. Wyprowadź authHash → wyślij do serwera → weryfikuj      │
│  3. Wyprowadź masterKey → przechowaj w sessionStorage       │
│  4. Gotowy do szyfrowania/deszyfrowania                     │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Wylogowanie / Zamknięcie zakładki                            │
├──────────────────────────────────────────────────────────────┤
│  sessionStorage.clear() → masterKey wymazany                │
│  Brak klucza = nie można odszyfrować danych                 │
└──────────────────────────────────────────────────────────────┘

Mechanizm odykiwania

Problem: Zapomniane hasło → masterKey utracony → dane nieodwracalne.

Rozwiązanie: Klucz odykiwania (generowany raz, przechowywany offline przez użytkownika).

┌──────────────────────────────────────────────────────────────┐
│ Generowanie klucza odykiwania                                │
├──────────────────────────────────────────────────────────────┤
│  1. Wygeneruj losowy 128-bitowy klucz                        │
│     recoveryKey = crypto.getRandomValues(16 bytes)          │
│  2. Koduj: "A83Z-KL9P-MM4X-VN2Q-8JC7" (20 znaków)           │
│  3. Zaszyfruj klucz główny: AES-GCM(masterKey, recoveryKey) │
│  4. Przechowaj zaszyfrowany klucz główny na serwerze        │
│  5. Pokaż użytkownikowi: ⚠️ ZAPISZ TO LUB STRACISZ DANE NA ZAWSZE │
│     [Pobierz PDF] [Drukuj] [Zapisałem/am]                   │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Przepływ odykiwania                                          │
├──────────────────────────────────────────────────────────────┤
│  1. Zapomniałeś/aś hasła? → Wpisz klucz odykiwania          │
│  2. Pobierz zaszyfrowany klucz główny z serwera             │
│  3. Odszyfruj klucz główny kluczem odykiwania               │
│  4. Ustaw NOWE hasło                                        │
│  5. Ponownie wyprowadź authHash + masterKey z nowego hasła  │
│  6. Sukces → dostęp przywrócony                             │
└──────────────────────────────────────────────────────────────┘

Ograniczenie prób: Maks. 5 prób odykiwania na godzinę (ochrona przed brute-force).


Powierzchnia ataku

Zneutralizowane zagrożenia

Zagrożenie Łagodzenie Phase
Naruszenie bazy danych ✅ E2EE (dane zaszyfrowane) 45
Kompromitacja serwera ✅ Zero-knowledge (brak kluczy deszyfrowania) 45
Zagrożenie wewnętrzne (admin) ✅ Nie może odszyfrować danych użytkownika 45
Atak MITM ✅ HTTPS + HSTS 42
Atak XSS ✅ Nagłówki CSP, bez skryptów inline 45
Path traversal ✅ Walidacja safePath 42
SSRF ✅ Allowlista (HTTPS + sprawdzanie domeny) 42
Brute-force hasła ✅ Bcrypt koszt 12 + ograniczanie prób 42
Atak powtórkowy ✅ Tagi auth AES-GCM 45
Wyciek multi-tenancy ✅ Bramki owner_id 42

Poza zakresem (odpowiedzialność użytkownika)

Zagrożenie Status
Fizyczny dostęp do urządzenia (odblokowany laptop) ❌ Użytkownik musi blokować ekran
Złośliwe rozszerzenie przeglądarki ❌ Może ukraść masterKey z pamięci
Keylogger na urządzeniu ❌ Przechwytuje hasło podczas logowania
Inżynieria społeczna (phishing klucza odykiwania) ❌ Edukacja użytkownika
Obliczenia kwantowe (złamanie AES-256) ⚠️ Bezpieczne do ~2040 (plan NIST)

Zgodność

RODO (Rozporządzenie UE 2016/679)

Artykuł Wymaganie Status
17 Prawo do usunięcia danych („prawo do bycia zapomnianym") 🎯 Planowane (#55)
20 Prawo do przenoszenia danych (eksport) 🎯 Planowane (#44)
25 Ochrona danych w fazie projektowania ✅ E2EE domyślnie
32 Bezpieczeństwo przetwarzania ✅ AES-256 + bcrypt
33 Powiadomienie o naruszeniu (72h) ✅ Plan incydentowy

SOC 2 Type II (Przyszłość)

Planowane dla enterprise:


Historia audytów

Phase 42: Bezpieczeństwo multi-tenancy (2026-04-23)

Audytor: Sentinel (wewnętrzny agent bezpieczeństwa)
Zakres: Izolacja multi-tenant, SSRF, path traversal, walidacja wejścia
Ustalenia: 16 problemów (wszystkie naprawione)
Werdykt: 🟢 ZIELONY
Pełny raport: docs/security/audit-2026-04-23.md

Phase 43: Bezpieczeństwo UI/UX (2026-04-24)

Audytor: Vanguard (design + dostępność)
Zakres: Wektory XSS, luki CSP, skrypty inline
Ustalenia: 21 problemów (wszystkie naprawione)
Werdykt: A- (95/100)
Pełny raport: docs/design/ui-ux-audit-2026-04-23.md

Phase 45: Implementacja E2EE (2026-04-28)

Zaimplementowane przez: Właściciel produktu + Claude
Zakres: Szyfrowanie at-rest, klucze odykiwania, nagłówki CSP, sanityzacja PII
Pod-fazy:

Phase 45: Test penetracyjny E2EE (PLANOWANY)

Audytor: Zewnętrzny tester penetracyjny (TBD)
Zakres: WebCrypto, zarządzanie kluczami, wycieki side-channel
Budżet: $5 000
Termin: Po ukończeniu Phase 45


Kontakt

Problemy z bezpieczeństwem: GitHub Security Advisory (prywatne ujawnienie)
Ogólny: [email protected]
Bug Bounty: $100–$5 000 (Phase 46+)


Ostatni audyt: Phase 45 E2EE (2026-04-28). Następny: Zewnętrzny test penetracyjny.