Аудит безопасности · июнь 2026Security Audit · June 2026
32
выполненоpassed
3
инфоinfo
0
проблемissues
Все пункты проверены по исходному коду приложения и сервера.All items verified against application and server source code.
Блок 1 — КриптографияBlock 1 — Cryptography
✅
Стандартные алгоритмыStandard algorithms
X25519, Ed25519, AES-256-GCM, HKDF-SHA256 через Apple CryptoKit. Без собственной криптографии.X25519, Ed25519, AES-256-GCM, HKDF-SHA256 via Apple CryptoKit. No custom cryptography.
✅
Сервер не видит сообщенияServer cannot read messages
Приватные ключи хранятся только на устройстве.Private keys are stored on the device only.
✅
Nonce не повторяютсяNonces never repeat
Детерминированы от уникального messageKey через SHA256.Derived deterministically from a unique messageKey via SHA256.
✅
Forward SecrecyForward Secrecy
Double Ratchet v2: chainKey + DHRatchet + rootKey rotation. Компрометация одного ключа не раскрывает прошлые сообщения.Double Ratchet v2: chainKey + DHRatchet + rootKey rotation. Compromise of one key does not expose past messages.
✅
Аутентификация каждого сообщения (AAD)Per-message authentication (AAD)
Метаданные (версия, номер, DH-ключ) криптографически привязаны к содержимому. Подмена в транзите обнаруживается немедленно.Metadata (version, sequence, DH key) is cryptographically bound to content. Any in-transit tampering is detected immediately.
✅
Безопасная ротация ключейSecure key rotation
Gap-защита maxAllowedGap=500 исключает десинхронизацию сессии при потере пакетов.Gap protection (maxAllowedGap=500) prevents session desynchronisation on packet loss.
Каждый файл шифруется отдельным случайным ключом (32 байта). Ключ передаётся только внутри E2E-зашифрованного сообщения. Integrity check: SHA256(blob) == digest.Each file is encrypted with a separate random key (32 bytes). The key is transmitted only inside the E2E-encrypted message. Integrity check: SHA256(blob) == digest.
ℹ️
HMAC вместо HKDF для медиаключейHMAC instead of HKDF for media keys
Академическое замечание: HKDF нужен для нормализации неравномерной энтропии. Поскольку mediaKey — 32 байта криптографически случайных данных, HMAC как PRF эквивалентен HKDF в данном контексте. Реальной уязвимости нет.Academic note: HKDF is needed to normalise uneven entropy. Since mediaKey is 32 bytes of cryptographically random data, HMAC as a PRF is equivalent to HKDF in this context. No real vulnerability.
ИнформационноInformational
Блок 2 — Хранение ключейBlock 2 — Key Storage
✅
Ключи только в KeychainKeys stored in Keychain only
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — ключи физически не покидают устройство.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly — keys physically cannot leave the device.
✅
Ключи не в базе данных и не в логахKeys not in database or logs
Сессии зашифрованы AES-GCM, master key хранится в Keychain.Sessions are encrypted with AES-GCM; master key is stored in Keychain.
✅
Исключено из iCloud BackupExcluded from iCloud Backup
Обнаружение orphaned Keychain → уведомление сервера → полная очистка данных.Orphaned Keychain detection → server notification → full data wipe.
Блок 3 — СетьBlock 3 — Network
✅
TLS 1.2+
Все соединения через TLS (iOS ATS по умолчанию). WebSocket через WSS.All connections via TLS (iOS ATS by default). WebSocket over WSS.
✅
Certificate Pinning
SPKI hash pinning через URLSessionDelegate (SHA256, EC P-256). Поддельный сертификат от любого CA не принимается.SPKI hash pinning via URLSessionDelegate (SHA256, EC P-256). Fraudulent certificates from any CA are rejected.
✅
MITM не позволяет читать сообщенияMITM cannot expose messages
Соединение разрывается при подозрении на MITM. Double Ratchet ciphertext без ключей бесполезен.Connection is terminated on MITM suspicion. Double Ratchet ciphertext is useless without keys.
✅
Токены сессии в заголовкахSession tokens in headers
sessionId и userId перенесены из URL в X-Session-ID / X-User-ID заголовки. URL-логи сервера не содержат токенов.sessionId and userId moved from URL to X-Session-ID / X-User-ID headers. Server URL logs contain no tokens.
Блок 4 — АутентификацияBlock 4 — Authentication
✅
Защита от replay-атак (3 уровня)Replay attack protection (3 layers)
1) Double Ratchet: ключ одноразовый. 2) Клиент: дедупликация по messageId. 3) Сервер: Redis-проверка по messageId.1) Double Ratchet: one-time key. 2) Client: deduplication by messageId. 3) Server: Redis check by messageId.
✅
Захват аккаунта невозможенAccount takeover is impossible
Ed25519 challenge-response: сервер выдаёт случайный challenge → клиент подписывает приватным ключом → сервер верифицирует. Без приватного ключа аутентификация невозможна.Ed25519 challenge-response: server issues a random challenge → client signs with private key → server verifies. Authentication is impossible without the private key.
✅
TURN credentials одноразовыеTURN credentials are one-time
HMAC-SHA1, TTL 1 час, два независимых сервера. Перехваченные credentials невозможно использовать повторно.HMAC-SHA1, 1-hour TTL, two independent servers. Intercepted credentials cannot be reused.
ℹ️
ICE host-кандидатыICE host candidates
Локальный IP виден только доверенному собеседнику (не серверу). Сознательное решение — альтернатива .relay сломает P2P. Приемлемо для модели угроз HomeChat.Local IP is visible only to the trusted contact (not the server). Deliberate choice — .relay would break P2P. Acceptable for HomeChat's threat model.
ИнформационноInformational
Блок 5 — Локальное хранениеBlock 5 — Local Storage
✅
База данных зашифрованаDatabase is encrypted
SQLite хранит только зашифрованные блобы (AES-GCM). Ключ в Keychain. Метаданные: id, timestamp, sender/receiver.SQLite stores only encrypted blobs (AES-GCM). Key in Keychain. Metadata: id, timestamp, sender/receiver.
✅
Push не раскрывают текстPush notifications hide content
Body всегда «🔒 Зашифрованное сообщение» — без текста, без медиа, без данных переписки.Body is always "🔒 Encrypted message" — no text, no media, no conversation data.
✅
Логи без чувствительных данныхLogs contain no sensitive data
dlog обёрнут в #if DEBUG — в Release полностью вырезается компилятором.dlog is wrapped in #if DEBUG — completely removed by the compiler in Release builds.
Блок 6 — API и серверBlock 6 — API & Server
✅
Нет IDORNo IDOR
Права проверяются на каждый запрос. Пользователь не может получить данные чужого диалога.Access rights are checked on every request. A user cannot access data from another's conversation.
✅
Rate limiting на HTTP APIHTTP API rate limiting
60 запросов/мин на upload и download. При превышении — 429.60 requests/min on upload and download endpoints. Returns 429 when exceeded.
Upload принимается только от авторизованного пользователя с активным WebSocket сеансом. Download защищён 64-символьным SHA256 идентификатором + AES-GCM шифрованием контента.Upload is only accepted from an authorised user with an active WebSocket session. Download is protected by a 64-char SHA256 identifier and AES-GCM content encryption.
Блок 7 — ВерификацияBlock 7 — Verification
✅
Safety Numbers
60-цифровой код для верификации собеседника вне приложения. Совпадение кодов доказывает что ключи шифрования не подменялись.60-digit code to verify your contact out of band. Matching codes prove that encryption keys have not been substituted.
✅
Алерт при смене ключаKey change alert
При смене ключа безопасности собеседника приложение немедленно показывает предупреждение с инструкцией сверить Safety Numbers.When a contact's security key changes, the app immediately displays a warning with instructions to verify Safety Numbers.
✅
Сервер не может подменить ключServer cannot substitute a key
IdentityKey собеседника берётся из локального хранилища. Любая подмена вызывает authenticationFailure при расшифровке.The contact's IdentityKey is taken from local storage. Any substitution triggers authenticationFailure during decryption.
Блок 8 — ПриложениеBlock 8 — Application
✅
Нет секретов в кодеNo secrets in code
Secrets.plist вне git. TURN credentials только в .env на сервере. SPKI hash — публичный хэш публичного ключа, не секрет.Secrets.plist is outside git. TURN credentials only in .env on the server. SPKI hash is a public hash of a public key — not a secret.
✅
Логи отключены в ReleaseLogs disabled in Release
Функция dlog обёрнута в #if DEBUG — компилятор полностью удаляет её в Production сборке.The dlog function is wrapped in #if DEBUG — the compiler fully removes it in Production builds.
ℹ️
ОбфускацияObfuscation
Стандартная Swift компиляция с оптимизацией -O. Дополнительная обфускация не применяется.Standard Swift compilation with -O optimisation. No additional obfuscation is applied.
ИнформационноInformational
Дополнительная защитаDefence in Depth
🔒
DTLS Fingerprint TOFU
При первом звонке с контактом приложение сохраняет DTLS fingerprint. При последующих звонках — сверяет. Несовпадение: звонок автоматически завершается с предупреждением.On the first call with a contact, the app saves the DTLS fingerprint. On subsequent calls, it is verified. Mismatch: the call is automatically terminated with a warning.
Defence in depthDefence in depth
🔒
UI-алерты безопасностиSecurity UI alerts
Три независимых предупреждения: смена ключа безопасности, изменение отпечатка шифрования звонка, устаревший клиент при загрузке медиа.Three independent warnings: security key change, call encryption fingerprint change, outdated client on media upload.