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

241 lines
13 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.

# 06. Восстановление кошелька — Montana Android v6.5.0
## §1. Recovery scenario — что должно работать
**Сценарий:** пользователь записал 24 слова на бумаге при создании кошелька. Через месяц установил Montana app на новое устройство, ввёл 24 слова в `s-recover` экран. Ожидание: тот же адрес, тот же баланс на сервере.
**Условия:**
1. Любая Android-версия с API ≥ 24
2. Любая модель устройства (ARM64 / ARMv7 / x86_64 / x86)
3. Любая Android WebView версия (включая Android System WebView обновления)
4. Любое часовое пояс / язык системы
---
## §2. Recovery cipher chain
```
24 слова mnemonic (BIP39 EN)
▼ entropyToMnemonic⁻¹ (= mnemonicToEntropy)
│ ↳ validate каждое слово в wordlist
│ ↳ collect 264 бит → split 256 entropy + 8 checksum
│ ↳ verify SHA-256(entropy)[0:8] == checksum
▼ entropy 32 байта
mnemonicToSeed(mnemonic, "")
│ ↳ password = NFKD(mnemonic)
│ ↳ salt = NFKD("mnemonic")
│ ↳ iter = 2048
│ ↳ HMAC-SHA512
│ ↳ dkLen = 64 байта
▼ seed 64 байта
SHA-256("montana-v1:" || seed)
▼ hash 32 байта
hash[0..20]
▼ 20 байт → hex
address 40 hex chars
```
Каждый шаг **детерминирован**. Voor восстановления требуется только 24 слова — всё остальное вычисляется.
---
## §3. Тестовый вектор воспроизводимости
### Vector 1 — нулевой mnemonic
**Input:** 24 BIP39 слова представляющие entropy = `0x0000000000000000000000000000000000000000000000000000000000000000`.
Поскольку SHA-256(0×32) = `66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925`, checksum bits = `01100110` = `0x66`.
264 бит = 256 zeroes + `01100110` checksum:
```
00000000 00000000 00000000 ... 00000000 01100110
```
Split на 24 куска по 11 бит. Каждый соответствует word index в BIP39 wordlist:
- Первые 23 индекса = 0 → слово `abandon`
- 24-й индекс binary = `00000110011` (последние 3 бита от 256-го entropy + 8 checksum) — math see standard BIP39 test vector
Окончательный mnemonic (well-known BIP39 test vector for 256-bit zero entropy):
```
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art
```
**Seed** (PBKDF2-HMAC-SHA512 от mnemonic):
```
bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8
```
**Montana адрес** (SHA-256("montana-v1:" || seed) [0:20]):
Вычисляется детерминированно. На каждом устройстве с тем же mnemonic должен получиться идентичный результат.
### Vector 2 — все единицы entropy (тоже стандартный BIP39 test)
**Input entropy:** `0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff` (256 единиц).
**SHA-256(0xff×32) checksum bits** — стандартное значение.
**Mnemonic** (BIP39 test vector):
```
zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote
```
### Vector 3 — мой текущий тестовый кошелёк (Pixel)
**Mnemonic:** (из скриншота v6.4.1)
```
ranch basket resource enemy bridge spray holiday thing yellow round army mimic renew head test cradle piece public differ diamond connect leisure wrong ask
```
**Адрес:** `2f8714b236118011647ec51d0ca6ad40d286bec7`
Этот тестовый mnemonic больше не используется (стёрт при `pm clear`), но запись остаётся в `balances.json` сервера. Может служить inline тестом совместимости при следующих сборках.
---
## §4. Что НЕ протестировано (открытые риски)
### Pass 20 Recovery trace gap
Согласно `Montana-Protocol/CRITIC.md` §«Pass 20 User recovery trace» — для полного закрытия требуется:
1. **Binding test vectors в спеке**у нас нет отдельной спеки приложения, тест-векторы только в этом аудиторском пакете. **OPEN**.
2. **Implementation determinism**`crypto.subtle.digest` и `deriveBits` WebCrypto API. Стандартизованы W3C, но empirical verification на разных Android WebView версиях не выполнена. **OPEN — рекомендуется**.
3. **E2E integration test** — нет `e2e_recovery.kt` который автоматически берёт mnemonic + порождает адрес + сравнивает с expected hex. **OPEN**.
4. **Manual validation demo** — нет binary который автор может запустить на двух Pixel-ах и сравнить выходы. **OPEN**.
**Closure path** (Phase 2 audit):
- Создать `Android/Внешний-аудит/приложения/вектора-тестов-bip39.md` с 5-10 vectors mnemonic → adr (выполнено частично сейчас, см. §3)
- Создать `Android/MontanaApp/app/src/androidTest/RecoveryE2ETest.kt` — instrumented test на устройстве
- Создать `Android/MontanaApp/app/src/test/BIP39DerivationTest.kt` — JVM unit test (без WebView)
### WebCrypto SubtleCrypto determinism across Android WebView versions
WebView Chromium обновляется автоматически через Google Play Services / Android System WebView. Версии могут отличаться на разных устройствах одного и того же Android API.
**Гипотеза:** SubtleCrypto спецификация требует bit-exact реализации SHA-256 / HMAC / PBKDF2. Все Chromium implementations должны давать одинаковые результаты.
**Verification status:** **не проверено empirically**. Рекомендация для аудита — взять 3-5 устройств с разными Android API + WebView versions, ввести один и тот же mnemonic, сравнить полученный адрес.
**Если расхождение обнаружится** — это **fundamental bug** (P0): пользователь не сможет восстановить кошелёк на новом устройстве. Closure: native crypto (через Conscrypt / BouncyCastle) вместо WebCrypto.
---
## §5. Совместимость с другими BIP39 кошельками
### Часть совместимая
**Mnemonic generation** — полностью совместимо. Те же 24 слова можно ввести в:
- python-bip39 (`mnemonic.bip39.Mnemonic("english").to_entropy(mnemonic)`)
- bitcoinjs/bip39 npm
- Trust Wallet (Android), MetaMask (нативный BIP39)
- Bitcoin Core wallet
Все эти кошельки **поймут** что слова валидны и вычислят то же `entropy` и тот же `seed`.
### Часть НЕсовместимая
**Address derivation** — Montana использует **нестандартный** path: `SHA-256("montana-v1:" || seed)[0..20]`.
Все BIP44-совместимые кошельки используют:
1. seed → BIP32 master key (HMAC-SHA512(key="Bitcoin seed", msg=seed))
2. → BIP44 derivation path `m/44'/<coin_type>'/<account>'/<change>/<index>`
3. → child secp256k1 keypair
4. → public key
5. → coin-specific address (Bitcoin: RIPEMD160(SHA256(pubkey)), Ethereum: Keccak256(pubkey)[12..32])
Это значит:
- Импортировав те же 24 слова в Trust/MetaMask → пользователь получит **другие** адреса (Ethereum mainnet, BTC mainnet и т.п.) с **нулевым балансом**
- Пользователь может подумать «деньги украдены» — на самом деле его Montana-баланс остаётся, просто кошелёк не умеет вычислять Montana-address
- Без официального SLIP-44 ID Montana, эта incompatibility останется
### UX mitigation в приложении
При показе seed в `s-account` экране — добавить предупреждение:
```
24 слова Montana — стандарт BIP39 EN.
ВНИМАНИЕ: эти слова дают вашему кошельку Montana адрес.
Импорт в Trust Wallet / MetaMask / другие кошельки покажет другие
адреса с нулевым балансом — это нормально, ваш баланс
сохраняется на адресе Montana и восстанавливается только в
приложении Montana.
```
Текущая v6.5.0 этого предупреждения **не имеет**. Closure path: добавить в `s-account` screen rendering.
---
## §6. Backup confirmation flow — не реализовано
**Текущая v6.5.0:**
```
User → "Создать ключ" → moментально кошелёк создан и активен → "Войти в Монтану"
Account screen: "Показать 24 слова"
```
Пользователь может **никогда** не нажать "Показать 24 слова" → не записать seed → потерять кошелёк при `pm clear` / переустановке.
**Production-grade pattern:**
1. Создать → показать 24 слова и попросить записать
2. Подтвердить — ввести 3-4 случайных слова из этих 24 обратно
3. Только после подтверждения активировать кошелёк
**Closure path:** новый экран `s-backup-verify` между `s-auth` и `s-main` при создании.
**Severity:** **высокий UX risk** (потеря денег пользователем). Не cryptographic finding, но критично для real users.
---
## §7. Logout flow
**Текущая v6.5.0:**
```javascript
function logoutAcc(){
if(!confirm('Точно выйти? Кошелёк не пропадёт — но в этом устройстве его придётся восстанавливать по 24 словам.')) return;
localStorage.removeItem(L_S); localStorage.removeItem(L_A);
st.addr=null; st.bal=0; st.sec=0;
applyAuth(); render(); hideSeed(); show('main');
}
```
`window.confirm()` — простое OK/Cancel.
**Слабость:** пользователь нажимающий OK без прочтения теряет seed безвозвратно.
**Production-grade pattern:** требовать ввести 2-3 случайных слова из seed как proof что seed записан.
**Closure path:** новый экран `s-logout-verify`. Severity: средний.
---
## §8. Determinism guarantees verification
### Что детерминировано **математически**:
- `SHA-256` per FIPS 180-4 — bit-exact на любой platform
- `HMAC-SHA512` per FIPS 198-1 — bit-exact
- `PBKDF2` per RFC 2898 — детерминирован (iter count и salt fixed)
- BIP39 mnemonic ↔ entropy mapping — bit-exact
### Что детерминировано **implementation**:
- `crypto.subtle.digest('SHA-256', ...)` — спецификация W3C требует bit-exact
- `crypto.subtle.deriveBits('PBKDF2', ...)` — то же
- `TextEncoder.encode(s)` — UTF-8, стандартизовано
- `String.prototype.normalize('NFKD')` — Unicode NFKD, стандартизовано
### Что **может** быть source of non-determinism:
- Разные WebView Chromium versions могут иметь баги в WebCrypto — крайне маловероятно но не доказано
- Локализация системы (Russian vs English Android) — может влиять на TextEncoder normalization при non-ASCII inputs (но BIP39 EN ASCII only, не наш кейс)
**Empirical verification status:** не выполнено для разных WebView versions. **Открыто**.