Skip to content
Snippets Groups Projects
Unverified Commit 0a7a015c authored by Chilja Gossow's avatar Chilja Gossow Committed by GitHub
Browse files

Qr code decoding (EXPOSUREAPP-7254) (#3174)


* NOOP vaccination data background update logic for now.

* Adjust tests.

* Remove proof certificate :(

* remove code object and refactor

* remove unused mock data

* merge removal

* remove env key

* remove proof server

* more removals

* fix test

* fix chain

* clean

* expose qr code

* fix tests

* fix tests

Co-authored-by: default avatarMatthias Urhahn <matthias.urhahn@sap.com>
parent c9342efb
No related branches found
No related tags found
No related merge requests found
Showing
with 82 additions and 275 deletions
package de.rki.coronawarnapp.util.compression package de.rki.coronawarnapp.util.compression
import okio.Buffer import okio.Buffer
import okio.ByteString
import okio.inflate import okio.inflate
import java.util.zip.Inflater import java.util.zip.Inflater
import javax.inject.Inject import javax.inject.Inject
class ZLIBCompression @Inject constructor() { class ZLIBCompression @Inject constructor() {
@Suppress("NestedBlockDepth") @Suppress("NestedBlockDepth")
fun decompress(input: ByteString, sizeLimit: Long = -1L): ByteString = try { fun decompress(input: ByteArray, sizeLimit: Long = -1L): ByteArray = try {
val inflaterSource = input.let { val inflaterSource = input.let {
val buffer = Buffer().write(it) val buffer = Buffer().write(it)
buffer.inflate(Inflater()) buffer.inflate(Inflater())
...@@ -24,10 +23,10 @@ class ZLIBCompression @Inject constructor() { ...@@ -24,10 +23,10 @@ class ZLIBCompression @Inject constructor() {
} }
} }
sink.readByteString() sink.readByteArray()
} catch (e: Throwable) { } catch (e: Throwable) {
throw InvalidInputException("ZLIB decompression failed.", e) throw InvalidInputException("ZLIB decompression failed.", e)
} }
} }
fun ByteString.inflate(sizeLimit: Long = -1L) = ZLIBCompression().decompress(this, sizeLimit) fun ByteArray.inflate(sizeLimit: Long = -1L) = ZLIBCompression().decompress(this, sizeLimit)
package de.rki.coronawarnapp.util.encoding package de.rki.coronawarnapp.util.encoding
import okio.ByteString
import okio.ByteString.Companion.toByteString
/** /**
* Decodes [String] into [ByteString] using Base45 decoder * Decodes [String] into [ByteArray] using Base45 decoder
* @return [ByteString] * @return [ByteArray]
*/ */
fun String.decodeBase45(): ByteString = Base45Decoder.decode(this).toByteString() fun String.decodeBase45(): ByteArray = Base45Decoder.decode(this)
/** /**
* Encodes [ByteString] into base45 [String] * Encodes [ByteArray] into base45 [String]
* @return [String] * @return [String]
*/ */
fun ByteString.base45(): String = Base45Decoder.encode(this.toByteArray()) fun ByteArray.base45(): String = Base45Decoder.encode(this)
package de.rki.coronawarnapp.vaccination.core package de.rki.coronawarnapp.vaccination.core
import de.rki.coronawarnapp.ui.Country import de.rki.coronawarnapp.ui.Country
import de.rki.coronawarnapp.vaccination.core.qrcode.QrCodeString
import org.joda.time.Instant import org.joda.time.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
...@@ -27,4 +28,6 @@ interface VaccinationCertificate { ...@@ -27,4 +28,6 @@ interface VaccinationCertificate {
val issuer: String val issuer: String
val issuedAt: Instant val issuedAt: Instant
val expiresAt: Instant val expiresAt: Instant
val vaccinationQrCodeString: QrCodeString
} }
...@@ -9,7 +9,7 @@ import javax.inject.Inject ...@@ -9,7 +9,7 @@ import javax.inject.Inject
class HealthCertificateCOSEDecoder @Inject constructor() { class HealthCertificateCOSEDecoder @Inject constructor() {
fun decode(input: RawCOSEObject): CBORObject = try { fun decode(input: RawCOSEObject): CBORObject = try {
val messageObject = CBORObject.DecodeFromBytes(input.asByteArray).validate() val messageObject = CBORObject.DecodeFromBytes(input).validate()
val content = messageObject[2].GetByteString() val content = messageObject[2].GetByteString()
CBORObject.DecodeFromBytes(content) CBORObject.DecodeFromBytes(content)
} catch (e: InvalidHealthCertificateException) { } catch (e: InvalidHealthCertificateException) {
......
package de.rki.coronawarnapp.vaccination.core.certificate package de.rki.coronawarnapp.vaccination.core.certificate
import com.google.gson.JsonParseException typealias RawCOSEObject = ByteArray
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString
import org.json.JSONObject
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type
import javax.inject.Inject
data class RawCOSEObject(
val data: ByteString
) {
constructor(data: ByteArray) : this(data.toByteString())
val asByteArray: ByteArray
get() = data.toByteArray()
companion object {
val EMPTY = RawCOSEObject(data = ByteString.EMPTY)
}
class JsonAdapter : TypeAdapter<RawCOSEObject>() {
override fun write(out: JsonWriter, value: RawCOSEObject?) {
if (value == null) out.nullValue()
else value.data.base64().let { out.value(it) }
}
override fun read(reader: JsonReader): RawCOSEObject? = when (reader.peek()) {
JSONObject.NULL -> reader.nextNull().let { null }
else -> {
val raw = reader.nextString()
raw.decodeBase64()?.let { RawCOSEObject(data = it) }
?: throw JsonParseException("Can't decode base64 ByteArray: $raw")
}
}
}
class RetroFitConverterFactory @Inject constructor() : Converter.Factory() {
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<out Annotation>,
methodAnnotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<RawCOSEObject, RequestBody> {
return ConverterToBody()
}
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, RawCOSEObject> {
return ConverterFromBody()
}
class ConverterFromBody : Converter<ResponseBody, RawCOSEObject> {
override fun convert(value: ResponseBody): RawCOSEObject {
val rawData = value.byteString()
return RawCOSEObject(rawData)
}
}
class ConverterToBody : Converter<RawCOSEObject, RequestBody> {
override fun convert(value: RawCOSEObject): RequestBody {
return value.data.toRequestBody(
"application/octet-stream".toMediaTypeOrNull()
)
}
}
}
}
...@@ -35,9 +35,9 @@ class VaccinationDGCV1Parser @Inject constructor( ...@@ -35,9 +35,9 @@ class VaccinationDGCV1Parser @Inject constructor(
if (vaccinationDatas.isEmpty()) { if (vaccinationDatas.isEmpty()) {
throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY)
} }
// Force date parsing
dateOfBirth dateOfBirth
vaccinationDatas.forEach { vaccinationDatas.forEach {
// Force date parsing
it.vaccinatedAt it.vaccinatedAt
} }
return this return this
......
package de.rki.coronawarnapp.vaccination.core.common
import com.google.gson.JsonParseException
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import okio.ByteString
import okio.ByteString.Companion.decodeBase64
import okio.ByteString.Companion.toByteString
import org.json.JSONObject
data class RawCOSEObject(
val data: ByteString
) {
constructor(data: ByteArray) : this(data.toByteString())
val asByteArray: ByteArray
get() = data.toByteArray()
companion object {
val EMPTY = RawCOSEObject(data = ByteString.EMPTY)
}
class JsonAdapter : TypeAdapter<RawCOSEObject>() {
override fun write(out: JsonWriter, value: RawCOSEObject?) {
if (value == null) out.nullValue()
else value.data.base64().let { out.value(it) }
}
override fun read(reader: JsonReader): RawCOSEObject? = when (reader.peek()) {
JSONObject.NULL -> reader.nextNull().let { null }
else -> {
val raw = reader.nextString()
raw.decodeBase64()?.let { RawCOSEObject(data = it) }
?: throw JsonParseException("Can't decode base64 ByteArray: $raw")
}
}
}
}
package de.rki.coronawarnapp.vaccination.core.qrcode
import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder
import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1Parser
import timber.log.Timber
import javax.inject.Inject
class VaccinationCertificateCOSEParser @Inject constructor(
private val coseDecoder: HealthCertificateCOSEDecoder,
private val headerParser: HealthCertificateHeaderParser,
private val bodyParser: VaccinationDGCV1Parser,
) {
fun parse(rawCOSEObject: RawCOSEObject): VaccinationCertificateData {
Timber.v("Parsing COSE for vaccination certificate.")
val cbor = coseDecoder.decode(rawCOSEObject)
return VaccinationCertificateData(
header = headerParser.parse(cbor),
certificate = bodyParser.parse(cbor)
).also {
Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized)
}
}
}
...@@ -4,7 +4,7 @@ import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader ...@@ -4,7 +4,7 @@ import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader
import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1
/** /**
* Represents the information gained from data in COSE representation * Represents the parsed data from the QR code
*/ */
data class VaccinationCertificateData( data class VaccinationCertificateData(
val header: CoseCertificateHeader, val header: CoseCertificateHeader,
......
package de.rki.coronawarnapp.vaccination.core.qrcode package de.rki.coronawarnapp.vaccination.core.qrcode
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
data class VaccinationCertificateQRCode( data class VaccinationCertificateQRCode(
val qrCodeString: QrCodeString,
val parsedData: VaccinationCertificateData, val parsedData: VaccinationCertificateData,
val certificateCOSE: RawCOSEObject,
) { ) {
val uniqueCertificateIdentifier: String val uniqueCertificateIdentifier: String
get() = parsedData.certificate.vaccinationDatas.single().uniqueCertificateIdentifier get() = parsedData.certificate.vaccinationDatas.single().uniqueCertificateIdentifier
} }
typealias QrCodeString = String
...@@ -2,51 +2,68 @@ package de.rki.coronawarnapp.vaccination.core.qrcode ...@@ -2,51 +2,68 @@ package de.rki.coronawarnapp.vaccination.core.qrcode
import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
import de.rki.coronawarnapp.util.compression.inflate import de.rki.coronawarnapp.util.compression.inflate
import de.rki.coronawarnapp.util.encoding.decodeBase45 import de.rki.coronawarnapp.util.encoding.Base45Decoder
import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder
import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser
import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import okio.ByteString import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1Parser
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class VaccinationQRCodeExtractor @Inject constructor( class VaccinationQRCodeExtractor @Inject constructor(
private val vaccinationCertificateCOSEParser: VaccinationCertificateCOSEParser, private val coseDecoder: HealthCertificateCOSEDecoder,
private val headerParser: HealthCertificateHeaderParser,
private val bodyParser: VaccinationDGCV1Parser,
) : QrCodeExtractor<VaccinationCertificateQRCode> { ) : QrCodeExtractor<VaccinationCertificateQRCode> {
override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX) override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX)
override fun extract(rawString: String): VaccinationCertificateQRCode { override fun extract(rawString: String): VaccinationCertificateQRCode {
val rawCOSEObject = rawString val parsedData = rawString
.removePrefix(PREFIX) .removePrefix(PREFIX)
.tryDecodeBase45() .decodeBase45()
.decompress() .decompress()
.parse()
return VaccinationCertificateQRCode( return VaccinationCertificateQRCode(
parsedData = vaccinationCertificateCOSEParser.parse(rawCOSEObject), parsedData = parsedData,
certificateCOSE = rawCOSEObject, qrCodeString = rawString,
) )
} }
private fun String.tryDecodeBase45(): ByteString = try { private fun String.decodeBase45(): ByteArray = try {
this.decodeBase45() Base45Decoder.decode(this)
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e) Timber.e(e)
throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED) throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED)
} }
private fun ByteString.decompress(): RawCOSEObject = try { private fun ByteArray.decompress(): RawCOSEObject = try {
RawCOSEObject(this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT)) this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT)
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e) Timber.e(e)
throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
} }
fun RawCOSEObject.parse(): VaccinationCertificateData {
Timber.v("Parsing COSE for vaccination certificate.")
val cbor = coseDecoder.decode(this)
return VaccinationCertificateData(
header = headerParser.parse(cbor),
certificate = bodyParser.parse(cbor)
).also {
Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized)
}
}
companion object { companion object {
private const val PREFIX = "HC1:" private const val PREFIX = "HC1:"
// Zip bomb // Zip bomb
const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB
} }
} }
...@@ -10,8 +10,8 @@ import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson ...@@ -10,8 +10,8 @@ import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
import de.rki.coronawarnapp.vaccination.core.personIdentifier import de.rki.coronawarnapp.vaccination.core.personIdentifier
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException
import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData
import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer
...@@ -36,8 +36,8 @@ class VaccinationRepository @Inject constructor( ...@@ -36,8 +36,8 @@ class VaccinationRepository @Inject constructor(
dispatcherProvider: DispatcherProvider, dispatcherProvider: DispatcherProvider,
private val timeStamper: TimeStamper, private val timeStamper: TimeStamper,
private val storage: VaccinationStorage, private val storage: VaccinationStorage,
private val valueSetsRepository: ValueSetsRepository, valueSetsRepository: ValueSetsRepository,
private val vaccionationCoseParser: VaccinationCertificateCOSEParser, private val vaccinationQRCodeExtractor: VaccinationQRCodeExtractor,
) { ) {
private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow( private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow(
...@@ -102,7 +102,7 @@ class VaccinationRepository @Inject constructor( ...@@ -102,7 +102,7 @@ class VaccinationRepository @Inject constructor(
val newCertificate = qrCode.toVaccinationContainer( val newCertificate = qrCode.toVaccinationContainer(
scannedAt = timeStamper.nowUTC, scannedAt = timeStamper.nowUTC,
coseParser = vaccionationCoseParser, qrCodeExtractor = vaccinationQRCodeExtractor,
) )
val modifiedPerson = originalPerson.copy( val modifiedPerson = originalPerson.copy(
......
...@@ -7,14 +7,14 @@ import com.google.gson.reflect.TypeToken ...@@ -7,14 +7,14 @@ import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter import com.google.gson.stream.JsonWriter
import dagger.Reusable import dagger.Reusable
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
@Reusable @Reusable
class ContainerPostProcessor @Inject constructor( class ContainerPostProcessor @Inject constructor(
private val vaccinationCertificateCOSEParser: VaccinationCertificateCOSEParser, private val qrCodeExtractor: VaccinationQRCodeExtractor,
) : TypeAdapterFactory { ) : TypeAdapterFactory {
override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> { override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
val delegate = gson.getDelegateAdapter(this, type) val delegate = gson.getDelegateAdapter(this, type)
...@@ -30,7 +30,7 @@ class ContainerPostProcessor @Inject constructor( ...@@ -30,7 +30,7 @@ class ContainerPostProcessor @Inject constructor(
when (obj) { when (obj) {
is VaccinationContainer -> { is VaccinationContainer -> {
Timber.v("Injecting VaccinationContainer %s", obj.hashCode()) Timber.v("Injecting VaccinationContainer %s", obj.hashCode())
obj.parser = vaccinationCertificateCOSEParser obj.qrCodeExtractor = qrCodeExtractor
} }
} }
......
...@@ -6,33 +6,33 @@ import de.rki.coronawarnapp.ui.Country ...@@ -6,33 +6,33 @@ import de.rki.coronawarnapp.ui.Country
import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinatedPersonIdentifier
import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader import de.rki.coronawarnapp.vaccination.core.certificate.CoseCertificateHeader
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1
import de.rki.coronawarnapp.vaccination.core.personIdentifier import de.rki.coronawarnapp.vaccination.core.personIdentifier
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser import de.rki.coronawarnapp.vaccination.core.qrcode.QrCodeString
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateData
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet import de.rki.coronawarnapp.vaccination.core.server.valueset.VaccinationValueSet
import org.joda.time.Instant import org.joda.time.Instant
import org.joda.time.LocalDate import org.joda.time.LocalDate
@Keep @Keep
data class VaccinationContainer internal constructor( data class VaccinationContainer internal constructor(
@SerializedName("vaccinationCertificateCOSE") val vaccinationCertificateCOSE: RawCOSEObject, @SerializedName("vaccinationQrCode") val vaccinationQrCode: QrCodeString,
@SerializedName("scannedAt") val scannedAt: Instant, @SerializedName("scannedAt") val scannedAt: Instant,
) { ) {
// Either set by [ContainerPostProcessor] or via [toVaccinationContainer] // Either set by [ContainerPostProcessor] or via [toVaccinationContainer]
@Transient lateinit var parser: VaccinationCertificateCOSEParser @Transient lateinit var qrCodeExtractor: VaccinationQRCodeExtractor
@Transient internal var preParsedData: VaccinationCertificateData? = null @Transient internal var preParsedData: VaccinationCertificateData? = null
// Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null
@Suppress("unused") @Suppress("unused")
constructor() : this(RawCOSEObject.EMPTY, Instant.EPOCH) constructor() : this("", Instant.EPOCH)
@delegate:Transient @delegate:Transient
private val certificateData: VaccinationCertificateData by lazy { private val certificateData: VaccinationCertificateData by lazy {
preParsedData ?: parser.parse(vaccinationCertificateCOSE) preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode).parsedData
} }
val header: CoseCertificateHeader val header: CoseCertificateHeader
...@@ -91,16 +91,19 @@ data class VaccinationContainer internal constructor( ...@@ -91,16 +91,19 @@ data class VaccinationContainer internal constructor(
get() = header.issuedAt get() = header.issuedAt
override val expiresAt: Instant override val expiresAt: Instant
get() = header.expiresAt get() = header.expiresAt
override val vaccinationQrCodeString: QrCodeString
get() = vaccinationQrCode
} }
} }
fun VaccinationCertificateQRCode.toVaccinationContainer( fun VaccinationCertificateQRCode.toVaccinationContainer(
scannedAt: Instant, scannedAt: Instant,
coseParser: VaccinationCertificateCOSEParser, qrCodeExtractor: VaccinationQRCodeExtractor,
) = VaccinationContainer( ) = VaccinationContainer(
vaccinationCertificateCOSE = certificateCOSE, vaccinationQrCode = this.qrCodeString,
scannedAt = scannedAt, scannedAt = scannedAt,
).apply { ).apply {
parser = coseParser this.qrCodeExtractor = qrCodeExtractor
preParsedData = parsedData preParsedData = parsedData
} }
...@@ -6,7 +6,6 @@ import com.google.gson.Gson ...@@ -6,7 +6,6 @@ import com.google.gson.Gson
import de.rki.coronawarnapp.util.di.AppContext import de.rki.coronawarnapp.util.di.AppContext
import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.BaseGson
import de.rki.coronawarnapp.util.serialization.fromJson import de.rki.coronawarnapp.util.serialization.fromJson
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
...@@ -25,7 +24,6 @@ class VaccinationStorage @Inject constructor( ...@@ -25,7 +24,6 @@ class VaccinationStorage @Inject constructor(
private val gson by lazy { private val gson by lazy {
// Allow for custom type adapter. // Allow for custom type adapter.
baseGson.newBuilder().apply { baseGson.newBuilder().apply {
registerTypeAdapter(RawCOSEObject::class.java, RawCOSEObject.JsonAdapter())
registerTypeAdapterFactory(containerPostProcessor) registerTypeAdapterFactory(containerPostProcessor)
}.create() }.create()
} }
......
...@@ -12,13 +12,13 @@ class ZLIBCompressionTest : BaseTest() { ...@@ -12,13 +12,13 @@ class ZLIBCompressionTest : BaseTest() {
@Test @Test
fun `basic decompression`() { fun `basic decompression`() {
ZLIBCompression().decompress(compressed).utf8() shouldBe "The Cake Is A Lie" ZLIBCompression().decompress(compressed.toByteArray()) shouldBe "The Cake Is A Lie".toByteArray()
} }
@Test @Test
fun `invalid decompression`() { fun `invalid decompression`() {
val error = shouldThrow<InvalidInputException> { val error = shouldThrow<InvalidInputException> {
ZLIBCompression().decompress(compressed.substring(5)) ZLIBCompression().decompress(compressed.substring(5).toByteArray())
} }
error.causes().first { it is DataFormatException }.message shouldBe "incorrect header check" error.causes().first { it is DataFormatException }.message shouldBe "incorrect header check"
} }
......
package de.rki.coronawarnapp.util.encoding package de.rki.coronawarnapp.util.encoding
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import okio.ByteString.Companion.toByteString
import okio.internal.commonAsUtf8ToByteArray import okio.internal.commonAsUtf8ToByteArray
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import testhelpers.BaseTest import testhelpers.BaseTest
...@@ -10,15 +9,15 @@ class Base45ExtensionsTest : BaseTest() { ...@@ -10,15 +9,15 @@ class Base45ExtensionsTest : BaseTest() {
@Test @Test
fun `encode - extension`() { fun `encode - extension`() {
"AB".toByteArray().toByteString().base45() shouldBe "BB8" "AB".toByteArray().base45() shouldBe "BB8"
"Hello!!".commonAsUtf8ToByteArray().toByteString().base45() shouldBe "%69 VD92EX0" "Hello!!".commonAsUtf8ToByteArray().base45() shouldBe "%69 VD92EX0"
"base-45".commonAsUtf8ToByteArray().toByteString().base45() shouldBe "UJCLQE7W581" "base-45".commonAsUtf8ToByteArray().base45() shouldBe "UJCLQE7W581"
} }
@Test @Test
fun `decode - extension`() { fun `decode - extension`() {
"BB8".decodeBase45() shouldBe "AB".toByteArray().toByteString() "BB8".decodeBase45() shouldBe "AB".toByteArray()
"%69 VD92EX0".decodeBase45() shouldBe "Hello!!".toByteArray().toByteString() "%69 VD92EX0".decodeBase45() shouldBe "Hello!!".toByteArray()
"UJCLQE7W581".decodeBase45() shouldBe "base-45".toByteArray().toByteString() "UJCLQE7W581".decodeBase45() shouldBe "base-45".toByteArray()
} }
} }
package de.rki.coronawarnapp.vaccination.core
import com.google.gson.GsonBuilder
import de.rki.coronawarnapp.util.serialization.fromJson
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import testhelpers.BaseTest
class RawCOSEObjectTest : BaseTest() {
@BeforeEach
fun setup() {
}
@Test
fun `comparison and conversion`() {
val rawRaw = "The Cake Is A Lie!".toByteArray()
val rawRaw2 = "The Cake Is Not A Lie!".toByteArray() // This is a lie
val rawCOSEObject1 = RawCOSEObject(rawRaw)
val rawCOSEObject2 = RawCOSEObject(rawRaw2)
rawRaw shouldNotBe rawRaw2
rawCOSEObject1 shouldNotBe rawCOSEObject2
rawCOSEObject1.asByteArray shouldBe rawRaw
rawCOSEObject2.asByteArray shouldBe rawRaw2
}
@Test
fun `serialization and deserialization`() {
val rawRaw = "The Cake Is A Lie!".toByteArray()
val rawRaw2 = "The Cake Is Not A Lie!".toByteArray() // This is a lie
val rawCOSEObject1 = RawCOSEObject(rawRaw)
val rawCOSEObject2 = RawCOSEObject(rawRaw2)
val gson = GsonBuilder().apply {
registerTypeAdapter(RawCOSEObject::class.java, RawCOSEObject.JsonAdapter())
}.create()
val json1 = gson.toJson(rawCOSEObject1)
json1 shouldBe "\"VGhlIENha2UgSXMgQSBMaWUh\""
gson.fromJson<RawCOSEObject>(json1) shouldBe rawCOSEObject1
val json2 = gson.toJson(rawCOSEObject2)
json2 shouldBe "\"VGhlIENha2UgSXMgTm90IEEgTGllIQ\\u003d\\u003d\""
gson.fromJson<RawCOSEObject>(json2) shouldBe rawCOSEObject2
}
}
package de.rki.coronawarnapp.vaccination.core package de.rki.coronawarnapp.vaccination.core
import de.rki.coronawarnapp.util.compression.inflate
import de.rki.coronawarnapp.util.encoding.decodeBase45
import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1
import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateCOSEParser import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData
import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinationContainer
import org.joda.time.Instant import org.joda.time.Instant
import javax.inject.Inject import javax.inject.Inject
class VaccinationTestData @Inject constructor( class VaccinationTestData @Inject constructor(
private val vaccinationCertificateCOSEParser: VaccinationCertificateCOSEParser, private var qrCodeExtractor: VaccinationQRCodeExtractor,
) { ) {
// AndreasAstra1.pdf // AndreasAstra1.pdf
val personAVac1QR = val personAVac1QR =
"HC1:6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3KXH9M9ESIGUBA KWML%6S5B9-+P70Q5VC9:BPCNYKMXEE1JAA/CXGG0JK1WL260X638J3-E3ND3DAJ-43TTTO3HK1H3QBCWNZ83UQJ:T0/8F7V0HKN:Q8.HBV+0SZ4GH00T9UKP0T9WC5PF6846A\$Q$76QW6%V98T5\$FQMI5DN9QZ5Y0Q\$UPE%5MZ5*T57ZA\$O7T6LEJOA+MZ55EII-EB1EKC422JBBD0D2K.EJJ14B2MP41WTRZPQEC5L64HX6IAS 8S8FT/MAMXP6QS03L0QIRR97I2HOAXL92L0. KOKG8VG5SI:TU+MMPZ55%PBT1YEGEA7IB65C94JBQ2NLEE:NQ% GC3MXHFLF9OIFN0IZ95LJL80P1FDLW452I8941:HH3M41GTNP8EFUNT$.FTD852IWKP/HLIJL8JF8JF172IMAS EDAHMXFBFBQSKJE72KV\$FHJ%3O%6:XM+1QD+T2/VKKER3L3%1THL7MGY.1S:T:GLOX6OCE7+RWYL3.C-L27WNV0G::M74O%K7C50AAEI4" "HC1:6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3KXH9M9ESIGUBA KWML%6S5B9-+P70Q5VC9:BPCNYKMXEE1JAA/CXGG0JK1WL260X638J3-E3ND3DAJ-43TTTO3HK1H3QBCWNZ83UQJ:T0/8F7V0HKN:Q8.HBV+0SZ4GH00T9UKP0T9WC5PF6846A\$Q$76QW6%V98T5\$FQMI5DN9QZ5Y0Q\$UPE%5MZ5*T57ZA\$O7T6LEJOA+MZ55EII-EB1EKC422JBBD0D2K.EJJ14B2MP41WTRZPQEC5L64HX6IAS 8S8FT/MAMXP6QS03L0QIRR97I2HOAXL92L0. KOKG8VG5SI:TU+MMPZ55%PBT1YEGEA7IB65C94JBQ2NLEE:NQ% GC3MXHFLF9OIFN0IZ95LJL80P1FDLW452I8941:HH3M41GTNP8EFUNT$.FTD852IWKP/HLIJL8JF8JF172IMAS EDAHMXFBFBQSKJE72KV\$FHJ%3O%6:XM+1QD+T2/VKKER3L3%1THL7MGY.1S:T:GLOX6OCE7+RWYL3.C-L27WNV0G::M74O%K7C50AAEI4"
val personAVac1COSE: RawCOSEObject = personAVac1QR
.removePrefix("HC1:")
.decodeBase45().inflate()
.let { RawCOSEObject(data = it) }
val personAVac1Certificate = VaccinationDGCV1( val personAVac1Certificate = VaccinationDGCV1(
version = "1.0.0", version = "1.0.0",
nameData = VaccinationDGCV1.NameData( nameData = VaccinationDGCV1.NameData(
...@@ -50,20 +42,15 @@ class VaccinationTestData @Inject constructor( ...@@ -50,20 +42,15 @@ class VaccinationTestData @Inject constructor(
val personAVac1Container = VaccinationContainer( val personAVac1Container = VaccinationContainer(
scannedAt = Instant.ofEpochMilli(1620062834471), scannedAt = Instant.ofEpochMilli(1620062834471),
vaccinationCertificateCOSE = personAVac1COSE, vaccinationQrCode = personAVac1QR,
).apply { ).apply {
parser = vaccinationCertificateCOSEParser qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor
} }
// AndreasAstra2.pdf // AndreasAstra2.pdf
val personAVac2QR = val personAVac2QR =
"6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3D H9M9ESIGUBA KWMLYX1HXK 0DV:D5VC9:BPCNYKMXEE1JAA/CZIK0JK1WL260X638J3-E3ND3DAJ-43TTTMDF6S8:B73QN VNZ.0K6HYI3CNN96BPHNW*0I85V.499TXY9KK9%OC+G9QJPNF67J6QW67KQ9G66PPM4MLJE+.PDB9L6Q2+PFQ5DB96PP5/P-59A%N+892 7J235II3NJ7PK7SLQMIPUBN9CIZI.EJJ14B2MP41IZRZPQEC5L64HX6IAS 8SAFT/MAMXP6QS03L0QIRR97I2HOAXL92L0. KOKGGVG5SI:TU+MMPZ55%PBT1YEGEA7IB65C94JBQ2NLEE:NQ% GC3MXHFLF9OIFN0IZ95LJL80P1FDLW452I8941:HH3M41GTNP8EFUNT\$.FTD852IWKP/HLIJL8JF8JF172E2JA0K*WDQMPB8T3%KLUSR43M.F\$QBQDR\$VT7V01Y7J0BOZLH+D-QF6MO\$R3%XB+.4QI596GY\$SITJP5BS0DFROC.7B.2RTB*UNYSM$*00HIL+H" "6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3D H9M9ESIGUBA KWMLYX1HXK 0DV:D5VC9:BPCNYKMXEE1JAA/CZIK0JK1WL260X638J3-E3ND3DAJ-43TTTMDF6S8:B73QN VNZ.0K6HYI3CNN96BPHNW*0I85V.499TXY9KK9%OC+G9QJPNF67J6QW67KQ9G66PPM4MLJE+.PDB9L6Q2+PFQ5DB96PP5/P-59A%N+892 7J235II3NJ7PK7SLQMIPUBN9CIZI.EJJ14B2MP41IZRZPQEC5L64HX6IAS 8SAFT/MAMXP6QS03L0QIRR97I2HOAXL92L0. KOKGGVG5SI:TU+MMPZ55%PBT1YEGEA7IB65C94JBQ2NLEE:NQ% GC3MXHFLF9OIFN0IZ95LJL80P1FDLW452I8941:HH3M41GTNP8EFUNT\$.FTD852IWKP/HLIJL8JF8JF172E2JA0K*WDQMPB8T3%KLUSR43M.F\$QBQDR\$VT7V01Y7J0BOZLH+D-QF6MO\$R3%XB+.4QI596GY\$SITJP5BS0DFROC.7B.2RTB*UNYSM$*00HIL+H"
val personAVac2COSE: RawCOSEObject = personAVac2QR
.removePrefix("HC1:")
.decodeBase45().inflate()
.let { RawCOSEObject(data = it) }
val personAVac2Certificate = VaccinationDGCV1( val personAVac2Certificate = VaccinationDGCV1(
version = "1.0.0", version = "1.0.0",
nameData = VaccinationDGCV1.NameData( nameData = VaccinationDGCV1.NameData(
...@@ -91,9 +78,9 @@ class VaccinationTestData @Inject constructor( ...@@ -91,9 +78,9 @@ class VaccinationTestData @Inject constructor(
val personAVac2Container = VaccinationContainer( val personAVac2Container = VaccinationContainer(
scannedAt = Instant.ofEpochMilli(1620069934471), scannedAt = Instant.ofEpochMilli(1620069934471),
vaccinationCertificateCOSE = personAVac2COSE, vaccinationQrCode = personAVac2QR,
).apply { ).apply {
parser = vaccinationCertificateCOSEParser qrCodeExtractor = this@VaccinationTestData.qrCodeExtractor
} }
val personAData2Vac1Proof = VaccinatedPersonData( val personAData2Vac1Proof = VaccinatedPersonData(
......
...@@ -70,10 +70,10 @@ class VaccinationStorageTest : BaseTest() { ...@@ -70,10 +70,10 @@ class VaccinationStorageTest : BaseTest() {
{ {
"vaccinationData": [ "vaccinationData": [
{ {
"vaccinationCertificateCOSE": "${testData.personAVac1COSE.data.base64()}", "vaccinationQrCode": "${testData.personAVac1QR}",
"scannedAt": 1620062834471 "scannedAt": 1620062834471
}, { }, {
"vaccinationCertificateCOSE": "${testData.personAVac2COSE.data.base64()}", "vaccinationQrCode": "${testData.personAVac2QR}",
"scannedAt": 1620069934471 "scannedAt": 1620069934471
} }
] ]
......
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