montana/Android/Внешний-аудит/05-Состояние-и-хранилище.md
2026-05-18 22:11:45 +03:00

255 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.