Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

DOB hash calculation & wiring (EXPOSUREAPP-7488, EXPOSUREAPP-7509) #3317

Merged
merged 15 commits into from
Jun 1, 2021
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.rki.coronawarnapp.coronatest.server

data class RegistrationData(
val registrationToken: String,
val testResultResponse: CoronaTestResultResponse
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.rki.coronawarnapp.coronatest.server

import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey

data class RegistrationRequest(
val key: String,
val type: VerificationKeyType,
val dateOfBirthKey: DateOfBirthKey? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import retrofit2.http.POST
interface VerificationApiV1 {

data class RegistrationTokenRequest(
@SerializedName("keyType") val keyType: String? = null,
@SerializedName("key") val key: String? = null,
@SerializedName("requestPadding") val requestPadding: String? = null
@SerializedName("keyType") val keyType: VerificationKeyType,
@SerializedName("key") val key: String,
@SerializedName("keyDob") val dateOfBirthKey: String? = null,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new optional attribute

@SerializedName("requestPadding") val requestPadding: String? = null,
)

data class RegistrationTokenResponse(
Expand All @@ -25,8 +26,8 @@ interface VerificationApiV1 {
): RegistrationTokenResponse

data class RegistrationRequest(
@SerializedName("registrationToken") val registrationToken: String? = null,
@SerializedName("requestPadding") val requestPadding: String? = null
@SerializedName("registrationToken") val registrationToken: String,
@SerializedName("requestPadding") val requestPadding: String
)

data class TestResultResponse(
Expand All @@ -42,8 +43,8 @@ interface VerificationApiV1 {
): TestResultResponse

data class TanRequestBody(
@SerializedName("registrationToken") val registrationToken: String? = null,
@SerializedName("requestPadding") val requestPadding: String? = null
@SerializedName("registrationToken") val registrationToken: String,
@SerializedName("requestPadding") val requestPadding: String
)

data class TanResponse(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
package de.rki.coronawarnapp.coronatest.server

import com.google.gson.annotations.SerializedName

enum class VerificationKeyType {
GUID, TELETAN;
@SerializedName("GUID")
GUID,

@SerializedName("TELETAN")
TELETAN;
Comment on lines +6 to +10
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We previously just relied on the enums never being renamed or obfuscated, this was a bit too fragile for my taste.

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,45 +20,92 @@ class VerificationServer @Inject constructor(
get() = verificationAPI.get()

suspend fun retrieveRegistrationToken(
key: String,
keyType: VerificationKeyType
request: RegistrationRequest
): RegistrationToken = withContext(Dispatchers.IO) {
Timber.tag(TAG).v("retrieveRegistrationToken(key=%s, keyType=%s)", key, keyType)
val keyStr = if (keyType == VerificationKeyType.GUID) {
HashHelper.hash256(key)
} else {
key
}
Timber.tag(TAG).v("retrieveRegistrationToken(request=%s)", request)

val requiredHeaderPadding = run {
var size = HEADER_SIZE_OUR_DATA
size -= HEADER_SIZE_OVERHEAD

val paddingLength = when (keyType) {
VerificationKeyType.GUID -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID
VerificationKeyType.TELETAN -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN
// `POST /version/v1/registrationToken`
size -= 34

size
}
val requiredBodyPadding = run {
var size = BODY_SIZE_EXPECTED
size -= BODY_SIZE_OVERHEAD

size -= when (request.type) {
VerificationKeyType.GUID -> 17 // `"keyType":"GUID",`
VerificationKeyType.TELETAN -> 20 // `"keyType":"TELETAN",`
}

size -= when (request.type) {
VerificationKeyType.GUID -> {
73 // `"key":"75552e6e1dae7a520bad64e92b7569447d0f5ca3c539335e0418a7695606147e",`
}
VerificationKeyType.TELETAN -> {
19 // `"key":"ERYCJMM4DC",`
}
}

size -= when (request.dateOfBirthKey) {
null -> 0
else -> 76 // `"keyDob":"x9acafb78b330522e32b4bf4c90a3ebb7a4d20d8af8cc32018c550ea86a38cc1",`
}
size
}

val response = api.getRegistrationToken(
fake = "0",
headerPadding = requestPadding(PADDING_LENGTH_HEADER_REGISTRATION_TOKEN),
headerPadding = requestPadding(requiredHeaderPadding),
requestBody = VerificationApiV1.RegistrationTokenRequest(
keyType = keyType.name,
key = keyStr,
requestPadding = requestPadding(paddingLength)
keyType = request.type,
key = when (request.type) {
VerificationKeyType.GUID -> HashHelper.hash256(request.key)
else -> request.key
},
dateOfBirthKey = request.dateOfBirthKey?.key,
requestPadding = requestPadding(requiredBodyPadding),
)
)

Timber.tag(TAG).d("retrieveRegistrationToken(key=%s, keyType=%s) -> %s", key, keyType, response)
Timber.tag(TAG).d("retrieveRegistrationToken(request=%s) -> %s", request, response)
response.registrationToken
}

suspend fun pollTestResult(
token: RegistrationToken
): CoronaTestResultResponse = withContext(Dispatchers.IO) {
Timber.tag(TAG).v("retrieveTestResults(token=%s)", token)

val requiredHeaderPadding = run {
var size = HEADER_SIZE_OUR_DATA
size -= HEADER_SIZE_OVERHEAD

// `POST /version/v1/testresult`
size -= 27

size
}
val requiredBodyPadding = run {
var size = BODY_SIZE_EXPECTED
size -= BODY_SIZE_OVERHEAD

// `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
size -= 59

size
}

val response = api.getTestResult(
fake = "0",
headerPadding = requestPadding(PADDING_LENGTH_HEADER_TEST_RESULT),
headerPadding = requestPadding(requiredHeaderPadding),
request = VerificationApiV1.RegistrationRequest(
token,
requestPadding(PADDING_LENGTH_BODY_TEST_RESULT)
requestPadding(requiredBodyPadding)
)
)

Expand All @@ -71,12 +118,31 @@ class VerificationServer @Inject constructor(
registrationToken: RegistrationToken
): String = withContext(Dispatchers.IO) {
Timber.tag(TAG).v("retrieveTan(registrationToken=%s)", registrationToken)
val requiredHeaderPadding = run {
var size = HEADER_SIZE_OUR_DATA
size -= HEADER_SIZE_OVERHEAD

// `POST /version/v1/tan`
size -= 20

size
}
val requiredBodyPadding = run {
var size = BODY_SIZE_EXPECTED
size -= BODY_SIZE_OVERHEAD

// `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
size -= 59

size
}

val response = api.getTAN(
fake = "0",
headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN),
headerPadding = requestPadding(requiredHeaderPadding),
requestBody = VerificationApiV1.TanRequestBody(
registrationToken,
requestPadding(PADDING_LENGTH_BODY_TAN)
requestPadding(requiredBodyPadding)
)
)

Expand All @@ -86,41 +152,72 @@ class VerificationServer @Inject constructor(

suspend fun retrieveTanFake() = withContext(Dispatchers.IO) {
Timber.tag(TAG).v("retrieveTanFake()")
val requiredHeaderPadding = run {
var size = HEADER_SIZE_OUR_DATA
size -= HEADER_SIZE_OVERHEAD

// `POST /version/v1/tan`
size -= 20

size
}
val requiredBodyPadding = run {
var size = BODY_SIZE_EXPECTED
size -= BODY_SIZE_OVERHEAD

// `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
size -= 59

size
}

val response = api.getTAN(
fake = "1",
headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN),
headerPadding = requestPadding(requiredHeaderPadding),
requestBody = VerificationApiV1.TanRequestBody(
registrationToken = DUMMY_REGISTRATION_TOKEN,
requestPadding = requestPadding(PADDING_LENGTH_BODY_TAN_FAKE)
requestPadding = requestPadding(requiredBodyPadding)
)
)
Timber.tag(TAG).v("retrieveTanFake() -> %s", response)
response
}

companion object {
// padding registration token
private const val VERIFICATION_BODY_FILL = 139
/**
* The specific sizes are not important, but all requests should be padded up to the same size.
* Pick a total size that is guaranteed to be above or equal to the maximum size a request can be.
*/
// `"requestPadding":""`
private const val BODY_SIZE_PADDING_OVERHEAD = 19 //

// `{}` json brackets
private const val BODY_SIZE_OVERHEAD = BODY_SIZE_PADDING_OVERHEAD + 2
private const val BODY_SIZE_EXPECTED = 250

/**
* The header itself is larger.
* We care about the header fields we set that are request specific.
* We don't need to pad for device specific fields set by OK http.
*/
// `POST /version/v1/registrationToken` -> 34 (longest method + url atm) use 64 to have a buffer
private const val HEADER_SIZE_LONGEST_METHOD = 34

const val PADDING_LENGTH_HEADER_REGISTRATION_TOKEN = 0
const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN = 51 + VERIFICATION_BODY_FILL
const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID = 0 + VERIFICATION_BODY_FILL
// `cwa-fake 0\n` -> 12
private const val HEADER_SIZE_VAL_FAKE = 12

// padding test result
const val PADDING_LENGTH_HEADER_TEST_RESULT = 7
const val PADDING_LENGTH_BODY_TEST_RESULT = 31 + VERIFICATION_BODY_FILL
// `cwa-header-padding\n` -> 22
private const val HEADER_SIZE_VAL_PADDING = 22
private const val HEADER_SIZE_OVERHEAD = HEADER_SIZE_VAL_FAKE + HEADER_SIZE_VAL_PADDING
private const val HEADER_SIZE_OUR_DATA = HEADER_SIZE_LONGEST_METHOD + HEADER_SIZE_OVERHEAD
Comment on lines +187 to +212
Copy link
Member Author

@d4rken d4rken May 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We pad the request for plausible deniability, so they all have the same size.
Previously the padding was difficult to understand why a certain value was picked, as we summed up the differences to other API requests, this was a bit too 🧙.
This PR changes the padding calculation to start of with an expected size, and then each API call subtracts its own size, and what we are left with is the required padding.


// padding tan
const val PADDING_LENGTH_HEADER_TAN = 14
const val PADDING_LENGTH_BODY_TAN = 31 + VERIFICATION_BODY_FILL
const val PADDING_LENGTH_BODY_TAN_FAKE = 31 + VERIFICATION_BODY_FILL
const val DUMMY_REGISTRATION_TOKEN = "11111111-2222-4444-8888-161616161616"

/**
* Test is available for this long on the server.
* After this period the server will delete it and return PENDING if the regtoken is polled again.
*/
val TEST_AVAILABLBILITY = Duration.standardDays(60)
val TEST_AVAILABLBILITY: Duration = Duration.standardDays(60)

private const val TAG = "VerificationServer"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package de.rki.coronawarnapp.coronatest.type

import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
import de.rki.coronawarnapp.coronatest.server.RegistrationData
import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
import de.rki.coronawarnapp.deniability.NoiseScheduler
import de.rki.coronawarnapp.playbook.Playbook
import de.rki.coronawarnapp.worker.BackgroundConstants
Expand All @@ -13,44 +14,22 @@ class CoronaTestService @Inject constructor(
private val noiseScheduler: NoiseScheduler,
) {

suspend fun asyncRequestTestResult(registrationToken: String): CoronaTestResultResponse {
suspend fun checkTestResult(registrationToken: String): CoronaTestResultResponse {
return playbook.testResult(registrationToken)
}

suspend fun asyncRegisterDeviceViaGUID(guid: String): RegistrationData {
val (registrationToken, testResult) =
playbook.initialRegistration(
guid,
VerificationKeyType.GUID
)
suspend fun registerTest(tokenRequest: RegistrationRequest): RegistrationData {
val response = playbook.initialRegistration(tokenRequest)

Timber.d("Scheduling background noise.")
scheduleDummyPattern()

return RegistrationData(registrationToken, testResult)
}

suspend fun asyncRegisterDeviceViaTAN(tan: String): RegistrationData {
val (registrationToken, testResult) =
playbook.initialRegistration(
tan,
VerificationKeyType.TELETAN
)

Timber.d("Scheduling background noise.")
scheduleDummyPattern()

return RegistrationData(registrationToken, testResult)
return response
}

private fun scheduleDummyPattern() {
if (BackgroundConstants.NUMBER_OF_DAYS_TO_RUN_PLAYBOOK > 0) {
noiseScheduler.setPeriodicNoise(enabled = true)
}
}

data class RegistrationData(
val registrationToken: String,
val testResultResponse: CoronaTestResultResponse
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package de.rki.coronawarnapp.coronatest.type.common

import de.rki.coronawarnapp.util.HashExtensions.toSHA256
import org.joda.time.LocalDate
import org.joda.time.format.DateTimeFormat

data class DateOfBirthKey constructor(
private val testGuid: String,
private val dateOfBirth: LocalDate,
) {

init {
require(testGuid.isNotEmpty()) { "GUID can't be empty." }
}

val key by lazy {
val dobFormatted = dateOfBirth.toString(DOB_FORMATTER)
val keyHash = "${testGuid}$dobFormatted".toSHA256()
"x${keyHash.substring(1)}"
}
Comment on lines +16 to +20
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date of Birth hash calculation


companion object {
private val DOB_FORMATTER = DateTimeFormat.forPattern("ddMMYYYY")
}
}