Skip to content

Latest commit

 

History

History
239 lines (193 loc) · 19.3 KB

File metadata and controls

239 lines (193 loc) · 19.3 KB

Hypervault — криптографическая архитектура (черновик v0.1)

Note (EN): This is the normative crypto-architecture document, currently available in Russian only; an English translation is planned. Any divergence between this document and the code is a bug.

Статус: черновик, нормативное ТЗ фазы 1. После реализации и аудита станет публичным whitepaper. Любое отклонение реализации от этого документа — баг: либо чинится код, либо осознанно обновляется документ.

Модель угроз (инварианты I1–I6) ведётся в репозитории приложения Hypervault и будет опубликована вместе с ним.

1. Принципы

  1. Zero-knowledge, offline-first — все операции локальны, секреты не покидают устройство (I1).
  2. Никакой самописной криптографии — только примитивы из проверенных библиотек (Tink / BouncyCastle / libsodium-биндинг; выбор фиксируется в фазе 1 отдельным ADR).
  3. Все случайные значения — только из CSPRNG (SecureRandom).
  4. Секреты в памяти — только зануляемые ByteArray, никогда String.
  5. Версионирование формата: каждый файл несёт версию схемы, миграции только вперёд.

2. Ключевая иерархия

мастер-пароль (S1)      Secret Key SK (S2, 128 бит, CSPRNG,
                        генерируется при создании vault)
       Argon2id                
                               
     MUK (256 бит) ─────────────┤
                                
                  KEK = HKDF-SHA-256(MUK  SK,
                        salt = hkdfSalt, info = "hypervault/kek/v1")
                                  AES-256-GCM (wrap)
                                
                        DEK (256 бит, CSPRNG, один на vault)
                                  AES-256-GCM (body)
                                
                     payload хранилища (S4, S6)
  • MUK (Master Unlock Key) — Argon2id от мастер-пароля.
  • SK (Secret Key) — вторая компонента энтропии: файл хранилища, украденный без устройства (A5), нерасшифровываем даже при слабом мастер-пароле (I2). Хранится: (а) в приватном хранилище приложения, зашифрованным ключом Android Keystore; (б) в Recovery Kit пользователя.
  • KEK — ключ обёртки; меняется при смене мастер-пароля.
  • DEK — ключ данных. Прослойка DEK/KEK (envelope) нужна для: (а) второй обёртки того же DEK ключом Keystore для биометрии (фаза 3); (б) быстрых перезаписей тела без пересчёта Argon2id. Смена мастер-пароля выполняет полную ротацию DEK (см. §5).

3. Параметры примитивов

Примитив Выбор Параметры
KDF Argon2id (RFC 9106) m ≥ 64 MiB, t ≥ 3, p = 4; калибруются на устройстве до ~1000 мс, но не ниже минимумов; сохраняются в заголовке
KDF (FIPS-профиль сборки, позже) PBKDF2-HMAC-SHA-256 ≥ 600 000 итераций (OWASP 2023+)
Расширение ключа HKDF-SHA-256 (RFC 5869) отдельные info-строки на каждое назначение ключа
AEAD AES-256-GCM (NIST SP 800-38D) nonce 96 бит CSPRNG, уникален на каждую запись; заголовок файла — AAD
Соль Argon2id 256 бит CSPRNG генерируется при создании vault, хранится в заголовке
Идентификаторы UUIDv4 из CSPRNG

Лимит NIST на случайные GCM-nonce (2^32 шифрований одним ключом) недостижим для файла хранилища; контролируется счётчиком записей в заголовке — при приближении к порогу форсируется ротация DEK.

4. Формат файла хранилища (HV1)

[магия "HVLT"] [версия u16]
[заголовок, plaintext, участвует в AAD:
    kdf_id, argon2_m, argon2_t, argon2_p, kdf_salt,
    hkdf_salt, cipher_id, write_counter,
    wrapped_dek = AES-GCM(KEK): {nonce, ciphertext, tag}]
[body: AES-GCM(DEK, AAD = заголовок): {nonce, ciphertext, tag}]
  • Payload перед шифрованием сериализуется kotlinx-serialization (CBOR).
  • Метаданные записей (названия, URL) — внутри body, снаружи нет ничего, кроме крипто-параметров (актив S6).
  • Подмена любого байта заголовка или body → ошибка аутентификации GCM (I4).
  • Запись файла атомарна: tmp-файл → fsync → atomic rename (I6).

5. Жизненный цикл ключей

Событие Действия
Создание vault CSPRNG: SK, DEK, соли; ввод мастер-пароля → MUK → KEK → wrap DEK; показ Recovery Kit
Разблокировка паролем Argon2id → MUK → KEK → unwrap DEK; MUK и KEK зануляются сразу после unwrap; в памяти остаётся только DEK
Разблокировка биометрией Копия DEK хранится зашифрованной ключом Keystore (StrongBox при наличии, setUserAuthenticationRequired(true), setUserAuthenticationParameters(0, BIOMETRIC_STRONG), CryptoObject, setInvalidatedByBiometricEnrollment(true)). DEK вместо KEK: доступен в сессии без пересчёта KDF, модель доверия идентична (оба открывают body при компрометации TEE). Только API 30+; при смене пароля (ротация DEK) обёртка устаревает и отключается при первой неудачной попытке. Компромисс доверия к TEE документируется публично
Блокировка (таймаут / фон / onTrimMemory / kill) Зануление DEK и всех производных; состояние UI сбрасывается (I5)
Смена мастер-пароля Новые соли и DEK, полная перешифровка body (ротация DEK включена в смену пароля). Причины: body привязан AAD к полному заголовку — склейка старого заголовка с новым body (splicing, чтобы старый пароль открывал новые данные) невозможна; заодно обесценивается ранее утёкший KEK. Стоимость перешифровки десятков КБ незаметна на фоне ~1 c Argon2id
Ротация DEK Совмещена со сменой пароля; отдельно — по счётчику записей
Смена биометрии в ОС Keystore-ключ инвалидируется системой → биометрический вход отключается до входа паролем
Включение PIN CSPRNG: pinSalt, pinGuard (32 байта). pinGuard хранится зашифрованным ключом Keystore (StrongBox). pinKey = Argon2id(PIN, pinSalt); wrapKey = HKDF(pinKey ‖ pinGuard, info="hypervault/pin/v1"); хранится wrappedDEK = AES-GCM(wrapKey, DEK) + счётчик неудач = 0
Разблокировка по PIN pinKey = Argon2id(PIN), wrapKey = HKDF(‖ pinGuard из Keystore), unwrap DEK. Неверный PIN → AEAD-ошибка, счётчик++. После N=5 неудач: удаление pinGuard из Keystore + wrapped-блоба → PIN необратимо отключён, остаётся мастер-пароль
Duress-PIN Отдельный PIN, обёрнутый тем же способом, но с флагом duress. При совпадении введённого PIN с duress: (1) разблокировка обычным DEK; (2) удаление записей с destroyUnderDuress + перезапись файла; (3) промоут duress-слота в обычный — введённый PIN становится единственным PIN, прежний обычный слот затирается, признак duress снимается (перестановка боксов без KDF/DEK: правдоподобность §5a); (4) UI показывает рабочее хранилище без стёртых записей. Оба PIN-пути выполняют Argon2id — время ответа не различает нормальный/duress PIN

5a. PIN-разблокировка — модель доверия

PIN — удобный фактор, не корень доверия. Мастер-пароль + Secret Key остаются единственным способом, стойкость которого не зависит от железа. Свойства PIN-обёртки:

  • Украденный файл без устройства: pinGuard в Keystore недоступен → PIN-обёртку нельзя даже начать перебирать (сильнее, чем просто слабый PIN).
  • Украденное устройство: перебор PIN ограничен счётчиком (N=5), после чего pinGuard удаляется из Keystore необратимо. Argon2id удорожает каждую из немногих попыток.
  • Честный предел: root в момент разблокировки читает DEK из памяти (угроза A4) — как и для пароля/биометрии.

Duress-PIN — функция для сценария принуждения (A1/A7 расширенные). Стирает только записи с флагом destroyUnderDuress, необратимо, без подтверждения (подтверждение выдало бы факт стирания принуждающему). После срабатывания введённый duress-PIN промоутится в обычный, а прежний обычный PIN исчезает: у хранилища остаётся ровно один PIN, повторный вход тем же PIN не вызывает второго стирания и не создаёт подозрений при проверке принуждающим. Бэкапы уже отключены (фаза 0), поэтому восстановление стёртого невозможно. Документируется риск случайного срабатывания.

6. Защита от перебора на устройстве (A2)

  • Экспоненциальная задержка после неудачных попыток (в дополнение к стоимости Argon2id).
  • Счётчик попыток — в зашифрованном Keystore-стейте, а не в plain SharedPreferences.
  • Опция «стереть vault после N неудач» (по умолчанию выключена).

6a. Криптокошелёк — air-gapped подписант EVM (фаза 4c)

Кошелёк не вводит новых секретов в иерархию ключей хранилища: его корневой секрет — энтропия BIP-39 (S9, 128 бит, CSPRNG), которая хранится как обычное поле зашифрованного payload (та же защита, что S4/S6) и зануляется при блокировке.

Деривация (детерминированная, ничего кроме энтропии не хранится):

энтропия (S9) ──BIP-39──▶ мнемоника (12 слов)
                               PBKDF2-HMAC-SHA512 (2048)
                              
                         seed (512 бит)
                               BIP-32 (secp256k1)
                              
              m/44'/60'/0' (аккаунт, xpub → watch-кошелёк)


              m/44'/60'/0'/0/i (ключи подписи EVM)
  • Подпись: детерминированная ECDSA (RFC 6979), low-s, recovery id.
  • Транспорт: EIP-4527 (BC-UR поверх анимированных QR): crypto-hdkey (только публичные данные) → watch-кошелёк; eth-sign-request ← watch-кошелёк; eth-signature → watch-кошелёк.
  • Сетевого кода нет: приложение остаётся полностью оффлайн, broadcast выполняет watch-кошелёк.
  • Приватный ключ выводится из энтропии на время одной подписи и зануляется (ограничение JVM — §8.5 действует и здесь).
  • Путь деривации из запроса ограничен префиксом m/44'/60' — запрос не может запросить подпись ключом вне EVM-дерева.
  • Duress: кошелёк с флагом destroyUnderDuress уничтожается вместе с помеченными записями (правдоподобность сохраняется).

7. Что публикуем

Whitepaper (этот документ после фазы 1), исходный код крипто-ядра (модуль crypto), тестовые векторы формата HV1, отчёты внешних аудитов.

8. Открытые вопросы фазы 1 (ADR)

  1. Библиотека примитивов: Tink vs libsodium (Lazysodium) vs BC — критерий: Argon2id + зануляемые байтовые API + перспектива FIPS-профиля.
  2. CBOR vs protobuf для payload.
  3. Выделять ли крипто-ядро в отдельный gradle-модуль crypto (рекомендация: да, чистый Kotlin без Android API — проще открывать код и аудировать).
  4. XChaCha20-Poly1305 как альтернативный cipher_id формата (nonce 192 бита).
  5. Защита ключей в памяти (фаза 2): секреты вне ART-кучи (moving GC копирует ByteArray, оставляя незанулённые копии) — sodium_malloc + mlock + guard pages + sodium_memzero; KEK/DEK между операциями держать обёрнутыми эфемерным ключом из большого prekey-буфера (схема OpenSSH): защита от частичных утечек памяти (Spectre-класс, cold boot, Heartbleed-класс). Честный предел: от root с полным дампом памяти — только удорожание.
  6. Вынос unwrap DEK внутрь Keystore/StrongBox (AES-GCM в TEE, ключ не покидает железо) — оценить производительность на реальном размере vault.

9. Формат конверта шеринга (HVS1) — черновик (ADR-002)

Шеринг записей и файлов «только получателю»: асимметричный запечатанный конверт с подписью отправителя. Обмен ключами — out-of-band (QR/строка), серверов нет (I1, I3). Полное обоснование — docs/adr/ADR-002-secure-sharing.md.

Ключи личности (payload HV1, payload v5)

При первом шеринге лениво генерируются две долговременные пары (CSPRNG), хранятся в body HV1 (шифруются DEK, зануляются при блокировке):

  • idX — X25519 (шифрование к пользователю);
  • idEd — Ed25519 (подпись пользователя).

Публичные половины — несекретный share-адрес. Контакты {label, xPub, edPub} (публичные ключи) хранятся в payload; секретом не являются.

Примитивы

Назначение Выбор Параметры
Согласование ключа X25519 (RFC 7748) эфемерная пара отправителя на каждый конверт
Подпись Ed25519 (RFC 8032) над (context ‖ itemKind ‖ item)
Вывод ключа конверта HKDF-SHA-256 info = "hypervault/share/v1", salt = ephPub ‖ recipient.xPub
AEAD AES-256-GCM nonce 96 бит CSPRNG; заголовок конверта — AAD

Формат (sign-then-encrypt — личность отправителя тоже под шифром)

inner (CBOR, шифруется):
    senderEdPub (32)  signature (64)  itemKind (u8)  item (bytes)

[магия "HVS1"] [версия u16]
[заголовок (plaintext, в AAD): ephemeralXPub (32), nonce (12)]
[body = AES-256-GCM(key, AAD = заголовок): {ciphertext, tag}]

key = HKDF(ss, salt = ephemeralXPub  recipient.xPub, info="hypervault/share/v1")
ss  = X25519(eph.priv, recipient.xPub)          (у отправителя)
    = X25519(idX.priv, ephemeralXPub)           (у получателя)
  • Перехватчик видит только ephemeralXPub, nonce и шифртекст — ни содержимого, ни личности отправителя (sign-then-encrypt).
  • Вскрыть может только владелец idX.priv; подмена байта → ошибка GCM (I4).
  • Получатель проверяет Ed25519-подпись и сверяет senderEdPub с контактами.
  • Транспорт .hvs — шифртекст, идёт через недоверенный канал (share-sheet).

Остаточные риски

  • MITM при out-of-band обмене ключами → сверка отпечатка вторым каналом (safety-number, как в Signal); сервера доверия нет намеренно.
  • Replay конверта → дедуп по id при импорте.
  • Размер .hvs раскрывает примерный объём данных; факт передачи виден каналу — скрытие трафика вне модели.