feat: Add comprehensive encryption architecture documentation for Rosette Messenger
feat: Implement Firebase Cloud Messaging (FCM) integration documentation for push notifications docs: Outline remaining tasks for complete FCM integration in the project fix: Resolve WebSocket connection issues after user registration
This commit is contained in:
689
docs/ENCRYPTION_EXPLAINED.md
Normal file
689
docs/ENCRYPTION_EXPLAINED.md
Normal file
@@ -0,0 +1,689 @@
|
||||
# 🔐 Rosette Messenger - Криптографическая Архитектура
|
||||
|
||||
## Обзор
|
||||
|
||||
Rosette Messenger использует гибридную систему шифрования с двумя уровнями:
|
||||
|
||||
1. **XChaCha20-Poly1305** — для шифрования содержимого сообщений
|
||||
2. **ECDH + AES-256-CBC** — для безопасной передачи ключей между пользователями
|
||||
|
||||
---
|
||||
|
||||
## 📨 Архитектура Шифрования Сообщения
|
||||
|
||||
### Уровень 1: Шифрование Содержимого (XChaCha20-Poly1305)
|
||||
|
||||
#### Параметры
|
||||
|
||||
- **Алгоритм**: XChaCha20-Poly1305 (AEAD - Authenticated Encryption with Associated Data)
|
||||
- **Размер ключа**: 32 байта (256 бит)
|
||||
- **Размер nonce**: 24 байта (192 бита) — расширенный nonce для XChaCha
|
||||
- **Размер тега аутентификации**: 16 байт (128 бит, Poly1305 MAC)
|
||||
|
||||
#### Процесс Шифрования
|
||||
|
||||
```kotlin
|
||||
fun encryptMessage(plaintext: String): EncryptedMessage {
|
||||
// 1. Генерация случайных параметров
|
||||
val key = SecureRandom.nextBytes(32) // 256-bit ключ
|
||||
val nonce = SecureRandom.nextBytes(24) // 192-bit nonce
|
||||
|
||||
// 2. Шифрование с XChaCha20-Poly1305
|
||||
val ciphertext = xchacha20Poly1305Encrypt(
|
||||
plaintext.toByteArray(Charsets.UTF_8),
|
||||
key,
|
||||
nonce
|
||||
)
|
||||
|
||||
return EncryptedMessage(
|
||||
ciphertext = ciphertext.toHex(),
|
||||
key = key.toHex(),
|
||||
nonce = nonce.toHex()
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 XChaCha20-Poly1305: Детальная Реализация
|
||||
|
||||
### Этап 1: HChaCha20 Subkey Derivation
|
||||
|
||||
XChaCha20 использует расширенный 192-битный nonce. Чтобы уместить его в стандартный ChaCha20 (96-битный nonce), используется **HChaCha20** для получения производного ключа.
|
||||
|
||||
```kotlin
|
||||
// Вход: 256-bit ключ + первые 128 бит (16 байт) nonce
|
||||
// Выход: 256-bit subkey
|
||||
val subkey = hchacha20(key, nonce[0..15])
|
||||
```
|
||||
|
||||
**HChaCha20** — это модифицированная версия ChaCha20, которая:
|
||||
|
||||
- Берёт 256-битный ключ и 128 бит из nonce
|
||||
- Выполняет 20 раундов ChaCha quarter rounds
|
||||
- Возвращает первые и последние 4 слова состояния (32 байта)
|
||||
|
||||
### Этап 2: Формирование ChaCha20 Nonce
|
||||
|
||||
```kotlin
|
||||
// ChaCha20 использует 96-битный nonce (12 байт)
|
||||
val chacha20Nonce = ByteArray(12)
|
||||
// [0,0,0,0] + последние 8 байт оригинального nonce
|
||||
System.arraycopy(nonce, 16, chacha20Nonce, 4, 8)
|
||||
// Структура: [counter: 4 bytes][nonce: 8 bytes]
|
||||
```
|
||||
|
||||
### Этап 3: Инициализация ChaCha20 Engine
|
||||
|
||||
**КРИТИЧНО**: Используется **ОДИН** engine для всего процесса!
|
||||
|
||||
```kotlin
|
||||
val engine = ChaCha7539Engine()
|
||||
engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
|
||||
```
|
||||
|
||||
### Этап 4: Генерация Poly1305 Ключа
|
||||
|
||||
**КРИТИЧЕСКАЯ ДЕТАЛЬ**: Poly1305 ключ — это первые 32 байта первого блока ChaCha20 keystream (counter = 0).
|
||||
|
||||
```kotlin
|
||||
// Генерируем первый 64-байтный блок keystream (counter = 0)
|
||||
val poly1305KeyBlock = ByteArray(64)
|
||||
engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0)
|
||||
|
||||
// Poly1305 ключ = первые 32 байта
|
||||
val poly1305Key = poly1305KeyBlock.copyOfRange(0, 32)
|
||||
```
|
||||
|
||||
**Почему 64 байта?**
|
||||
|
||||
- ChaCha20 генерирует keystream блоками по 64 байта
|
||||
- Первые 32 байта → Poly1305 ключ
|
||||
- Остальные 32 байта → не используются
|
||||
- После этого counter автоматически увеличивается до 1
|
||||
|
||||
### Этап 5: Шифрование Plaintext
|
||||
|
||||
```kotlin
|
||||
// Engine продолжает с counter = 1
|
||||
val ciphertext = ByteArray(plaintext.size)
|
||||
engine.processBytes(plaintext, 0, plaintext.size, ciphertext, 0)
|
||||
```
|
||||
|
||||
**Keystream Layout**:
|
||||
|
||||
```
|
||||
Block 0 (counter=0): [Poly1305 key: 32 bytes][unused: 32 bytes]
|
||||
Block 1 (counter=1): [Plaintext byte 0...63] ⊕ [Keystream]
|
||||
Block 2 (counter=2): [Plaintext byte 64...127] ⊕ [Keystream]
|
||||
...
|
||||
```
|
||||
|
||||
### Этап 6: Вычисление Poly1305 MAC
|
||||
|
||||
Poly1305 — это MAC (Message Authentication Code) для аутентификации ciphertext.
|
||||
|
||||
```kotlin
|
||||
val mac = Poly1305()
|
||||
mac.init(KeyParameter(poly1305Key))
|
||||
|
||||
// Обновляем MAC с ciphertext
|
||||
mac.update(ciphertext, 0, ciphertext.size)
|
||||
|
||||
// Padding до 16 байт (AEAD требование)
|
||||
val padding = (16 - (ciphertext.size % 16)) % 16
|
||||
if (padding > 0) {
|
||||
mac.update(ByteArray(padding), 0, padding)
|
||||
}
|
||||
|
||||
// Length fields (little-endian)
|
||||
mac.update(ByteArray(8), 0, 8) // AAD length (0 в нашем случае)
|
||||
mac.update(longToLittleEndian(ciphertext.size), 0, 8)
|
||||
|
||||
val tag = ByteArray(16)
|
||||
mac.doFinal(tag, 0)
|
||||
```
|
||||
|
||||
### Этап 7: Финальный Формат
|
||||
|
||||
```kotlin
|
||||
return ciphertext + tag // [ciphertext][16-byte Poly1305 tag]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔓 Расшифровка XChaCha20-Poly1305
|
||||
|
||||
Процесс зеркальный, но с проверкой аутентификации **перед** расшифровкой:
|
||||
|
||||
```kotlin
|
||||
fun xchacha20Poly1305Decrypt(
|
||||
ciphertextWithTag: ByteArray,
|
||||
key: ByteArray,
|
||||
nonce: ByteArray
|
||||
): ByteArray {
|
||||
// 1. Разделение ciphertext и tag
|
||||
val ciphertext = ciphertextWithTag[0..-17]
|
||||
val tag = ciphertextWithTag[-16..-1]
|
||||
|
||||
// 2. HChaCha20 subkey derivation (как при шифровании)
|
||||
val subkey = hchacha20(key, nonce[0..15])
|
||||
|
||||
// 3. ChaCha20 nonce
|
||||
val chacha20Nonce = [0,0,0,0] + nonce[16..23]
|
||||
|
||||
// 4. Инициализация engine
|
||||
val engine = ChaCha7539Engine()
|
||||
engine.init(true, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
|
||||
|
||||
// 5. Генерация Poly1305 ключа (тот же процесс)
|
||||
val poly1305KeyBlock = ByteArray(64)
|
||||
engine.processBytes(ByteArray(64), 0, 64, poly1305KeyBlock, 0)
|
||||
|
||||
// 6. Проверка Poly1305 tag
|
||||
val mac = Poly1305()
|
||||
mac.init(KeyParameter(poly1305KeyBlock[0..31]))
|
||||
mac.update(ciphertext)
|
||||
// ... padding и length fields ...
|
||||
val computedTag = mac.doFinal()
|
||||
|
||||
if (!tag.contentEquals(computedTag)) {
|
||||
throw SecurityException("Authentication failed")
|
||||
}
|
||||
|
||||
// 7. Расшифровка (только после проверки!)
|
||||
// Создаём новый engine для расшифровки
|
||||
val decryptEngine = ChaCha7539Engine()
|
||||
decryptEngine.init(false, ParametersWithIV(KeyParameter(subkey), chacha20Nonce))
|
||||
|
||||
// Пропускаем первые 64 байта (Poly1305 key block)
|
||||
decryptEngine.processBytes(ByteArray(64), 0, 64, ByteArray(64), 0)
|
||||
|
||||
// Расшифровываем
|
||||
val plaintext = ByteArray(ciphertext.size)
|
||||
decryptEngine.processBytes(ciphertext, 0, ciphertext.size, plaintext, 0)
|
||||
|
||||
return plaintext
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Уровень 2: Шифрование Ключей (ECDH + AES-256-CBC)
|
||||
|
||||
После шифрования сообщения XChaCha20, нужно безопасно передать ключ и nonce получателю.
|
||||
|
||||
### Схема: Elliptic Curve Diffie-Hellman + AES
|
||||
|
||||
#### Параметры
|
||||
|
||||
- **Эллиптическая кривая**: secp256k1 (та же что в Bitcoin)
|
||||
- **AES режим**: CBC (Cipher Block Chaining)
|
||||
- **Размер ключа AES**: 256 бит
|
||||
- **Размер IV**: 16 байт (128 бит)
|
||||
- **Padding**: PKCS5Padding
|
||||
|
||||
#### Процесс Шифрования Ключа
|
||||
|
||||
```kotlin
|
||||
fun encryptKeyForRecipient(
|
||||
keyAndNonce: ByteArray, // 56 байт: 32 (key) + 24 (nonce)
|
||||
recipientPublicKeyHex: String
|
||||
): String {
|
||||
// 1. Генерация эфемерной пары ключей
|
||||
val ephemeralPrivateKey = SecureRandom.nextBytes(32)
|
||||
val ephemeralPublicKey = secp256k1.G × ephemeralPrivateKey
|
||||
|
||||
// 2. ECDH: Вычисление shared secret
|
||||
val recipientPublicKey = parsePublicKey(recipientPublicKeyHex)
|
||||
val sharedPoint = recipientPublicKey × ephemeralPrivateKey
|
||||
val sharedSecret = sharedPoint.x.toHex() // X-координата точки
|
||||
|
||||
// 3. Генерация IV для AES
|
||||
val iv = SecureRandom.nextBytes(16)
|
||||
|
||||
// 4. КРИТИЧНО: Latin1 → UTF-8 encoding
|
||||
// Эмуляция поведения crypto-js в JavaScript
|
||||
val latin1String = String(keyAndNonce, Charsets.ISO_8859_1)
|
||||
val utf8Bytes = latin1String.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// 5. AES-256-CBC шифрование
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(
|
||||
Cipher.ENCRYPT_MODE,
|
||||
SecretKeySpec(sharedSecret.hexToBytes(), "AES"),
|
||||
IvParameterSpec(iv)
|
||||
)
|
||||
val encryptedKey = cipher.doFinal(utf8Bytes)
|
||||
|
||||
// 6. Формат: iv:ciphertext:ephemeralPrivateKey
|
||||
val combined = "${iv.toHex()}:${encryptedKey.toHex()}:${ephemeralPrivateKey.toHex()}"
|
||||
|
||||
// 7. Base64 encoding
|
||||
return Base64.encodeToString(combined.toByteArray(), Base64.NO_WRAP)
|
||||
}
|
||||
```
|
||||
|
||||
#### Почему Latin1 → UTF-8?
|
||||
|
||||
**JavaScript (crypto-js) поведение**:
|
||||
|
||||
```javascript
|
||||
// React Native
|
||||
const key = Buffer.concat([keyBytes, nonceBytes]); // 56 байт
|
||||
const keyString = key.toString('binary'); // Latin1 строка (56 символов)
|
||||
const encrypted = crypto.AES.encrypt(keyString, ...);
|
||||
// crypto-js внутри делает: Utf8.parse(keyString) → конвертирует в UTF-8
|
||||
```
|
||||
|
||||
**Kotlin эмуляция**:
|
||||
|
||||
```kotlin
|
||||
// Байты → Latin1 строка (символы с кодами 0-255)
|
||||
val latin1String = String(keyAndNonce, Charsets.ISO_8859_1)
|
||||
// Latin1 строка → UTF-8 байты (байты > 127 становятся multi-byte)
|
||||
val utf8Bytes = latin1String.toByteArray(Charsets.UTF_8)
|
||||
```
|
||||
|
||||
**Пример трансформации**:
|
||||
|
||||
```
|
||||
Байт: [0xCC] (204)
|
||||
↓
|
||||
Latin1 char: 'Ì' (charCode = 204)
|
||||
↓
|
||||
UTF-8 bytes: [0xC3, 0x8C] (2 байта)
|
||||
```
|
||||
|
||||
Результат: 56 байт → 88 байт UTF-8 (байты > 127 занимают 2 байта в UTF-8)
|
||||
|
||||
#### Формат Зашифрованного Ключа
|
||||
|
||||
```
|
||||
Base64( ivHex : ciphertextHex : ephemeralPrivateKeyHex )
|
||||
|
||||
Пример:
|
||||
MjdmZTMzYTIyYjczYjNiNDhmOTIzYmY3YmJjNDhmMzE6MzIwNDc4NzMzNzM2NTQ1MzYy...
|
||||
```
|
||||
|
||||
Декодированная строка:
|
||||
|
||||
```
|
||||
27fe33a22b73b3b48f923bf7bbc48f31:32047873373654536225d8b1....:6ed2d3c3391fcccd...
|
||||
[ IV (32 hex) ]:[ Ciphertext (192 hex) ]:[Ephemeral Key (64 hex)]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔓 Расшифровка Ключа
|
||||
|
||||
```kotlin
|
||||
fun decryptKeyFromSender(
|
||||
encryptedKeyBase64: String,
|
||||
myPrivateKeyHex: String
|
||||
): ByteArray {
|
||||
// 1. Декодирование Base64
|
||||
val decoded = String(Base64.decode(encryptedKeyBase64, Base64.DEFAULT))
|
||||
val parts = decoded.split(":")
|
||||
val ivHex = parts[0]
|
||||
val ciphertextHex = parts[1]
|
||||
val ephemeralPrivateKeyHex = parts[2]
|
||||
|
||||
// 2. Парсинг ключей
|
||||
val ephemeralPrivateKey = ephemeralPrivateKeyHex.hexToBytes()
|
||||
val myPrivateKey = myPrivateKeyHex.hexToBytes()
|
||||
|
||||
// 3. ECDH: Вычисление того же shared secret
|
||||
val myPublicKey = secp256k1.G × myPrivateKey
|
||||
val ephemeralPublicKey = secp256k1.G × ephemeralPrivateKey
|
||||
val sharedPoint = myPublicKey × ephemeralPrivateKey
|
||||
val sharedSecret = sharedPoint.x.toHex()
|
||||
|
||||
// 4. AES-256-CBC расшифровка
|
||||
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
SecretKeySpec(sharedSecret.hexToBytes(), "AES"),
|
||||
IvParameterSpec(ivHex.hexToBytes())
|
||||
)
|
||||
val decryptedUtf8Bytes = cipher.doFinal(ciphertextHex.hexToBytes())
|
||||
|
||||
// 5. UTF-8 → Latin1 (обратная конвертация)
|
||||
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
|
||||
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
return originalBytes // 56 байт: 32 (key) + 24 (nonce)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Полный Цикл: Отправка Сообщения
|
||||
|
||||
```kotlin
|
||||
fun encryptForSending(
|
||||
plaintext: String,
|
||||
recipientPublicKey: String
|
||||
): Pair<String, String> {
|
||||
// 1. Шифрование сообщения XChaCha20-Poly1305
|
||||
val encrypted = encryptMessage(plaintext)
|
||||
// encrypted.ciphertext = зашифрованное сообщение (hex)
|
||||
// encrypted.key = 32-byte ключ (hex)
|
||||
// encrypted.nonce = 24-byte nonce (hex)
|
||||
|
||||
// 2. Объединение key + nonce
|
||||
val keyAndNonce = encrypted.key.hexToBytes() + encrypted.nonce.hexToBytes()
|
||||
// keyAndNonce = 56 байт
|
||||
|
||||
// 3. Шифрование ключа для получателя (ECDH + AES)
|
||||
val encryptedKey = encryptKeyForRecipient(keyAndNonce, recipientPublicKey)
|
||||
// encryptedKey = Base64 строка
|
||||
|
||||
return Pair(encrypted.ciphertext, encryptedKey)
|
||||
}
|
||||
```
|
||||
|
||||
### Формат Пакета Сообщения
|
||||
|
||||
```kotlin
|
||||
data class PacketMessage(
|
||||
val from: String, // Публичный ключ отправителя (hex)
|
||||
val to: String, // Публичный ключ получателя (hex)
|
||||
val content: String, // Зашифрованное сообщение (hex)
|
||||
val chachaKey: String, // Зашифрованный ключ (Base64)
|
||||
val timestamp: Long,
|
||||
val messageId: String
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📬 Полный Цикл: Получение Сообщения
|
||||
|
||||
```kotlin
|
||||
fun decryptReceived(
|
||||
encryptedContent: String,
|
||||
encryptedKey: String,
|
||||
myPrivateKey: String
|
||||
): String {
|
||||
// 1. Расшифровка ключа (ECDH + AES)
|
||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||
|
||||
// 2. Разделение на key и nonce
|
||||
val key = keyAndNonce.copyOfRange(0, 32)
|
||||
val nonce = keyAndNonce.copyOfRange(32, 56)
|
||||
|
||||
// 3. Расшифровка сообщения (XChaCha20-Poly1305)
|
||||
val plaintext = decryptMessage(
|
||||
encryptedContent.hexToBytes(),
|
||||
key,
|
||||
nonce
|
||||
)
|
||||
|
||||
return String(plaintext, Charsets.UTF_8)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Критические Детали Реализации
|
||||
|
||||
### 1. Counter Management в XChaCha20
|
||||
|
||||
**ПРОБЛЕМА**: Если создать два отдельных ChaCha20 engine, оба начнут с counter=0.
|
||||
|
||||
❌ **Неправильно**:
|
||||
|
||||
```kotlin
|
||||
// Engine 1: Poly1305 ключ
|
||||
val engine1 = ChaCha7539Engine()
|
||||
engine1.processBytes(zeros, ...) // counter = 0
|
||||
|
||||
// Engine 2: Шифрование
|
||||
val engine2 = ChaCha7539Engine() // ❌ Снова counter = 0!
|
||||
engine2.processBytes(plaintext, ...)
|
||||
```
|
||||
|
||||
✅ **Правильно**:
|
||||
|
||||
```kotlin
|
||||
// Один engine для всего процесса
|
||||
val engine = ChaCha7539Engine()
|
||||
engine.init(true, ParametersWithIV(key, nonce))
|
||||
|
||||
// Poly1305 ключ (counter = 0)
|
||||
engine.processBytes(zeros[64], ..., poly1305KeyBlock, ...)
|
||||
|
||||
// Шифрование (counter автоматически = 1)
|
||||
engine.processBytes(plaintext, ..., ciphertext, ...)
|
||||
```
|
||||
|
||||
### 2. JavaScript/Kotlin Совместимость
|
||||
|
||||
**crypto-js особенности**:
|
||||
|
||||
```javascript
|
||||
crypto.AES.encrypt(string, key, { iv });
|
||||
// Внутри: crypto.enc.Utf8.parse(string)
|
||||
// Это берёт charCode каждого символа как байт
|
||||
```
|
||||
|
||||
**Kotlin эмуляция**:
|
||||
|
||||
```kotlin
|
||||
// Байты → Latin1 (каждый байт = один символ)
|
||||
val latin1 = String(bytes, Charsets.ISO_8859_1)
|
||||
// Latin1 → UTF-8 (символы > 127 становятся multi-byte)
|
||||
val utf8 = latin1.toByteArray(Charsets.UTF_8)
|
||||
```
|
||||
|
||||
### 3. ECDH Shared Secret
|
||||
|
||||
**JavaScript (elliptic.js)**:
|
||||
|
||||
```javascript
|
||||
const shared = ephemeralKey.derive(publicKey).toString(16);
|
||||
// .toString(16) НЕ добавляет ведущие нули!
|
||||
```
|
||||
|
||||
**Kotlin эмуляция**:
|
||||
|
||||
```kotlin
|
||||
val xCoord = sharedPoint.normalize().xCoord.toBigInteger()
|
||||
var hex = xCoord.toString(16) // Может быть без ведущих нулей
|
||||
|
||||
// Если нечётная длина, добавить ведущий 0 для парсинга
|
||||
if (hex.length % 2 != 0) {
|
||||
hex = "0$hex"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Little-Endian в Poly1305
|
||||
|
||||
Длины (AAD length, ciphertext length) должны быть в **little-endian** формате:
|
||||
|
||||
```kotlin
|
||||
fun longToLittleEndian(n: Long): ByteArray {
|
||||
val bs = ByteArray(8)
|
||||
bs[0] = n.toByte()
|
||||
bs[1] = (n ushr 8).toByte()
|
||||
bs[2] = (n ushr 16).toByte()
|
||||
bs[3] = (n ushr 24).toByte()
|
||||
bs[4] = (n ushr 32).toByte()
|
||||
bs[5] = (n ushr 40).toByte()
|
||||
bs[6] = (n ushr 48).toByte()
|
||||
bs[7] = (n ushr 56).toByte()
|
||||
return bs
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Безопасность
|
||||
|
||||
### Сильные Стороны
|
||||
|
||||
1. **Forward Secrecy**: Каждое сообщение использует уникальный эфемерный ключ
|
||||
2. **Authenticated Encryption**: Poly1305 MAC защищает от модификации
|
||||
3. **Extended Nonce**: XChaCha20 позволяет безопасно использовать случайные nonce
|
||||
4. **ECDH**: Асимметричное шифрование ключей без необходимости предварительного обмена
|
||||
|
||||
### Важные Замечания
|
||||
|
||||
⚠️ **Передача Ephemeral Private Key**:
|
||||
|
||||
```kotlin
|
||||
// Формат: iv:ciphertext:ephemeralPrivateKey
|
||||
```
|
||||
|
||||
В текущей реализации ephemeral **private** key передаётся вместе с ciphertext. Это работает, но не является стандартной практикой. Обычно передаётся ephemeral **public** key, и получатель использует свой private key для ECDH.
|
||||
|
||||
**Текущая схема**:
|
||||
|
||||
- Отправитель: `ephemeralPrivate × recipientPublic = sharedSecret`
|
||||
- Получатель: `ephemeralPrivate × myPublic = sharedSecret`
|
||||
|
||||
**Стандартная схема**:
|
||||
|
||||
- Отправитель: `ephemeralPrivate × recipientPublic = sharedSecret`
|
||||
- Получатель: `myPrivate × ephemeralPublic = sharedSecret`
|
||||
|
||||
Текущая схема работает корректно, но передача private key нестандартна.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Размеры Данных
|
||||
|
||||
### Одно Сообщение
|
||||
|
||||
```
|
||||
Plaintext: N байт
|
||||
XChaCha20: N + 16 байт (ciphertext + Poly1305 tag)
|
||||
Key+Nonce: 56 байт (32 + 24)
|
||||
Latin1→UTF-8: ~88 байт (зависит от содержимого)
|
||||
AES+padding: 96 байт (после PKCS5 padding)
|
||||
Зашифр. ключ: ~388 символов Base64
|
||||
|
||||
Итого overhead: ~452 символа (независимо от размера сообщения)
|
||||
```
|
||||
|
||||
### Пример
|
||||
|
||||
```
|
||||
Сообщение: "Hello" (5 байт)
|
||||
↓
|
||||
XChaCha20: 21 байт (5 + 16 tag)
|
||||
Key: 32 байт, Nonce: 24 байт → 56 байт
|
||||
↓
|
||||
AES: 96 байт (после padding)
|
||||
↓
|
||||
Base64: 388 символов
|
||||
|
||||
Передаётся:
|
||||
- content: 42 символа (21 байт hex)
|
||||
- chachaKey: 388 символов (Base64)
|
||||
Итого: ~430 символов для передачи "Hello"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
### Unit Test для XChaCha20
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun testXChaCha20Compatibility() {
|
||||
val key = "ccd8617e4e328baee60fc2d5de0cca9aea7ac382330aa1daafb188bc875baa68"
|
||||
val nonce = "3cc45a27fe3910913bd429f6a814039fb2c7c564d6656727"
|
||||
val plaintext = "kdkdkdkd"
|
||||
|
||||
val encrypted = xchacha20Poly1305Encrypt(
|
||||
plaintext.toByteArray(),
|
||||
key.hexToBytes(),
|
||||
nonce.hexToBytes()
|
||||
)
|
||||
|
||||
// Должно совпадать с @noble/ciphers
|
||||
val decrypted = xchacha20Poly1305Decrypt(
|
||||
encrypted,
|
||||
key.hexToBytes(),
|
||||
nonce.hexToBytes()
|
||||
)
|
||||
|
||||
assertEquals(plaintext, String(decrypted, Charsets.UTF_8))
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Test
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun testFullEncryptionCycle() {
|
||||
val alicePrivate = generatePrivateKey()
|
||||
val alicePublic = derivePublicKey(alicePrivate)
|
||||
|
||||
val bobPrivate = generatePrivateKey()
|
||||
val bobPublic = derivePublicKey(bobPrivate)
|
||||
|
||||
// Alice → Bob
|
||||
val message = "Secret message"
|
||||
val (ciphertext, encryptedKey) = encryptForSending(message, bobPublic)
|
||||
|
||||
// Bob получает и расшифровывает
|
||||
val decrypted = decryptReceived(ciphertext, encryptedKey, bobPrivate)
|
||||
|
||||
assertEquals(message, decrypted)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Библиотеки
|
||||
|
||||
### Kotlin (Android)
|
||||
|
||||
- **BouncyCastle** 1.77 — secp256k1, AES, ChaCha20, Poly1305
|
||||
- **Android Crypto** — SecureRandom
|
||||
|
||||
### JavaScript (React Native/Desktop)
|
||||
|
||||
- **@noble/ciphers** — XChaCha20-Poly1305
|
||||
- **crypto-js** — AES, PBKDF2, кодировки
|
||||
- **elliptic** — secp256k1, ECDH
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
### Исправления 2026-01-11
|
||||
|
||||
#### Проблема: XChaCha20 Counter Management
|
||||
|
||||
**Симптом**: Desktop показывал кракозябры при получении сообщений от Kotlin Android app.
|
||||
|
||||
**Причина**: Kotlin создавал два отдельных `ChaCha7539Engine`:
|
||||
|
||||
1. Первый для генерации Poly1305 ключа (counter = 0)
|
||||
2. Второй для шифрования plaintext (counter = 0 снова!)
|
||||
|
||||
Это приводило к тому что plaintext шифровался тем же keystream что и Poly1305 ключ.
|
||||
|
||||
**Решение**: Использовать **один** engine для всего процесса:
|
||||
|
||||
```kotlin
|
||||
// 1. Генерация Poly1305 ключа (counter = 0, байты 0-63)
|
||||
engine.processBytes(ByteArray(64), ..., poly1305KeyBlock, ...)
|
||||
|
||||
// 2. Шифрование (counter автоматически = 1, байты 64+)
|
||||
engine.processBytes(plaintext, ..., ciphertext, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Дополнительные Ресурсы
|
||||
|
||||
- [XChaCha20-Poly1305 RFC Draft](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha)
|
||||
- [ChaCha20 и Poly1305 для IETF](https://datatracker.ietf.org/doc/html/rfc8439)
|
||||
- [secp256k1 на SEC2](https://www.secg.org/sec2-v2.pdf)
|
||||
- [Elliptic Curve Diffie-Hellman](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman)
|
||||
Reference in New Issue
Block a user