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))
|
||||
|
||||
val decryptedUtf8Bytes = cipher.doFinal(encryptedKey)
|
||||
android.util.Log.d("MessageCrypto", "🔓 AES decrypted raw bytes: ${decryptedUtf8Bytes.size}, hex=${decryptedUtf8Bytes.toHex()}")
|
||||
|
||||
// ⚠️ КРИТИЧНО: Обратная конвертация UTF-8 → Latin1!
|
||||
// Desktop: decrypted.toString(crypto.enc.Utf8) → Buffer.from(str, 'binary')
|
||||
// Это декодирует UTF-8 в строку, потом берёт charCode каждого символа
|
||||
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)
|
||||
|
||||
android.util.Log.d("MessageCrypto", "🔓 Latin1 bytes: ${originalBytes.size}, hex=${originalBytes.toHex().take(80)}...")
|
||||
|
||||
return originalBytes
|
||||
}
|
||||
@@ -514,39 +516,67 @@ object MessageCrypto {
|
||||
* Формат: ivBase64:ciphertextBase64
|
||||
* Использует PBKDF2 + AES-256-CBC + zlib decompression
|
||||
*
|
||||
* КРИТИЧНО: Desktop использует ВЕСЬ keyAndNonce (56 bytes) как password!
|
||||
* Desktop: chachaDecryptedKey.toString('utf-8') - конвертирует все 56 байт в UTF-8 строку
|
||||
*
|
||||
* @param encryptedData зашифрованный контент (ivBase64:ciphertextBase64)
|
||||
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ (32 bytes)
|
||||
* @param chachaKeyPlain Уже расшифрованный ChaCha ключ+nonce (56 bytes: 32 key + 24 nonce)
|
||||
*/
|
||||
fun decryptAttachmentBlobWithPlainKey(
|
||||
encryptedData: String,
|
||||
chachaKeyPlain: ByteArray
|
||||
): String? {
|
||||
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)
|
||||
val keyOnly = chachaKeyPlain.copyOfRange(0, 32)
|
||||
|
||||
// 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
|
||||
// Конвертируем байты в UTF-8 строку как Desktop: Buffer.toString('utf-8')
|
||||
val password = bytesToJsUtf8String(chachaKeyPlain)
|
||||
decryptAttachmentBlobWithPassword(encryptedData, password)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlobWithPlainKey failed: ${e.message}", e)
|
||||
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 расшифровкой ключа)
|
||||
* Формат: ivBase64:ciphertextBase64
|
||||
@@ -560,11 +590,11 @@ object MessageCrypto {
|
||||
return try {
|
||||
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)
|
||||
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)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("MessageCrypto", "❌ decryptAttachmentBlob failed: ${e.message}", e)
|
||||
@@ -575,16 +605,66 @@ object MessageCrypto {
|
||||
/**
|
||||
* Генерация PBKDF2 ключа (совместимо с crypto-js / RN)
|
||||
* ВАЖНО: 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 {
|
||||
val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
|
||||
val spec = javax.crypto.spec.PBEKeySpec(
|
||||
password.toCharArray(),
|
||||
salt.toByteArray(Charsets.UTF_8),
|
||||
iterations,
|
||||
256 // 32 bytes
|
||||
)
|
||||
return factory.generateSecret(spec).encoded
|
||||
// Crypto-js: WordArray.create(password) использует UTF-8
|
||||
val passwordBytes = password.toByteArray(Charsets.UTF_8)
|
||||
val saltBytes = salt.toByteArray(Charsets.UTF_8)
|
||||
|
||||
// PBKDF2-HMAC-SHA1 ручная реализация для совместимости с crypto-js
|
||||
val keyLength = 32 // 256 bits
|
||||
val mac = javax.crypto.Mac.getInstance("HmacSHA1")
|
||||
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.
|
||||
* 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 {
|
||||
try {
|
||||
// Шифрование
|
||||
val (encryptedContent, encryptedKey) = MessageCrypto.encryptForSending(
|
||||
val encryptResult = MessageCrypto.encryptForSending(
|
||||
text.trim(),
|
||||
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
|
||||
val attachmentsJson = serializeAttachments(attachments)
|
||||
@@ -306,6 +312,11 @@ class MessageRepository private constructor(private val context: Context) {
|
||||
val dialogKey = getDialogKey(packet.fromPublicKey)
|
||||
|
||||
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(
|
||||
packet.content,
|
||||
|
||||
@@ -192,11 +192,16 @@ fun ImageAttachment(
|
||||
downloadStatus = DownloadStatus.DECRYPTING
|
||||
Log.d(TAG, "🔓 Decrypting image...")
|
||||
|
||||
// Расшифровываем
|
||||
val decrypted = MessageCrypto.decryptAttachmentBlob(
|
||||
// КРИТИЧНО: chachaKey ЗАШИФРОВАН в БД (как в Desktop)
|
||||
// Сначала расшифровываем его: 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,
|
||||
chachaKey,
|
||||
privateKey
|
||||
decryptKeyString
|
||||
)
|
||||
downloadProgress = 0.8f
|
||||
|
||||
@@ -390,10 +395,14 @@ fun FileAttachment(
|
||||
|
||||
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,
|
||||
chachaKey,
|
||||
privateKey
|
||||
decryptKeyString
|
||||
)
|
||||
downloadProgress = 0.9f
|
||||
|
||||
@@ -581,10 +590,14 @@ fun AvatarAttachment(
|
||||
val encryptedContent = TransportManager.downloadFile(attachment.id, downloadTag)
|
||||
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,
|
||||
chachaKey,
|
||||
privateKey
|
||||
decryptKeyString
|
||||
)
|
||||
|
||||
if (decrypted != null) {
|
||||
|
||||
Reference in New Issue
Block a user