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

13 KiB
Raw Blame History

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 determinismcrypto.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:

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. Открыто.