diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..7643783 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,123 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ae388c2..7b3006b 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,16 +4,16 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 4515aa3..4251b72 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..931b96c --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/invincible/jedishare/data/chat/BluetoothDataTransferService.kt b/app/src/main/java/com/invincible/jedishare/data/chat/BluetoothDataTransferService.kt index 94dd59e..d070e40 100644 --- a/app/src/main/java/com/invincible/jedishare/data/chat/BluetoothDataTransferService.kt +++ b/app/src/main/java/com/invincible/jedishare/data/chat/BluetoothDataTransferService.kt @@ -6,6 +6,8 @@ import com.invincible.jedishare.domain.chat.BluetoothMessage import com.invincible.jedishare.domain.chat.TransferFailedException import com.invincible.jedishare.presentation.BluetoothViewModel import com.invincible.jedishare.presentation.components.CustomProgressIndicator +import com.invincible.jedishare.utils.CryptoUtils +import com.invincible.jedishare.utils.KeyUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -14,17 +16,43 @@ import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import java.io.IOException import java.nio.ByteBuffer +import javax.crypto.SecretKey +import java.security.KeyPair +import java.security.PublicKey const val BUFFER_SIZE = 990 // Adjust the buffer size as needed const val FILE_DELIMITER = "----FILE_DELIMITER----" val repeatedString = FILE_DELIMITER.repeat(40) class BluetoothDataTransferService( - private val socket: BluetoothSocket + private val socket: BluetoothSocket, + private var aesKey: SecretKey? = null ) { private val incomingDataStream = ByteArrayOutputStream() + // Start the Diffie-Hellman Key Exchange + fun startKeyExchange(): ByteArray { + val keyPair: KeyPair = KeyUtils.generateDHKeyPair() + val publicKeyBytes = KeyUtils.serializePublicKey(keyPair.public) + // Send the public key bytes to the other device + // Store the private key for computing the shared secret later + dhPrivateKey = keyPair.private + return publicKeyBytes + } + + // Complete the Key Exchange and generate the AES key + fun completeKeyExchange(otherPublicKeyBytes: ByteArray) { + if (dhPrivateKey == null) { + Log.e("BluetoothDataTransfer", "Private key is null. Key exchange failed!") + throw IllegalStateException("Key exchange must be completed before data transfer.") + } + + val otherPublicKey: PublicKey = KeyUtils.deserializePublicKey(otherPublicKeyBytes) + val sharedSecret: ByteArray = KeyUtils.generateSharedSecret(dhPrivateKey!!, otherPublicKey) + aesKey = KeyUtils.generateAESKeyFromSharedSecret(sharedSecret) + } + fun listenForIncomingMessages(viewModel: BluetoothViewModel): Flow { return flow { if (!socket.isConnected) { @@ -37,24 +65,36 @@ class BluetoothDataTransferService( } catch (e: IOException) { throw TransferFailedException() } - var bufferRed = buffer.copyOfRange(0, byteCount) - Log.e("HELLOME", "Received: " + bufferRed.size.toString()) + val encryptedChunk = buffer.copyOfRange(0, byteCount) -// processIncomingData(bufferRed) -// checkForFiles(viewModel)?.let { fileBytes -> -// emit(fileBytes) -// } - Log.e("MYTAG", "Bytes Read : " + bufferRed.size) + // Read the hash digest (for integrity check) + val hashBuffer = ByteArray(32) + socket.inputStream.read(hashBuffer) + val receivedHash = hashBuffer.copyOfRange(0, hashBuffer.size) - if(bufferRed.size == 880){ - viewModel?.isFirst = true - }else{ - emit(bufferRed) + // Verify the hash to ensure data integrity + val computedHash = CryptoUtils.generateSHA256Hash(encryptedChunk) + if (!computedHash.contentEquals(receivedHash)) { + throw TransferFailedException() } -// emit( -// bufferRed -// ) + // Ensure AES key is ready before decrypting + if (aesKey == null) { + throw TransferFailedException() + } + + // Decrypt the chunk using AES + val decryptedData = CryptoUtils.decrypt(encryptedChunk, aesKey!!) + + Log.e("BluetoothDataTransfer", "Received and decrypted: ${decryptedData.size} bytes") + + processIncomingData(decryptedData) + + if (decryptedData.size == 880) { + viewModel?.isFirst = true + } else { + emit(decryptedData) + } } }.flowOn(Dispatchers.IO) } @@ -63,58 +103,70 @@ class BluetoothDataTransferService( incomingDataStream.write(data) } + private fun checkForFiles(viewModel: BluetoothViewModel?): ByteArray? { val data = incomingDataStream.toByteArray() - val delimiterIndex = findIndexOfSubArray(incomingDataStream.toByteArray(), repeatedString.toByteArray()) - - -// Log.e("MYTAG","delimiter size" + FILE_DELIMITER.toByteArray().size.toString()) -// Log.e("MYTAG","byte array size" + data.size) - -// val delimiterIndex = data.indexOf(FILE_DELIMITER.toByteArray()) - if (delimiterIndex == 0) { -// val fileBytes = data.copyOfRange(0, delimiterIndex) -// incomingDataStream.reset() -// Log.e("MYTAG","END OF FILE" + delimiterIndex) -// Log.e("MYTAG","main array size" + incomingDataStream.toByteArray().size.toString()) -// Log.e("MYTAG","delimiter size" + repeatedString.toByteArray().size.toString()) + val delimiterIndex = findIndexOfSubArray(data, repeatedString.toByteArray()) + + if (delimiterIndex >= 0) { + // File boundary found, extract the file data + val fileBytes = data.copyOfRange(0, delimiterIndex) + incomingDataStream.reset() // Reset the stream after processing the file + // Write the remaining bytes back into the stream (after the delimiter) + if (delimiterIndex + repeatedString.toByteArray().size < data.size) { + incomingDataStream.write( + data, + delimiterIndex + repeatedString.toByteArray().size, + data.size - (delimiterIndex + repeatedString.toByteArray().size) + ) + } viewModel?.isFirst = true -// incomingDataStream.write(data, delimiterIndex + FILE_DELIMITER.length, data.size - (delimiterIndex + FILE_DELIMITER.length)) -// return fileBytes - incomingDataStream.reset() - return null -// return incomingDataStream.toByteArray() + return fileBytes } -// else if(viewModel?.isFirst == true && data.size == 990){ -// incomingDataStream.reset() -// return null -// } + incomingDataStream.reset() - return data + return null } fun findIndexOfSubArray(mainArray: ByteArray, subArray: ByteArray): Int { - if(mainArray.size == repeatedString.toByteArray().size) - return 0 -// for (i in 0 until mainArray.size - subArray.size + 1) { -// if (mainArray.copyOfRange(i, i + subArray.size).contentEquals(subArray)) { -// return i -// } -// } - return -1 // Return -1 if subArray is not found in mainArray + // Properly find the delimiter in the incoming data + for (i in 0 until mainArray.size - subArray.size + 1) { + if (mainArray.copyOfRange(i, i + subArray.size).contentEquals(subArray)) { + return i + } + } + return -1 // Return -1 if subArray is not found } suspend fun sendMessage(bytes: ByteArray): Boolean { return withContext(Dispatchers.IO) { - try { - socket.outputStream.write(bytes) - Log.e("HELLOME", "Sent: " + bytes.size.toString()) + try { + // Ensure AES key is ready before encrypting + if (aesKey == null) { + throw IllegalStateException("AES key not initialized!") + } + + // Encrypt the data chunk using AES + val encryptedData = CryptoUtils.encrypt(bytes, aesKey!!) + + // Generate SHA-256 hash for integrity check + val hashDigest = CryptoUtils.generateSHA256Hash(encryptedData) + + // Send the encrypted chunk + socket.outputStream.write(encryptedData) - } catch (e: IOException) { + // Send the hash digest for integrity verification + socket.outputStream.write(hashDigest) + + Log.e("HELLOME", "Sent encrypted data: ${encryptedData.size} bytes") + Log.e("HELLOME", "Sent hash digest: ${hashDigest.size} bytes") + + } catch (e: IOException) { return@withContext false - } + } - true + true } } -} + private var dhPrivateKey: java.security.PrivateKey? = null +} \ No newline at end of file diff --git a/app/src/main/java/com/invincible/jedishare/utils/CryptoUtils.kt b/app/src/main/java/com/invincible/jedishare/utils/CryptoUtils.kt new file mode 100644 index 0000000..674f870 --- /dev/null +++ b/app/src/main/java/com/invincible/jedishare/utils/CryptoUtils.kt @@ -0,0 +1,36 @@ +package com.invincible.jedishare.utils + +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec +import java.security.MessageDigest + +object CryptoUtils { + private const val AES_TRANSFORMATION = "AES/ECB/PKCS5Padding" + private const val AES_ALGORITHM = "AES" + + // Encrypt data using AES + fun encrypt(data: ByteArray, key: SecretKey): ByteArray { + val cipher = Cipher.getInstance(AES_TRANSFORMATION) + cipher.init(Cipher.ENCRYPT_MODE, key) + return cipher.doFinal(data) + } + + // Decrypt data using AES + fun decrypt(data: ByteArray, key: SecretKey): ByteArray { + val cipher = Cipher.getInstance(AES_TRANSFORMATION) + cipher.init(Cipher.DECRYPT_MODE, key) + return cipher.doFinal(data) + } + + // Generate SHA-256 hash for integrity check + fun generateSHA256Hash(data: ByteArray): ByteArray { + val digest = MessageDigest.getInstance("SHA-256") + return digest.digest(data) + } + + // Generate a SecretKey from a byte array (used for generating AES key) + fun generateSecretKey(keyBytes: ByteArray): SecretKey { + return SecretKeySpec(keyBytes, AES_ALGORITHM) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/invincible/jedishare/utils/KeyUtils.kt b/app/src/main/java/com/invincible/jedishare/utils/KeyUtils.kt new file mode 100644 index 0000000..80125b9 --- /dev/null +++ b/app/src/main/java/com/invincible/jedishare/utils/KeyUtils.kt @@ -0,0 +1,44 @@ +package com.invincible.jedishare.utils + +import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.PublicKey +import java.security.spec.X509EncodedKeySpec +import javax.crypto.KeyAgreement +import javax.crypto.SecretKey +import javax.crypto.spec.SecretKeySpec + +object KeyUtils { + // Generate Diffie-Hellman Key Pair + fun generateDHKeyPair(): KeyPair { + val keyPairGenerator = KeyPairGenerator.getInstance("DH") + keyPairGenerator.initialize(2048) // Strong 2048-bit key + return keyPairGenerator.generateKeyPair() + } + + // Serialize public key to send over the network + fun serializePublicKey(publicKey: PublicKey): ByteArray { + return publicKey.encoded + } + + // Deserialize public key received from the other device + fun deserializePublicKey(publicKeyBytes: ByteArray): PublicKey { + val keyFactory = java.security.KeyFactory.getInstance("DH") + val spec = X509EncodedKeySpec(publicKeyBytes) + return keyFactory.generatePublic(spec) + } + + // Generate shared secret using Diffie-Hellman + fun generateSharedSecret(ownPrivateKey: java.security.PrivateKey, otherPublicKey: PublicKey): ByteArray { + val keyAgreement = KeyAgreement.getInstance("DH") + keyAgreement.init(ownPrivateKey) + keyAgreement.doPhase(otherPublicKey, true) + return keyAgreement.generateSecret() + } + + // Generate AES key from shared secret + fun generateAESKeyFromSharedSecret(sharedSecret: ByteArray): SecretKey { + // Use the first 16 bytes of the shared secret for AES-128 + return SecretKeySpec(sharedSecret.copyOf(16), "AES") + } +} \ No newline at end of file