Архітектура безпеки — Arc OS

«Ми не можемо прочитати ваші дані, навіть якби хотіли»
Zero-knowledge, наскрізне шифрування для приватності користувачів.

Останнє оновлення: 2026-04-28 (Phase 45 — E2EE Architecture DONE ✅)
Поточна фаза: 48 (декомпозицію архітектури завершено)
Статус безпеки: 🟢 GREEN (Phase 42 multi-tenancy + Phase 45 E2EE завершено)


Зміст

  1. Модель безпеки
  2. Zero-Knowledge архітектураNEW (Phase 45)
  3. Безпека multi-tenancy (Phase 42)
  4. Деталі шифрування
  5. Керування ключами
  6. Поверхня атаки
  7. Відповідність вимогам
  8. Історія аудитів

Модель безпеки

Поточний стан (Phase 48)

Автентифікація та авторизація:

Дані at-rest (Phase 45 — DONE ✅):

Вердикт: безпечно як для multi-tenancy, ТАК І для приватності даних користувача at-rest.

Реалізація (Phase 45 — гібридна архітектура)

Проєктне рішення: справжнє zero-knowledge E2EE неможливе, коли сервер мусить обробляти дані (Claude CLI потребує API-ключів у відкритому вигляді, child-bot — повідомлень у відкритому вигляді для AI-обробки). Рішення: гібридний підхід — клієнтська криптографічна основа + серверне шифрування at-rest.

Client (Browser):
  WebCrypto PBKDF2 (100k iter) → AES-256-GCM master key
  Master key lifecycle: login → sessionStorage → logout/401 → clear
  Recovery key: encrypt master key → store on server

Server (Bun + SQLite):
  vault.ts encryptField() → AES-256-GCM at rest for API keys
  db.ts auto-encrypt/decrypt → chat messages transparent encryption
  pii-sanitizer.ts → redact PII from JSONL logs

Zero-Knowledge архітектура

Phase 45 (DONE ✅ 2026-04-28) — задачі #16-#20

Принцип проєктування

Сервер є недовіреним. Навіть маючи root-доступ по SSH до бази даних, адміністратори не можуть розшифрувати дані користувача без його пароля.

Модель: E2EE у стилі Signal, адаптоване для колаборації в AI-робочому просторі.

1. Виведення майстер-ключа

З пароля користувача виводяться ДВА незалежні ключі:

User Password
    │
    ├─ PBKDF2(password, "auth-salt", 100k iterations) 
    │     ↓
    │  authHash (hashed again with bcrypt, cost 12)
    │     ↓
    │  Sent to server for authentication (login)
    │
    └─ PBKDF2(password, "master-salt", 100k iterations)
          ↓
       masterKey (AES-256-GCM encryption key)
          ↓
       NEVER sent to server (stays in browser sessionStorage)

Властивість безпеки: компрометація сервера → атакувальник отримує authHash → не може вивести masterKey (інша сіль).

2. Потік клієнтського шифрування

Надсилання повідомлення в чаті:

// 1. User types in browser
const plaintext = "sk-ant-abc123xyz (my API key)";

// 2. Browser encrypts with master key
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  masterKey,
  new TextEncoder().encode(plaintext)
);

// 3. Send encrypted blob to server (NO plaintext)
POST /api/crm/projects/arc-v2/chat {
  content_encrypted: base64(ciphertext),  // server cannot read this
  content_iv: base64(iv)
}

Зберігання на сервері (SQLite):

INSERT INTO chat_messages (content_encrypted, content_iv, timestamp)
VALUES (
  X'8a9f3c...blob...',  -- encrypted, opaque to server
  X'7b2e1a...iv...',
  '2026-04-24T10:30:00Z'
);

Адмін робить запит до бази:

SELECT content_encrypted FROM chat_messages WHERE id = 1;
-- Returns: blob (meaningless without master key)

Отримання повідомлення:

// 1. Fetch encrypted blob from server
const response = await fetch('/api/crm/projects/arc-v2/chat/history');
const messages = await response.json();

// 2. Browser decrypts with master key
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. Що шифрується

Тип даних Шифрується? Issue Примітки
Повідомлення чату ✅ Так #40 Усі розмови користувач ↔ AI
API-ключі (Anthropic, OpenAI) ✅ Так #41 Зберігаються в таблиці account_settings
TOTP-секрети (2FA-сіди) ✅ Так #42 Клієнтська генерація OTP
Env-змінні проєкту ✅ Так Майбутнє .env-файли зашифровано
Email-адреса ❌ Ні N/A Потрібна для логіну/auth
Назви проєктів ❌ Ні N/A Потрібні для рендерингу UI
Часові мітки ❌ Ні N/A Безпечні метадані
Хеш пароля (bcrypt) ❌ Ні N/A Виводиться з authHash, не з masterKey

4. Зміни серверної схеми

До (Phase 43):

CREATE TABLE chat_messages (
  id INTEGER PRIMARY KEY,
  content TEXT NOT NULL,  -- ❌ plaintext
  timestamp TEXT
);

Після (Phase 45):

CREATE TABLE chat_messages (
  id INTEGER PRIMARY KEY,
  content_encrypted BLOB NOT NULL,  -- ✅ AES-GCM ciphertext
  content_iv BLOB NOT NULL,          -- ✅ initialization vector
  timestamp TEXT,
  key_version INTEGER DEFAULT 1      -- for key rotation
);

Безпека multi-tenancy

Phase 42 (COMPLETE) — повний звіт аудиту: docs/security/audit-2026-04-23.md

Модель ізоляції

Кожен користувач володіє своїми проєктами. Жоден користувач не має доступу до даних проєктів іншого (крім admin/CEO).

Gate-функція (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;  // superuser bypass
  
  // DB SSOT: owner_id check
  const project = projectQueries.findByName(projectName);
  return project?.owner_id === chatId;
}

Застосовується на:

Шари захисту (мережа → застосунок)

┌─────────────────────────────────────────────────────────────┐
│ Шар 1: Інфраструктура                                       │
│   - Лише SSH-автентифікація за ключем (без пароля)         │
│   - Fail2ban (5 невдалих спроб → бан на 10 хв)             │
│   - Файрвол UFW (лише 22, 80, 443)                         │
├─────────────────────────────────────────────────────────────┤
│ Шар 2: Мережа                                               │
│   - Bun біндиться лише на 127.0.0.1 (без зовнішнього       │
│     доступу)                                                │
│   - Реверс-проксі Nginx (блокування шляхів: /.*, /config/) │
│   - HTTPS (TLS 1.3) + HSTS                                 │
├─────────────────────────────────────────────────────────────┤
│ Шар 3: Автентифікація                                       │
│   - JWT (HMAC-SHA256, TTL 24 год, секрет у vault)          │
│   - OAuth (Google, GitHub) із CSRF-токенами                │
│   - Email-верифікація (TTL 24 год)                         │
│   - Rate limiting (логін: 5/хв)                            │
├─────────────────────────────────────────────────────────────┤
│ Шар 4: Авторизація                                          │
│   - Multi-tenancy gates (перевірки owner_id)               │
│   - Інтерактивний термінал лише для адмінів                │
│   - JWT-токени з обмеженням на проєкт                      │
├─────────────────────────────────────────────────────────────┤
│ Шар 5: Валідація вводу                                      │
│   - Regex isValidProjectName                               │
│   - safePath (запобігання path traversal)                  │
│   - SSRF-allowlist (HTTPS + whitelist доменів)             │
├─────────────────────────────────────────────────────────────┤
│ Шар 6: Захист даних (Phase 45)                              │
│   - E2EE (клієнтське шифрування)                           │
│   - Zero-knowledge архітектура                             │
│   - CSP-заголовки (запобігання XSS)                        │
└─────────────────────────────────────────────────────────────┘

Патчі Phase 42 (16 виправлень, усі завершено)

ID Виправлення Статус
SEC-1 Multi-tenancy gate на SSE-маршрутах
SEC-2 Gate на WebSocket-терміналі + interactive лише для адмінів
SEC-3 Entry-gate блоку CLI/MCP (12+ ендпоінтів)
SEC-4 Bun.serve bind 127.0.0.1
SEC-5 Валідація /api/internal/chat/save
SEC-6 Path traversal у handleSaveSkill
SEC-REG1 Підтримка SSE ?token=
SEC-NEW1 SSRF-allowlist у handleScoutAnalyze
SEC-NEW2 Канарка проксі-заголовків на /api/internal/*
SEC-NEW4 redirect:"manual" — блок ланцюга SSRF
SEC-NEW6 Rate limit для скидання пароля/верифікації

Вердикт: 🟢 GREEN — готово до багатокористувацького режиму (відомих векторів ескалації привілеїв немає)


Деталі шифрування

Алгоритми (Phase 45)

Компонент Алгоритм Розмір ключа Ітерації/Cost
Виведення майстер-ключа PBKDF2-SHA256 256-bit 100 000 (OWASP 2025)
Шифрування даних AES-GCM 256-bit N/A (симетричне)
Auth-хеш пароля bcrypt 12 раундів (4 096 ітер.)
Ключ відновлення Випадкові байти 128-bit N/A

Реалізація PBKDF2

// Auth hash (sent to server)
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 }
);

// Master key (kept in browser)
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,  // NOT extractable
  ["encrypt", "decrypt"]
);

Шифрування AES-GCM

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

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

Чому AES-GCM?


Керування ключами

Життєвий цикл

┌──────────────────────────────────────────────────────────────┐
│ Реєстрація / Перший логін                                    │
├──────────────────────────────────────────────────────────────┤
│  1. Користувач вводить пароль                                │
│  2. Браузер виводить authHash + masterKey (PBKDF2)          │
│  3. Надсилає authHash на сервер (bcrypt → зберегти)         │
│  4. Зберігає masterKey у sessionStorage (ефемерно)          │
│  5. Генерує ключ відновлення (шифрує masterKey → на сервер) │
│  6. Користувач завантажує recovery-PDF (ОБОВ'ЯЗКОВО         │
│     ЗБЕРЕГТИ!)                                              │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Наступні логіни                                              │
├──────────────────────────────────────────────────────────────┤
│  1. Користувач вводить пароль                                │
│  2. Виводить authHash → надсилає на сервер → верифікація    │
│  3. Виводить masterKey → зберігає у sessionStorage          │
│  4. Готово до шифрування/дешифрування                       │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Логаут / Закриття вкладки                                    │
├──────────────────────────────────────────────────────────────┤
│  sessionStorage.clear() → masterKey стерто                  │
│  Немає ключа = неможливо розшифрувати дані                  │
└──────────────────────────────────────────────────────────────┘

Механізм відновлення

Проблема: забутий пароль → masterKey втрачено → дані не відновити.

Рішення: ключ відновлення (генерується один раз, зберігається користувачем офлайн).

┌──────────────────────────────────────────────────────────────┐
│ Генерація ключа відновлення                                  │
├──────────────────────────────────────────────────────────────┤
│  1. Згенерувати випадковий 128-бітний ключ                   │
│     recoveryKey = crypto.getRandomValues(16 bytes)          │
│  2. Закодувати: "A83Z-KL9P-MM4X-VN2Q-8JC7" (20 символів)    │
│  3. Зашифрувати майстер-ключ: AES-GCM(masterKey, recoveryKey)│
│  4. Зберегти зашифрований майстер-ключ на сервері           │
│  5. Показати користувачу: ⚠️ ЗБЕРЕЖІТЬ, ІНАКШЕ ВТРАТИТЕ     │
│     ДАНІ НАЗАВЖДИ                                           │
│     [Download PDF] [Print] [I Saved It]                     │
└──────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────┐
│ Потік відновлення                                            │
├──────────────────────────────────────────────────────────────┤
│  1. Забули пароль? → Введіть ключ відновлення               │
│  2. Отримати зашифрований майстер-ключ із сервера           │
│  3. Розшифрувати майстер-ключ ключем відновлення            │
│  4. Встановити НОВИЙ пароль                                 │
│  5. Перевивести authHash + masterKey з нового пароля        │
│  6. Успіх → доступ відновлено                               │
└──────────────────────────────────────────────────────────────┘

Rate Limit: максимум 5 спроб відновлення на годину (захист від brute-force).


Поверхня атаки

Нейтралізовані загрози

Загроза Захист Фаза
Витік бази даних ✅ E2EE (дані зашифровано) 45
Компрометація сервера ✅ Zero-knowledge (ключів дешифрування немає) 45
Інсайдерська загроза (адмін) ✅ Не може розшифрувати дані користувача 45
MITM-атака ✅ HTTPS + HSTS 42
XSS-атака ✅ CSP-заголовки, без inline-скриптів 45
Path traversal ✅ Валідація safePath 42
SSRF ✅ Allowlist (HTTPS + перевірка домену) 42
Brute-force пароля ✅ Bcrypt cost 12 + rate limiting 42
Replay-атака ✅ Auth-теги AES-GCM 45
Витік multi-tenancy ✅ Gates за owner_id 42

Поза охопленням (відповідальність користувача)

Загроза Статус
Фізичний доступ до пристрою (розблокований ноутбук) ❌ Користувач має блокувати екран
Зловмисне розширення браузера ❌ Може вкрасти masterKey з пам'яті
Кейлогер на пристрої ❌ Перехоплює пароль під час логіну
Соціальна інженерія (фішинг ключа відновлення) ❌ Освіта користувачів
Квантові обчислення (злам AES-256) ⚠️ Безпечно до ~2040 (план NIST)

Відповідність вимогам

GDPR (Регламент ЄС 2016/679)

Стаття Вимога Статус
17 Право на стирання («право бути забутим») 🎯 Заплановано (#55)
20 Право на портативність даних (експорт) 🎯 Заплановано (#44)
25 Захист даних за задумом (by design) ✅ E2EE за замовчуванням
32 Безпека обробки ✅ AES-256 + bcrypt
33 Сповіщення про витік (72 год) ✅ План реагування на інциденти

SOC 2 Type II (майбутнє)

Заплановано для enterprise:


Історія аудитів

INC-2026-06-11: Криптомайнер на Contabo VPS (2026-06-11) — ⚠️ CONTAINED

Тип: інцидент безпеки (не аудит) Серйозність: P0 — активна компрометація хоста, колокованого з продакшеном Першопричина: сторонній стек автономних агентів (OpenClaw), що співіснував на машині Arc OS, створив sudo-здатний акаунт (aiuser) із заздалегідь підкладеним SSH-ключем атакувальника; Monero-майнер працював ~76 днів на 294% CPU під цим акаунтом. Вплив на Arc OS: нічого не зламано — root чистий, vault недоступний для aiuser, латерального просування немає. Вкрадено лише CPU. Статус: локалізовано (загрозу знешкоджено); повна ремедіація (перебудова Contabo, рішення щодо OpenClaw, ротація облікових даних) відкрита. Повний звіт: docs/security/incident-2026-06-11-contabo-miner.md · задача #474

Phase 42: Безпека multi-tenancy (2026-04-23)

Аудитор: Sentinel (внутрішній агент безпеки)
Охоплення: ізоляція multi-tenant, SSRF, path traversal, валідація вводу
Знахідки: 16 проблем (усі виправлено)
Вердикт: 🟢 GREEN
Повний звіт: docs/security/audit-2026-04-23.md

Phase 43: Безпека UI/UX (2026-04-24)

Аудитор: Vanguard (дизайн + доступність)
Охоплення: XSS-вектори, прогалини CSP, inline-скрипти
Знахідки: 21 проблема (усі виправлено)
Вердикт: A- (95/100)
Повний звіт: docs/design/ui-ux-audit-2026-04-23.md

Phase 45: Реалізація E2EE (2026-04-28)

Реалізували: Product Owner + Claude
Охоплення: шифрування at-rest, ключі відновлення, CSP-заголовки, санітизація PII
Підфази:

Phase 45: Пентест E2EE (PLANNED)

Аудитор: зовнішній пентестер (TBD)
Охоплення: WebCrypto, керування ключами, side-channel витоки
Бюджет: $5,000
Терміни: після завершення Phase 45


Контакти

Проблеми безпеки: GitHub Security Advisory (приватне розкриття)
Загальні питання: [email protected]
Bug Bounty: $100 – $5,000 (Phase 46+)


Останній аудит: Phase 45 E2EE (2026-04-28). Далі: зовнішній пентест.