# 05. Состояние и хранилище — Montana Android v6.5.0 ## §1. Where state lives | Сущность | Хранилище | Доступ | Защита | |----------|-----------|--------|--------| | BIP39 24-слова (seed) | localStorage WebView `"m.seed"` | JS app.html | Android sandbox 0700 | | Адрес кошелька | localStorage WebView `"m.addr"` | JS app.html | Android sandbox 0700 | | Баланс (last sync) | `MontanaVpnService.lastBalance` (volatile) | Kotlin bridge | RAM only, теряется при stop | | Текущий exit-узел | `MontanaVpnService.connectedNode` | Kotlin bridge | RAM only | | Authoritative баланс | `/var/lib/montana-vpn-balance/balances.json` (Moscow) | flask backend | single-server, не реплицировано | | Reality private keys узлов | `/usr/local/etc/xray/config.json` (по узлам) | xray user nobody | UNIX 0640, root и xray-user only | | haproxy stick-table | RAM на Helsinki | haproxy process | persisten 24h в RAM | | Genesis keystore | `Montana/Android/keystore/montana.keystore` | сборщик APK | у автора, не в git | --- ## §2. Android app data dir layout ``` /data/data/quest.montana.vpn/ ├── app_webview/ │ └── Default/ │ ├── Local Storage/ │ │ └── leveldb/ ← здесь "m.seed" и "m.addr" │ ├── Cookies │ └── databases/ ├── files/ │ ├── geoip.dat ← xray-data, copied на первом запуске │ ├── geosite.dat │ └── hev.yaml ← runtime config hev-socks5-tunnel ├── cache/ │ └── ... ├── shared_prefs/ │ └── ... └── lib -> /data/app/.../lib/arm64/ ├── libgojni.so ← libv2ray (xray-core) ├── libhev-socks5-tunnel.so └── libhysteria2.so ← не используется в 6.5.0 (legacy artifact) ``` Permissions: ``` drwx------ owner=u0_a352 /data/data/quest.montana.vpn/ ``` UID `u0_a352` — Android per-app UID. Другие приложения видят только свои dir-ы. --- ## §3. localStorage WebView — формат После создания кошелька через `createKey()`: ```javascript localStorage.setItem("m.seed", "ranch basket resource enemy bridge spray holiday thing yellow round army mimic renew head test cradle piece public differ diamond connect leisure wrong ask") localStorage.setItem("m.addr", "2f8714b236118011647ec51d0ca6ad40d286bec7") ``` **Размер:** ~250 байт (24 слова × среднее 7 букв + spaces) + 40 hex address = ~290 байт total. **Persistence:** WebView Chromium leveldb. Сохраняется между запусками приложения. Удаляется при: - `pm clear quest.montana.vpn` (ADB или Android Settings → Apps → Clear data) - Деинсталляция приложения - Vasily `logoutAcc()` в самом app **Encryption:** **отсутствует** на уровне приложения. Полагаемся только на Android FBE (File-Based Encryption) при выключенном устройстве. --- ## §4. Recovery procedure `recover()` в app.html: ```javascript async function recover(){ const t = document.getElementById('seed-in').value.trim().toLowerCase(); const w = t.split(/\s+/).filter(Boolean); const errEl = document.getElementById('err'); errEl.textContent = ''; try { const addr = await deriveAddr(w); // checks checksum + derives localStorage.setItem(L_S, w.join(' ')); localStorage.setItem(L_A, addr); st.addr = addr; applyAuth(); show('main'); } catch(e) { errEl.textContent = e.message; // "контрольная сумма не совпадает" / "неизвестное слово" } } ``` **Validation:** - Точно 24 слова (иначе throw) - Каждое слово ∈ BIP39 EN wordlist (`indexOf` returns -1 → throw) - Checksum bits совпадают с `SHA-256(entropy)[0:8 bits]` → throw if mismatch После всех проверок — детерминированно выводится адрес. Сохраняется в localStorage. Главный экран показывает баланс. --- ## §5. Server-side state: balances.json **Файл:** `/var/lib/montana-vpn-balance/balances.json` (UNIX 0644 owner www-data). **Формат:** ```json { "2f8714b236118011647ec51d0ca6ad40d286bec7": { "balance": 0.043, "seconds": 43.5, "last_hb": 1779120000.5, "created": 1779119900.2, "last_node": "newyork" }, ... } ``` **Поля:** - `balance` — накопленные Ɉ (float, точность 1e-6) - `seconds` — суммарное время online (float) - `last_hb` — UNIX timestamp последнего heartbeat - `created` — UNIX timestamp первого heartbeat - `last_node` — символическое имя узла последнего heartbeat (`helsinki`/`frankfurt`/`newyork`) **Atomic locking** через `with_state_lock()`: ```python def with_state_lock(mutator): LOCK_PATH = DATA.with_suffix(".lock") with open(LOCK_PATH, "a+") as lock_f: fcntl.flock(lock_f, fcntl.LOCK_EX) try: with open(DATA, "r") as f: data = json.load(f) result = mutator(data) tmp = DATA.with_suffix(".tmp") with open(tmp, "w") as f: json.dump(data, f) os.replace(tmp, DATA) return result finally: fcntl.flock(lock_f, fcntl.LOCK_UN) ``` Один LOCK_EX охватывает read+modify+write — concurrent heartbeats сериализуются. --- ## §6. Cleanup policy **`/api/vpn/admin/purge`** (localhost-only): ```python PURGE_INACTIVE_DAYS = 30 def _purge_inactive(data): cutoff = time.time() - PURGE_INACTIVE_DAYS * 86400 to_remove = [] for addr, rec in data.items(): last_hb = rec.get("last_hb", 0) balance = rec.get("balance", 0) if last_hb < cutoff and balance < 1e-9: to_remove.append(addr) for addr in to_remove: del data[addr] return to_remove ``` **Критерий удаления:** `last_hb` старше 30 дней **И** `balance < 1e-9`. **Не удаляются:** - Записи с положительным балансом (даже если давно неактивны) — пользователь может вернуться - Записи моложе 30 дней **Manual trigger:** на текущий момент cron job не настроен. Cleanup вызывается вручную автором (`curl -X POST http://127.0.0.1:5008/api/vpn/admin/purge`). **Closure path** (Phase 2): cron daily + автоматический purge при каждом 1000-м heartbeat. --- ## §7. Состояние при сбое узла Moscow **Сценарий:** Moscow упал, недоступен. **Последствия:** - Heartbeats клиентов получают TCP timeout / connection refused - В Kotlin это catch → `Log.w(TAG, "hb err: ${e.message}")` → продолжение цикла - В JS UI counter продолжает идти оптимистично (+0.001/sec) - При восстановлении Moscow → следующий heartbeat прошёл → backend сравнивает client `last_hb` с now → начисляет gap но не более `MAX_GAP_SECONDS = 30` секунд **Что теряется:** - Heartbeats во время outage пропадают (никогда не доедут до backend) - При outage > 30 секунд — клиент теряет credit за время свыше 30s **Closure path:** - Phase 2: Postgres replica на другой узел - Phase 3: TimeChain consensus — Moscow становится один из validator, отказ Moscow не теряет state --- ## §8. Состояние при сбое узла VPN **Сценарий:** Frankfurt упал, активные cascade сессии разрываются. **Последствия:** - haproxy TCP-check за 6 сек detected → backend `fra` DOWN - `option redispatch retries 2` — sticked клиенты с этого backend → автоматический реrouting на следующий best (по leastconn) - Stick-table entry для этих IP обновляется на новый backend - Существующие TCP сессии разрываются — клиентский xray делает reconnect → новое TCP → новый backend **От пользователя UX:** короткий disconnect (5-10 секунд), затем восстановление через другой exit. **Empirical verification:** требует injection test, не выполнено в этой сессии. --- ## §9. Genesis keystore **Файл:** `/Users/kh./Python/Ничто/Montana/Android/keystore/montana.keystore` + `.password` рядом. **Метаданные:** - Alias: `montana` - Owner: `CN=Montana VPN, O=Montana Network, L=Genesis, C=RU` - Valid: 2026-05-06 → 2126-04-12 (100 лет) - Подписной алгоритм: SHA384withRSA, 4096-bit - SHA-256 fingerprint: `305bc99b40e6106f28c6fcc5dce4772761d2630d5aca9fee076dc0691913ce4d` **Security:** - **Не в git** (исключён через `.gitignore` родительского репозитория + hub-sync excludes) - Хранится у автора локально - Password в `.password` рядом — **не encrypted file system level** **Сценарий компрометации keystore:** - Атакующий может выпустить malicious APK с тем же signature → Android воспринимает его как обновление приложения - Пользователь обновляется через Google Play / direct APK → malicious code исполняется в context уже-доверенного приложения - Доступ ко всем localStorage данным, перехват heartbeat, кража кошельков **Mitigation:** - Хранение keystore offline (текущее: на устройстве автора) - Регулярное создание backup (текущее: не автоматизировано) - Closure path: hardware security module (HSM) для подписи APK при mainnet --- ## §10. Сводка по защищённости состояния | Данные | At rest | In transit | Гарантия persistence | |--------|---------|------------|----------------------| | Seed 24 слова | **plain text** localStorage | n/a (не покидает устройство) | до `pm clear` | | Адрес | plain text localStorage | в каждом heartbeat | до `pm clear` | | Баланс (client) | RAM only | в JSON ответе от backend | теряется при stop service | | Баланс (server) | JSON-файл с LOCK_EX | TLS Reality | до 30 дней inactive с пустым balance | | Reality privкeys | UNIX 0640 | n/a | пока хостинг провайдер не упадёт | | haproxy stick-table | RAM Helsinki | n/a | 24h expire | | Genesis keystore | plain `.keystore` file | n/a (offline) | пока автор хранит | **Главная слабость:** seed в plain text + single point of failure Moscow для balance. Closure paths указаны в `07-Известные-ограничения.md`.