Skip to content

Commit

Permalink
Add an experiment of connecting as Glypse to the car
Browse files Browse the repository at this point in the history
  • Loading branch information
hufman committed Aug 26, 2024
1 parent fa58608 commit 562bf2b
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.bimmergestalt.idriveconnectkit.android.security

import java.io.IOException
import java.io.InputStream
import java.security.KeyStore
import java.security.MessageDigest
import java.security.PrivateKey
import java.security.Signature
import java.security.UnrecoverableKeyException
import java.util.Base64
import kotlin.experimental.xor
import kotlin.jvm.Throws

object PrivateKeyHandling {
/**
* Given a token and a package name from a BMW certificate
* calculate the passphrase for the private pkcs12 file
* Throws IllegalArgumentException for errors decoding
*/
@Throws(IllegalArgumentException::class)
fun decodePassphrase(token: String, name: String): String {
val tokenArray = Base64.getDecoder().decode(token)
val decoded = StringBuffer(tokenArray.size / 4)

if (tokenArray.size < 5) {
throw IllegalArgumentException("Token is too small")
}

var newChar = tokenArray[0]
for (i in 4 ..< tokenArray.size-3 step 4) {
if (tokenArray[i+1] != 0.toByte()) {
throw IllegalArgumentException("High byte found at ${i+1}")
}
if (tokenArray[i+3] != 0.toByte()) {
throw IllegalArgumentException("High byte found at ${i+3}")
}
val nameIndex = newChar xor tokenArray[2] xor tokenArray[i]
if (nameIndex < 0 || nameIndex >= name.length) {
throw IllegalArgumentException("Out of bounds $nameIndex when decoding token at $i")
}

newChar = newChar xor tokenArray[i+2] xor 0x17 xor name[nameIndex.toInt()].code.toByte()
decoded.append(newChar.toInt().toChar())
}

return decoded.toString()
}

/**
* Opens the given pkcs12 file, unlocking with the given passphrase
* Returns null if no private key was found
* Throws IllegalArgumentException for invalid passphrase
*/
@Throws(IllegalArgumentException::class)
fun loadPrivateKey(pkcs12: InputStream, passphrase: String): PrivateKey? {
val password = passphrase.toCharArray()
val keystore = KeyStore.getInstance("PKCS12")
try {
keystore.load(pkcs12, password)
} catch (e: IOException) {
throw IllegalArgumentException("Incorrect keystore password", e)
}
for (entryName in keystore.aliases()) {
val key = try {
keystore.getKey(entryName, password)
} catch (e: UnrecoverableKeyException) {
throw IllegalArgumentException("Incorrect key password", e)
}
if (key is PrivateKey) {
return key
}
}
return null
}

@OptIn(ExperimentalStdlibApi::class)
fun signChallenge(challenge: ByteArray, key: PrivateKey, applyXor: Boolean): ByteArray {
val hasher = MessageDigest.getInstance("MD5")
hasher.update(byteArrayOf(0x02, 0x00, 0x00, 0x00), 0, 4)
val halfway = hasher.digest()
val output = ByteArray(challenge.size)
challenge.forEachIndexed { index, byte ->
output[index] = challenge[index] xor halfway[index % halfway.size]
}
println(output.toHexString())

val sign = Signature.getInstance("MD5withRSA")
sign.initSign(key)
sign.update(if (applyXor) output else challenge)
return sign.sign()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import io.bimmergestalt.idriveconnectkit.IDriveConnection
import io.bimmergestalt.idriveconnectkit.Utils.rhmi_setResourceCached
import io.bimmergestalt.idriveconnectkit.android.CarAppAssetResources
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus
import io.bimmergestalt.idriveconnectkit.android.security.PrivateKeyHandling
import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplication
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationEtch
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationIdempotent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationSynchronized
import java.io.ByteArrayInputStream


val TAG = "CarMap"
Expand All @@ -35,11 +37,25 @@ class CarApp(
try {
MainViewModel.log("Starting connecting to car")
// val appCert = carAppResources.getAppCertificate("common").readBytes()

MainViewModel.log("Loading key")
val rawKey = carAppResources.loadFile("carapplications/${carAppResources.name}/rhmi/common/${carAppResources.name}.p12")!!.readBytes()
val glympseToken = "AQABAAsAewBNAAYAMQBoACoACABPABMANQAJAE8AXAByAFQA" +
"SABEAHcAegB2ADsAPwB7AC8AJABtAFUASwANACIAJQBnACoA" +
"LAA9AGQAKAA0ABkAUgAQADgAIABrADEANAAOAEkABAAlAGUA" +
"MgApAGgALwA4AGoAeQBAAF4AFQAqAH4AKgAYAEwAEAA7ACwA" +
"dAAlAD0ANQB8AD4APgAQAEUAUwA="
val passphrase = PrivateKeyHandling.decodePassphrase(glympseToken, "com.glympse.iphone.glympse")
val key = PrivateKeyHandling.loadPrivateKey(ByteArrayInputStream(rawKey), passphrase)
if (key == null) {
MainViewModel.log("failed to decode key")
}

val rawCert = carAppResources.loadFile("carapplications/${carAppResources.name}/rhmi/common/${carAppResources.name}.p7b")!!.readBytes()
MainViewModel.log("Presenting cert")
val sas_challenge = carConnection.sas_certificate(rawCert)
MainViewModel.log("Signing cert")
val sas_response = securityAccess.signChallenge(challenge = sas_challenge)
MainViewModel.log("Signing challenge")
val sas_response = PrivateKeyHandling.signChallenge(sas_challenge, key!!, false)
MainViewModel.log("Logging in")
carConnection.sas_login(sas_response)

Expand All @@ -58,8 +74,9 @@ class CarApp(
MainViewModel.log("Creating RHMI")
val rhmiHandle = carConnection.rhmi_create(null, BMWRemoting.RHMIMetaData("io.bimmergestalt.idriveexperiments.certs", BMWRemoting.VersionInfo(0, 1, 0), "io.bimmergestalt.idriveexperiments.certs", "io.bimmergestalt"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.DESCRIPTION, carAppResources.getUiDescription())
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.TEXTDB, carAppResources.getTextsDB("mini"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.IMAGEDB, carAppResources.getImagesDB("mini"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.TEXTDB, carAppResources.getTextsDB(iDriveConnectionStatus.brand ?: "common"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.IMAGEDB, carAppResources.getImagesDB("common"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.IMAGEDB, carAppResources.getImagesDB(iDriveConnectionStatus.brand ?: "common"))
carConnection.rhmi_initialize(rhmiHandle)

MainViewModel.log("Created RHMI")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class CarAppService: Service() {
app = CarApp(
iDriveConnectionStatus,
securityAccess,
CarAppAssetResources(applicationContext, "spotify"),
CarAppAssetResources(applicationContext, "glympse"),
)
}
thread?.start()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package io.bimmergestalt.idriveconnectkit.android.security

import org.junit.Test

import org.junit.Assert.*

class PrivateKeyHandlingTest {

val TOKEN = "AAABAAsASgB+AF4AeQAmADEAJAAgAGoAaQByAGwAJAArAAcAUQAkAEcAEAA3AG8A" +
"JwAFAFoAVgBpAGIAZgBhAGYAbQBlAHoALABNAEQAMAA/ACYAbgBGAFQASABfAEAA" +
"RgAKADAAdAApAHAAOgBWAF8AfwB5AGkAcAAEAFAAYwBaAGAARgBFAGMAbwArAE0A" +
"OgBeAEcAAgA0ACIAKwBeAEAAYgA="

@Test
fun decodePassphrase() {
val decoded = PrivateKeyHandling.decodePassphrase(TOKEN, "de.bmw.a4a.BMWAppKit")
assertEquals(40, decoded.length)
assertTrue(decoded.startsWith("sw6+xm:ZG"))
}

@Test
fun decodePassphraseFail() {
assertThrows(IllegalArgumentException::class.java) {
PrivateKeyHandling.decodePassphrase(TOKEN, "de.bmw.a4a")
}
}

@Test
fun loadPrivateKey() {
val pkcs12 = this.javaClass.classLoader!!.getResourceAsStream("BMWAppKitDevelopment.p12")
val passphrase = PrivateKeyHandling.decodePassphrase(TOKEN, "de.bmw.a4a.BMWAppKit")
val key = PrivateKeyHandling.loadPrivateKey(pkcs12, passphrase)
assertNotNull(key)
}

@Test
fun loadPrivateKeyFail() {
val pkcs12 = this.javaClass.classLoader!!.getResourceAsStream("BMWAppKitDevelopment.p12")
val passphrase = PrivateKeyHandling.decodePassphrase(TOKEN, "de.bmw.a4a.BMWAppKit")
assertThrows(IllegalArgumentException::class.java) {
PrivateKeyHandling.loadPrivateKey(pkcs12, passphrase.substring(0..<10))
}
}

@OptIn(ExperimentalStdlibApi::class)
@Test
fun signChallenge() {
// the pkcs12 from BMW Connected 10.4 for iOS
val pkcs12 = this.javaClass.classLoader!!.getResourceAsStream("BMWAppKitDevelopment.p12")
val passphrase = PrivateKeyHandling.decodePassphrase(TOKEN, "de.bmw.a4a.BMWAppKit")
val key = PrivateKeyHandling.loadPrivateKey(pkcs12, passphrase)!!

val challenge = byteArrayOf(
0x6d, 0x58, 0x5f, 0x14,
0x72, 0x72, 0x19, 0x75,
0x4e, 0x73, 0x19, 0x38,
0x61, 0x2f, 0x50, 0x78)
val response = PrivateKeyHandling.signChallenge(challenge, key, true)
assertEquals(192, response.size)
print(response.toHexString())
assertEquals(0x4d.toByte(), response[0])
assertEquals(0x0e.toByte(), response[1])
assertEquals(0x33.toByte(), response[2])

val response2 = PrivateKeyHandling.signChallenge(challenge, key, false)
assertEquals(192, response.size)
print(response.toHexString())
assertEquals(0xc2.toByte(), response2[0])
assertEquals(0x59.toByte(), response2[1])
assertEquals(0xc5.toByte(), response2[2])
}
}
Binary file added certs/src/test/resources/BMWAppKitDevelopment.p12
Binary file not shown.

0 comments on commit 562bf2b

Please sign in to comment.