From b803af8037a2cfd47e06e691c54dea13f26d26c0 Mon Sep 17 00:00:00 2001 From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com> Date: Mon, 7 Jun 2021 11:06:35 +0200 Subject: [PATCH] COSE decryption, QR code generation, error codes (EXPOSUREAPP-7506) (#3346) * wip * wiring * wip * testing * refactor exception tests * tests * decompression * decompression * fix tests * encrypt * second test case * add error codes * fix tests * move exception * tests * error codes * error codes * error codes * address comments * add more logging * reverse string file Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com> --- Corona-Warn-App/build.gradle | 2 + .../coronatest/TestCertificateRepository.kt | 2 +- .../type/TestCertificateContainer.kt | 2 +- .../cryptography/AesCryptography.kt | 27 +++- .../InvalidHealthCertificateException.kt | 73 +++++++++++ .../InvalidTestCertificateException.kt | 92 +++++++++++++ .../InvalidVaccinationCertificateException.kt | 61 +++++++++ .../covidcertificate/test/TestCertificate.kt | 2 +- .../test/TestCertificateDccParser.kt | 62 +++++++++ .../test/TestCertificateDccV1.kt | 34 ++--- .../test/TestCertificateQRCodeExtractor.kt | 119 ++++++++++++++++- .../util/compression/ZLIBCompression.kt | 20 +++ .../util/encoding/Base45Decoder.kt | 1 - .../core/CertificatePersonIdentifier.kt | 11 +- .../HealthCertificateCOSEDecoder.kt | 35 ++++- .../HealthCertificateHeaderParser.kt | 13 +- .../InvalidHealthCertificateException.kt | 95 -------------- .../certificate/VaccinationDGCV1Parser.kt | 25 ++-- .../core/qrcode/VaccinationQRCodeExtractor.kt | 21 ++- .../core/qrcode/VaccinationQRCodeValidator.kt | 6 +- .../core/repository/VaccinationRepository.kt | 6 +- .../values-de/green_certificate_strings.xml | 2 +- .../res/values/green_certificate_strings.xml | 2 +- .../coronatest/CoronaTestTestData.kt | 8 +- .../cryptography/AesCryptographyTest.kt | 18 +++ .../test/TestCertificateDccParserTest.kt | 42 ++++++ .../TestCertificateQRCodeExtractorTest.kt | 121 ++++++++++++++++++ .../covidcertificate/test/TestData.java | 20 +++ .../core/VaccinatedPersonIdentifierTest.kt | 17 +-- .../qrcode/VaccinationQRCodeExtractorTest.kt | 22 ++-- .../repository/VaccinationRepositoryTest.kt | 12 +- 31 files changed, 780 insertions(+), 193 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle index 948ff9a9f..e019130e2 100644 --- a/Corona-Warn-App/build.gradle +++ b/Corona-Warn-App/build.gradle @@ -441,4 +441,6 @@ dependencies { // HCert implementation("com.upokecenter:cbor:4.4.1") + + implementation("bouncycastle:bcprov-jdk16:136") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt index c367daa52..8f3c1b148 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt @@ -330,7 +330,7 @@ class TestCertificateRepository @Inject constructor( val extractedData = qrCodeExtractor.extract( decryptionKey = encryptionkey.toByteArray(), - encryptedCoseComponents = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray() + rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray() ) val nowUtc = timeStamper.nowUTC diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt index 40f3c02ce..741279d37 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt @@ -85,7 +85,7 @@ abstract class TestCertificateContainer { get() = testCertificate.testNameAndManufactor?.let { valueSet?.getDisplayText(it) ?: it } override val sampleCollectedAt: Instant get() = testCertificate.sampleCollectedAt - override val testResultAt: Instant + override val testResultAt: Instant? get() = testCertificate.testResultAt override val testCenter: String get() = testCertificate.testCenter diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt index 80ad40d4c..261fc2a02 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt @@ -1,16 +1,37 @@ package de.rki.coronawarnapp.covidcertificate.cryptography +import com.google.android.gms.common.util.Hex import dagger.Reusable -import okio.ByteString +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.util.encoders.Base64.decode +import java.security.Security +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.SecretKeySpec import javax.inject.Inject @Reusable class AesCryptography @Inject constructor() { + private val ivParameterSpec + get() = IvParameterSpec(Hex.stringToBytes("00000000000000000000000000000000")) + fun decrypt( decryptionKey: ByteArray, - encryptedData: ByteString + encryptedData: ByteArray ): ByteArray { - throw NotImplementedError() + Security.addProvider(BouncyCastleProvider()) + val keySpec = SecretKeySpec(decode(decryptionKey), ALGORITHM) + val input = decode(encryptedData) + return with(Cipher.getInstance(TRANSFORMATION)) { + init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec) + val output = ByteArray(getOutputSize(input.size)) + var outputLength = update(input, 0, input.size, output, 0) + outputLength += doFinal(output, outputLength) + output.copyOfRange(0, outputLength) + } } } + +private const val ALGORITHM = "AES" +private const val TRANSFORMATION = "AES/CBC/PKCS5Padding" diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt new file mode 100644 index 000000000..16b90b3db --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt @@ -0,0 +1,73 @@ +package de.rki.coronawarnapp.covidcertificate.exception + +import android.content.Context +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException +import de.rki.coronawarnapp.util.HasHumanReadableError +import de.rki.coronawarnapp.util.HumanReadableError +import de.rki.coronawarnapp.util.ui.CachedString +import de.rki.coronawarnapp.util.ui.LazyString + +@Suppress("MaxLineLength") +open class InvalidHealthCertificateException( + val errorCode: ErrorCode +) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) { + enum class ErrorCode( + val message: String + ) { + HC_BASE45_DECODING_FAILED("Base45 decoding failed."), + HC_BASE45_ENCODING_FAILED("Base45 encoding failed."), + HC_ZLIB_DECOMPRESSION_FAILED("Zlib decompression failed."), + HC_ZLIB_COMPRESSION_FAILED("Zlib compression failed."), + HC_COSE_TAG_INVALID("COSE tag invalid."), + HC_COSE_MESSAGE_INVALID("COSE message invalid."), + HC_CBOR_DECODING_FAILED("CBOR decoding failed."), + VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."), + NO_TEST_ENTRY("Test certificate missing."), + VC_PREFIX_INVALID("Prefix invalid."), + VC_STORING_FAILED("Storing failed."), + JSON_SCHEMA_INVALID("Json schema invalid."), + VC_NAME_MISMATCH("Name does not match."), + VC_ALREADY_REGISTERED("Certificate already registered."), + VC_DOB_MISMATCH("Date of birth does not match."), + HC_CWT_NO_DGC("Dgc missing."), + HC_CWT_NO_EXP("Expiration date missing."), + HC_CWT_NO_HCERT("Health certificate missing."), + HC_CWT_NO_ISS("Issuer missing."), + AES_DECRYPTION_FAILED("AES decryption failed"), + DCC_COMP_202("DCC Test Certificate Components failed with error 202: DCC pending."), + DCC_COMP_400("DCC Test Certificate Components failed with error 400: Bad request (e.g. wrong format of registration token)"), + DCC_COMP_404("DCC Test Certificate Components failed with error 404: Registration token does not exist."), + DCC_COMP_410("DCC Test Certificate Components failed with error 410: DCC already cleaned up."), + DCC_COMP_412("DCC Test Certificate Components failed with error 412: Test result not yet received"), + DCC_COMP_500_INTERNAL("DCC Test Certificate Components failed with error 500: Internal server error."), + DCC_COMP_500_LAB_INVALID_RESPONSE("DCC Test Certificate Components failed with error 500: Lab Invalid response"), + DCC_COMP_500_SIGNING_CLIENT_ERROR("DCC Test Certificate Components failed with error 500: Signing client error"), + DCC_COMP_500_SIGNING_SERVER_ERROR("DCC Test Certificate Components failed with error 500: Signing server error"), + DCC_COMP_NO_NETWORK("DCC Test Certificate Components failed due to no network connection."), + DCC_COSE_MESSAGE_INVALID("COSE message invalid."), + DCC_COSE_TAG_INVALID("COSE tag invalid."), + PKR_400("Public Key Registration failed with error 400: Bad request (e.g. wrong format of registration token or public key)."), + PKR_403("Public Key Registration failed with error 403: Registration token is not allowed to issue a DCC."), + PKR_404("Public Key Registration failed with error 404: Registration token does not exist."), + PKR_409("Public Key Registration failed with error 409: Registration token is already assigned to a public key."), + PKR_500("Public Key Registration failed with error 500: Internal server error."), + PKR_FAILED("Private key request failed."), + PKR_NO_NETWORK("Private key request failed due to no network connection."), + RSA_DECRYPTION_FAILED("RSA decryption failed."), + RSA_KP_GENERATION_FAILED("RSA key pair generation failed."), + } + + open val errorMessage: LazyString + get() = CachedString { context -> + context.getString(ERROR_MESSAGE_GENERIC) + } + + override fun toHumanReadableError(context: Context): HumanReadableError { + return HumanReadableError( + description = errorMessage.get(context) + ) + } +} + +private const val ERROR_MESSAGE_GENERIC = R.string.errors_generic_text_unknown_error_cause diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt new file mode 100644 index 000000000..ca8909e46 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt @@ -0,0 +1,92 @@ +package de.rki.coronawarnapp.covidcertificate.exception + +import android.content.Context +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.HumanReadableError +import de.rki.coronawarnapp.util.ui.CachedString +import de.rki.coronawarnapp.util.ui.LazyString + +class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) { + override fun toHumanReadableError(context: Context): HumanReadableError { + var errorCodeString = errorCode.toString() + errorCodeString = if (errorCodeString.startsWith(PREFIX_TC)) errorCodeString else PREFIX_TC + errorCodeString + return HumanReadableError( + description = errorMessage.get(context) + "\n\n$errorCodeString" + ) + } + + override val errorMessage: LazyString + get() = when (errorCode) { + + ErrorCode.DCC_COMP_NO_NETWORK, + ErrorCode.PKR_NO_NETWORK -> CachedString { context -> + context.getString(ERROR_MESSAGE_NO_NETWORK) + } + + ErrorCode.DCC_COMP_202 -> CachedString { context -> + context.getString(ERROR_MESSAGE_TRY_AGAIN_DCC_NOT_AVAILABLE_YET) + } + + ErrorCode.DCC_COMP_410 -> CachedString { context -> + context.getString(ERROR_MESSAGE_DCC_EXPIRED) + } + + // TODO +/* ErrorCode.HC_BASE45_DECODING_FAILED, + ErrorCode.HC_CBOR_DECODING_FAILED, + ErrorCode.HC_COSE_MESSAGE_INVALID, + ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED, + ErrorCode.HC_COSE_TAG_INVALID, + ErrorCode.HC_CWT_NO_DGC, + ErrorCode.HC_CWT_NO_EXP, + ErrorCode.HC_CWT_NO_HCERT, + ErrorCode.HC_CWT_NO_ISS, + ErrorCode.JSON_SCHEMA_INVALID,*/ + + ErrorCode.AES_DECRYPTION_FAILED, + ErrorCode.RSA_DECRYPTION_FAILED, + ErrorCode.DCC_COSE_MESSAGE_INVALID, + ErrorCode.DCC_COSE_TAG_INVALID, + ErrorCode.DCC_COMP_404, + ErrorCode.DCC_COMP_412, + ErrorCode.PKR_403, + ErrorCode.PKR_404 -> CachedString { context -> + context.getString(ERROR_MESSAGE_E2E_ERROR_CALL_HOTLINE) + } + + ErrorCode.DCC_COMP_400, + ErrorCode.PKR_400 -> CachedString { context -> + context.getString(ERROR_MESSAGE_CLIENT_ERROR_CALL_HOTLINE) + } + + ErrorCode.PKR_FAILED, + ErrorCode.RSA_KP_GENERATION_FAILED, + ErrorCode.PKR_500, + ErrorCode.DCC_COMP_500_INTERNAL -> CachedString { context -> + context.getString(ERROR_MESSAGE_TRY_AGAIN) + } + + ErrorCode.HC_BASE45_ENCODING_FAILED, + ErrorCode.HC_ZLIB_COMPRESSION_FAILED, + ErrorCode.NO_TEST_ENTRY, + ErrorCode.DCC_COMP_500_LAB_INVALID_RESPONSE, + ErrorCode.DCC_COMP_500_SIGNING_CLIENT_ERROR, + ErrorCode.DCC_COMP_500_SIGNING_SERVER_ERROR, + ErrorCode.PKR_409 -> CachedString { context -> + context.getString(ERROR_MESSAGE_GENERIC) + } + else -> super.errorMessage + } +} + +private const val PREFIX_TC = "TC_" +private const val ERROR_MESSAGE_GENERIC = R.string.errors_generic_text_unknown_error_cause + +// TODO change to correct error message once provided +private const val ERROR_MESSAGE_TRY_AGAIN = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_DCC_NOT_SUPPORTED_BY_LAB = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_NO_NETWORK = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_E2E_ERROR_CALL_HOTLINE = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_TRY_AGAIN_DCC_NOT_AVAILABLE_YET = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_CLIENT_ERROR_CALL_HOTLINE = ERROR_MESSAGE_GENERIC +private const val ERROR_MESSAGE_DCC_EXPIRED = ERROR_MESSAGE_GENERIC diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt new file mode 100644 index 000000000..0309cf245 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt @@ -0,0 +1,61 @@ +package de.rki.coronawarnapp.covidcertificate.exception + +import android.content.Context +import de.rki.coronawarnapp.R +import de.rki.coronawarnapp.util.HumanReadableError +import de.rki.coronawarnapp.util.ui.CachedString +import de.rki.coronawarnapp.util.ui.LazyString + +class InvalidVaccinationCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) { + override fun toHumanReadableError(context: Context): HumanReadableError { + var errorCodeString = errorCode.toString() + errorCodeString = if (errorCodeString.startsWith(PREFIX_VC)) errorCodeString else PREFIX_VC + errorCodeString + return HumanReadableError( + description = errorMessage.get(context) + "\n\n$errorCodeString" + ) + } + + override val errorMessage: LazyString + get() = when (errorCode) { + ErrorCode.VC_PREFIX_INVALID, + ErrorCode.HC_BASE45_DECODING_FAILED, + ErrorCode.HC_CBOR_DECODING_FAILED, + ErrorCode.HC_COSE_MESSAGE_INVALID, + ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED, + ErrorCode.HC_COSE_TAG_INVALID, + ErrorCode.HC_CWT_NO_DGC, + ErrorCode.HC_CWT_NO_EXP, + ErrorCode.HC_CWT_NO_HCERT, + ErrorCode.HC_CWT_NO_ISS, + ErrorCode.JSON_SCHEMA_INVALID, + -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_INVALID) + } + + ErrorCode.VC_NO_VACCINATION_ENTRY -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) + } + + ErrorCode.VC_STORING_FAILED -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) + } + + ErrorCode.VC_NAME_MISMATCH, + ErrorCode.VC_DOB_MISMATCH -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON) + } + + ErrorCode.VC_ALREADY_REGISTERED -> CachedString { context -> + context.getString(ERROR_MESSAGE_VC_ALREADY_REGISTERED) + } + else -> super.errorMessage + } +} + +private const val PREFIX_VC = "VC_" + +private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid +private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported +private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again +private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person +private const val ERROR_MESSAGE_VC_ALREADY_REGISTERED = R.string.error_vc_already_registered diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt index 4d246e1dc..85196fbe9 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt @@ -28,7 +28,7 @@ interface TestCertificate { */ val testNameAndManufactor: String? val sampleCollectedAt: Instant - val testResultAt: Instant + val testResultAt: Instant? val testCenter: String val certificateIssuer: String diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt new file mode 100644 index 000000000..78fe73ea1 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt @@ -0,0 +1,62 @@ +package de.rki.coronawarnapp.covidcertificate.test + +import com.google.gson.Gson +import com.upokecenter.cbor.CBORObject +import dagger.Reusable +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY +import de.rki.coronawarnapp.covidcertificate.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.util.serialization.BaseGson +import de.rki.coronawarnapp.util.serialization.fromJson +import timber.log.Timber +import javax.inject.Inject + +@Reusable +class TestCertificateDccParser @Inject constructor( + @BaseGson private val gson: Gson, +) { + + fun parse(map: CBORObject): TestCertificateDccV1 = try { + val certificate: TestCertificateDccV1 = map[keyHCert]?.run { + this[keyEuDgcV1]?.run { + toCertificate() + } ?: throw InvalidTestCertificateException(HC_CWT_NO_DGC) + } ?: throw InvalidTestCertificateException(HC_CWT_NO_HCERT) + + certificate.validate() + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED) + } + + private fun TestCertificateDccV1.validate(): TestCertificateDccV1 { + if (testCertificateData.isNullOrEmpty()) { + throw InvalidTestCertificateException(NO_TEST_ENTRY) + } + // Force date parsing + dateOfBirth + testCertificateData.forEach { + it.testResultAt + it.sampleCollectedAt + } + return this + } + + private fun CBORObject.toCertificate() = try { + val json = ToJSONString() + gson.fromJson<TestCertificateDccV1>(json) + } catch (e: Throwable) { + Timber.e(e) + throw InvalidTestCertificateException(JSON_SCHEMA_INVALID) + } + + companion object { + private val keyEuDgcV1 = CBORObject.FromObject(1) + private val keyHCert = CBORObject.FromObject(-260) + } +} diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt index 35182a1ce..1ef5539ab 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt @@ -8,7 +8,7 @@ data class TestCertificateDccV1( @SerializedName("ver") val version: String, @SerializedName("nam") val nameData: NameData, @SerializedName("dob") val dob: String, - @SerializedName("v") val testCertificateData: List<TestCertificateData>, + @SerializedName("t") val testCertificateData: List<TestCertificateData>, ) { data class NameData( @SerializedName("fn") val familyName: String?, @@ -20,22 +20,19 @@ data class TestCertificateDccV1( data class TestCertificateData( // Disease or agent targeted, e.g. "tg": "840539006" @SerializedName("tg") val targetId: String, - // Type of Test (required) + // Type of Test (required) eg "LP217198-3" @SerializedName("tt") val testType: String, - // Test Result (required) + // Test Result (required) e. g. "tr": "260415000" @SerializedName("tr") val testResult: String, - // NAA Test Name (only for PCR tests, but not required) - @SerializedName("nm") val testName: String?, + // NAA Test Name (only for PCR tests, but not required) "nm": "Roche LightCycler qPCR", + @SerializedName("nm") val testName: String? = null, // RAT Test name and manufacturer (only for RAT tests, but not required) - @SerializedName("ma") val testNameAndManufactor: String?, - // Date/Time of Sample Collection (required) - // "sc": "2021-04-13T14:20:00+00:00", - @SerializedName("sc") val sampleCollectedAt: Instant, - // Date/Time of Test Result - // "dr": "2021-04-13T14:40:01+00:00", - @SerializedName("dr") val testResultAt: Instant, - // Testing Center (required) - // "tc": "GGD Fryslân, L-Heliconweg", + @SerializedName("ma") val testNameAndManufactor: String? = null, + // Date/Time of Sample Collection (required) "sc": "2021-04-13T14:20:00+00:00" + @SerializedName("sc") val sc: String, + // Date/Time of Test Result "dr": "2021-04-13T14:40:01+00:00", + @SerializedName("dr") val dr: String? = null, + // Testing Center (required) "tc": "GGD Fryslân, L-Heliconweg", @SerializedName("tc") val testCenter: String, // Country of Test (required) @SerializedName("co") val countryOfTest: String, @@ -43,7 +40,14 @@ data class TestCertificateDccV1( @SerializedName("is") val certificateIssuer: String, // Unique Certificate Identifier, e.g. "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ" @SerializedName("ci") val uniqueCertificateIdentifier: String - ) + ) { + + val testResultAt: Instant? + get() = dr?.let { Instant.parse(it) } + + val sampleCollectedAt: Instant + get() = Instant.parse(sc) + } val dateOfBirth: LocalDate get() = LocalDate.parse(dob) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt index 6737944ce..92c075703 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt @@ -1,25 +1,130 @@ package de.rki.coronawarnapp.covidcertificate.test +import com.upokecenter.cbor.CBORObject import dagger.Reusable +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.util.compression.deflate +import de.rki.coronawarnapp.util.compression.inflate +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.RawCOSEObject +import timber.log.Timber import javax.inject.Inject @Reusable -class TestCertificateQRCodeExtractor @Inject constructor() { +class TestCertificateQRCodeExtractor @Inject constructor( + private val coseDecoder: HealthCertificateCOSEDecoder, + private val headerParser: HealthCertificateHeaderParser, + private val bodyParser: TestCertificateDccParser, +) { /** - * May throw an **[InvalidHealthCertificateException]** + * May throw an **[InvalidTestCertificateException]** */ fun extract( decryptionKey: ByteArray, - encryptedCoseComponents: ByteArray, + rawCoseObjectEncrypted: ByteArray, ): TestCertificateQRCode { - throw NotImplementedError() + val rawCoseObject = rawCoseObjectEncrypted.decrypt(decryptionKey) + return TestCertificateQRCode( + testCertificateData = rawCoseObject.decode(), + qrCode = rawCoseObject.encode() + ) } /** - * May throw an **[InvalidHealthCertificateException]** + * May throw an **[InvalidTestCertificateException]** */ - fun extract(qrCode: String): TestCertificateQRCode { - throw NotImplementedError() + fun extract(qrCode: String) = TestCertificateQRCode( + testCertificateData = qrCode.extract(), + qrCode = qrCode + ) + + private fun RawCOSEObject.decrypt(decryptionKey: ByteArray): RawCOSEObject = try { + coseDecoder.decryptMessage( + input = this, + decryptionKey = decryptionKey + ) + } catch (e: InvalidHealthCertificateException) { + throw InvalidTestCertificateException(e.errorCode) + } catch (e: Throwable) { + Timber.e(e, HC_COSE_MESSAGE_INVALID.toString()) + throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID) + } + + private fun String.extract(): TestCertificateData = + removePrefix(PREFIX) + .decodeBase45() + .decompress() + .decode() + + private fun RawCOSEObject.encode(): String { + return PREFIX + compress().encodeBase45() + } + + private fun RawCOSEObject.decode(): TestCertificateData = try { + coseDecoder.decode(this).parse() + } catch (e: InvalidHealthCertificateException) { + throw InvalidTestCertificateException(e.errorCode) + } catch (e: Throwable) { + Timber.e(e, HC_COSE_MESSAGE_INVALID.toString()) + throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID) + } + + private fun CBORObject.parse(): TestCertificateData = try { + TestCertificateData( + header = headerParser.parse(this), + certificate = bodyParser.parse(this) + ).also { + Timber.v("Parsed test certificate for %s", it.certificate.nameData.givenNameStandardized) + } + } catch (e: InvalidHealthCertificateException) { + throw InvalidTestCertificateException(e.errorCode) + } catch (e: Throwable) { + Timber.e(e, HC_CBOR_DECODING_FAILED.toString()) + throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED) + } + + private fun String.decodeBase45(): ByteArray = try { + Base45Decoder.decode(this) + } catch (e: Throwable) { + Timber.e(e, HC_BASE45_DECODING_FAILED.toString()) + throw InvalidTestCertificateException(HC_BASE45_DECODING_FAILED) + } + + private fun ByteArray.encodeBase45(): String = try { + Base45Decoder.encode(this) + } catch (e: Throwable) { + Timber.e(e, HC_BASE45_ENCODING_FAILED.toString()) + throw InvalidTestCertificateException(HC_BASE45_ENCODING_FAILED) + } + + private fun RawCOSEObject.compress(): ByteArray = try { + this.deflate() + } catch (e: Throwable) { + Timber.e(e, HC_ZLIB_COMPRESSION_FAILED.toString()) + throw InvalidTestCertificateException(HC_ZLIB_COMPRESSION_FAILED) + } + + private fun ByteArray.decompress(): RawCOSEObject = try { + this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT) + } catch (e: Throwable) { + Timber.e(e, HC_ZLIB_DECOMPRESSION_FAILED.toString()) + throw InvalidTestCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) + } + + companion object { + private const val PREFIX = "HC1:" + + // Zip bomb + private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt index d03ef22a3..e4ddeec39 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt @@ -2,6 +2,9 @@ package de.rki.coronawarnapp.util.compression import okio.Buffer import okio.inflate +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.util.zip.Deflater import java.util.zip.Inflater import javax.inject.Inject @@ -25,8 +28,25 @@ class ZLIBCompression @Inject constructor() { sink.readByteArray() } catch (e: Throwable) { + Timber.e(e) throw InvalidInputException("ZLIB decompression failed.", e) } + + fun compress(input: ByteArray): ByteArray { + val deflater = Deflater() + deflater.setInput(input) + val outputStream = ByteArrayOutputStream(input.size) + deflater.finish() + val buffer = ByteArray(1024) + while (!deflater.finished()) { + val count = deflater.deflate(buffer) + outputStream.write(buffer, 0, count) + } + outputStream.close() + return outputStream.toByteArray() + } } fun ByteArray.inflate(sizeLimit: Long = -1L) = ZLIBCompression().decompress(this, sizeLimit) + +fun ByteArray.deflate() = ZLIBCompression().compress(this) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt index 39320eb27..b8cebf7a4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt @@ -23,7 +23,6 @@ import java.math.BigInteger * Based on * https://github.com/ehn-digital-green-development/hcert-kotlin/blob/23203fbb71f53524ee643a9df116264f87b5b32a/src/main/kotlin/ehn/techiop/hcert/kotlin/chain/common/Base45Encoder.kt */ -@OptIn(ExperimentalUnsignedTypes::class) object Base45Decoder { private const val alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" private val int45 = BigInteger.valueOf(45) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt index 4350879cc..3c4744fc3 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt @@ -1,10 +1,11 @@ package de.rki.coronawarnapp.vaccination.core +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.covidcertificate.test.TestCertificateDccV1 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCode import de.rki.coronawarnapp.util.HashExtensions.toSHA256 -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode import org.joda.time.LocalDate @@ -37,15 +38,15 @@ data class CertificatePersonIdentifier( fun requireMatch(other: CertificatePersonIdentifier) { if (lastNameStandardized != other.lastNameStandardized) { Timber.d("Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized") - throw InvalidHealthCertificateException(ErrorCode.VC_NAME_MISMATCH) + throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH) } if (firstNameStandardized != other.firstNameStandardized) { Timber.d("Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized") - throw InvalidHealthCertificateException(ErrorCode.VC_NAME_MISMATCH) + throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH) } if (dateOfBirth != other.dateOfBirth) { Timber.d("Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth") - throw InvalidHealthCertificateException(ErrorCode.VC_DOB_MISMATCH) + throw InvalidVaccinationCertificateException(VC_DOB_MISMATCH) } } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt index fbb291dcd..2fd765e1e 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt @@ -1,12 +1,18 @@ package de.rki.coronawarnapp.vaccination.core.certificate import com.upokecenter.cbor.CBORObject -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID +import de.rki.coronawarnapp.covidcertificate.cryptography.AesCryptography +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.AES_DECRYPTION_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID +import de.rki.coronawarnapp.util.encoding.base64 import timber.log.Timber import javax.inject.Inject -class HealthCertificateCOSEDecoder @Inject constructor() { +class HealthCertificateCOSEDecoder @Inject constructor( + private val aesEncryptor: AesCryptography +) { fun decode(input: RawCOSEObject): CBORObject = try { val messageObject = CBORObject.DecodeFromBytes(input).validate() @@ -19,6 +25,29 @@ class HealthCertificateCOSEDecoder @Inject constructor() { throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID) } + fun decryptMessage(input: RawCOSEObject, decryptionKey: ByteArray): RawCOSEObject = try { + val messageObject = CBORObject.DecodeFromBytes(input).validate() + val content = messageObject[2].GetByteString() + val decrypted = content.decrypt(decryptionKey) + messageObject[2] = CBORObject.FromObject(decrypted) + messageObject.EncodeToBytes() + } catch (e: InvalidHealthCertificateException) { + throw e + } catch (e: Throwable) { + Timber.e(e) + throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID) + } + + private fun ByteArray.decrypt(decryptionKey: ByteArray) = try { + aesEncryptor.decrypt( + decryptionKey = decryptionKey, + encryptedData = this.base64().toByteArray() + ) + } catch (e: Throwable) { + Timber.e(e) + throw InvalidHealthCertificateException(AES_DECRYPTION_FAILED) + } + private fun CBORObject.validate(): CBORObject { if (size() != 4) { throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID) diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt index e93687820..3bfa90a6d 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt @@ -2,9 +2,10 @@ package de.rki.coronawarnapp.vaccination.core.certificate import com.upokecenter.cbor.CBORObject import dagger.Reusable -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_EXP +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS import org.joda.time.Instant import javax.inject.Inject @@ -12,15 +13,15 @@ import javax.inject.Inject class HealthCertificateHeaderParser @Inject constructor() { fun parse(map: CBORObject): CoseCertificateHeader = try { - val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS) + val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(HC_CWT_NO_ISS) val issuedAt: Instant = map[keyIssuedAt]?.run { Instant.ofEpochSecond(AsNumber().ToInt64Checked()) - } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS) + } ?: throw InvalidHealthCertificateException(HC_CWT_NO_ISS) val expiresAt: Instant = map[keyExpiresAt]?.run { Instant.ofEpochSecond(AsNumber().ToInt64Checked()) - } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_EXP) + } ?: throw InvalidHealthCertificateException(HC_CWT_NO_EXP) HealthCertificateHeader( issuer = issuer, diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt deleted file mode 100644 index 700ab2ddc..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt +++ /dev/null @@ -1,95 +0,0 @@ -package de.rki.coronawarnapp.vaccination.core.certificate - -import android.content.Context -import de.rki.coronawarnapp.R -import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException -import de.rki.coronawarnapp.util.HasHumanReadableError -import de.rki.coronawarnapp.util.HumanReadableError -import de.rki.coronawarnapp.util.ui.CachedString -import de.rki.coronawarnapp.util.ui.LazyString -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_STORING_FAILED - -class InvalidHealthCertificateException( - val errorCode: ErrorCode -) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) { - enum class ErrorCode( - val message: String - ) { - HC_BASE45_DECODING_FAILED("Base45 decoding failed."), - HC_ZLIB_DECOMPRESSION_FAILED("Zlib decompression failed."), - HC_COSE_TAG_INVALID("COSE tag invalid."), - HC_COSE_MESSAGE_INVALID("COSE message invalid."), - HC_CBOR_DECODING_FAILED("CBOR decoding failed."), - VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."), - VC_PREFIX_INVALID("Prefix invalid."), - VC_STORING_FAILED("Storing failed."), - VC_JSON_SCHEMA_INVALID("Json schema invalid."), - VC_NAME_MISMATCH("Name does not match."), - VC_ALREADY_REGISTERED("Certificate already registered."), - VC_DOB_MISMATCH("Date of birth does not match."), - VC_HC_CWT_NO_DGC("Dgc missing."), - VC_HC_CWT_NO_EXP("Expiration date missing."), - VC_HC_CWT_NO_HCERT("Health certificate missing."), - VC_HC_CWT_NO_ISS("Issuer missing."), - } - - val errorMessage: LazyString - get() = when (errorCode) { - HC_BASE45_DECODING_FAILED, - HC_CBOR_DECODING_FAILED, - HC_COSE_MESSAGE_INVALID, - HC_ZLIB_DECOMPRESSION_FAILED, - HC_COSE_TAG_INVALID, - VC_PREFIX_INVALID, - VC_HC_CWT_NO_DGC, - VC_HC_CWT_NO_EXP, - VC_HC_CWT_NO_HCERT, - VC_HC_CWT_NO_ISS, - VC_JSON_SCHEMA_INVALID, - -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_INVALID) - } - VC_NO_VACCINATION_ENTRY -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED) - } - VC_STORING_FAILED -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN) - } - VC_NAME_MISMATCH, VC_DOB_MISMATCH -> CachedString { context -> - context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON) - } - VC_ALREADY_REGISTERED -> CachedString { context -> - context.getString(ERROR_MESSAGE_ALREADY_REGISTERED) - } - } - - override fun toHumanReadableError(context: Context): HumanReadableError { - var errorCodeString = errorCode.toString() - errorCodeString = if (errorCodeString.startsWith(PREFIX)) errorCodeString else PREFIX + errorCodeString - return HumanReadableError( - description = errorMessage.get(context) + "\n\n$errorCodeString" - ) - } -} - -private const val PREFIX = "VC_" -private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid -private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported -private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again -private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person -private const val ERROR_MESSAGE_ALREADY_REGISTERED = R.string.error_vc_already_registered diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt index e25defbc9..2f9e46d59 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt @@ -3,13 +3,15 @@ package de.rki.coronawarnapp.vaccination.core.certificate import com.google.gson.Gson import com.upokecenter.cbor.CBORObject import dagger.Reusable +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.serialization.BaseGson import de.rki.coronawarnapp.util.serialization.fromJson -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import javax.inject.Inject @Reusable @@ -21,19 +23,18 @@ class VaccinationDGCV1Parser @Inject constructor( val certificate: VaccinationDGCV1 = map[keyHCert]?.run { this[keyEuDgcV1]?.run { toCertificate() - } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC) - } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT) - + } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_DGC) + } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_HCERT) certificate.validate() } catch (e: InvalidHealthCertificateException) { throw e } catch (e: Throwable) { - throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED) + throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED) } private fun VaccinationDGCV1.validate(): VaccinationDGCV1 { - if (vaccinationDatas.isEmpty()) { - throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY) + if (vaccinationDatas.isNullOrEmpty()) { + throw InvalidVaccinationCertificateException(VC_NO_VACCINATION_ENTRY) } // Force date parsing dateOfBirth @@ -47,7 +48,7 @@ class VaccinationDGCV1Parser @Inject constructor( val json = ToJSONString() gson.fromJson<VaccinationDGCV1>(json) } catch (e: Throwable) { - throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID) + throw InvalidVaccinationCertificateException(JSON_SCHEMA_INVALID) } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt index ae3065df3..b987371d4 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt @@ -2,13 +2,15 @@ package de.rki.coronawarnapp.vaccination.core.qrcode import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.compression.inflate 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.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.RawCOSEObject import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1Parser import timber.log.Timber @@ -41,21 +43,21 @@ class VaccinationQRCodeExtractor @Inject constructor( Base45Decoder.decode(this) } catch (e: Throwable) { Timber.e(e) - throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED) + throw InvalidVaccinationCertificateException(HC_BASE45_DECODING_FAILED) } private fun ByteArray.decompress(): RawCOSEObject = try { this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT) } catch (e: Throwable) { Timber.e(e) - throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) + throw InvalidVaccinationCertificateException(HC_ZLIB_DECOMPRESSION_FAILED) } - fun RawCOSEObject.parse(): VaccinationCertificateData { + fun RawCOSEObject.parse(): VaccinationCertificateData = try { Timber.v("Parsing COSE for vaccination certificate.") val cbor = coseDecoder.decode(this) - return VaccinationCertificateData( + VaccinationCertificateData( header = headerParser.parse(cbor), certificate = bodyParser.parse(cbor) ).also { @@ -63,6 +65,11 @@ class VaccinationQRCodeExtractor @Inject constructor( }.also { Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized) } + } catch (e: InvalidHealthCertificateException) { + throw InvalidVaccinationCertificateException(e.errorCode) + } catch (e: Throwable) { + Timber.e(e) + throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED) } companion object { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt index 3e7f60396..c5dae3f0c 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt @@ -2,8 +2,8 @@ package de.rki.coronawarnapp.vaccination.core.qrcode import dagger.Reusable import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import timber.log.Timber import javax.inject.Inject @@ -19,7 +19,7 @@ class VaccinationQRCodeValidator @Inject constructor( return findExtractor(rawString) ?.extract(rawString) ?.also { Timber.i("Extracted data from QR code is %s", it) } - ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID) + ?: throw InvalidVaccinationCertificateException(VC_PREFIX_INVALID) } private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? { diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt index c134c34fc..7e38a1bbc 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt @@ -1,6 +1,8 @@ package de.rki.coronawarnapp.vaccination.core.repository import de.rki.coronawarnapp.bugreporting.reportProblem +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.coroutine.AppScope import de.rki.coronawarnapp.util.coroutine.DispatcherProvider @@ -9,8 +11,6 @@ import de.rki.coronawarnapp.util.flow.combine import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode import de.rki.coronawarnapp.vaccination.core.personIdentifier import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor @@ -101,7 +101,7 @@ class VaccinationRepository @Inject constructor( if (originalPerson.data.vaccinations.any { it.certificateId == qrCode.uniqueCertificateIdentifier }) { Timber.tag(TAG).e("Certificate is already registered: %s", qrCode.uniqueCertificateIdentifier) - throw InvalidHealthCertificateException(ErrorCode.VC_ALREADY_REGISTERED) + throw InvalidVaccinationCertificateException(VC_ALREADY_REGISTERED) } val newCertificate = qrCode.toVaccinationContainer( diff --git a/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml b/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml index 0e998b4cb..3061c74c4 100644 --- a/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml +++ b/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml @@ -48,4 +48,4 @@ <string name="info_banner_title_2">COVID-Testzertifikat</string> <!-- XTXT: Green certificate info card body --> <string name="info_banner_body">Registrieren Sie einen Test auf der Startseite und stimmen Sie zu, ein digitales Testzertifikat zu erhalten. Sobald das Zertifikat vorliegt, wird es hier angezeigt.</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml b/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml index 231c92379..946f819e0 100644 --- a/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml +++ b/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml @@ -49,4 +49,4 @@ <!-- XTXT: Green certificate info card body --> <string name="info_banner_body">Registrieren Sie einen Test auf der Startseite und stimmen Sie zu, ein digitales Testzertifikat zu erhalten. Sobald das Zertifikat vorliegt, wird es hier angezeigt.</string> -</resources> \ No newline at end of file +</resources> diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt index 9b4ce4c49..102501c7d 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt @@ -39,8 +39,8 @@ class CoronaTestTestData @Inject constructor( TestCertificateDccV1.TestCertificateData( targetId = "840539006", countryOfTest = "DE", - sampleCollectedAt = Instant.EPOCH, - testResultAt = Instant.EPOCH, + sc = Instant.EPOCH.toDateTime().toString(), + dr = Instant.EPOCH.toDateTime().toString(), testCenter = "TODO", testName = "TODO", testNameAndManufactor = "TODO", @@ -100,8 +100,8 @@ class CoronaTestTestData @Inject constructor( TestCertificateDccV1.TestCertificateData( targetId = "840539006", countryOfTest = "DE", - sampleCollectedAt = Instant.EPOCH, - testResultAt = Instant.EPOCH, + sc = Instant.EPOCH.toDateTime().toString(), + dr = Instant.EPOCH.toDateTime().toString(), testCenter = "TODO", testName = "TODO", testNameAndManufactor = "TODO", diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt new file mode 100644 index 000000000..1d9ecfde2 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt @@ -0,0 +1,18 @@ +package de.rki.coronawarnapp.covidcertificate.cryptography + +import io.kotest.matchers.shouldBe +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class AesCryptographyTest : BaseTest() { + + @Test + fun `decrypt Hello World`() { + val des = "d56t/juMw5r4qNx1n1igs1pobUjZBT5yq0Ct7MHUuKM=".toByteArray() + val encryptedString = "WFOLewp8DWqY/8IWUHEDwg==".toByteArray() + AesCryptography().decrypt( + des, + encryptedData = encryptedString + ) shouldBe "Hello World".toByteArray() + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt new file mode 100644 index 000000000..64d989fb9 --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt @@ -0,0 +1,42 @@ +package de.rki.coronawarnapp.covidcertificate.test + +import com.google.gson.Gson +import com.upokecenter.cbor.CBORObject +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeHex +import org.joda.time.LocalDate +import org.junit.jupiter.api.Test + +class TestCertificateDccParserTest { + + private val bodyParser = TestCertificateDccParser(Gson()) + + @Test + fun `happy path cose decryption with Ellen Cheng`() { + val coseObject = CBORObject.DecodeFromBytes(TestData.cborObject.decodeHex().toByteArray()) + with(bodyParser.parse(coseObject)) { + + with(nameData) { + familyName shouldBe "Musterfrau-Gößinger" + familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER" + givenName shouldBe "Gabriele" + givenNameStandardized shouldBe "GABRIELE" + } + dob shouldBe "1998-02-26" + dateOfBirth shouldBe LocalDate.parse("1998-02-26") + version shouldBe "1.2.1" + + with(testCertificateData[0]) { + uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3" + countryOfTest shouldBe "AT" + certificateIssuer shouldBe "Ministry of Health, Austria" + targetId shouldBe "840539006" + sampleCollectedAt shouldBe org.joda.time.Instant.parse("2021-02-20T12:34:56+00:00") + testType shouldBe "LP217198-3" + testCenter shouldBe "Testing center Vienna 1" + testNameAndManufactor shouldBe "1232" + testResult shouldBe "260415000" + } + } + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt new file mode 100644 index 000000000..ef3f817ac --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt @@ -0,0 +1,121 @@ +package de.rki.coronawarnapp.covidcertificate.test + +import com.google.gson.Gson +import de.rki.coronawarnapp.covidcertificate.cryptography.AesCryptography +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException +import de.rki.coronawarnapp.covidcertificate.exception.InvalidTestCertificateException +import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData +import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder +import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.matchers.shouldBe +import okio.ByteString.Companion.decodeBase64 +import org.joda.time.Instant +import org.joda.time.LocalDate +import org.junit.jupiter.api.Test +import testhelpers.BaseTest + +class TestCertificateQRCodeExtractorTest : BaseTest() { + private val coseDecoder = HealthCertificateCOSEDecoder(AesCryptography()) + private val headerParser = HealthCertificateHeaderParser() + private val bodyParser = TestCertificateDccParser(Gson()) + private val extractor = TestCertificateQRCodeExtractor(coseDecoder, headerParser, bodyParser) + + @Test + fun `happy path qr code`() { + val qrCode = extractor.extract(TestData.qrCodeTestCertificate) + with(qrCode.testCertificateData.header) { + issuer shouldBe "AT" + issuedAt shouldBe Instant.parse("2021-06-01T10:12:48.000Z") + expiresAt shouldBe Instant.parse("2021-06-03T10:12:48.000Z") + } + + with(qrCode.testCertificateData.certificate) { + with(nameData) { + familyName shouldBe "Musterfrau-Gößinger" + familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER" + givenName shouldBe "Gabriele" + givenNameStandardized shouldBe "GABRIELE" + } + dob shouldBe "1998-02-26" + dateOfBirth shouldBe LocalDate.parse("1998-02-26") + version shouldBe "1.2.1" + + with(testCertificateData[0]) { + uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3" + countryOfTest shouldBe "AT" + certificateIssuer shouldBe "Ministry of Health, Austria" + targetId shouldBe "840539006" + sampleCollectedAt shouldBe Instant.parse("2021-02-20T12:34:56+00:00") + testType shouldBe "LP217198-3" + testCenter shouldBe "Testing center Vienna 1" + testNameAndManufactor shouldBe "1232" + testResult shouldBe "260415000" + } + } + } + + @Test + fun `happy path cose decryption with Ellen Cheng`() { + with(TestData.EllenCheng()) { + val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray() + val dek = dek.toByteArray() + val result = extractor.extract(dek, coseObject) + with(result.testCertificateData.certificate.nameData) { + familyName shouldBe "Cheng" + givenName shouldBe "Ellen" + } + val result2 = extractor.extract(result.qrCode) + with(result2.testCertificateData.certificate.nameData) { + familyName shouldBe "Cheng" + givenName shouldBe "Ellen" + } + } + } + + @Test + fun `happy path cose decryption with Brian Calamandrei`() { + with(TestData.BrianCalamandrei()) { + val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray() + val dek = dek.toByteArray() + val result = extractor.extract(dek, coseObject) + with(result.testCertificateData.certificate.nameData) { + familyName shouldBe "Calamandrei" + givenName shouldBe "Brian" + } + val result2 = extractor.extract(result.qrCode) + with(result2.testCertificateData.certificate.nameData) { + familyName shouldBe "Calamandrei" + givenName shouldBe "Brian" + } + } + } + + @Test + fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() { + shouldThrow<InvalidTestCertificateException> { + extractor.extract(VaccinationQrCodeTestData.validEncoded) + }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS + } + + @Test + fun `random string fails with HC_BASE45_DECODING_FAILED`() { + shouldThrow<InvalidTestCertificateException> { + extractor.extract("nothing here to see") + }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED + } + + @Test + fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() { + shouldThrow<InvalidTestCertificateException> { + extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:") + }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED + } + + @Test + fun `certificate missing fails with VC_NO_VACCINATION_ENTRY`() { + shouldThrow<InvalidTestCertificateException> { + extractor.extract(VaccinationQrCodeTestData.certificateMissing) + }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY + } +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java new file mode 100644 index 000000000..035f48b7f --- /dev/null +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java @@ -0,0 +1,20 @@ +package de.rki.coronawarnapp.covidcertificate.test; + +public class TestData { + + static public class EllenCheng { + public String dek = "/9o5eVNb9us5CsGD4F3J36Ju1enJ71Y6+FpVvScGWkE="; + public String coseWithDecryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAWGkAWtjd2EtYXBwLWNsaQQaYpdzwAYaYLZAQDkBA6EBpGN2ZXJlMS4wLjBjbmFtpGJnbmVFbGxlbmJmbmVDaGVuZ2NnbnRlRUxMRU5jZm50ZUNIRU5HY2RvYmoxOTY3LTA3LTE5YXSBq2J0Z2k4NDA1MzkwMDZidHRqTFAyMTcxOTgtM2JubXgZSVZDSUoxR1FaTTc4NzhFTE1VSU1BMDhER2JtYWQxMjQ0YnNjeBgyMDIxLTA1LTMxVDE2OjEzOjE2LjgzNlpiZHJ4GDIwMjEtMDYtMDFUMTQ6MTI6MTYuODM2WmJ0cmkyNjA0MTUwMDBidGNtVGVzdCBDZW50cmUgMWJjb2JERWJpc3ghQnVuZGVzbWluaXN0ZXJpdW0gZsO8ciBHZXN1bmRoZWl0YmNpeC8wMURFLzAwMDAwLzExMTkzNDkwMDcvRzdQU0JBWE1YVkEyTjBITTNVOFlWN1pNVFhA1NqKSb93S2El9dA0icVjK+DV4LbwVWajZmTmhqcsgzWhvl4/PmtAJ5/iT57FfoQvuOvlyhxRPgGSg33IuDnBCg=="; + public String coseWithEncryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAXBxvo73+06cLc73F5KIFuQdo7fLUnb7yF9QFtX9tIEmgSzHIXKbHcEiep5RTtb2UVS80vybmnwYa1k36HR2R2yTKGwvDWAUumw2ZjCnfp8CxKx3zQVRl6JrVdLiskWmo4qiK/EwyTHrw/5PZy4rd11vt9Y6wuZtlpOvFGDIDhGKpcgK93zfIQWY59xjxusr/4J3FCWpcy9YNehB6m4Az1NozXxOrL9DmFM38mWCkiHaPeWgedbqfKTg3x/vSrXSkXYnLpc6QHsRqW99r7yTXJffbK8X44KvgkUI9sIlVU5+2+IuwT4XBY2p/MLW4d9gfnAhZYTsn0nGuoj4KFHTo6fNkXsuZ6BWm5MurXR0dqiCd00B1ZKuTNV0QhdzaaB2pYtwBnxD65TW8D0VDrDDjZuYRzni032f5hgB7YDlvcWYWiv7o6T8DeCNAsJ0RdL/X1qe3bHvLOBvzF9XlTrg4vNF/3aeRn9libOf+0ufr5dEcVhA1NqKSb93S2El9dA0icVjK+DV4LbwVWajZmTmhqcsgzWhvl4/PmtAJ5/iT57FfoQvuOvlyhxRPgGSg33IuDnBCg=="; + } + + static public class BrianCalamandrei { + public String dek = "RinMlpTdzQGw7kllamU9Pz6bTHEWVZi1Ocb4q8wfFSk="; + public String coseWithEncryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAXAN4hKvLEngs3MYcLe/cIyy0q0+0auk5A/Bme/WlymolXU8JSLJJcj3D7kwXCJoEOvsnU9P/IrVlTNF2fJBdWF6Oq22UzhyOPRuQiF7PvspbVzyeEo+H/PmtvbTZss6l/wLDoXPixjtCOaFn7Com6Z2pNQOZqkYGZzz4BanJfchoggM4HaH20H4AzANfMMqYa/rytHnz4BjlR82ZSOlg5e/Jbl78NRen6RkgLTwT3YSI1XV+gbLPK6Fhp5saqRmQgUQTTSO99Q/rdk6BG18RZxqw70zKb77ddxBzolgySbmRdUrpWdK9SvnsnivN3V1Auv5X18KpHO58SwyFoex7OUq73q6FAS9p+MdI2jh1e3LcwU12ZJaN56bRbTEAmT5MelZsYY+c6WWvcIND7tj3aDI5o8D9PyWZHPdz/uHn/Cesn7MgVEXvLQnfCVvuPkLDSGAGi47nRRmUoaN7+7GjPRYvTyrX5VWwnMK31QLADg9kFhAQ7d9IZ02KQ5OXt/fc3bpcombylOcXT2U+JXDwQadrFwHQdjeK1dw+RZM7UkD4l/TOQjO9B8JN13DlidhiqljGw=="; + public String coseWithDecryptedPayload = "kzriyBRYZu5L/VqCz6qsomYv8vjOdjXp/giOGRa8vpXBM7Mjc3mjGaSUpHy8QLlCrFfrDAh8vwY4PKsI/ad/jFyEpyZwI13R6PW+Vft1Ld1nxHD74BwiNbhNv1ZjlmDohQsHXaq6MEbfNfT/uqPbZHToZlTPO3V9aeOD0E+kuMx8npVIbB+zbL48AS3VsrVyYthINdpplYmNsXyFdxsCvZfefGO3G5XIRsDfMBqFPVZO3uCIq1nLb4U8dcbZ5O2yrMG5UOJ2fiwWiiqSLevyCnlYjOvyv8Ot6nM1X7fncXwze3i4OT7Y/Y+36SAds/bCcpypOso0QIim8BBVecy3MN7bQOivc/DKyRX9dHHtc5iD/dgT5i5HBwFEcvTqAa/rlJyTSO4XL6CRevHD7lt3wdd/guRMWxilY1kgjao1w/KEum48frD/FD+J2IgFX+R9aP0CLqvvwvC8OuIj8O+F1WlGp55gKEd+Ps35o1Y8UfHyKDTUMVN5yZno6V15bVFP"; + } + + public static String qrCodeTestCertificate = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH+VEIOOP-IARC$E08WAN$6%W0AT4V22F/8X*G3M9BM9Z0BFU2P4JY73JC3KD34LT7A3523*BBXSJ$IJGX8R/S+:KLD3JJ3.+IJYCDN0TA3RK37MBZD3K%I17JLXKB6J57TJK57ALD-I3 28XGL4LQ/SLE50$499TVU1PK9PYLPN1VUU8C1VTE5ZM376 IE2IML07Z ESF6C.M1EE*2NY*6UJE4:6Q56FPEXM6IS6-Q64Y6N$E.%66PP33M19V2+PFQ51C5EWAC1A.GUQ$9WC54FQ68ENV0XTCM*4CZKHKB-43.E3KD3OAJA70:ZH/O1:O1AT1NQ1 WUQRENS431TN$IK.G47HB%0WT072HFHN/SNP.0FVV/SN7Y431TCRV$.PTW5CL52U50$EZ*N4IU4FKCPACPI2YUFJ6LX3+KG% BTVB3UQFJ6GL28LHXOAYJAUVPQRHIY1* HOQ1PRAAUICO1 IGBY05*H%XGU-PNQCSJ1A+3HJ5VBQR6G/:DS3GH9FW-SAOPNRFBYMBPJEODEN6%99SLNS9KKFC$OKT1GVKJTCW.IIX2S3V0$1D243B/J.H1RJVN01HI7T.H"; + + public static String cborObject = "a4041a60bc9f38061a60b9fc3801624154390103a101a4617481a962736374323032312d30322d32305431323a33343a35365a626d6164313233326274746a4c503231373139382d336274637754657374696e672063656e746572205669656e6e61203162636f624154626369783155524e3a555643493a30313a41543a37314545323535394445333843364246373330344642363541314134353145432333626973781b4d696e6973747279206f66204865616c74682c20417573747269616274676938343035333930303662747269323630343135303030636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e322e3163646f626a313939382d30322d3236"; +} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt index 571eff96c..faac0915b 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt @@ -1,7 +1,8 @@ package de.rki.coronawarnapp.vaccination.core -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import io.kotest.assertions.throwables.shouldNotThrowAny import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe @@ -54,16 +55,16 @@ class VaccinatedPersonIdentifierTest : BaseTest() { testPersonMaxData.requireMatch(testPersonMaxData) } - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(firstNameStandardized = "nope")) - }.errorCode shouldBe ErrorCode.VC_NAME_MISMATCH + }.errorCode shouldBe VC_NAME_MISMATCH - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(lastNameStandardized = "nope")) - }.errorCode shouldBe ErrorCode.VC_NAME_MISMATCH + }.errorCode shouldBe VC_NAME_MISMATCH - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { testPersonMaxData.requireMatch(testPersonMaxData.copy(dateOfBirth = LocalDate.parse("1900-12-31"))) - }.errorCode shouldBe ErrorCode.VC_DOB_MISMATCH + }.errorCode shouldBe VC_DOB_MISMATCH } } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt index ef2d3bd05..5b125bfcc 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt @@ -1,13 +1,13 @@ package de.rki.coronawarnapp.vaccination.core.qrcode +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData import de.rki.coronawarnapp.vaccination.core.VaccinationTestData -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_ZLIB_DECOMPRESSION_FAILED -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import org.joda.time.Instant @@ -80,29 +80,29 @@ class VaccinationQRCodeExtractorTest : BaseTest() { } @Test - fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() { - shouldThrow<InvalidHealthCertificateException> { + fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() { + shouldThrow<InvalidVaccinationCertificateException> { extractor.extract(VaccinationQrCodeTestData.validEncoded) - }.errorCode shouldBe VC_HC_CWT_NO_ISS + }.errorCode shouldBe HC_CWT_NO_ISS } @Test fun `random string fails with HC_BASE45_DECODING_FAILED`() { - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { extractor.extract("nothing here to see") }.errorCode shouldBe HC_BASE45_DECODING_FAILED } @Test fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() { - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:") }.errorCode shouldBe HC_ZLIB_DECOMPRESSION_FAILED } @Test fun `vaccination certificate missing fails with VC_NO_VACCINATION_ENTRY`() { - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { extractor.extract(VaccinationQrCodeTestData.certificateMissing) }.errorCode shouldBe VC_NO_VACCINATION_ENTRY } diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt index f0703302c..5b100c24c 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt @@ -1,9 +1,11 @@ package de.rki.coronawarnapp.vaccination.core.repository +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED +import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH +import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent import de.rki.coronawarnapp.vaccination.core.VaccinationTestData -import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData @@ -109,9 +111,9 @@ class VaccinationRepositoryTest : BaseTest() { val instance = createInstance(this) advanceUntilIdle() - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { instance.registerVaccination(vaccinationTestData.personBVac1QRCode) - }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH + }.errorCode shouldBe VC_NAME_MISMATCH testStorage shouldBe setOf(vaccinationTestData.personAData2Vac) } @@ -127,9 +129,9 @@ class VaccinationRepositoryTest : BaseTest() { val instance = createInstance(this) advanceUntilIdle() - shouldThrow<InvalidHealthCertificateException> { + shouldThrow<InvalidVaccinationCertificateException> { instance.registerVaccination(vaccinationTestData.personAVac1QRCode) - }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED + }.errorCode shouldBe VC_ALREADY_REGISTERED testStorage.first() shouldBe dataBefore } -- GitLab