241 lines
13 KiB
Markdown
241 lines
13 KiB
Markdown
|
|
# 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. **Открыто**.
|