montana/Android/Внешний-аудит/03-Криптография.md
2026-05-18 22:11:45 +03:00

16 KiB
Raw Blame History

03. Криптография — Montana Android v6.5.0

Все криптографические примитивы используемые приложением, с derivation параметров согласно правилам «Академическое обоснование констант» из Montana-Protocol/CLAUDE.md (роль архитектора).


§1. Источник энтропии при создании кошелька

Примитив: crypto.getRandomValues(new Uint8Array(32)) (WebCrypto API).

Класс: security. Target: 256 бит криптографически стойкой энтропии для BIP39 mnemonic, level 1 безопасности по NIST PQ-классификации (Grover ослабляет до 128 бит — приемлемо per [I-1]). References: W3C WebCrypto API §15.4 RandomSource.getRandomValues. RFC 4086 Randomness Requirements. NIST SP 800-90B Recommendation for the Entropy Sources Used for Random Bit Generation. Derivation: BIP39 minimum 128 bits → 12 words. Production wallets обычно используют 256 bits → 24 words (industry standard since Trezor 2017). Выбрано 256 для maximum recovery resilience. Sensitivity: 256 → 128 уменьшает word count до 12 (приемлемо UX), но снижает security margin для post-quantum (Grover на 128 бит → 64-bit effective → недостаточно). Pin на 256. Defense:

  • Почему не 192? — 192-bit entropy → 18 слов, non-standard в BIP39. Большинство кошельков используют 12 или 24.
  • Почему не 512? — BIP39 не определяет 48-word mnemonics. Совместимость с другими wallets важна (хотя сейчас derivation несовместима — см. §5).

§2. BIP39 wordlist EN — integrity guarantee

Файл: app/src/main/assets/bip39-en.txt, 13116 байт, 2048 строк-слов.

Происхождение: официальный BIP39 English wordlist от Bitcoin Foundation, опубликован 2013. SHA-256 эталонного wordlist хорошо известен и проверяется библиотеками BIP39 во всех экосистемах.

SHA-256: 2f5eed53a4727b4bf8880d8f3f199efc90e58503646d9ff8eff3a2ed3b24dbda

Защита от подмены: в MainActivity.kt метод bip39():

val bytes = activity.assets.open("bip39-en.txt").use { it.readBytes() }
val actual = MessageDigest.getInstance("SHA-256").digest(bytes)
    .joinToString("") { "%02x".format(it) }
if (actual != "2f5eed53...dbda") {
    Log.e("MontanaBridge", "bip39 integrity FAIL: $actual")
    return ""
}

Если злоумышленник заменит bip39-en.txt в APK при сборке — bip39() возвращает пустую строку → loadBip39() в JS бросает исключение «bip39 list size: 0» → пользователь не может создать кошелёк (fail-closed).

Класс: security (integrity). Target: обнаружить подмену wordlist в скомпрометированной сборке APK с probability 1.0. References: BIP39 specification, [I-7] minimal crypto surface (используется уже существующий SHA-256, не вводится новый примитив). Derivation: SHA-256 эталонной копии вычислен и захардкожен. Любое изменение wordlist → другой хэш → fail-closed. Sensitivity: атакующий должен подменить wordlist и изменить захардкоженный хэш в APK. Это требует full rebuild и подписи Genesis-keystore — невозможно без compromise keystore.


§3. BIP39 mnemonic generation (24 words)

Алгоритм (см. BIP39 §5 «Generating the mnemonic»):

  1. Energetic input: 32 bytes = 256 бит = entropy
  2. Checksum: первые 8 бит SHA-256(entropy)
  3. Concatenated bits: entropy || checksum = 264 бит
  4. Split на 24 куска по 11 бит → 24 индекса в wordlist
  5. Каждый индекс ∈ [0, 2047] → слово

Реализация в app.html функция entropyToMnemonic(entropy) (см. 09-Инвентаризация-кода.md).

Класс: standard compliance. Target: byte-exact совместимость с BIP39 wallet'ами (если кто-то импортирует наш seed в python-bip39 / bitcoinjs/bip39 — слова те же). References: BIP39 specification. Standard since 2013, multiple production implementations. Derivation: прямой следование спецификации BIP39 для 24-word case. Sensitivity: любая девиация (например, неверный bit-packing) → несовместимые слова. Тестовые векторы — см. приложения/вектора-тестов-bip39.md.


§4. Mnemonic → master seed (PBKDF2-HMAC-SHA512)

Алгоритм (см. BIP39 §6 «From mnemonic to seed»):

seed = PBKDF2(
    password = NFKD(mnemonic),
    salt = NFKD("mnemonic" || passphrase),
    iterations = 2048,
    hash = HMAC-SHA512,
    dkLen = 512 bits (64 bytes)
)

В нашей реализации passphrase пуст ("").

Реализация через WebCrypto:

const key = await crypto.subtle.importKey('raw', enc.encode(mnemonic.normalize('NFKD')),
    {name:'PBKDF2'}, false, ['deriveBits']);
const bits = await crypto.subtle.deriveBits(
    {name:'PBKDF2', salt: enc.encode('mnemonic' + (passphrase||'').normalize('NFKD')),
     iterations: 2048, hash: 'SHA-512'}, key, 512);

Класс: security (key stretching). Target: замедлить brute-force на recovery (если seed утерян, атакующий не может попробовать 2¹²⁸ кандидатов offline быстро). References:

  • BIP39 §6, RFC 2898 PKCS#5 PBKDF2.
  • NIST SP 800-132 Recommendation for Password-Based Key Derivation.
  • Bitcoin Core / Trezor / Ledger используют идентичные параметры (это de-facto стандарт). Derivation:
  • iter = 2048 — фиксировано BIP39 spec, derived от 2013 hardware budget (~10ms на reference CPU)
  • HMAC-SHA512 — длинный hash для domain separation с другими HMAC-уструктурами
  • dkLen = 512 — стандарт BIP39 для master seed Sensitivity:
  • Уменьшение iter → быстрее brute-force атакующим
  • Увеличение iter → пользователь дольше ждёт при recovery (но это разовая операция)
  • 2048 — общепринятый baseline; в современных Argon2-based кошельках iter выше, но мы следуем стандарту BIP39 для cross-wallet совместимости Defense:
  • Почему PBKDF2, не Argon2? — BIP39 жёстко фиксирует PBKDF2-HMAC-SHA512, отклонение ломает совместимость.
  • Почему salt = "mnemonic"? — фикс BIP39 для всех implementations. Passphrase optional postfix.

§5. Seed → Montana address

Алгоритм (специфично для Montana, не BIP32/BIP44):

domain = "montana-v1:"          (11 bytes ASCII)
preimage = domain || seed       (11 + 64 = 75 bytes)
hash = SHA-256(preimage)        (32 bytes)
address = hash[0..20]           (20 bytes → 40 hex chars)

Класс: identity derivation. Target: детерминированно вывести 20-байтный адрес из BIP39 seed, отличимый от Ethereum/Bitcoin addresses, защищённый от collision при ограниченном supply ≤2⁸⁰. References:

  • Ethereum использует Keccak-256(pubkey)[12..32] (последние 20 байт) — мы используем SHA-256(seed)[0..20] (первые 20 байт)
  • Bitcoin использует RIPEMD-160(SHA-256(pubkey)) (тоже 20 байт)
  • Domain separator pattern — стандартный во всех protocol designs (BIP32 uses "Bitcoin seed", Ethereum uses keccak256, etc.) Derivation:
  • 20 байт = 160 бит → 2¹⁶⁰ адресов → collision при N ≈ 2⁸⁰ генерациях (birthday bound). Для Montana с baseline ≤10⁹ users требуется ≥2⁴⁰ collision safety — 160 бит даёт огромный запас.
  • montana-v1: префикс — version & domain separator. Защищает от случайного использования другим protocol's seed. Sensitivity:
  • Изменение префикса (например "montana-v2:") → все адреса другие → polno breaking change
  • Уменьшение до 16 байт (128 бит) → 2⁶⁴ collision safety → недостаточно Defense:
  • Почему не BIP32/BIP44? — Montana ещё pre-mainnet. Стандартный BIP44 derivation требует выбора SLIP-44 coin type ID, которого у Montana нет. Простой SHA-256(seed) детерминирован и достаточен для MVP. Closure path: SLIP-44 application + BIP44 при mainnet.
  • Почему первые 20 байт, не последние? — соответствует Ethereum convention с одной разницей (Ethereum использует Keccak, мы SHA-256).
  • Почему SHA-256, не SHAKE-256? — [I-7] minimal crypto surface, [I-1] PQ-secure (Grover на SHA-256 даёт 128-bit effective — приемлемо).

Ограничение: address не содержит публичный ключ. Текущая реализация не выводит keypair — только адрес. Это блокирует Falcon-signed heartbeats (требуется в M-VPN-3, см. 07 F-2 closure).


§6. Reality TLS — outbound VPN

Примитив: XTLS Reality (vless+reality+xtls-rprx-vision).

Параметры:

  • serverName = "www.googletagmanager.com" — SNI mimicry target
  • publicKey = "EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8" — X25519 публичный ключ Helsinki
  • shortId = "302805bc0c25e504" — 8-байтный identifier
  • fingerprint = "chrome" — emulate Chrome JA3/JA4
  • flow = "xtls-rprx-vision" — vision mode (zero-copy splice when possible)

Класс: anti-DPI / anti-active-probing. Target: на пассивный DPI не отличаться от обычной TLS-1.3 сессии к googletagmanager. На активное зондирование — отдавать реальный googletagmanager response (SNI Echo). References:

  • XTLS Reality: https://github.com/XTLS/Xray-core/tree/main/transport/internet/reality
  • Реализован 2023, deployed в GFW-strict регионах (Iran, China) с 2024
  • Independent reviews: https://github.com/XTLS/Xray-core/discussions/2236 Derivation:
  • X25519 — fast EC key exchange, размер ключа 32 байта
  • ChaCha20-Poly1305 AEAD (стандарт TLS-1.3)
  • SHA-256 для key derivation Sensitivity:
  • Compromise privateKey Helsinki → атакующий может MITM весь трафик каскада
  • Private key хранится в /usr/local/etc/xray/config.json на Helsinki, права 0640 owner xray Defense:
  • Почему не WireGuard? — WireGuard виден DPI (характерный handshake), не маскируется. Не подходит для России / Китая.
  • Почему не Tor? — Tor существенно медленнее, не годится для VPN-сервиса
  • Почему не VLESS без Reality? — без Reality protocol обнаруживается active probing

[I-1] PQ-compatibility: Reality использует классический X25519 — уязвим к квантовому компьютеру (Shor). Это известное ограничение. ML-KEM-768 hybrid в xray-core — work in progress upstream, не наша работа. Closure path: переход на PQ-Reality когда XTLS upstream добавит.


§7. HTTPS heartbeat — Android Kotlin TLS

Стек: JVM SSLSocket через SSLContext.getDefault() → Android AndroidKeyStore trust store → стандартный TLS-1.3.

ALPN pin: arrayOf("http/1.1") — принудительно HTTP/1.1, не HTTP/2.

val p = sslSock.sslParameters
p.applicationProtocols = arrayOf("http/1.1")
sslSock.sslParameters = p

Класс: parser robustness. Target: избежать HTTP/2 negotiation которая ломает наш текстовый парсер строки статуса HTTP/1.1 200 OK. References: RFC 7301 ALPN, RFC 7540 HTTP/2. Derivation:

  • Без ALPN-pin nginx + JVM SSL могут согласовать HTTP/2
  • HTTP/2 — бинарный протокол, наш парсер не справится
  • HTTP/1.1 простой, текстовый — robust для нашего use case
  • Производительность не критична (1 heartbeat в 5 сек) Sensitivity: если ALPN-pin не сработает (старый Android API без sslParameters) — fallback в catch с логом Defense:
  • Почему не OkHttp? — добавление dependency для одного запроса в 5 сек избыточно
  • Почему не raw socket? — нужен TLS-1.3 для совместимости с nginx

§8. Сериализация состояния — localStorage

Хранится:

  • localStorage["m.seed"] — 24 слова через space-separated (~150-200 байт)
  • localStorage["m.addr"] — 40-hex-character address

Кодировка: UTF-8 plain text.

Класс: persistence. Target: воспроизводимое хранение между запусками приложения. References: W3C Web Storage API (synchronous, per-origin, max 10 MB). Sensitivity: plain text, без шифрования — см. 02-Угрозы-и-модель.md атакующие D, F. Closure path: Android Keystore wrap или PBKDF2(user_passcode) шифрование (см. 07 F-6).


§9. Summary таблица примитивов

Примитив Назначение NIST Level PQ-secure? Открытость стандарта
SHA-256 hash, integrity, address derive 1 (Grover) условно ([I-1] OK) FIPS 180-4
HMAC-SHA512 PBKDF2 PRF 1 условно FIPS 198-1
PBKDF2 key stretching mnemonic→seed n/a n/a RFC 2898
crypto.getRandomValues entropy source n/a n/a W3C WebCrypto
BIP39 mnemonic encoding n/a n/a BIP39 (industry de-facto)
X25519 (Reality) TLS key exchange n/a нет (Shor) RFC 7748
ChaCha20-Poly1305 TLS AEAD 1 условно RFC 8439
TLS-1.3 transport security 1 partially RFC 8446

Соответствие глобальным инвариантам Montana Protocol:

  • [I-1] PQ-secure: частично — Reality X25519 не PQ; closure path = upstream PQ-Reality
  • [I-7] minimal crypto surface: да — нет custom primitives, всё стандарт
  • [I-9] bit-exact deterministic arithmetic: да для seed derivation (PBKDF2 детерминирован, SHA-256 детерминирован)

§10. Что НЕ используется (явно отвергнуто)

  • ECDSA / Ed25519 — классические EC подписи, нарушают [I-1]
  • RSA — нарушает [I-1]
  • Ring signatures / stealth addresses — нарушают [I-2] (открытость финансового слоя) и [I-6] (регуляторная совместимость)
  • Zero-knowledge proofs (zk-SNARK/STARK) — пока не нужны в текущем scope приложения
  • Custom hash functions — нарушают [I-7]
  • Argon2 / scrypt — нарушают BIP39 cross-wallet совместимость