-
Notifications
You must be signed in to change notification settings - Fork 499
DOB hash calculation & wiring (EXPOSUREAPP-7488, EXPOSUREAPP-7509) #3317
Changes from all commits
4f92815
a6c0f2a
5fccd89
0ff088f
66ca106
c8f8f90
d114420
1891a1e
239a94a
ea85ac5
14ca90e
34d6e4d
91fce93
f75c7e2
2ec84bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
---|---|---|
|
@@ -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) | ||
) | ||
) | ||
|
||
|
@@ -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) | ||
) | ||
) | ||
|
||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
||
// 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" | ||
} | ||
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new optional attribute