Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V0 ignore keys #36

Merged
merged 4 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [0.8.0] - 2024-1-31

- Fix `Json {} ignoreUnknownKeys` error
- Add deprecated `sendCommandV05` method to support 0.5.0 API

## [0.7.5] - 2023-11-29

### Added
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=0.7.5
version=0.8.0
kotlin.code.style=official
18 changes: 18 additions & 0 deletions hmkit-fleet/src/main/kotlin/HMKitFleet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@ object HMKitFleet {
koin.get<TelematicsRequests>().sendCommand(command, vehicleAccess.accessCertificate)
}

/**
* Send a telematics command to the vehicle.
*
* This is a legacy method that returns the response as a [Bytes] object. If possible, use the normal [sendCommand]
* method that returns the correct error format.
*
* @param vehicleAccess The vehicle access object returned in [getVehicleAccess]
* @param command The command that is sent to the vehicle.
* @return The response command from the vehicle.
*/
@Deprecated("Please use sendCommand instead")
fun sendCommandV05(
command: Bytes,
vehicleAccess: VehicleAccess
): CompletableFuture<Response<Bytes>> = GlobalScope.future {
koin.get<TelematicsRequests>().sendCommandV05(command, vehicleAccess.accessCertificate)
}

/**
* Revoke the vehicle clearance. After this, the [VehicleAccess] object is invalid.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal class AccessCertificateRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_CREATED) { body ->
val jsonResponse = Json.parseToJsonElement(body) as JsonObject
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val certBytes = jsonResponse.jsonObject[apiDeviceCertKey]?.jsonPrimitive?.content
val cert = AccessCertificate(certBytes)
Response(cert, null)
Expand Down
2 changes: 1 addition & 1 deletion hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal class AuthTokenRequests(

return try {
if (response.code == HttpURLConnection.HTTP_CREATED) {
cache.authToken = Json.decodeFromString(responseBody)
cache.authToken = jsonIg.decodeFromString(responseBody)
Response(cache.authToken)
} else {
parseError(responseBody)
Expand Down
18 changes: 9 additions & 9 deletions hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val jsonElement = Json.parseToJsonElement(responseBody) as JsonObject
val jsonElement = jsonIg.parseToJsonElement(responseBody) as JsonObject
val statuses = jsonElement["vehicles"] as JsonArray
for (statusElement in statuses) {
val status =
Json.decodeFromJsonElement<RequestClearanceResponse>(statusElement)
jsonIg.decodeFromJsonElement<RequestClearanceResponse>(statusElement)
if (status.vin == vin) {
return Response(status, null)
}
Expand All @@ -100,11 +100,11 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val statuses = Json.parseToJsonElement(responseBody) as JsonArray
val statuses = jsonIg.parseToJsonElement(responseBody) as JsonArray

val builder = Array(statuses.size) {
val statusElement = statuses[it]
val status = Json.decodeFromJsonElement<ClearanceStatus>(statusElement)
val status = jsonIg.decodeFromJsonElement<ClearanceStatus>(statusElement)
status
}

Expand All @@ -130,7 +130,7 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val status = Json.decodeFromString<ClearanceStatus>(responseBody)
val status = jsonIg.decodeFromString<ClearanceStatus>(responseBody)
Response(status)
}
}
Expand All @@ -154,7 +154,7 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val status = Json.decodeFromString<RequestClearanceResponse>(responseBody)
val status = jsonIg.decodeFromString<RequestClearanceResponse>(responseBody)
Response(status)
}
}
Expand All @@ -166,14 +166,14 @@ internal class ClearanceRequests(
): RequestBody {
val vehicle = buildJsonObject {
put("vin", vin)
put("brand", Json.encodeToJsonElement(brand))
put("brand", jsonIg.encodeToJsonElement(brand))
if (controlMeasures != null) {
putJsonObject("control_measures") {
for (controlMeasure in controlMeasures) {
// polymorphism adds type key to child controlmeasure classes. remove with filter
val json = Json.encodeToJsonElement(controlMeasure)
val json = jsonIg.encodeToJsonElement(controlMeasure)
val valuesWithoutType = json.jsonObject.filterNot { it.key == "type" }
val jsonTrimmed = Json.encodeToJsonElement(valuesWithoutType)
val jsonTrimmed = jsonIg.encodeToJsonElement(valuesWithoutType)
put("odometer", jsonTrimmed)
}
}
Expand Down
21 changes: 14 additions & 7 deletions hmkit-fleet/src/main/kotlin/network/Requests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ internal open class Requests(
val mediaType = "application/json; charset=utf-8".toMediaType()
val baseHeaders = Headers.Builder().add("Content-Type", "application/json").build()

protected val jsonIg = Json {
ignoreUnknownKeys = true
}

private val jsonIgPr = Json {
ignoreUnknownKeys = true
prettyPrint = true
}

inline fun <T> tryParseResponse(
response: Response,
expectedResponseCode: Int,
Expand All @@ -61,14 +70,12 @@ internal open class Requests(
}

fun printRequest(request: Request) {
val format = Json { prettyPrint = true }

// parse into json, so can log it out with pretty print
val body = request.bodyAsString()
var bodyInPrettyPrint = ""
if (!body.isNullOrBlank()) {
val jsonElement = format.decodeFromString<JsonElement>(body)
bodyInPrettyPrint = format.encodeToString(jsonElement)
val jsonElement = jsonIgPr.decodeFromString<JsonElement>(body)
bodyInPrettyPrint = jsonIgPr.encodeToString(jsonElement)
}

logger.debug(
Expand All @@ -85,14 +92,14 @@ internal open class Requests(
}

fun <T> parseError(responseBody: String): com.highmobility.hmkitfleet.network.Response<T> {
val json = Json.parseToJsonElement(responseBody)
val json = jsonIg.parseToJsonElement(responseBody)
if (json is JsonObject) {
// there are 3 error formats
val errors = json["errors"] as? JsonArray

return if (errors != null && errors.size > 0) {
val error =
Json.decodeFromJsonElement<Error>(errors.first())
jsonIg.decodeFromJsonElement<Error>(errors.first())
Response(null, error)
} else {
val error = Error(
Expand All @@ -104,7 +111,7 @@ internal open class Requests(
}
} else if (json is JsonArray) {
if (json.size > 0) {
val error = Json.decodeFromJsonElement<Error>(json.first())
val error = jsonIg.decodeFromJsonElement<Error>(json.first())
return Response(null, error)
}
}
Expand Down
68 changes: 65 additions & 3 deletions hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ internal class TelematicsRequests(
return postCommand(encryptedCommand, accessCertificate)
}

suspend fun sendCommandV05(
command: Bytes,
accessCertificate: AccessCertificate
): Response<Bytes> {
val nonce = getNonce()

if (nonce.error != null) return Response(null, nonce.error)

val encryptedCommand =
crypto.createTelematicsContainer(
command,
privateKey,
certificate.serial,
accessCertificate,
Bytes(nonce.response!!)
)

val encryptedCommandResponse = postCommandV05(encryptedCommand, accessCertificate)

if (encryptedCommandResponse.error != null) return encryptedCommandResponse

val decryptedResponseCommand = crypto.getPayloadFromTelematicsContainer(
encryptedCommandResponse.response!!,
privateKey,
accessCertificate,
)

return Response(decryptedResponseCommand)
}


private suspend fun getNonce(): Response<String> {
val request = Request.Builder()
.url("${baseUrl}/nonces")
Expand All @@ -83,7 +114,7 @@ internal class TelematicsRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_CREATED) { body ->
val jsonResponse = Json.parseToJsonElement(body) as JsonObject
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val nonce = jsonResponse.jsonObject["nonce"]?.jsonPrimitive?.content
Response(nonce, null)
}
Expand Down Expand Up @@ -114,7 +145,7 @@ internal class TelematicsRequests(

val responseObject = try {
if (response.code == 200 || response.code == 400 || response.code == 404 || response.code == 408) {
val telematicsResponse = Json.decodeFromString<TelematicsCommandResponse>(responseBody)
val telematicsResponse = jsonIg.decodeFromString<TelematicsCommandResponse>(responseBody)

// Server only returns encrypted data if status is OK
val decryptedData = if (telematicsResponse.status == TelematicsCommandResponse.Status.OK) {
Expand All @@ -137,12 +168,43 @@ internal class TelematicsRequests(
} else {
// try to parse the normal server error format.
// it will throw and will be caught if server returned unknown format
TelematicsResponse(errors = Json.decodeFromString(responseBody))
TelematicsResponse(errors = jsonIg.decodeFromString(responseBody))
}
} catch (e: Exception) {
TelematicsResponse(errors = listOf(Error(title = "Unknown server response", detail = e.message)))
}

return responseObject
}

private suspend fun postCommandV05(
encryptedCommand: Bytes,
accessCertificate: AccessCertificate,
): Response<Bytes> {
val request = Request.Builder()
.url("${baseUrl}/telematics_commands")
.headers(baseHeaders)
.post(
requestBody(
mapOf(
"serial_number" to accessCertificate.gainerSerial.hex,
"issuer" to certificate.issuer.hex,
"data" to encryptedCommand.base64
)
)
)
.build()

printRequest(request)

val call = client.newCall(request)
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { body ->
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val encryptedResponseCommand =
jsonResponse.jsonObject["response_data"]?.jsonPrimitive?.content
Response(Bytes(encryptedResponseCommand), null)
}
}
}
6 changes: 3 additions & 3 deletions hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal class UtilityRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val eligibilityStatus = Json.decodeFromString<EligibilityStatus>(responseBody)
val eligibilityStatus = jsonIg.decodeFromString<EligibilityStatus>(responseBody)
if (eligibilityStatus.vin != vin) logger.warn("VIN in response does not match VIN in request")
Response(eligibilityStatus, null)
}
Expand All @@ -82,10 +82,10 @@ internal class UtilityRequests(
): RequestBody {
val vehicle = buildJsonObject {
put("vin", vin)
put("brand", Json.encodeToJsonElement(brand))
put("brand", jsonIg.encodeToJsonElement(brand))
}

val body = Json.encodeToString(vehicle).toRequestBody(mediaType)
val body = jsonIg.encodeToString(vehicle).toRequestBody(mediaType)
return body
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,56 @@ internal class TelematicsRequestsTest : BaseTest() {
assert(response.response?.responseData!! == decryptedReceivedCommand)
}

@Test
fun createAccessTokenV05() {
mockNonceResponse()
mockTelematicsResponse()

val baseUrl: HttpUrl = mockWebServer.url("")

val telematicsRequests = TelematicsRequests(
client, mockLogger, baseUrl.toString(), privateKey, certificate, crypto
)

val response = runBlocking {
telematicsRequests.sendCommandV05(Diagnostics.GetState(), mockAccessCert)
}

verify {
crypto.createTelematicsContainer(
Diagnostics.GetState(), privateKey, certificate.serial, mockAccessCert, nonce
)
}

// first request is nonce
val nonceRequest: RecordedRequest = mockWebServer.takeRequest()
assert(nonceRequest.path!!.endsWith("/nonces"))

// verify request
val nonceRequestBody = Json.parseToJsonElement(nonceRequest.body.readUtf8()) as JsonObject
assert(nonceRequestBody["serial_number"]!!.jsonPrimitive.contentOrNull == certificate.serial.hex)

// second request is telematics command
val commandRequest: RecordedRequest = mockWebServer.takeRequest()
assert(commandRequest.path!!.endsWith("/telematics_commands"))

// verify request
val jsonBody = Json.parseToJsonElement(commandRequest.body.readUtf8()) as JsonObject
assert(jsonBody["serial_number"]!!.jsonPrimitive.contentOrNull == certificate.serial.hex)
assert(jsonBody["issuer"]!!.jsonPrimitive.contentOrNull == certificate.issuer.hex)
assert(jsonBody["data"]!!.jsonPrimitive.contentOrNull == encryptedSentCommand.base64)

// verify command decrypted
verify {
crypto.getPayloadFromTelematicsContainer(
encryptedReceivedCommand, privateKey, mockAccessCert
)
}

// verify final telematics command response
assert(response.response!! == decryptedReceivedCommand)
}

private fun mockTelematicsResponse() {
val mockResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody(
"""
Expand Down
Loading