1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Finished implementation of old crypto system as much until course change.

This commit is contained in:
Koen 2023-12-21 14:18:47 +01:00
parent 137a6f3178
commit b8bd78d90d
20 changed files with 4143 additions and 118 deletions

View file

@ -4,15 +4,15 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View file

@ -1,6 +1,6 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="19" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" default="true" project-jdk-name="19" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View file

@ -0,0 +1,113 @@
package com.futo.fcast.receiver
import android.util.Base64
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
import java.security.KeyFactory
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import javax.crypto.interfaces.DHPrivateKey
import javax.crypto.spec.SecretKeySpec
@RunWith(AndroidJUnit4::class)
class EncryptionTest {
@Test
fun testDHEncryptionSelf() {
val keyPair1 = FCastSession.generateKeyPair()
val keyPair2 = FCastSession.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 = FCastSession.getKeyExchangeMessage(keyPair1)
val keyExchangeMessage2 = FCastSession.getKeyExchangeMessage(keyPair2)
Log.i("testDHEncryptionSelf", "publics (1: ${keyExchangeMessage1.publicKey}, 2: ${keyExchangeMessage2.publicKey})")
val aesKey1 = FCastSession.computeSharedSecret(keyPair1.private, keyExchangeMessage2)
val aesKey2 = FCastSession.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 = PlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastSession.encryptMessage(aesKey1, DecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
Log.i("testDHEncryptionSelf", Json.encodeToString(encryptedMessage))
val decryptedMessage = FCastSession.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 = KeyExchangeMessage(1, case[0])
val keyFactory = KeyFactory.getInstance("DH")
val privateKeySpec = PKCS8EncodedKeySpec(decodedPrivateKey1)
val privateKey = keyFactory.generatePrivate(privateKeySpec)
val aesKey1 = FCastSession.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 = KeyExchangeMessage(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 = FCastSession.computeSharedSecret(privateKey, keyExchangeMessage2)
assertEquals("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=", Base64.encodeToString(aesKey1.encoded, Base64.NO_WRAP))
val message = PlayMessage("text/html")
val serializedBody = Json.encodeToString(message)
val encryptedMessage = FCastSession.encryptMessage(aesKey1, DecryptedMessage(Opcode.Play.value.toLong(), serializedBody))
val decryptedMessage = FCastSession.decryptMessage(aesKey1, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals(serializedBody, decryptedMessage.message)
}
@Test
fun testDecryptMessageKnown() {
val encryptedMessage = Json.decodeFromString<EncryptedMessage>("{\"version\":1,\"iv\":\"C4H70VC5FWrNtkty9/cLIA==\",\"blob\":\"K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA==\"}")
val aesKey = SecretKeySpec(Base64.decode("+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=", Base64.NO_WRAP), "AES")
val decryptedMessage = FCastSession.decryptMessage(aesKey, encryptedMessage)
assertEquals(Opcode.Play.value.toLong(), decryptedMessage.opcode)
assertEquals("{\"container\":\"text/html\"}", decryptedMessage.message)
}
}

View file

@ -1,24 +0,0 @@
package com.futo.fcast.receiver
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.futo.fcast.receiver", appContext.packageName)
}
}

View file

@ -1,14 +1,28 @@
package com.futo.fcast.receiver
import android.util.Base64
import android.util.Log
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.DataOutputStream
import java.io.OutputStream
import java.math.BigInteger
import java.net.SocketAddress
import java.nio.ByteBuffer
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 java.util.UUID
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 SessionState {
Idle,
@ -29,7 +43,17 @@ enum class Opcode(val value: Byte) {
SetVolume(8),
PlaybackError(9),
SetSpeed(10),
Version(11)
Version(11),
KeyExchange(12),
Encrypted(13),
Ping(14),
Pong(15),
StartEncryption(16);
companion object {
private val _map = values().associateBy { it.value }
fun find(value: Byte): Opcode = _map[value] ?: Opcode.None
}
}
const val LENGTH_BYTES = 4
@ -41,8 +65,16 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
private var _packetLength = 0
private var _state = SessionState.WaitingForLength
private var _outputStream: DataOutputStream? = DataOutputStream(outputStream)
private val _keyPair: KeyPair = generateKeyPair()
private var _aesKey: SecretKeySpec? = null
private val _queuedEncryptedMessages = arrayListOf<EncryptedMessage>()
private var _encryptionStarted = false
val id = UUID.randomUUID()
init {
send(Opcode.KeyExchange, getKeyExchangeMessage(_keyPair))
}
fun sendVersion(value: VersionMessage) {
send(Opcode.Version, value)
}
@ -59,17 +91,15 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
send(Opcode.VolumeUpdate, value)
}
private inline fun <reified T> send(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)
}
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, DecryptedMessage(opcode.value.toLong(), message)))
return
}
try {
val data: ByteArray = message?.encodeToByteArray() ?: ByteArray(0)
val size = 1 + data.size
val outputStream = _outputStream
if (outputStream == null) {
@ -92,13 +122,22 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
outputStream.write(data)
}
Log.d(TAG, "Sent $size bytes: '$jsonString'.")
Log.d(TAG, "Sent $size bytes: (opcode: $opcode, body: $message).")
} catch (e: Throwable) {
Log.i(TAG, "Failed to send message ${id}.", 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 ${id}.", e)
throw e
}
}
fun processBytes(data: ByteBuffer) {
Log.i(TAG, "${data.remaining()} bytes received from ${_remoteSocketAddress}")
if (!data.hasArray()) {
@ -171,7 +210,7 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
if (_bytesRead >= _packetLength) {
Log.i(TAG, "Packet finished receiving from ${_remoteSocketAddress} of $_packetLength bytes.")
handlePacket()
handleNextPacket()
_state = SessionState.WaitingForLength
_packetLength = 0
@ -184,14 +223,19 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
}
}
private fun handlePacket() {
private fun handleNextPacket() {
Log.i(TAG, "Processing packet of $_bytesRead bytes from ${_remoteSocketAddress}")
val opcode = Opcode.values().firstOrNull { it.value == _buffer[0] } ?: Opcode.None
val opcode = Opcode.find(_buffer[0])
val body = if (_packetLength > 1) _buffer.copyOfRange(1, _packetLength)
.toString(Charsets.UTF_8) else null
Log.i(TAG, "Received packet (opcode: ${opcode}, body: '${body}')")
handlePacket(opcode, body)
}
private fun handlePacket(opcode: Opcode, body: String?) {
Log.i(TAG, "Processing packet (opcode: $opcode, size: ${body?.length ?: 0}, from ${_remoteSocketAddress})")
try {
when (opcode) {
@ -202,15 +246,109 @@ class FCastSession(outputStream: OutputStream, private val _remoteSocketAddress:
Opcode.Seek -> _service.onCastSeek(json.decodeFromString(body!!))
Opcode.SetVolume -> _service.onSetVolume(json.decodeFromString(body!!))
Opcode.SetSpeed -> _service.onSetSpeed(json.decodeFromString(body!!))
Opcode.KeyExchange -> {
val keyExchangeMessage: KeyExchangeMessage = json.decodeFromString(body!!)
_aesKey = computeSharedSecret(_keyPair.private, keyExchangeMessage)
send(Opcode.StartEncryption)
synchronized(_queuedEncryptedMessages) {
for (queuedEncryptedMessages in _queuedEncryptedMessages) {
val decryptedMessage = decryptMessage(_aesKey!!, queuedEncryptedMessages)
val o = Opcode.find(decryptedMessage.opcode.toByte())
handlePacket(o, decryptedMessage.message)
}
_queuedEncryptedMessages.clear()
}
}
Opcode.Ping -> send(Opcode.Pong)
Opcode.Encrypted -> {
val encryptedMessage: EncryptedMessage = json.decodeFromString(body!!)
if (_aesKey != null) {
val decryptedMessage = decryptMessage(_aesKey!!, encryptedMessage)
val o = Opcode.find(decryptedMessage.opcode.toByte())
handlePacket(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 -> { }
}
} catch (e: Throwable) {
Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')")
Log.e(TAG, "Failed to handle packet (opcode: ${opcode}, body: '${body}')", e)
}
}
companion object {
const val TAG = "FCastSession"
private val json = Json { ignoreUnknownKeys = true }
fun getKeyExchangeMessage(keyPair: KeyPair): KeyExchangeMessage {
return KeyExchangeMessage(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: KeyExchangeMessage): 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: DecryptedMessage): EncryptedMessage {
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 EncryptedMessage(
version = 1,
iv = Base64.encodeToString(iv, Base64.NO_WRAP),
blob = Base64.encodeToString(encrypted, Base64.NO_WRAP)
)
}
fun decryptMessage(aesKey: SecretKeySpec, encryptedMessage: EncryptedMessage): DecryptedMessage {
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))
}
}
}

View file

@ -49,4 +49,23 @@ data class SetVolumeMessage(
@Serializable
data class VersionMessage(
val version: Long
)
@Serializable
data class KeyExchangeMessage(
val version: Long,
val publicKey: String
)
@Serializable
data class DecryptedMessage(
val opcode: Long,
val message: String?
)
@Serializable
data class EncryptedMessage(
val version: Long,
val iv: String?,
val blob: String
)

View file

@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['<rootDir>/test/**/*.test.ts'],
modulePathIgnorePatterns: ["<rootDir>/packaging/fcast/fcast-receiver-linux-x64/resources/app/package.json"],
};

File diff suppressed because it is too large Load diff

View file

@ -7,13 +7,17 @@
"license": "MIT",
"scripts": {
"build": "rm -rf dist/ && webpack --config ./webpack.config.js && cp -r src/player dist/player && cp -r src/main dist/main && cp app.ico dist/ && cp app.png dist/",
"start": "npm run build && electron ."
"start": "npm run build && electron .",
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/workerpool": "^6.1.1",
"@types/ws": "^8.5.10",
"electron": "^22.2.0",
"jest": "^29.7.0",
"mdns-js": "github:mdns-js/node-mdns-js",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"webpack": "^5.75.0",
@ -21,6 +25,7 @@
},
"dependencies": {
"bufferutil": "^4.0.8",
"crypto": "^1.0.1",
"qrcode": "^1.5.3",
"utf-8-validate": "^6.0.3",
"ws": "^8.14.2"

View file

@ -1,6 +1,7 @@
import net = require('net');
import * as crypto from 'crypto';
import { EventEmitter } from 'node:events';
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { DecryptedMessage, EncryptedMessage, KeyExchangeMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { WebSocket } from 'ws';
enum SessionState {
@ -10,7 +11,7 @@ enum SessionState {
Disconnected,
};
enum Opcode {
export enum Opcode {
None = 0,
Play = 1,
Pause = 2,
@ -22,7 +23,12 @@ enum Opcode {
SetVolume = 8,
PlaybackError = 9,
SetSpeed = 10,
Version = 11
Version = 11,
KeyExchange = 12,
Encrypted = 13,
Ping = 14,
Pong = 15,
StartEncryption = 16
};
const LENGTH_BYTES = 4;
@ -36,11 +42,22 @@ export class FCastSession {
writer: (data: Buffer) => void;
state: SessionState;
emitter = new EventEmitter();
encryptionStarted = false;
private aesKey: Buffer;
private dh: crypto.DiffieHellman;
private queuedEncryptedMessages: EncryptedMessage[] = [];
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
this.socket = socket;
this.writer = writer;
this.state = SessionState.WaitingForLength;
this.dh = generateKeyPair();
const keyExchangeMessage = getKeyExchangeMessage(this.dh);
console.log(`Sending KeyExchangeMessage: ${keyExchangeMessage}`);
this.send(Opcode.KeyExchange, keyExchangeMessage);
}
sendVersion(value: VersionMessage) {
@ -60,6 +77,16 @@ export class FCastSession {
}
private send(opcode: number, message = null) {
if (this.encryptionStarted && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) {
const decryptedMessage: DecryptedMessage = {
opcode,
message
};
this.send(Opcode.Encrypted, encryptMessage(this.aesKey, decryptedMessage));
return;
}
const json = message ? JSON.stringify(message) : null;
let data: Uint8Array;
if (json) {
@ -149,7 +176,7 @@ export class FCastSession {
if (this.bytesRead >= this.packetLength) {
console.log(`Packet finished receiving from of ${this.packetLength} bytes.`);
this.handlePacket();
this.handleNextPacket();
this.state = SessionState.WaitingForLength;
this.packetLength = 0;
@ -162,12 +189,8 @@ export class FCastSession {
}
}
private handlePacket() {
console.log(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
console.log('body', body);
private handlePacket(opcode: number, body: string | undefined) {
console.log(`handlePacket (opcode: ${opcode}, body: ${body})`);
try {
switch (opcode) {
@ -192,9 +215,95 @@ export class FCastSession {
case Opcode.SetSpeed:
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
break;
case Opcode.KeyExchange:
const keyExchangeMessage = JSON.parse(body) as KeyExchangeMessage;
this.aesKey = computeSharedSecret(this.dh, keyExchangeMessage);
this.send(Opcode.StartEncryption);
for (const encryptedMessage of this.queuedEncryptedMessages) {
const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage);
this.handlePacket(decryptedMessage.opcode, decryptedMessage.message);
}
this.queuedEncryptedMessages = [];
break;
case Opcode.Ping:
this.send(Opcode.Pong);
break;
case Opcode.Encrypted:
const encryptedMessage = JSON.parse(body) as EncryptedMessage;
if (this.aesKey) {
const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage);
this.handlePacket(decryptedMessage.opcode, decryptedMessage.message);
} else {
if (this.queuedEncryptedMessages.length === 15) {
this.queuedEncryptedMessages.shift();
}
this.queuedEncryptedMessages.push(encryptedMessage);
}
break;
}
} catch (e) {
console.warn(`Error handling packet from.`, e);
}
}
private handleNextPacket() {
console.log(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
console.log('body', body);
this.handlePacket(opcode, body);
}
}
export function getKeyExchangeMessage(dh: crypto.DiffieHellman): KeyExchangeMessage {
return { version: 1, publicKey: dh.getPublicKey().toString('base64') };
}
export function computeSharedSecret(dh: crypto.DiffieHellman, keyExchangeMessage: KeyExchangeMessage): Buffer {
console.log("private", dh.getPrivateKey().toString('base64'));
const theirPublicKey = Buffer.from(keyExchangeMessage.publicKey, 'base64');
console.log("theirPublicKey", theirPublicKey.toString('base64'));
const secret = dh.computeSecret(theirPublicKey);
console.log("secret", secret.toString('base64'));
const digest = crypto.createHash('sha256').update(secret).digest();
console.log("digest", digest.toString('base64'));
return digest;
}
export function encryptMessage(aesKey: Buffer, decryptedMessage: DecryptedMessage): EncryptedMessage {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
let encrypted = cipher.update(JSON.stringify(decryptedMessage), 'utf8', 'base64');
encrypted += cipher.final('base64');
return {
version: 1,
iv: iv.toString('base64'),
blob: encrypted
};
}
export function decryptMessage(aesKey: Buffer, encryptedMessage: EncryptedMessage): DecryptedMessage {
const iv = Buffer.from(encryptedMessage.iv, 'base64');
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);
let decrypted = decipher.update(encryptedMessage.blob, 'base64', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted) as DecryptedMessage;
}
export function generateKeyPair() {
const dh = createDiffieHellman();
dh.generateKeys();
return dh;
}
export function createDiffieHellman(): crypto.DiffieHellman {
const p = Buffer.from('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 'hex');
const g = Buffer.from('02', 'hex');
return crypto.createDiffieHellman(p, g);
}

View file

@ -6,6 +6,7 @@ import { DiscoveryService } from './DiscoveryService';
import { Updater } from './Updater';
import { WebSocketListenerService } from './WebSocketListenerService';
import * as os from 'os';
import * as sodium from 'libsodium-wrappers';
export default class Main {
static shouldOpenMainWindow = true;
@ -195,6 +196,12 @@ export default class Main {
}
static openMainWindow() {
(async () => {
console.log("waiting for sodium...");
await sodium.ready;
console.log("sodium ready");
})();
if (Main.mainWindow) {
Main.mainWindow.focus();
return;

View file

@ -53,4 +53,26 @@ export class VersionMessage {
constructor(
public version: number,
) {}
}
export class KeyExchangeMessage {
constructor(
public version: number,
public publicKey: string
) {}
}
export class DecryptedMessage {
constructor(
public opcode: number,
public message: string | undefined
) {}
}
export class EncryptedMessage {
constructor(
public version: number,
public iv: string | undefined,
public blob: string
) {}
}

View file

@ -33,6 +33,8 @@ export class TcpListenerService {
server.close();
}
sendPlaybackError(value: PlaybackErrorMessage) {
console.info("Sending playback error.", value);

View file

@ -0,0 +1,76 @@
import { EncryptedMessage, DecryptedMessage, KeyExchangeMessage } from '../src/Packets';
import { generateKeyPair, computeSharedSecret, encryptMessage, decryptMessage, createDiffieHellman, Opcode } from '../src/FCastSession';
/*test("testDHEncryptionSelf", () => {
const keyPair1 = generateKeyPair();
const keyPair2 = generateKeyPair();
const aesKey1 = computeSharedSecret(keyPair1, { version:1, publicKey: keyPair2.getPublicKey().toString('base64') });
const aesKey2 = computeSharedSecret(keyPair2, { version:1, publicKey: keyPair1.getPublicKey().toString('base64') });
expect(aesKey1.toString('base64')).toBe(aesKey2.toString('base64'));
const message: DecryptedMessage = { opcode: 1, message: 'text/html' };
const encryptedMessage: EncryptedMessage = encryptMessage(aesKey1, message);
const decryptedMessage: DecryptedMessage = decryptMessage(aesKey1, encryptedMessage);
expect(decryptedMessage.opcode).toBe(message.opcode);
expect(decryptedMessage.message).toBe(message.message);
});*/
test("testDHEncryptionKnown", () => {
const encodedPrivateKey1 = "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==";
const keyExchangeMessage2: KeyExchangeMessage = { version: 1, publicKey: "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=" };
const dh = createDiffieHellman();
dh.setPrivateKey(Buffer.from(encodedPrivateKey1, 'base64'));
const aesKey1 = computeSharedSecret(dh, keyExchangeMessage2);
expect(aesKey1.toString('base64')).toBe("vI5LGE625zGEG350ggkyBsIAXm2y4sNohiPcED1oAEE=");
const message = { opcode: 1, message: 'text/html' };
const serializedBody = JSON.stringify(message);
const encryptedMessage = encryptMessage(aesKey1, message as DecryptedMessage);
const decryptedMessage = decryptMessage(aesKey1, encryptedMessage as EncryptedMessage);
expect(decryptedMessage.opcode).toBe(1);
expect(decryptedMessage.message).toBe(serializedBody);
});
/*test("testAESKeyGeneration", () => {
const testCases = [
{
publicKey: "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgEnOS0oHteVA+3kND3u4yXe7GGRohy1LkR9Q5tL4c4ylC5n4iSwWSoIhcSIvUMWth6KAhPhu05sMcPY74rFMSS2AGTNCdT/5KilediipuUMdFVvjGqfNMNH1edzW5mquIw3iXKdfQmfY/qxLTI2wccyDj4hHFhLCZL3Y+shsm3KF",
privateKey: "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAeo/ceIeH8Jt1ZRNKX5aTHkMi23GCV1LtcS2O6Tktn9k8DCv7gIoekysQUhMyWtR+MsZlq2mXjr1JFpAyxl89rqoEPU6QDsGe9q8R4O8eBZ2u+48mkUkGSh7xPGRQUBvmhH2yk4hIEA8aK4BcYi1OTsCZtmk7pQq+uaFkKovD/8M=",
expectedAES: "7dpl1/6KQTTooOrFf2VlUOSqgrFHi6IYxapX0IxFfwk="
},
{
publicKey: "MIIBHzCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECA4GEAAKBgGvIlCP/S+xpAuNEHSn4cEDOL1esUf+uMuY2Kp5J10a7HGbwzNd+7eYsgEc4+adddgB7hJgTvjsGg7lXUhHQ7WbfbCGgt7dbkx8qkic6Rgq4f5eRYd1Cgidw4MhZt7mEIOKrHweqnV6B9rypbXjbqauc6nGgtwx+Gvl6iLpVATRK",
privateKey: "MIIBIQIBADCBlQYJKoZIhvcNAQMBMIGHAoGBAP//////////yQ/aoiFowjTExmKLgNwc0SkCTgiKZ8x0Agu+pjsTmyJRSgh5jjQE3e+VGbPNOkMbMCsKbfJfFDdP4TVtbVHCReSFtXZiXn7G9ExC6aY37WsL/1y29Aa37e44a/taiZ+lrp8kEXxLH+ZJKGZR7OZTgf//////////AgECBIGDAoGAMXmiIgWyutbaO+f4UiMAb09iVVSCI6Lb6xzNyD2MpUZyk4/JOT04Daj4JeCKFkF1Fq79yKhrnFlXCrF4WFX00xUOXb8BpUUUH35XG5ApvolQQLL6N0om8/MYP4FK/3PUxuZAJz45TUsI/v3u6UqJelVTNL83ltcFbZDIfEVftRA=",
expectedAES: "a2tUSxnXifKohfNocAQHkAlPffDv6ReihJ7OojBGt0Q="
}
];
testCases.forEach(({ publicKey, privateKey, expectedAES }) => {
const theirPublicKey = Buffer.from(publicKey, 'base64');
const dh = createDiffieHellman();
dh.setPrivateKey(Buffer.from(privateKey, 'base64'));
const aesKey = computeSharedSecret(dh, { version: 1, publicKey: theirPublicKey.toString('base64') });
expect(aesKey.toString('base64')).toBe(expectedAES);
});
});*/
/*test("testDecryptMessageKnown", () => {
const encryptedMessage: EncryptedMessage = {
version: 1,
iv: "C4H70VC5FWrNtkty9/cLIA==",
blob: "K6/N7JMyi1PFwKhU0mFj7ZJmd/tPp3NCOMldmQUtDaQ7hSmPoIMI5QNMOj+NFEiP4qTgtYp5QmBPoQum6O88pA=="
};
const aesKeyBase64 = "+hr9Jg8yre7S9WGUohv2AUSzHNQN514JPh6MoFAcFNU=";
const aesKey = Buffer.from(aesKeyBase64, 'base64');
const decryptedMessage = decryptMessage(aesKey, encryptedMessage);
expect(decryptedMessage.opcode).toBe(Opcode.Play);
expect(decryptedMessage.message).toBe("{\"container\":\"text/html\"}");
});*/

View file

@ -11,5 +11,5 @@
"suppressImplicitAnyIndexErrors": true ,
"outDir": "dist"
},
"exclude": [ "node_modules" ]
"exclude": [ "node_modules", "test" ]
}