# THREAT MODEL — крипта и транспорт зала ·22 «СВЯЗЬ»

> Документ собран ИЗ КОДА v1.25.1 (крипта.mjs v3.0.0, СИНГУЛЯР_22_СВЯЗЬ.html ядро 1.5.0),
> а не из пожеланий. Каждый пункт проверяется тестом из ИНСТРУМЕНТЫ/ — ссылки рядом.
> Честное правило СИНГУЛЯРА: если чего-то модель НЕ защищает — так и написано.

## 1. Что защищаем (активы)

| Актив | Где живёт | Что считается провалом |
|---|---|---|
| Смысл текстов зала | экраны участников + durable журнал кристалла на их устройствах | открытый текст у любого, кто не знает фразу |
| Парольная фраза зала | только голова человека (изо рта в ухо) | фраза восстановлена по перехваченному проводу |
| Ключ эпохи | память вкладки (не localStorage, не IndexedDB, не диск) | ключ вытащен из хранилища или уехал по проводу |
| Рукопожатие (SDP/ICE) | сигналинг Nostr-релеев | оператор релея собирает IP/тайминги и коррелирует залы |
| Личность ·19 | приватный ECDSA в IndexedDB, non-extractable | подпись подделана, имя присвоено |

## 2. Противники (что мы им даём сделать)

1. **Оператор публичного Nostr-релея.** Видит: факт подключения, номер события, зашифрованное
   рукопожатие. Не видит: IP пиров (SDP зашифрован паролем зала — v1.25.1), текстов (их по релею
   нет вообще), залов один за другим (соль сигналинга комнатная — таблицу на все залы посчитать нельзя).
2. **Участник зала без фразы («Европа»).** Видит: шифровки (kid/ep/kdf — метаданные печати),
   подписанные приветствия (имя + публичный JWK), тайминги. Не видит: текстов. Честно: метаданные
   переписки (кто/когда/сколько) Европа видит — это не шифруется в этой модели.
3. **Перехватчик записи P2P-провода** (участник зала, сохраняющий конверты). Видит шифровки.
   Не может словарём вскрыть фразу: kid с v3 живёт внутри растяжки PBKDF2-600k — цена одной
   догадки = полная растяжка (тест: `v3 ЩИТ: kid = хвост растяжки`), быстрых хэшей фразы на проводе больше нет.
4. **Подделыватель пакетов.** GCM-auth: подмена iv/ct/kid/kdf ломает вскрытие (тесты: подделка
   ct — и v3, и наследие). DoS-печать с миллиардом итераций отсечена пределом 100k–2M (тест: печатные границы).
5. **Кража устройства.** Приватная личность non-extractable; ключ эпохи только в памяти вкладки
   и уходит при закрытии зала. Честно: перезагруженная вкладка помнит историю журнала (тот же домен,
   что экран), а фразу человек вводит снова.

## 3. Границы модели (защищает ли это всё? НЕТ — честно)

- **Фраза — корень.** Слабая фраза = слабый зал. Растяжка делает перебор ДОРОГИМ, не невозможным:
  600k итераций × словарь — часы-недели GPU на одну фразу одного зала, а не триллионы в секунду.
- **Форвард-секретности НЕТ.** Носитель фразы выводит ключи всех её эпох. «Стереть прошлое» —
  стирание на устройстве, не математика; вскрытие чтит стёртые эпохи (тест: стёртая эпоха не выводится).
- **Метаданные signalling-уровня** (сам факт связи, размеры, тайминги) — видны на релеях; шифруется
  содержимое, не факт общения.
- **UI-журнал участника** хранит вскрытый текст на его устройстве. Канонический журнал кристалла — только шифровку.
- **Голос/файлы меж устройств — следующий такт** (едут локально, запечataн только текст).
- **Разные версии клиента.** Сигналинг под паролем несовместим с клиентами до v1.25.1 (старый клиент
  не ответит на challenge) — зал для межустройственной связи должен быть на одной версии. Это осознанная
  цена закрытия дыры, а не регресс.
- **ASR браузера** («речь → текст» в Chromium) шлёт аудио в облако Google — граница названа в UI ·22.

## 4. Механизмы защиты (где в коде)

| Механизм | Код | Тест |
|---|---|---|
| kid из растяжки: PBKDF2 320 бит = 32Б ключ + 8Б kid | крипта.mjs `растяниV3` | `v3 ЩИТ: kid = хвост растяжки…` |
| 600 000 итераций (OWASP PBKDF2-HMAC-SHA256) | крипта.mjs `ИТЕРАЦИИ` | `v3 параметры` |
| kdf-параметры в печати v3 + предел 100k–2M | крипта.mjs `запечатай`/`вскрытьПечать` | `v3 ЩИТ: печатные границы` |
| Читаемость старых печатей v1/v2 (fallback по версии, GCM-auth) | крипта.mjs `ключЭпохиНаследие` | `v3 ЩИТ: НАСЛЕДИЕ…` |
| Свежий 96-битный IV на сообщение, AES-256-GCM | крипта.mjs `запечатай` | `свежий IV на каждое сообщение` |
| Ключ только в памяти вкладки; карта эпох; стирание прошлого | С22 `КРИПТА22`, `стеретьПрошлое` | `стереть прошлое держит только текущую эпоху` |
| Верификация личности: ECDSA над канонБайты | крипта.mjs `завериФакт/верьФакту` | `v2 ВЕРИФИКАЦИЯ ·19` |
| Открытый текст не доезжает до релея (правило `текстНаРеле`) | С22 `проводТекстПослать` | сниф: открытый текст НЕ в фреймах |
| Сигналинг под паролем зала: PBKDF2(код), соль комнатная | С22 `парольСигналинга`, `password:` | сниф: SDP/ICE не читаются |
| onJoinError: отказ входа называется честно | С22 `onJoinError` | `onJoinError честно называет отказ входа` |
| Гвард зала: чужой код/комната-сирота не доходит до журнала | С22 `принять` | `зал-гвард: чужой код зала…` |
| Токен не ездит в URL: одноразовый ws-ticket 30 с | server.mjs `/api/ws/ticket` | test_s19_human: `тот же ticket второй раз НЕ пускает` |
| ws:// только для localhost, иначе честный отказ | ·19 `соединить` (S1-инвариант) | review v1.25.1 (С1 закрыт) |

## 5. Что изменилось против v1.25 (повод для этого документа)

Ревью трёх независимых читателей (код + официальные доки) нашло: kid = одинарный SHA-256(домен:код:фраза)
едет в каждой печати — словарная атака со скоростью хэша, PBKDF2 обходится целиком. В v1.25.1:
kid спрятан в растяжку (проверить догадку = 600k итераций), итерации подняты по OWASP, параметры
вынесены в печать с пределами, старые печати остались читаемыми, сигналинг зашифрован паролем зала,
отказы входа названы вслух, чужой зал отсечён гвардом, ws-токен убран из URL. Формулы наследия
сохранены ТОЛЬКО для чтения уже отправленного — новые печати их не производят.
