16 KiB
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»):
- Energetic input: 32 bytes = 256 бит =
entropy - Checksum: первые 8 бит
SHA-256(entropy) - Concatenated bits:
entropy || checksum= 264 бит - Split на 24 куска по 11 бит → 24 индекса в wordlist
- Каждый индекс ∈ [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 targetpublicKey = "EkTs2aGKnFNgFZ0f7wgft2sJp3VjwFQqIrwkZKM4gD8"— X25519 публичный ключ HelsinkishortId = "302805bc0c25e504"— 8-байтный identifierfingerprint = "chrome"— emulate Chrome JA3/JA4flow = "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 совместимость