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
- Model bezpieczeństwa
- Architektura Zero-Knowledge ⭐ NOWE (Phase 45)
- Bezpieczeństwo multi-tenancy (Phase 42)
- Szczegóły szyfrowania
- Zarządzanie kluczami
- Powierzchnia ataku
- Zgodność
- Historia audytów
Model bezpieczeństwa
Stan obecny (Phase 48)
Uwierzytelnianie i autoryzacja:
- ✅ JWT (HMAC-SHA256, TTL 24h)
- ✅ OAuth (Google, GitHub)
- ✅ Izolacja multi-tenancy (bramki
owner_id) - ✅ Ochrona przed path traversal
- ✅ Allowlisty SSRF
- ✅ Nagłówki CSP (
default-src 'self',X-Frame-Options: DENY) - ✅ Nagłówki bezpieczeństwa (
X-Content-Type-Options: nosniff,Referrer-Policy)
Dane at-rest (Phase 45 — DONE ✅):
- ✅ Klucze API szyfrowane przez vault AES-256-GCM (
encryptField/decryptField) - ✅ Wiadomości czatu szyfrowane at-rest w SQLite (migracja 015, auto encrypt/decrypt)
- ✅ Sanityzacja PII w logach JSONL (emaile, klucze API, JWT, numery kart)
- ✅ Zarządzanie kluczami odykiwania (styl 1Password
XXXX-XXXX-XXXX-XXXX-XXXX)
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:
/api/crm/projects/:name/*(62+ endpointów)/api/sse/logs/:name,/api/sse/consultant/:name/ws/terminal/:name/api/cli/*,/api/mcp/*(API wiedzy)
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?
- ✅ Uwierzytelnione szyfrowanie (odporne na manipulacje)
- ✅ Akcelerowane sprzętowo (AES-NI na x86)
- ✅ Zatwierdzone przez NIST, używane przez Signal/WhatsApp/TLS 1.3
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:
- Dziennik audytu dostępu
- Polityka rotacji kluczy (coroczna)
- Testy penetracyjne (kwartalne)
- Ocena ryzyka dostawców
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:
- 45.1 — Fundament WebCrypto (
frontend/src/crm/crypto/e2ee.ts, 214 linii) ✅ - 45.2 — Szyfrowanie vault kluczy API (
shared/vault.tsencryptField/decryptField) ✅ - 45.3 — Szyfrowanie wiadomości czatu at-rest (migracja 015, db.ts auto-encrypt) ✅
- 45.4 — Klucze odykiwania (migracja 016, 4 endpointy API, UI RecoveryKeySection) ✅
- 45.5 — Nagłówki CSP + sanityzator PII (
shared/pii-sanitizer.ts) ✅ Werdykt: 🟢 Wszystkie pozycje P0+P1 ukończone. Zaawansowane funkcje P2 (forward secrecy, synchronizacja multi-device) odroczone.
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.