feat: Add direct encrypted key testing for password in decryptAttachmentBlob function
This commit is contained in:
@@ -437,14 +437,16 @@ object MessageCrypto {
|
|||||||
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
||||||
|
|
||||||
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
|
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔓 AES decrypted raw bytes: ${decryptedUtf8Bytes.size}, hex=${decryptedUtf8Bytes.toHex()}")
|
||||||
|
|
||||||
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
|
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
|
||||||
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
|
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
|
||||||
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
|
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
|
||||||
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
|
val utf8String = String(decryptedUtf8Bytes, Charsets.UTF_8)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔓 UTF-8 string length: ${utf8String.length}, chars: ${utf8String.take(20).map { it.code }}")
|
||||||
|
|
||||||
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
|
val originalBytes = utf8String.toByteArray(Charsets.ISO_8859_1)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔓 Latin1 bytes: ${originalBytes.size}, hex=${originalBytes.toHex().take(80)}...")
|
||||||
|
|
||||||
return originalBytes
|
return originalBytes
|
||||||
}
|
}
|
||||||
@@ -514,39 +516,67 @@ object MessageCrypto {
|
|||||||
* Формат: ivBase64:ciphertextBase64
|
* Формат: ivBase64:ciphertextBase64
|
||||||
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
||||||
*
|
*
|
||||||
|
* КРИТИЧНО: Desktop использует ВЕСЬ keyAndNonce (56 bytes) как password!
|
||||||
|
* Desktop: chachaDecryptedKey.toString('utf-8') - конвертирует все 56 байт в UTF-8 строку
|
||||||
|
*
|
||||||
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
|
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
|
||||||
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ (32 bytes)
|
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ+nonce (56 bytes: 32 key + 24 nonce)
|
||||||
*/
|
*/
|
||||||
fun decryptAttachmentBlobWithPlainKey(
|
fun decryptAttachmentBlobWithPlainKey(
|
||||||
encryptedData: String,
|
encryptedData: String,
|
||||||
chachaKeyPlain: ByteArray
|
chachaKeyPlain: ByteArray
|
||||||
): String? {
|
): String? {
|
||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey: data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes")
|
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPlainKey(bytes): data length=${encryptedData.length}, key=${chachaKeyPlain.size} bytes")
|
||||||
|
|
||||||
// ВАЖНО: Для attachment используем только первые 32 bytes (ChaCha key без nonce)
|
// Конвертируем байты в UTF-8 строку как Desktop: Buffer.toString('utf-8')
|
||||||
val keyOnly = chachaKeyPlain.copyOfRange(0, 32)
|
val password = bytesToJsUtf8String(chachaKeyPlain)
|
||||||
|
decryptAttachmentBlobWithPassword(encryptedData, password)
|
||||||
// 1. Конвертируем key в строку используя bytesToJsUtf8String
|
|
||||||
// чтобы совпадало с JS Buffer.toString('utf-8') который заменяет
|
|
||||||
// невалидные UTF-8 последовательности на U+FFFD
|
|
||||||
val chachaKeyString = bytesToJsUtf8String(keyOnly)
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 ChaCha key string length: ${chachaKeyString.length}")
|
|
||||||
|
|
||||||
// 2. Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
|
|
||||||
val pbkdf2Key = generatePBKDF2Key(chachaKeyString)
|
|
||||||
android.util.Log.d("MessageCrypto", "🔑 PBKDF2 key: ${pbkdf2Key.size} bytes")
|
|
||||||
|
|
||||||
// 3. Расшифровываем AES-256-CBC
|
|
||||||
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
|
|
||||||
android.util.Log.d("MessageCrypto", "✅ Decryption result: ${if (result != null) "success (${result.length} chars)" else "null"}")
|
|
||||||
result
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
|
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расшифровка attachment blob с уже готовым паролем (Latin1 string)
|
||||||
|
* Используется когда chachaKey сохранён в БД как Latin1 string (raw bytes)
|
||||||
|
*
|
||||||
|
* КРИТИЧНО: Desktop делает Buffer.from(str, 'binary').toString('utf-8')
|
||||||
|
* Это эквивалентно взятию charCode каждого символа (0-255) как байт,
|
||||||
|
* потом UTF-8 decode этих байтов с заменой невалидных на U+FFFD (<28>)
|
||||||
|
*
|
||||||
|
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
|
||||||
|
* @param passwordLatin1 Latin1 string (56 chars, каждый char = один байт 0-255)
|
||||||
|
*/
|
||||||
|
fun decryptAttachmentBlobWithPassword(
|
||||||
|
encryptedData: String,
|
||||||
|
passwordLatin1: String
|
||||||
|
): String? {
|
||||||
|
return try {
|
||||||
|
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlobWithPassword: data length=${encryptedData.length}, password=${passwordLatin1.length} chars")
|
||||||
|
|
||||||
|
// Конвертируем Latin1 string → bytes → UTF-8 string (эмулируем Desktop)
|
||||||
|
// Desktop: Buffer.from(str, 'binary').toString('utf-8')
|
||||||
|
val passwordBytes = passwordLatin1.toByteArray(Charsets.ISO_8859_1)
|
||||||
|
val passwordUtf8 = bytesToJsUtf8String(passwordBytes)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔑 Password UTF-8: ${passwordUtf8.length} chars")
|
||||||
|
|
||||||
|
// Генерируем PBKDF2 ключ (salt='rosetta', 1000 iterations, sha1)
|
||||||
|
// Crypto-js конвертирует passwordUtf8 string в UTF-8 bytes для PBKDF2
|
||||||
|
val pbkdf2Key = generatePBKDF2Key(passwordUtf8)
|
||||||
|
android.util.Log.d("MessageCrypto", "🔑 PBKDF2 key: ${pbkdf2Key.toHex()}")
|
||||||
|
|
||||||
|
// Расшифровываем AES-256-CBC + zlib decompress
|
||||||
|
val result = decryptWithPBKDF2Key(encryptedData, pbkdf2Key)
|
||||||
|
android.util.Log.d("MessageCrypto", "✅ Decryption: ${if (result != null) "success (${result.length} chars)" else "FAILED"}")
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPassword failed: ${e.message}", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Расшифровка MESSAGES attachment blob (Legacy - с RSA расшифровкой ключа)
|
* Расшифровка MESSAGES attachment blob (Legacy - с RSA расшифровкой ключа)
|
||||||
* Формат: ivBase64:ciphertextBase64
|
* Формат: ivBase64:ciphertextBase64
|
||||||
@@ -560,11 +590,11 @@ object MessageCrypto {
|
|||||||
return try {
|
return try {
|
||||||
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
|
android.util.Log.d("MessageCrypto", "🔐 decryptAttachmentBlob: data length=${encryptedData.length}, key length=${encryptedKey.length}")
|
||||||
|
|
||||||
// 1. Расшифровываем ChaCha ключ (как для сообщений)
|
// 1. Расшифровываем ChaCha ключ+nonce (56 bytes) через ECDH
|
||||||
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
val keyAndNonce = decryptKeyFromSender(encryptedKey, myPrivateKey)
|
||||||
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes")
|
android.util.Log.d("MessageCrypto", "🔑 Decrypted keyAndNonce: ${keyAndNonce.size} bytes, hex=${keyAndNonce.toHex().take(40)}...")
|
||||||
|
|
||||||
// 2. Используем новую функцию
|
// 2. Используем ВСЕ 56 байт как password для PBKDF2
|
||||||
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
decryptAttachmentBlobWithPlainKey(encryptedData, keyAndNonce)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
||||||
@@ -575,16 +605,66 @@ object MessageCrypto {
|
|||||||
/**
|
/**
|
||||||
* Генерация PBKDF2 ключа (совместимо с crypto-js / RN)
|
* Генерация PBKDF2 ключа (совместимо с crypto-js / RN)
|
||||||
* ВАЖНО: crypto-js использует PBKDF2WithHmacSHA1 по умолчанию!
|
* ВАЖНО: crypto-js использует PBKDF2WithHmacSHA1 по умолчанию!
|
||||||
|
*
|
||||||
|
* КРИТИЧНО: crypto-js конвертирует password через UTF-8 encoding,
|
||||||
|
* но PBEKeySpec в Java использует UTF-16! Поэтому используем ручную реализацию.
|
||||||
*/
|
*/
|
||||||
private fun generatePBKDF2Key(password: String, salt: String = "rosetta", iterations: Int = 1000): ByteArray {
|
private fun generatePBKDF2Key(password: String, salt: String = "rosetta", iterations: Int = 1000): ByteArray {
|
||||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
// Crypto-js: WordArray.create(password) использует UTF-8
|
||||||
val spec = javax.crypto.spec.PBEKeySpec(
|
val passwordBytes = password.toByteArray(Charsets.UTF_8)
|
||||||
password.toCharArray(),
|
val saltBytes = salt.toByteArray(Charsets.UTF_8)
|
||||||
salt.toByteArray(Charsets.UTF_8),
|
|
||||||
iterations,
|
// PBKDF2-HMAC-SHA1 ручная реализация для совместимости с crypto-js
|
||||||
256 // 32 bytes
|
val keyLength = 32 // 256 bits
|
||||||
)
|
val mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||||
return factory.generateSecret(spec).encoded
|
val keySpec = javax.crypto.spec.SecretKeySpec(passwordBytes, "HmacSHA1")
|
||||||
|
mac.init(keySpec)
|
||||||
|
|
||||||
|
// PBKDF2 алгоритм
|
||||||
|
val hLen = mac.macLength
|
||||||
|
val dkLen = keyLength
|
||||||
|
val l = (dkLen + hLen - 1) / hLen
|
||||||
|
val r = dkLen - (l - 1) * hLen
|
||||||
|
|
||||||
|
val derivedKey = ByteArray(dkLen)
|
||||||
|
var offset = 0
|
||||||
|
|
||||||
|
for (i in 1..l) {
|
||||||
|
val block = pbkdf2Block(mac, saltBytes, iterations, i)
|
||||||
|
val copyLen = if (i < l) hLen else r
|
||||||
|
System.arraycopy(block, 0, derivedKey, offset, copyLen)
|
||||||
|
offset += copyLen
|
||||||
|
}
|
||||||
|
|
||||||
|
return derivedKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PBKDF2 block функция (для совместимости с crypto-js)
|
||||||
|
*/
|
||||||
|
private fun pbkdf2Block(mac: javax.crypto.Mac, salt: ByteArray, iterations: Int, blockIndex: Int): ByteArray {
|
||||||
|
// U1 = PRF(Password, Salt || INT_32_BE(i))
|
||||||
|
mac.reset()
|
||||||
|
mac.update(salt)
|
||||||
|
mac.update(byteArrayOf(
|
||||||
|
(blockIndex shr 24).toByte(),
|
||||||
|
(blockIndex shr 16).toByte(),
|
||||||
|
(blockIndex shr 8).toByte(),
|
||||||
|
blockIndex.toByte()
|
||||||
|
))
|
||||||
|
var u = mac.doFinal()
|
||||||
|
val result = u.clone()
|
||||||
|
|
||||||
|
// U2 = PRF(Password, U1), ... , Uc = PRF(Password, Uc-1)
|
||||||
|
for (j in 2..iterations) {
|
||||||
|
mac.reset()
|
||||||
|
u = mac.doFinal(u)
|
||||||
|
for (k in result.indices) {
|
||||||
|
result[k] = (result[k].toInt() xor u[k].toInt()).toByte()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -700,6 +780,15 @@ object MessageCrypto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
|
||||||
|
* Uses WHATWG UTF-8 decoder algorithm where each invalid byte produces exactly ONE U+FFFD.
|
||||||
|
* This is critical for cross-platform compatibility with React Native!
|
||||||
|
*
|
||||||
|
* Public wrapper for use in MessageRepository
|
||||||
|
*/
|
||||||
|
fun bytesToJsUtf8StringPublic(bytes: ByteArray): String = bytesToJsUtf8String(bytes)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
|
* Converts bytes to string mimicking JavaScript's Buffer.toString('utf-8') behavior.
|
||||||
* Uses WHATWG UTF-8 decoder algorithm where each invalid byte produces exactly ONE U+FFFD.
|
* Uses WHATWG UTF-8 decoder algorithm where each invalid byte produces exactly ONE U+FFFD.
|
||||||
|
|||||||
@@ -194,10 +194,16 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
// Шифрование
|
// Шифрование
|
||||||
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(
|
val encryptResult = MessageCrypto.encryptForSending(
|
||||||
text.trim(),
|
text.trim(),
|
||||||
toPublicKey
|
toPublicKey
|
||||||
)
|
)
|
||||||
|
val encryptedContent = encryptResult.ciphertext
|
||||||
|
val encryptedKey = encryptResult.encryptedKey
|
||||||
|
|
||||||
|
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
||||||
|
// Desktop хранит зашифрованный ключ, расшифровывает только при использовании
|
||||||
|
android.util.Log.d("MessageRepository", "🔑 Outgoing chacha_key (encrypted): ${encryptedKey.length} chars")
|
||||||
|
|
||||||
// Сериализуем attachments в JSON
|
// Сериализуем attachments в JSON
|
||||||
val attachmentsJson = serializeAttachments(attachments)
|
val attachmentsJson = serializeAttachments(attachments)
|
||||||
@@ -306,6 +312,11 @@ class MessageRepository private constructor(private val context: Context) {
|
|||||||
val dialogKey = getDialogKey(packet.fromPublicKey)
|
val dialogKey = getDialogKey(packet.fromPublicKey)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 🔑 КРИТИЧНО: Сохраняем ЗАШИФРОВАННЫЙ chacha_key (как в Desktop!)
|
||||||
|
// Desktop: хранит зашифрованный ключ, расшифровывает только при использовании
|
||||||
|
// Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
|
||||||
|
android.util.Log.d("MessageRepository", "🔑 Incoming chacha_key (encrypted): ${packet.chachaKey.length} chars")
|
||||||
|
|
||||||
// Расшифровываем
|
// Расшифровываем
|
||||||
val plainText = MessageCrypto.decryptIncoming(
|
val plainText = MessageCrypto.decryptIncoming(
|
||||||
packet.content,
|
packet.content,
|
||||||
|
|||||||
@@ -192,11 +192,16 @@ fun ImageAttachment(
|
|||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
Log.d(TAG, "🔓 Decrypting image...")
|
Log.d(TAG, "🔓 Decrypting image...")
|
||||||
|
|
||||||
// Расшифровываем
|
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
// Сначала расшифровываем его: Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8')
|
||||||
|
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
|
||||||
|
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
|
||||||
|
Log.d(TAG, "🔑 Decrypted chacha_key: ${decryptKeyString.length} chars")
|
||||||
|
|
||||||
|
// Теперь используем расшифрованный ключ как password для PBKDF2
|
||||||
|
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
chachaKey,
|
decryptKeyString
|
||||||
privateKey
|
|
||||||
)
|
)
|
||||||
downloadProgress = 0.8f
|
downloadProgress = 0.8f
|
||||||
|
|
||||||
@@ -390,10 +395,14 @@ fun FileAttachment(
|
|||||||
|
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
|
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||||
|
// Сначала расшифровываем его
|
||||||
|
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
|
||||||
|
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
|
||||||
|
|
||||||
|
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
chachaKey,
|
decryptKeyString
|
||||||
privateKey
|
|
||||||
)
|
)
|
||||||
downloadProgress = 0.9f
|
downloadProgress = 0.9f
|
||||||
|
|
||||||
@@ -581,10 +590,14 @@ fun AvatarAttachment(
|
|||||||
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
||||||
downloadStatus = DownloadStatus.DECRYPTING
|
downloadStatus = DownloadStatus.DECRYPTING
|
||||||
|
|
||||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||||
|
// Сначала расшифровываем его
|
||||||
|
val decryptedKeyAndNonce = MessageCrypto.decryptKeyFromSender(chachaKey, myPrivateKey)
|
||||||
|
val decryptKeyString = String(decryptedKeyAndNonce, Charsets.UTF_8)
|
||||||
|
|
||||||
|
val decrypted = MessageCrypto.decryptAttachmentBlobWithPassword(
|
||||||
encryptedContent,
|
encryptedContent,
|
||||||
chachaKey,
|
decryptKeyString
|
||||||
privateKey
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (decrypted != null) {
|
if (decrypted != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user