mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-04 15:19:48 +00:00
Added support for FCast encrypted connection upgrade and added support for ensuring threads are restarted onResume for FCast and ChromeCAst.
This commit is contained in:
parent
bf6e61ed90
commit
422cceb225
5 changed files with 591 additions and 298 deletions
|
@ -0,0 +1,111 @@
|
||||||
|
package com.futo.platformplayer
|
||||||
|
|
||||||
|
import android.util.Base64
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.futo.platformplayer.casting.FCastCastingDevice
|
||||||
|
import com.futo.platformplayer.casting.Opcode
|
||||||
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class FCastEncryptionTests {
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionSelf() {
|
||||||
|
val keyPair1 = FCastCastingDevice.generateKeyPair()
|
||||||
|
val keyPair2 = FCastCastingDevice.generateKeyPair()
|
||||||
|
Log.i("testDHEncryptionSelf", "privates (1: ${Base64.encodeToString(keyPair1.private.encoded, Base64.NO_WRAP)}, 2: ${Base64.encodeToString(keyPair2.private.encoded, Base64.NO_WRAP)})")
|
||||||
|
|
||||||
|
val keyExchangeMessage1 = FCastCastingDevice.getKeyExchangeMessage(keyPair1)
|
||||||
|
val keyExchangeMessage2 = FCastCastingDevice.getKeyExchangeMessage(keyPair2)
|
||||||
|
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
|
||||||
|
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
|
||||||
|
val aesKey2 = FCastCastingDevice.computeSharedSecret(keyPair2.private, keyExchangeMessage1)
|
||||||
|
|
||||||
|
assertEquals(Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP), Base64.encodeToString(aesKey2.encoded, Base64.NO_WRAP))
|
||||||
|
Log.i("testDHEncryptionSelf", "aesKey ${Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
|
||||||
|
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testAESKeyGeneration() {
|
||||||
|
val cases = listOf(
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
|
||||||
|
//AES
|
||||||
|
"7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
//Public other
|
||||||
|
"MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
|
||||||
|
//Private self
|
||||||
|
"MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
|
||||||
|
//AES
|
||||||
|
"a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (case in cases) {
|
||||||
|
val decodedPrivateKey1 = Base64.decode(case[1], Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, case[0])
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals(case[2], Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDHEncryptionKnown() {
|
||||||
|
val decodedPrivateKey1 = Base64.decode("MIIDJwIBADCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egSCAQQCggEAECNvEczf0y6IoX/IwhrPeWZ5IxrHcpwjcdVAuyZQLLlOq0iqnYMFcSD8QjMF8NKObfZZCDQUJlzGzRsG0oXsWiWtmoRvUZ9tQK0j28hDylpbyP00Bt9NlMgeHXkAy54P7Z2v/BPCd3o23kzjgXzYaSRuCFY7zQo1g1IQG8mfjYjdE4jjRVdVrlh8FS8x4OLPeglc+cp2/kuyxaVEfXAG84z/M8019mRSfdczi4z1iidPX6HgDEEWsN42Ud60mNKy5jsQpQYkRdOLmxR3+iQEtGFjdzbVhVCUr7S5EORU9B1MOl5gyPJpjfU3baOqrg6WXVyTvMDaA05YEnAHQNOOfA==", Base64.NO_WRAP)
|
||||||
|
val keyExchangeMessage2 = FCastKeyExchangeMessage(1, "MIIDJTCCAhgGCSqGSIb3DQEDATCCAgkCggEBAJVHXPXZPllsP80dkCrdAvQn9fPHIQMTu0X7TVuy5f4cvWeM1LvdhMmDa+HzHAd3clrrbC/Di4X0gHb6drzYFGzImm+y9wbdcZiYwgg9yNiW+EBi4snJTRN7BUqNgJatuNUZUjmO7KhSoK8S34Pkdapl1OwMOKlWDVZhGG/5i5/J62Du6LAwN2sja8c746zb10/WHB0kdfowd7jwgEZ4gf9+HKVv7gZteVBq3lHtu1RDpWOSfbxLpSAIZ0YXXIiFkl68ZMYUeQZ3NJaZDLcU7GZzBOJh+u4zs8vfAI4MP6kGUNl9OQnJJ1v0rIb/yz0D5t/IraWTQkLdbTvMoqQGywsCggEAQt67naWz2IzJVuCHh+w/Ogm7pfSLiJp0qvUxdKoPvn48W4/NelO+9WOw6YVgMolgqVF/QBTTMl/Hlivx4Ek3DXbRMUp2E355Lz8NuFnQleSluTICTweezy7wnHl0UrB3DhNQeC7Vfd95SXnc7yPLlvGDBhllxOvJPJxxxWuSWVWnX5TMzxRJrEPVhtC+7kMlGwsihzSdaN4NFEQD8T6AL0FG2ILgV68ZtvYnXGZ2yPoOPKJxOjJX/Rsn0GOfaV40fY0c+ayBmibKmwTLDrm3sDWYjRW7rGUhKlUjnPx+WPrjjXJQq5mR/7yXE0Al/ozgTEOZrZZWm+kaVG9JeGk8egOCAQUAAoIBAGlL9EYsrFz3I83NdlwhM241M+M7PA9P5WXgtdvS+pcalIaqN2IYdfzzCUfye7lchVkT9A2Y9eWQYX0OUhmjf8PPKkRkATLXrqO5HTsxV96aYNxMjz5ipQ6CaErTQaPLr3OPoauIMPVVI9zM+WT0KOGp49YMyx+B5rafT066vOVbF/0z1crq0ZXxyYBUv135rwFkIHxBMj5bhRLXKsZ2G5aLAZg0DsVam104mgN/v75f7Spg/n5hO7qxbNgbvSrvQ7Ag/rMk5T3sk7KoM23Qsjl08IZKs2jjx21MiOtyLqGuCW6GOTNK4yEEDF5gA0K13eXGwL5lPS0ilRw+Lrw7cJU=")
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
|
||||||
|
val privateKey = keyFactory.generatePrivate(privateKeySpec)
|
||||||
|
val aesKey1 = FCastCastingDevice.computeSharedSecret(privateKey, keyExchangeMessage2)
|
||||||
|
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
|
||||||
|
|
||||||
|
val message = FCastPlayMessage("text/html")
|
||||||
|
val serializedBody = Json.encodeToString(message)
|
||||||
|
val encryptedMessage = FCastCastingDevice.encryptMessage(aesKey1, FCastDecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey1, encryptedMessage)
|
||||||
|
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals(serializedBody, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testDecryptMessageKnown() {
|
||||||
|
val encryptedMessage = Json.decodeFromString<FCastEncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
|
||||||
|
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
|
||||||
|
val decryptedMessage = FCastCastingDevice.decryptMessage(aesKey, encryptedMessage)
|
||||||
|
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
|
||||||
|
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
|
||||||
|
}
|
||||||
|
}
|
|
@ -50,6 +50,8 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
private var _transportId: String? = null;
|
private var _transportId: String? = null;
|
||||||
private var _launching = false;
|
private var _launching = false;
|
||||||
private var _mediaSessionId: Int? = null;
|
private var _mediaSessionId: Int? = null;
|
||||||
|
private var _thread: Thread? = null;
|
||||||
|
private var _pingThread: Thread? = null;
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -270,7 +272,6 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val adrs = addresses ?: return;
|
|
||||||
if (_started) {
|
if (_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -283,11 +284,23 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
_launching = true;
|
_launching = true;
|
||||||
|
|
||||||
|
ensureThreadsStarted();
|
||||||
|
Logger.i(TAG, "Started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureThreadsStarted() {
|
||||||
|
val adrs = addresses ?: return;
|
||||||
|
|
||||||
|
val thread = _thread
|
||||||
|
val pingThread = _pingThread
|
||||||
|
if (thread == null || !thread.isAlive || pingThread == null || !pingThread.isAlive) {
|
||||||
|
Log.i(TAG, "Restarting threads because one of the threads has died")
|
||||||
|
|
||||||
_scopeIO?.cancel();
|
_scopeIO?.cancel();
|
||||||
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
||||||
_scopeIO = CoroutineScope(Dispatchers.IO);
|
_scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
||||||
Thread {
|
_thread = Thread {
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
|
@ -407,10 +420,11 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped connection loop.");
|
Logger.i(TAG, "Stopped connection loop.");
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
}.start();
|
_thread = null;
|
||||||
|
}.apply { start() };
|
||||||
|
|
||||||
//Start ping loop
|
//Start ping loop
|
||||||
Thread {
|
_pingThread = Thread {
|
||||||
Logger.i(TAG, "Started ping loop.")
|
Logger.i(TAG, "Started ping loop.")
|
||||||
|
|
||||||
val pingObject = JSONObject();
|
val pingObject = JSONObject();
|
||||||
|
@ -421,14 +435,16 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
sendChannelMessage("sender-0", "receiver-0", "urn:x-cast:com.google.cast.tp.heartbeat", pingObject.toString());
|
||||||
Thread.sleep(5000);
|
Thread.sleep(5000);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
Log.w(TAG, "Failed to send ping.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped ping loop.");
|
Logger.i(TAG, "Stopped ping loop.");
|
||||||
}.start();
|
_pingThread = null;
|
||||||
|
}.apply { start() };
|
||||||
Logger.i(TAG, "Started.");
|
} else {
|
||||||
|
Log.i(TAG, "Threads still alive, not restarted")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
|
private fun sendChannelMessage(sourceId: String, destinationId: String, namespace: String, json: String) {
|
||||||
|
@ -593,6 +609,8 @@ class ChromecastCastingDevice : CastingDevice {
|
||||||
Logger.i(TAG, "Cancelled scopeIO without open socket.")
|
Logger.i(TAG, "Cancelled scopeIO without open socket.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_pingThread = null;
|
||||||
|
_thread = null;
|
||||||
_scopeIO = null;
|
_scopeIO = null;
|
||||||
_socket = null;
|
_socket = null;
|
||||||
_outputStream = null;
|
_outputStream = null;
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
package com.futo.platformplayer.casting
|
package com.futo.platformplayer.casting
|
||||||
|
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.futo.platformplayer.UIDialogs
|
import com.futo.platformplayer.UIDialogs
|
||||||
|
import com.futo.platformplayer.casting.models.FCastDecryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastEncryptedMessage
|
||||||
|
import com.futo.platformplayer.casting.models.FCastKeyExchangeMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
import com.futo.platformplayer.casting.models.FCastPlayMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
import com.futo.platformplayer.casting.models.FCastPlaybackErrorMessage
|
||||||
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
import com.futo.platformplayer.casting.models.FCastPlaybackUpdateMessage
|
||||||
|
@ -26,22 +30,44 @@ import kotlinx.serialization.json.Json
|
||||||
import java.io.DataInputStream
|
import java.io.DataInputStream
|
||||||
import java.io.DataOutputStream
|
import java.io.DataOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.math.BigInteger
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.Socket
|
import java.net.Socket
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.KeyPair
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.security.PrivateKey
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
import javax.crypto.Cipher
|
||||||
|
import javax.crypto.KeyAgreement
|
||||||
|
import javax.crypto.spec.DHParameterSpec
|
||||||
|
import javax.crypto.spec.IvParameterSpec
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
|
|
||||||
enum class Opcode(val value: Byte) {
|
enum class Opcode(val value: Byte) {
|
||||||
NONE(0),
|
None(0),
|
||||||
PLAY(1),
|
Play(1),
|
||||||
PAUSE(2),
|
Pause(2),
|
||||||
RESUME(3),
|
Resume(3),
|
||||||
STOP(4),
|
Stop(4),
|
||||||
SEEK(5),
|
Seek(5),
|
||||||
PLAYBACK_UPDATE(6),
|
PlaybackUpdate(6),
|
||||||
VOLUME_UPDATE(7),
|
VolumeUpdate(7),
|
||||||
SET_VOLUME(8),
|
SetVolume(8),
|
||||||
PLAYBACK_ERROR(9),
|
PlaybackError(9),
|
||||||
SET_SPEED(10),
|
SetSpeed(10),
|
||||||
VERSION(11)
|
Version(11),
|
||||||
|
KeyExchange(12),
|
||||||
|
Encrypted(13),
|
||||||
|
Ping(14),
|
||||||
|
Pong(15),
|
||||||
|
StartEncryption(16);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val _map = entries.associateBy { it.value }
|
||||||
|
fun find(value: Byte): Opcode = _map[value] ?: Opcode.None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FCastCastingDevice : CastingDevice {
|
class FCastCastingDevice : CastingDevice {
|
||||||
|
@ -63,17 +89,26 @@ class FCastCastingDevice : CastingDevice {
|
||||||
private var _scopeIO: CoroutineScope? = null;
|
private var _scopeIO: CoroutineScope? = null;
|
||||||
private var _started: Boolean = false;
|
private var _started: Boolean = false;
|
||||||
private var _version: Long = 1;
|
private var _version: Long = 1;
|
||||||
|
private val _keyPair: KeyPair
|
||||||
|
private var _aesKey: SecretKeySpec? = null
|
||||||
|
private val _queuedEncryptedMessages = arrayListOf<FCastEncryptedMessage>()
|
||||||
|
private var _encryptionStarted = false
|
||||||
|
private var _thread: Thread? = null
|
||||||
|
|
||||||
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
constructor(name: String, addresses: Array<InetAddress>, port: Int) : super() {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.addresses = addresses;
|
this.addresses = addresses;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
|
||||||
|
_keyPair = generateKeyPair()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(deviceInfo: CastingDeviceInfo) : super() {
|
constructor(deviceInfo: CastingDeviceInfo) : super() {
|
||||||
this.name = deviceInfo.name;
|
this.name = deviceInfo.name;
|
||||||
this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray();
|
this.addresses = deviceInfo.addresses.map { a -> a.toInetAddress() }.filterNotNull().toTypedArray();
|
||||||
this.port = deviceInfo.port;
|
this.port = deviceInfo.port;
|
||||||
|
|
||||||
|
_keyPair = generateKeyPair()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAddresses(): List<InetAddress> {
|
override fun getAddresses(): List<InetAddress> {
|
||||||
|
@ -94,7 +129,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
setTime(resumePosition);
|
setTime(resumePosition);
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
send(Opcode.Play, FCastPlayMessage(
|
||||||
container = contentType,
|
container = contentType,
|
||||||
url = contentId,
|
url = contentId,
|
||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
|
@ -118,7 +153,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
setTime(resumePosition);
|
setTime(resumePosition);
|
||||||
setDuration(duration);
|
setDuration(duration);
|
||||||
sendMessage(Opcode.PLAY, FCastPlayMessage(
|
send(Opcode.Play, FCastPlayMessage(
|
||||||
container = contentType,
|
container = contentType,
|
||||||
content = content,
|
content = content,
|
||||||
time = resumePosition,
|
time = resumePosition,
|
||||||
|
@ -134,7 +169,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
sendMessage(Opcode.SET_VOLUME, FCastSetVolumeMessage(volume))
|
send(Opcode.SetVolume, FCastSetVolumeMessage(volume))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun changeSpeed(speed: Double) {
|
override fun changeSpeed(speed: Double) {
|
||||||
|
@ -143,7 +178,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpeed(speed);
|
setSpeed(speed);
|
||||||
sendMessage(Opcode.SET_SPEED, FCastSetSpeedMessage(speed))
|
send(Opcode.SetSpeed, FCastSetSpeedMessage(speed))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekVideo(timeSeconds: Double) {
|
override fun seekVideo(timeSeconds: Double) {
|
||||||
|
@ -151,7 +186,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.SEEK, FCastSeekMessage(
|
send(Opcode.Seek, FCastSeekMessage(
|
||||||
time = timeSeconds
|
time = timeSeconds
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -161,7 +196,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.RESUME);
|
send(Opcode.Resume);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun pauseVideo() {
|
override fun pauseVideo() {
|
||||||
|
@ -169,7 +204,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.PAUSE);
|
send(Opcode.Pause);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun stopVideo() {
|
override fun stopVideo() {
|
||||||
|
@ -177,7 +212,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendMessage(Opcode.STOP);
|
send(Opcode.Stop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
private fun invokeInIOScopeIfRequired(action: () -> Unit): Boolean {
|
||||||
|
@ -201,7 +236,6 @@ class FCastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
val adrs = addresses ?: return;
|
|
||||||
if (_started) {
|
if (_started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -209,11 +243,22 @@ class FCastCastingDevice : CastingDevice {
|
||||||
_started = true;
|
_started = true;
|
||||||
Logger.i(TAG, "Starting...");
|
Logger.i(TAG, "Starting...");
|
||||||
|
|
||||||
|
ensureThreadStarted();
|
||||||
|
Logger.i(TAG, "Started.");
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ensureThreadStarted() {
|
||||||
|
val adrs = addresses ?: return;
|
||||||
|
|
||||||
|
val thread = _thread
|
||||||
|
if (thread == null || !thread.isAlive) {
|
||||||
|
Log.i(TAG, "Restarting thread because the thread has died")
|
||||||
|
|
||||||
_scopeIO?.cancel();
|
_scopeIO?.cancel();
|
||||||
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
Logger.i(TAG, "Cancelled previous scopeIO because a new one is starting.")
|
||||||
_scopeIO = CoroutineScope(Dispatchers.IO);
|
_scopeIO = CoroutineScope(Dispatchers.IO);
|
||||||
|
|
||||||
Thread {
|
_thread = Thread {
|
||||||
connectionState = CastConnectionState.CONNECTING;
|
connectionState = CastConnectionState.CONNECTING;
|
||||||
|
|
||||||
while (_scopeIO?.isActive == true) {
|
while (_scopeIO?.isActive == true) {
|
||||||
|
@ -242,12 +287,8 @@ class FCastCastingDevice : CastingDevice {
|
||||||
_socket = Socket(usedRemoteAddress, port);
|
_socket = Socket(usedRemoteAddress, port);
|
||||||
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
Logger.i(TAG, "Successfully connected to FastCast at $usedRemoteAddress:$port");
|
||||||
|
|
||||||
try {
|
|
||||||
_outputStream = DataOutputStream(_socket?.outputStream);
|
_outputStream = DataOutputStream(_socket?.outputStream);
|
||||||
_inputStream = DataInputStream(_socket?.inputStream);
|
_inputStream = DataInputStream(_socket?.inputStream);
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to authenticate to FastCast.", e);
|
|
||||||
}
|
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
_socket?.close();
|
_socket?.close();
|
||||||
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
Logger.i(TAG, "Failed to connect to FastCast.", e);
|
||||||
|
@ -260,6 +301,9 @@ class FCastCastingDevice : CastingDevice {
|
||||||
localAddress = _socket?.localAddress;
|
localAddress = _socket?.localAddress;
|
||||||
connectionState = CastConnectionState.CONNECTED;
|
connectionState = CastConnectionState.CONNECTED;
|
||||||
|
|
||||||
|
Logger.i(TAG, "Sending KeyExchange.")
|
||||||
|
send(Opcode.KeyExchange, getKeyExchangeMessage(_keyPair))
|
||||||
|
|
||||||
val buffer = ByteArray(4096);
|
val buffer = ByteArray(4096);
|
||||||
|
|
||||||
Logger.i(TAG, "Started receiving.");
|
Logger.i(TAG, "Started receiving.");
|
||||||
|
@ -292,7 +336,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
handleMessage(Opcode.entries.first { it.value == opcode }, json);
|
handleMessage(Opcode.find(opcode), json);
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.w(TAG, "Failed to handle message.", e);
|
Logger.w(TAG, "Failed to handle message.", e);
|
||||||
}
|
}
|
||||||
|
@ -318,14 +362,18 @@ class FCastCastingDevice : CastingDevice {
|
||||||
|
|
||||||
Logger.i(TAG, "Stopped connection loop.");
|
Logger.i(TAG, "Stopped connection loop.");
|
||||||
connectionState = CastConnectionState.DISCONNECTED;
|
connectionState = CastConnectionState.DISCONNECTED;
|
||||||
}.start();
|
_thread = null;
|
||||||
|
}.apply { start() };
|
||||||
Logger.i(TAG, "Started.");
|
} else {
|
||||||
|
Log.i(TAG, "Thread was still alive, not restarted")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMessage(opcode: Opcode, json: String? = null) {
|
private fun handleMessage(opcode: Opcode, json: String? = null) {
|
||||||
|
Log.i(TAG, "Processing packet (opcode: $opcode, size: ${json?.length ?: 0})")
|
||||||
|
|
||||||
when (opcode) {
|
when (opcode) {
|
||||||
Opcode.PLAYBACK_UPDATE -> {
|
Opcode.PlaybackUpdate -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got playback update without JSON, ignoring.");
|
Logger.w(TAG, "Got playback update without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
|
@ -339,7 +387,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Opcode.VOLUME_UPDATE -> {
|
Opcode.VolumeUpdate -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got volume update without JSON, ignoring.");
|
Logger.w(TAG, "Got volume update without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
|
@ -348,7 +396,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
val volumeUpdate = FCastCastingDevice.json.decodeFromString<FCastVolumeUpdateMessage>(json);
|
||||||
setVolume(volumeUpdate.volume, volumeUpdate.generationTime);
|
setVolume(volumeUpdate.volume, volumeUpdate.generationTime);
|
||||||
}
|
}
|
||||||
Opcode.PLAYBACK_ERROR -> {
|
Opcode.PlaybackError -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got playback error without JSON, ignoring.");
|
Logger.w(TAG, "Got playback error without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
|
@ -357,7 +405,7 @@ class FCastCastingDevice : CastingDevice {
|
||||||
val playbackError = FCastCastingDevice.json.decodeFromString<FCastPlaybackErrorMessage>(json);
|
val playbackError = FCastCastingDevice.json.decodeFromString<FCastPlaybackErrorMessage>(json);
|
||||||
Logger.e(TAG, "Remote casting playback error received: $playbackError")
|
Logger.e(TAG, "Remote casting playback error received: $playbackError")
|
||||||
}
|
}
|
||||||
Opcode.VERSION -> {
|
Opcode.Version -> {
|
||||||
if (json == null) {
|
if (json == null) {
|
||||||
Logger.w(TAG, "Got version without JSON, ignoring.");
|
Logger.w(TAG, "Got version without JSON, ignoring.");
|
||||||
return;
|
return;
|
||||||
|
@ -367,72 +415,100 @@ class FCastCastingDevice : CastingDevice {
|
||||||
_version = version.version;
|
_version = version.version;
|
||||||
Logger.i(TAG, "Remote version received: $version")
|
Logger.i(TAG, "Remote version received: $version")
|
||||||
}
|
}
|
||||||
|
Opcode.KeyExchange -> {
|
||||||
|
if (json == null) {
|
||||||
|
Logger.w(TAG, "Got KeyExchange without JSON, ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyExchangeMessage: FCastKeyExchangeMessage = FCastCastingDevice.json.decodeFromString(json)
|
||||||
|
Logger.i(TAG, "Received public key: ${keyExchangeMessage.publicKey}")
|
||||||
|
_aesKey = computeSharedSecret(_keyPair.private, keyExchangeMessage)
|
||||||
|
|
||||||
|
synchronized(_queuedEncryptedMessages) {
|
||||||
|
for (queuedEncryptedMessages in _queuedEncryptedMessages) {
|
||||||
|
val decryptedMessage = decryptMessage(_aesKey!!, queuedEncryptedMessages)
|
||||||
|
val o = Opcode.find(decryptedMessage.opcode.toByte())
|
||||||
|
handleMessage(o, decryptedMessage.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedEncryptedMessages.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Opcode.Ping -> send(Opcode.Pong)
|
||||||
|
Opcode.Encrypted -> {
|
||||||
|
if (json == null) {
|
||||||
|
Logger.w(TAG, "Got Encrypted without JSON, ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
val encryptedMessage: FCastEncryptedMessage = FCastCastingDevice.json.decodeFromString(json)
|
||||||
|
if (_aesKey != null) {
|
||||||
|
val decryptedMessage = decryptMessage(_aesKey!!, encryptedMessage)
|
||||||
|
val o = Opcode.find(decryptedMessage.opcode.toByte())
|
||||||
|
handleMessage(o, decryptedMessage.message)
|
||||||
|
} else {
|
||||||
|
synchronized(_queuedEncryptedMessages) {
|
||||||
|
if (_queuedEncryptedMessages.size == 15) {
|
||||||
|
_queuedEncryptedMessages.removeAt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
_queuedEncryptedMessages.add(encryptedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Opcode.StartEncryption -> {
|
||||||
|
_encryptionStarted = true
|
||||||
|
//TODO: Send decrypted messages waiting for encryption to be established
|
||||||
|
}
|
||||||
else -> { }
|
else -> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendMessage(opcode: Opcode) {
|
private fun send(opcode: Opcode, message: String? = null) {
|
||||||
|
val aesKey = _aesKey
|
||||||
|
if (_encryptionStarted && aesKey != null && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) {
|
||||||
|
send(Opcode.Encrypted, encryptMessage(aesKey, FCastDecryptedMessage(opcode.value.toLong(), message)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val size = 1;
|
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
|
||||||
val outputStream = _outputStream;
|
val size = 1 + data.size
|
||||||
|
val outputStream = _outputStream
|
||||||
if (outputStream == null) {
|
if (outputStream == null) {
|
||||||
Logger.w(TAG, "Failed to send $size bytes, output stream is null.");
|
Log.w(TAG, "Failed to send $size bytes, output stream is null.")
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val serializedSizeLE = ByteArray(4);
|
val serializedSizeLE = ByteArray(4)
|
||||||
serializedSizeLE[0] = (size and 0xff).toByte();
|
serializedSizeLE[0] = (size and 0xff).toByte()
|
||||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
|
serializedSizeLE[1] = (size shr 8 and 0xff).toByte()
|
||||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
|
serializedSizeLE[2] = (size shr 16 and 0xff).toByte()
|
||||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
|
serializedSizeLE[3] = (size shr 24 and 0xff).toByte()
|
||||||
outputStream.write(serializedSizeLE);
|
outputStream.write(serializedSizeLE)
|
||||||
|
|
||||||
val opcodeBytes = ByteArray(1);
|
val opcodeBytes = ByteArray(1)
|
||||||
opcodeBytes[0] = opcode.value;
|
opcodeBytes[0] = opcode.value
|
||||||
outputStream.write(opcodeBytes);
|
outputStream.write(opcodeBytes)
|
||||||
|
|
||||||
Log.d(TAG, "Sent $size bytes.");
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
Logger.i(TAG, "Failed to send message.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inline fun <reified T> sendMessage(opcode: Opcode, message: T) {
|
|
||||||
try {
|
|
||||||
val data: ByteArray;
|
|
||||||
var jsonString: String? = null;
|
|
||||||
if (message != null) {
|
|
||||||
jsonString = json.encodeToString(message);
|
|
||||||
data = jsonString.encodeToByteArray();
|
|
||||||
} else {
|
|
||||||
data = ByteArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
val size = 1 + data.size;
|
|
||||||
val outputStream = _outputStream;
|
|
||||||
if (outputStream == null) {
|
|
||||||
Logger.w(TAG, "Failed to send $size bytes, output stream is null.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
val serializedSizeLE = ByteArray(4);
|
|
||||||
serializedSizeLE[0] = (size and 0xff).toByte();
|
|
||||||
serializedSizeLE[1] = (size shr 8 and 0xff).toByte();
|
|
||||||
serializedSizeLE[2] = (size shr 16 and 0xff).toByte();
|
|
||||||
serializedSizeLE[3] = (size shr 24 and 0xff).toByte();
|
|
||||||
outputStream.write(serializedSizeLE);
|
|
||||||
|
|
||||||
val opcodeBytes = ByteArray(1);
|
|
||||||
opcodeBytes[0] = opcode.value;
|
|
||||||
outputStream.write(opcodeBytes);
|
|
||||||
|
|
||||||
if (data.isNotEmpty()) {
|
if (data.isNotEmpty()) {
|
||||||
outputStream.write(data);
|
outputStream.write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Sent $size bytes: '$jsonString'.");
|
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Logger.i(TAG, "Failed to send message.", e);
|
Log.i(TAG, "Failed to send message.", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T> send(opcode: Opcode, message: T) {
|
||||||
|
try {
|
||||||
|
send(opcode, message?.let { Json.encodeToString(it) })
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.i(TAG, "Failed to encode message to string.", e)
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,6 +517,8 @@ class FCastCastingDevice : CastingDevice {
|
||||||
usedRemoteAddress = null;
|
usedRemoteAddress = null;
|
||||||
localAddress = null;
|
localAddress = null;
|
||||||
_started = false;
|
_started = false;
|
||||||
|
//TODO: Kill and/or join thread?
|
||||||
|
_thread = null;
|
||||||
|
|
||||||
val socket = _socket;
|
val socket = _socket;
|
||||||
val scopeIO = _scopeIO;
|
val scopeIO = _scopeIO;
|
||||||
|
@ -471,7 +549,65 @@ class FCastCastingDevice : CastingDevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG = "FastCastCastingDevice";
|
val TAG = "FCastCastingDevice";
|
||||||
private val json = Json { ignoreUnknownKeys = true }
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
fun getKeyExchangeMessage(keyPair: KeyPair): FCastKeyExchangeMessage {
|
||||||
|
return FCastKeyExchangeMessage(1, Base64.encodeToString(keyPair.public.encoded, Base64.NO_WRAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateKeyPair(): KeyPair {
|
||||||
|
//modp14
|
||||||
|
val p = BigInteger("ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff", 16)
|
||||||
|
val g = BigInteger("2", 16)
|
||||||
|
val dhSpec = DHParameterSpec(p, g)
|
||||||
|
|
||||||
|
val keyGen = KeyPairGenerator.getInstance("DH")
|
||||||
|
keyGen.initialize(dhSpec)
|
||||||
|
|
||||||
|
return keyGen.generateKeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun computeSharedSecret(privateKey: PrivateKey, keyExchangeMessage: FCastKeyExchangeMessage): SecretKeySpec {
|
||||||
|
val keyFactory = KeyFactory.getInstance("DH")
|
||||||
|
val receivedPublicKeyBytes = Base64.decode(keyExchangeMessage.publicKey, Base64.NO_WRAP)
|
||||||
|
val receivedPublicKeySpec = X509EncodedKeySpec(receivedPublicKeyBytes)
|
||||||
|
val receivedPublicKey = keyFactory.generatePublic(receivedPublicKeySpec)
|
||||||
|
|
||||||
|
val keyAgreement = KeyAgreement.getInstance("DH")
|
||||||
|
keyAgreement.init(privateKey)
|
||||||
|
keyAgreement.doPhase(receivedPublicKey, true)
|
||||||
|
|
||||||
|
val sharedSecret = keyAgreement.generateSecret()
|
||||||
|
Log.i(TAG, "sharedSecret ${Base64.encodeToString(sharedSecret, Base64.NO_WRAP)}")
|
||||||
|
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||||
|
val hashedSecret = sha256.digest(sharedSecret)
|
||||||
|
Log.i(TAG, "hashedSecret ${Base64.encodeToString(hashedSecret, Base64.NO_WRAP)}")
|
||||||
|
|
||||||
|
return SecretKeySpec(hashedSecret, "AES")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptMessage(aesKey: SecretKeySpec, decryptedMessage: FCastDecryptedMessage): FCastEncryptedMessage {
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, aesKey)
|
||||||
|
val iv = cipher.iv
|
||||||
|
val json = Json.encodeToString(decryptedMessage)
|
||||||
|
val encrypted = cipher.doFinal(json.toByteArray(Charsets.UTF_8))
|
||||||
|
return FCastEncryptedMessage(
|
||||||
|
version = 1,
|
||||||
|
iv = Base64.encodeToString(iv, Base64.NO_WRAP),
|
||||||
|
blob = Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptMessage(aesKey: SecretKeySpec, encryptedMessage: FCastEncryptedMessage): FCastDecryptedMessage {
|
||||||
|
val iv = Base64.decode(encryptedMessage.iv, Base64.NO_WRAP)
|
||||||
|
val encrypted = Base64.decode(encryptedMessage.blob, Base64.NO_WRAP)
|
||||||
|
|
||||||
|
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, aesKey, IvParameterSpec(iv))
|
||||||
|
val decryptedJson = cipher.doFinal(encrypted)
|
||||||
|
return Json.decodeFromString(String(decryptedJson, Charsets.UTF_8))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -205,6 +205,14 @@ class StateCasting {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onResume() {
|
fun onResume() {
|
||||||
|
val ad = activeDevice
|
||||||
|
if (ad != null) {
|
||||||
|
if (ad is FCastCastingDevice) {
|
||||||
|
ad.ensureThreadStarted()
|
||||||
|
} else if (ad is ChromecastCastingDevice) {
|
||||||
|
ad.ensureThreadsStarted()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
val resumeCastingDevice = _resumeCastingDevice
|
val resumeCastingDevice = _resumeCastingDevice
|
||||||
if (resumeCastingDevice != null) {
|
if (resumeCastingDevice != null) {
|
||||||
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
|
connectDevice(deviceFromCastingDeviceInfo(resumeCastingDevice))
|
||||||
|
@ -212,6 +220,7 @@ class StateCasting {
|
||||||
Log.i(TAG, "_resumeCastingDevice set to null onResume")
|
Log.i(TAG, "_resumeCastingDevice set to null onResume")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
|
|
|
@ -51,3 +51,22 @@ data class FCastPlaybackErrorMessage(
|
||||||
data class FCastVersionMessage(
|
data class FCastVersionMessage(
|
||||||
val version: Long
|
val version: Long
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastKeyExchangeMessage(
|
||||||
|
val version: Long,
|
||||||
|
val publicKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastDecryptedMessage(
|
||||||
|
val opcode: Long,
|
||||||
|
val message: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class FCastEncryptedMessage(
|
||||||
|
val version: Long,
|
||||||
|
val iv: String?,
|
||||||
|
val blob: String
|
||||||
|
)
|
Loading…
Add table
Add a link
Reference in a new issue