Skip to content
Snippets Groups Projects
Unverified Commit 275be0d2 authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Personal data is only valid if all is available, or nothing. (#2916)


Co-authored-by: default avatarI502720 <axel.herbstreith@sap.com>
parent 9050510f
No related branches found
No related tags found
No related merge requests found
......@@ -19,7 +19,10 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona
override fun extract(rawString: String): CoronaTestQRCode.RapidAntigen {
Timber.v("extract(rawString=%s)", rawString)
val payload = extractData(rawString)
val payload = CleanPayload(extractData(rawString))
payload.requireValidPersonalData()
return CoronaTestQRCode.RapidAntigen(
hash = payload.hash,
createdAt = payload.createdAt,
......@@ -29,14 +32,14 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona
)
}
private fun extractData(rawString: String): Payload {
private fun extractData(rawString: String): RawPayload {
return rawString
.removePrefix(PREFIX1)
.removePrefix(PREFIX2)
.decode()
}
private fun String.decode(): Payload {
private fun String.decode(): RawPayload {
val decoded = if (
this.contains("+") ||
this.contains("/") ||
......@@ -49,54 +52,56 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona
return Gson().fromJson(decoded.commonToUtf8String())
}
private data class Payload(
@SerializedName("hash")
val rawHash: String?,
@SerializedName("timestamp")
val rawTimestamp: Long?,
@SerializedName("fn")
val rawFirstName: String?,
@SerializedName("ln")
val rawLastName: String?,
@SerializedName("dob")
val rawDateOfBirth: String?
) {
val hash: String
get() {
if (rawHash == null || !rawHash.isSha256Hash()) throw InvalidQRCodeException("Hash is invalid")
return rawHash
}
private data class RawPayload(
@SerializedName("hash") val hash: String?,
@SerializedName("timestamp") val timestamp: Long?,
@SerializedName("fn") val firstName: String?,
@SerializedName("ln") val lastName: String?,
@SerializedName("dob") val dateOfBirth: String?
)
val createdAt: Instant
get() {
if (rawTimestamp == null || rawTimestamp <= 0) throw InvalidQRCodeException("Timestamp is invalid")
return Instant.ofEpochSecond(rawTimestamp)
}
private data class CleanPayload(val raw: RawPayload) {
val firstName: String?
get() {
if (rawFirstName.isNullOrEmpty()) return null
return rawFirstName
}
val hash: String by lazy {
if (raw.hash == null || !raw.hash.isSha256Hash()) throw InvalidQRCodeException("Hash is invalid")
raw.hash
}
val lastName: String?
get() {
if (rawLastName.isNullOrEmpty()) return null
return rawLastName
}
val createdAt: Instant by lazy {
if (raw.timestamp == null || raw.timestamp <= 0) throw InvalidQRCodeException("Timestamp is invalid")
Instant.ofEpochSecond(raw.timestamp)
}
val dateOfBirth: LocalDate?
get() {
if (rawDateOfBirth.isNullOrEmpty()) return null
return try {
LocalDate.parse(rawDateOfBirth)
} catch (e: Exception) {
Timber.e("Invalid date format")
throw InvalidQRCodeException(
"Date of birth has wrong format: $rawDateOfBirth. It should be YYYY-MM-DD"
)
}
val firstName: String? by lazy {
if (raw.firstName.isNullOrEmpty()) null else raw.firstName
}
val lastName: String? by lazy {
if (raw.lastName.isNullOrEmpty()) null else raw.lastName
}
val dateOfBirth: LocalDate? by lazy {
if (raw.dateOfBirth.isNullOrEmpty()) return@lazy null
try {
LocalDate.parse(raw.dateOfBirth)
} catch (e: Exception) {
Timber.e("Invalid date format")
throw InvalidQRCodeException(
"Date of birth has wrong format: ${raw.dateOfBirth}. It should be YYYY-MM-DD"
)
}
}
fun requireValidPersonalData() {
val allOrNothing = listOf(
firstName != null,
lastName != null,
dateOfBirth != null,
)
val complete = allOrNothing.all { it } || allOrNothing.all { !it }
if (!complete) throw InvalidQRCodeException("QRCode contains incomplete personal data: $raw")
}
}
companion object {
......
package de.rki.coronawarnapp.coronatest.qrcode
import de.rki.coronawarnapp.coronatest.type.CoronaTest
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import org.joda.time.Instant
import org.joda.time.LocalDate
......@@ -53,4 +54,9 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() {
data.lastName shouldBe null
data.firstName shouldBe null
}
@Test
fun `personal data is only valid if complete or completely missing`() {
shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData) }
}
}
......@@ -32,3 +32,15 @@ internal val raQrCode8 =
// }
internal val raQrCodeEmptyStrings =
"https://s.coronawarn.app?v=1#ewogICAgImZuIjoiIiwKICAgICJsbiI6IiIsCiAgICAiZG9iIjoiIiwKICAgICJ0ZXN0aWQiOiIwZTYwMGI0Mi1jYzIxLTRlNzUtOTE0Yy03MDYwMzE0M2I2Y2IiLAogICAgInRpbWVzdGFtcCI6IDE2MTkwMTI5NTIsCiAgICAic2FsdCI6IjUxMGYzZDc1MGMyZmM2MzFmYmNmZTMyY2IwNmJiYmE5IiwKICAgICJoYXNoIjoiZDZlNGQwMTgxZDgxMDliZjA1YjM0NmEwZDJlMGVmMGNjNDcyZWVkNzBkOWRmOGM0YjlhZTVjN2EwMDlmM2UzNCIKfQ"
// {
// "fn":"Max",
// "ln":"",
// "dob":"",
// "testid":"0e600b42-cc21-4e75-914c-70603143b6cb",
// "timestamp": 1619012952,
// "salt":"510f3d750c2fc631fbcfe32cb06bbba9",
// "hash":"d6e4d0181d8109bf05b346a0d2e0ef0cc472eed70d9df8c4b9ae5c7a009f3e34"
// }
internal val raQrIncompletePersonalData =
"https://s.coronawarn.app?v=1#ewogICAgImZuIjoiTWF4IiwKICAgICJsbiI6IiIsCiAgICAiZG9iIjoiIiwKICAgICJ0ZXN0aWQiOiIwZTYwMGI0Mi1jYzIxLTRlNzUtOTE0Yy03MDYwMzE0M2I2Y2IiLAogICAgInRpbWVzdGFtcCI6IDE2MTkwMTI5NTIsCiAgICAic2FsdCI6IjUxMGYzZDc1MGMyZmM2MzFmYmNmZTMyY2IwNmJiYmE5IiwKICAgICJoYXNoIjoiZDZlNGQwMTgxZDgxMDliZjA1YjM0NmEwZDJlMGVmMGNjNDcyZWVkNzBkOWRmOGM0YjlhZTVjN2EwMDlmM2UzNCIKfQ"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment