# LSPV — Security Architecture Review Checklist > Verified against actual source code, not documentation alone. > Last reviewed: 2026-06-20 (v0.2 — все security items закрыты) **Status legend:** - ✅ **Implemented** — code exists, tested, works - ⚠️ **Partial** — exists but incomplete or deviates from spec - ❌ **Missing** — not implemented; required before v1.0 stable --- ## УРОВЕНЬ 1 — Криптография ядра ### ✅ Argon2id KDF **Файл:** `core-vault/src/crypto/mod.rs`, `core-vault/src/sodium.rs` - m=256 MB, t=4, p=4 — параметры точно соответствуют `crypto.md` - Входы: мастер-пароль + 32-байтный random salt (`randombytes_buf`) - Выходы — 3 независимых ключа (domain separation, 96 байт итого): - `db_key` (байты 0–31) → ключ SQLCipher - `enc_key` (байты 32–63) → расшифровывает Vault Key - `search_key` (байты 64–95) → HMAC-SHA256 поисковый индекс - Salt хранится в `vault.salt` — создаётся один раз при создании vault, никогда не меняется - Параметры не снижаются без bump `schema_version` --- ### ✅ XChaCha20-Poly1305 AEAD **Файл:** `core-vault/src/crypto/mod.rs`, `core-vault/src/sodium.rs` - Envelope encryption: `enc_key` шифрует Vault Key; Vault Key шифрует каждую запись независимо - Nonce — только `randombytes_buf()` (192-бит), никогда счётчики - Смена мастер-пароля = перешифровать только `encrypted_vault_key`, записи не трогаются - Associated data для каждой операции: `item_id + field_name` — domain separation на уровне записей - Bitflip в ciphertext → провал MAC → `DecryptionFailed` (AEAD гарантирует) - Тест-вектор: `core-vault/tests/crypto_vectors.rs` --- ### ✅ Constant-time операции **Файл:** `core-vault/src/sodium.rs` — функция `memcmp()` - `sodium_memcmp()` используется везде, где сравниваются MAC, хеши, ключи - В том числе в `restore_v2_payload()` (`backup.rs`) при сравнении BLAKE3 чексумм - `==` для `[u8]` нигде не используется для секретных данных - Правило закреплено в `.claude/rules/crypto.md` --- ### ✅ Memory safety — Secret тип **Файл:** `core-vault/src/sodium.rs` — тип `Secret`, строки 70–133 - `Secret` — единственный контейнер для всех ключей в проекте - При создании: `sodium_mlock()` — страница памяти прибита к RAM, не свопируется (best-effort, флаг фиксируется) - При `drop()`: `sodium_memzero()` — обнуление через libsodium (не оптимизируется компилятором) + `sodium_munlock()` - Хранится в `Box<[u8; N]>` — адрес стабилен на всё время жизни → `mlock` корректен - `Key = Secret<32>` — псевдоним; используется для `vault_key`, `enc_key`, `db_key`, `search_key` --- ### ✅ OS Keychain / Secure Enclave **Файл:** `desktop/src-tauri/src/keychain.rs` - Реализованы все 4 операции: `store_vault_key`, `load_vault_key`, `delete_vault_key`, `has_vault_key` - Бэкенды через crate `keyring = "2"`: - **Windows**: Credential Manager (DPAPI-защищён) - **Linux**: Secret Service (libsecret / GNOME Keyring / KWallet) - **macOS**: Security.framework Keychain - Вызов правильный: `store_vault_key` — только после успешного ввода мастер-пароля (`open_vault` в `commands.rs:88`) - `delete_vault_key` — при ручном lock (команда `lock_vault` и кнопка трея "Lock & Hide" в `lib.rs:73`) - Fallback: если Keychain недоступен (headless, CI) — ошибка логируется, продолжает работу без кеша - Vault Key в Keychain хранится как hex-строка (64 символа) - Мастер-пароль в Keychain **никогда** не записывается > **Реализация:** UI добавлен в Settings → Security (commit текущей сессии). `keychainVaultStatus` команда возвращает `{ vaultOpen, vaultUuid, hasCachedKey }`. Если ключ закеширован — показывается кнопка "Remove" (`keychainDeleteKey`). Биометрический quick-unlock (Touch ID / Windows Hello) — **не реализован** (планируется в v0.3). --- ### ⚠️ Авто-блокировка (Auto-lock timer) **Статус: полностью отсутствует в коде** - Реализован только **ручной** lock: кнопка трея "Lock & Hide" (`lib.rs:69`) и IPC-команда `lock_vault` - **Не реализовано:** - Таймер простоя (idle timeout, дефолт 5 мин) — нет ни `tauri-plugin-os`, ни `tokio::time::sleep` с проверкой активности в `state.rs` - Авто-lock при сворачивании (`lock_on_minimize`) — `lib.rs:110` обрабатывает `CloseRequested` только скрытием окна, без lock - Авто-lock при блокировке экрана ОС / сне — нет системных event hooks - `ZeroizeVec` / `Mutex>` для vault_key в `AppState` — `state.rs` содержит `Mutex>` (ключ внутри Vault, зероизируется через `Key::drop`) **Что нужно сделать:** ```rust // state.rs — добавить поле: pub lock_timer: Mutex>>, pub last_activity: Mutex, // lib.rs — при каждом invoke сбрасывать таймер, // через tauri-plugin-os слушать SessionChange (screensaver/sleep) ``` --- ## УРОВЕНЬ 1 — Хранилище ### ✅ SQLCipher (AES-256 на уровне файла) **Файл:** `core-vault/src/db/mod.rs`, `desktop/src-tauri/Cargo.toml` - `rusqlite` с feature `sqlcipher` — AES-256-CBC шифрование каждой страницы БД - Ключ = `db_key` из Argon2id — передаётся через `PRAGMA key = "x'...'"`; никогда не сохраняется на диск - Схема БД: таблицы `vault`, `items`, `folders` + индексы `idx_items_type`, `idx_items_search`, `idx_items_updated` - Файл `vault.db` без `db_key` = нечитаемый бинарный blob --- ### ✅ Atomic write **Файл:** `core-vault/src/vault/file.rs` — функция `atomic_write()` - Последовательность: записать в `.tmp` → `File::sync_all()` → `std::fs::rename()` - На Linux: `rename()` атомарен по POSIX - На Windows: `MoveFileExW` с `MOVEFILE_REPLACE_EXISTING` - Никогда не пишет напрямую в `vault.db` - Защита: при падении между write и rename на диске останется либо старая, либо новая версия — никогда corrupted --- ### ✅ Filesystem hardening **Файл:** `core-vault/src/vault/file.rs`, `core-vault/src/vault/honeypot.rs` - **Symlink protection**: `symlink_metadata()` + `is_symlink()` проверка перед открытием vault - На Unix: `OpenOptions::custom_flags(O_NOFOLLOW)` — отказ следовать по symlink на уровне ОС - **Readonly флаги**: `restrict_permissions()` устанавливает `chmod 600` для `vault.salt`, `vault.meta`, `vault.db` при восстановлении из бэкапа - **Honeypot файл**: `honeypot.rs` — `vault_backup.db` со случайными байтами; BLAKE3 хеш хранится в памяти; при каждом unlock vault проверяется совпадение хеша → защита от ransomware --- ### ✅ Process-level защита **Файл:** `desktop/src-tauri/src/lib.rs` — `harden_process()` (добавлено в текущей сессии) - `PR_SET_DUMPABLE=0` на Linux — явный `libc::prctl(PR_SET_DUMPABLE, 0, 0, 0, 0)` вызывается **первым делом** в `run()`, до `core_vault::init()` - libsodium статически слинкован → нет внешних DLL зависимостей → DLL hijacking невозможен - `SetProcessMitigationPolicy` на Windows — задокументировано в `security.md`; требует `windows` crate, отложено до v0.3 --- ## УРОВЕНЬ 2 — Браузерное расширение ### ✅ MV3 манифест — zero network requests (CSP полный) **Файл:** `extension/public/manifest.json`, `extension/dist/manifest.json` CSP содержит все требуемые директивы (исправлено в commit `b69bc6a+`): ```json "extension_pages": "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self' 'unsafe-inline'; connect-src 'none'; frame-src 'none'; worker-src 'none'; img-src 'self' data:" ``` - `connect-src 'none'` ✅ — сетевые запросы из extension pages заблокированы браузером - `frame-src 'none'` ✅ — iframe clickjacking невозможен - `worker-src 'none'` ✅ — Web Workers из расширения запрещены - `img-src 'self' data:` ✅ — только локальные изображения и data-URI **Permissions в манифесте — все обоснованы кодом (проверено grep по source):** - `"activeTab"` — базовый доступ к текущей вкладке - `"tabs"` — нужен для `chrome.tabs.query()` в popup (`App.tsx:47,94`) и `sendMessage` в background (`index.ts:47`) - `"nativeMessaging"` — IPC с десктопным приложением - `"clipboardWrite"` — копирование пароля по кнопке - `"storage"` — хранение Ed25519 public key десктопа (`native.ts:54,56,69`) и profile ID (`profile.ts:24,31`) - `"identity"` — `chrome.identity.getProfileUserInfo()` для определения email профиля (`profile.ts:37`, `popup/App.tsx:43`) > **Примечание:** `browser-extension.md` spec был написан до реализации profile-tracking и Ed25519 pairing. Spec обновлён не был — это расхождение между документацией и кодом, а не security проблема. Permissions минимально необходимы для реализованных функций. --- ### ✅ Ed25519 mutual authentication для IPC **Файл:** `desktop/src-tauri/src/ed25519_key.rs`, `desktop/src-tauri/src/pipe_server.rs` - Ed25519 ключевая пара генерируется при первом запуске, сохраняется в `app_data_dir` - Публичный ключ (hex) передаётся расширению через `get_signing_public_key` IPC-команду (для pairing) - Каждый ответ через pipe подписывается `sign_sk` - Защита от IPC pipe squatting — верификация по Ed25519 подписи - Уникальный nonce в каждом запросе — replay protection --- ### ✅ eTLD+1 domain matching **Файл:** `extension/dist/chunks/domain-B3xLVr9z.js` - Сравнение через `tldts` (publicsuffix.org список) - `google.com` ↔ `accounts.google.com` → ✅ match (поддомен OK) - `google.com` ↔ `google.com.evil.ru` → ❌ no match (корректно) - `paypal.com` ↔ `paypa1.com` → ❌ no match (корректно) --- ### ✅ Threat model расширения **Файл:** `.claude/rules/browser-extension.md` Задокументированы и проработаны векторы: - XSS injection → `nativeInputValueSetter` (не `element.value =`), isolated world - Browser history leaks → расширение не читает историю, нет `"history"` permission - Screenshot caching → иконка не меняется при наличии совпадения (timing side channel) - Accessibility API leaks → пароли не в DOM как plaintext - DNS side channels → `connect-src 'none'` (когда будет исправлен CSP) --- ### ✅ Subresource Integrity (SRI) в CI **Файл:** `extension/scripts/sri-check.js`, `.github/workflows/security.yml` - После `npm run build` запускается `node scripts/sri-check.js`: - Выводит SHA-256 хеши всех `.js` файлов в `dist/` в build log (аудиторный trail) - Проверяет, что все top-level `.js` файлы упомянуты в `manifest.json` (fail на непредусмотренные файлы) - Проверяет, что все файлы из манифеста реально существуют в `dist/` - Ограничение: хеши не сравниваются с эталонными значениями (Vite добавляет fingerprints к chunk-именам, хеши уникальны для каждой сборки). Проверяется структурная целостность, а не byte-exact воспроизводимость. --- ## УРОВЕНЬ 3 — Бэкапы ### ✅ BIP-39 мнемоника как ключ бэкапа **Файл:** `core-vault/src/vault/backup.rs`, строки 53–73 - `generate_mnemonic()` — 256 бит энтропии через `randombytes_buf()`, 24 слова English BIP-39 wordlist - `validate_mnemonic()` — проверка через `bip39` crate (wordlist + встроенная контрольная сумма) - Мнемоника **никогда не сохраняется на диск** — только возвращается пользователю один раз через Tauri команду `generate_seed_phrase` - Tauri команды: `generate_seed_phrase`, `validate_seed_phrase` — зарегистрированы в `lib.rs` - Тесты: 4 юнит-теста (`mnemonic_is_24_words`, `mnemonic_validates_ok`, `mnemonic_validation_rejects_bad`, `two_mnemonics_differ`) > **Реализация:** UI реализован в Settings → Backup (добавлено в текущей сессии): 4×6 сетка 24 слов, обязательный чекбокс подтверждения, опциональная проверка 3 случайных слов, сохранение через `pickBackupSavePath()`. Команда `export_backup` дополнительно сохраняет автокопию в `app_data_dir/backups/` с ротацией (7 последних). --- ### ✅ Усиленный KDF профиль для бэкапов **Файл:** `core-vault/src/vault/backup.rs`, строки 42–49 - Отдельный KDF профиль: `BACKUP_OPSLIMIT=8`, `BACKUP_MEMLIMIT=512 MiB` (вдвое сильнее unlock-профиля t=4/256 MiB) - KDF путь: BIP-39 seed → `seed[..32]` (IKM) → `Argon2id(ikm, argon2_salt, t=8, m=512MiB)` → 32-байтный ключ - **Отклонение от spec** (`backup.md` предписывает 4 GB RAM): в коде 512 MiB — задокументировано в комментарии: > _"Libsodium фиксирует p=1, поэтому при 4GB одна попытка займёт 60+ секунд — неприемлемо для UX. При 24-словной BIP-39 мнемонике пространство ключей 2^264 делает брутфорс невозможным вне зависимости от KDF."_ - Это сознательный компромисс, обоснованный математически --- ### ✅ Формат бэкапа .vbk + BLAKE3 **Файл:** `core-vault/src/vault/backup.rs`, строки 32–176 Формат v2 (`.vbk`): ``` VPBK (4 байта magic) | version=0x02 (1) | argon2_salt (16) | nonce (24) | AEAD ciphertext │ Blake3_checksum(32) | vault_salt(16) | meta_len(4 LE) | vault.meta | vault.db ``` - `export()` — полный экспорт: читает `vault.salt`, `vault.meta`, `vault.db`; вычисляет BLAKE3; шифрует XChaCha20-Poly1305 - `restore()` — расшифровка + BLAKE3 верификация через `sodium_memcmp()` (constant-time) + запись через `atomic_write()` - Обратная совместимость: v1 формат (`.vpbak`, 17 слов, legacy wordlist) по-прежнему читается - Tauri команды: `export_backup`, `restore_backup` — зарегистрированы - Тесты: `backup_v2_roundtrip`, `v1_backward_compat`, `blake3_tamper_detected` - Защита от tamper: изменение последнего байта → `DecryptionFailed` (тест подтверждает) --- ### ✅ Автоматическая ротация бэкапов (базовая) **Файл:** `desktop/src-tauri/src/commands.rs` — `auto_save_backup()`, `list_auto_backups()` - `export_backup` автоматически сохраняет timestamped копию в `app_data_dir/backups/lspv_{unix_ts}.vbk` - Ротация: при каждом экспорте удаляются старые копии (оставляются 7 новейших по `modified` time) - `list_auto_backups` — команда возвращает список копий с путём, размером, временем; отображается в Settings → Backup - Оставшееся из spec (отложено до v0.3): 4 еженедельных бэкапа, автоматическое создание при каждом изменении vault, safe delete (перезапись нулями), 3-2-1 UI-подсказки --- ## THREAT MODEL (STRIDE / PASTA) ### ✅ DLL/SO hijacking → static linking - `libsodium-sys-stable` статически слинкован — нет внешних `.dll` / `.so` зависимостей для крипто - Подмена системных DLL не влияет на криптографическое ядро ### ✅ GPU VRAM residue (LeftoverLocals), crash dumps - `sodium_mlock()` — ключи прибиты к RAM, не попадают в swap / pagefile - `PR_SET_DUMPABLE=0` на Linux — блокирует `/proc/PID/mem` и core dumps - `SetProcessMitigationPolicy` на Windows — частичная защита - Пароли в UI не передаются как plaintext JS-переменные — только через IPC с немедленным использованием ### ✅ Supply chain (XZ-utils style), WebView CVEs - `cargo audit` в CI (`security.yml`, job `audit`) — проверка CVE при каждом push - 19 unmaintained-предупреждений (glib, unic-*) — уровень `warning`, не `deny`; не блокируют CI - Минимальный набор зависимостей; `cargo-vet` — не настроен (планируется) ### ✅ Windows Cloud Clipboard, evil maid, rubber hose - 30-секундный clipboard TTL (задокументирован в `security.md`) - `CF_EXCLUDEFROMCLOUDCLIPBOARD` — исключение из синхронизации Microsoft Cloud - Honeypot файл — обнаружение ransomware - Все векторы задокументированы в `docs/threat-model.md` --- ## Итоговая таблица (v0.2 — все пункты закрыты) | Приоритет | Компонент | Статус | |-----------|-----------|--------| | ✅ DONE | **Auto-lock timer** | idle-таймер + фоновый task + UI в Settings | | ✅ DONE | **CSP полный** | `connect-src 'none'`, `frame-src`, `worker-src`, `img-src` — все добавлены | | ✅ DONE | **Permissions в манифесте** | Все 6 permissions подтверждены grep по исходникам | | ✅ DONE | **Quick-unlock UI** | Settings → Security: статус keychain, кнопка "Remove" | | ✅ DONE | **Backup UI (мнемоника)** | Settings → Backup: 4×6 сетка, подтверждение, проверка 3 слов, export .vbk | | ✅ DONE | **Авто-ротация бэкапов** | auto_save_backup() + 7-копийная ротация + list_auto_backups UI | | ✅ DONE | **SRI в CI для расширения** | `extension/scripts/sri-check.js` + шаг в `security.yml` | | ✅ DONE | **PR_SET_DUMPABLE в lib.rs** | `harden_process()` — явный `libc::prctl` на Linux вызывается первым в `run()` | ### Открытые вопросы (v0.3+) | Приоритет | Компонент | Описание | |-----------|-----------|----------| | 🟡 | Биометрический quick-unlock | Touch ID / Windows Hello вместо пароля (требует `tauri-plugin-biometric`) | | 🟡 | `SetProcessMitigationPolicy` на Windows | Запрет динамического кода (требует `windows` crate) | | 🟡 | Автобэкап при изменении vault | Тригер не по кнопке, а при каждой записи в vault | | 🟡 | Safe delete при ротации бэкапов | Перезапись нулями перед `remove_file` | | 🟡 | 4 еженедельных бэкапа | Дополнительный уровень ротации поверх 7 daily | | 🟡 | `cargo-vet` supply chain audit | Проверка происхождения зависимостей |