diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/user/KoblenzUser.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/user/KoblenzUser.kt new file mode 100644 index 000000000..75f40a0d2 --- /dev/null +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/user/KoblenzUser.kt @@ -0,0 +1,7 @@ +package app.ehrenamtskarte.backend.user + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.annotation.JsonPropertyOrder + +@JsonPropertyOrder(alphabetic = true) +data class KoblenzUser(@get:JsonProperty("1") val fullname: String, @get:JsonProperty("2") val birthday: Int, @get:JsonProperty("3") val referenceNumber: String) diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasher.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasher.kt index b5d4ef1af..eed741823 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasher.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasher.kt @@ -1,5 +1,7 @@ + import app.ehrenamtskarte.backend.common.utils.Environment import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PEPPER_SYS_ENV +import app.ehrenamtskarte.backend.user.KoblenzUser import app.ehrenamtskarte.backend.verification.CanonicalJson import org.bouncycastle.crypto.generators.Argon2BytesGenerator import org.bouncycastle.crypto.params.Argon2Parameters @@ -48,12 +50,9 @@ class Argon2IdHasher { return stringBuilder.toString() } - fun hashUserData(cardInfo: Card.CardInfo): String? { - val canonicalJson = CanonicalJson.messageToMap(cardInfo) + fun hashKoblenzUserData(userData: KoblenzUser): String? { + val canonicalJson = CanonicalJson.koblenzUserToString(userData) val hashLength = 32 - if (!isCanonicalJsonValid(canonicalJson)) { - throw Exception("Invalid Json input for hashing") - } val pepper = Environment.getVariable(KOBLENZ_PEPPER_SYS_ENV) // TODO handle if Null val pepperByteArray = pepper?.toByteArray(StandardCharsets.UTF_8) @@ -70,16 +69,8 @@ class Argon2IdHasher { val generator = Argon2BytesGenerator() generator.init(params) val result = ByteArray(hashLength) - generator.generateBytes(CanonicalJson.serializeToString(canonicalJson).toCharArray(), result) + generator.generateBytes(canonicalJson.toCharArray(), result) return encode(result, params) } - - private fun isCanonicalJsonValid(canonicalJson: Map): Boolean { - val hasName = canonicalJson.get("1") != null - val hasExtensions = canonicalJson.get("3") as? Map - val hasKoblenzPassExtension = hasExtensions?.get("6") as? Map - val hasKoblenzPassId = hasKoblenzPassExtension?.get("1") != null - return hasName && hasKoblenzPassId - } } } diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJson.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJson.kt index 1dcd63be3..bd0282ba6 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJson.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJson.kt @@ -1,5 +1,7 @@ package app.ehrenamtskarte.backend.verification +import app.ehrenamtskarte.backend.user.KoblenzUser +import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.Descriptors import com.google.protobuf.Descriptors.FieldDescriptor.Type import com.google.protobuf.GeneratedMessageV3 @@ -89,6 +91,10 @@ class CanonicalJson { } } + fun koblenzUserToString(koblenzUser: KoblenzUser): String { + return ObjectMapper().writeValueAsString(koblenzUser) + } + fun serializeToString(message: GeneratedMessageV3) = serializeToString(messageToMap(message)) /** diff --git a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt index d0efacf66..a8ac4d33d 100644 --- a/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt +++ b/backend/src/main/kotlin/app/ehrenamtskarte/backend/verification/webservice/schema/CardMutationService.kt @@ -1,14 +1,11 @@ package app.ehrenamtskarte.backend.verification.webservice.schema -import Argon2IdHasher import Card import app.ehrenamtskarte.backend.application.database.repos.ApplicationRepository import app.ehrenamtskarte.backend.auth.database.AdministratorEntity import app.ehrenamtskarte.backend.auth.service.Authorizer import app.ehrenamtskarte.backend.common.webservice.GraphQLContext -import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PASS_PROJECT import app.ehrenamtskarte.backend.exception.service.ForbiddenException -import app.ehrenamtskarte.backend.exception.service.NotKoblenzProjectException import app.ehrenamtskarte.backend.exception.service.ProjectNotFoundException import app.ehrenamtskarte.backend.exception.service.UnauthorizedException import app.ehrenamtskarte.backend.exception.webservice.exceptions.InvalidCardHashException @@ -166,25 +163,6 @@ class CardMutationService { return activationCodes } - @GraphQLDescription("Creates a new digital koblenz card and returns it") - fun createCardsByUserData( - dfe: DataFetchingEnvironment, - project: String, - encodedCardInfo: String - ): Boolean { // CardCreationResultModel { - val context = dfe.getContext() - val projectConfig = - context.backendConfiguration.projects.find { it.id == project } - ?: throw ProjectNotFoundException(project) - if (project != KOBLENZ_PASS_PROJECT) { - throw NotKoblenzProjectException() - } - val cardInfoBytes = encodedCardInfo.decodeBase64Bytes() - val cardInfo = Card.CardInfo.parseFrom(cardInfoBytes) - val hashedUserData = Argon2IdHasher.hashUserData(cardInfo) - return false // Will be done in #1421 - } - @GraphQLDescription("Activate a dynamic entitlement card") fun activateCard( project: String, diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/ExampleUser.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/ExampleUser.kt new file mode 100644 index 000000000..3000b2bc1 --- /dev/null +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/helper/ExampleUser.kt @@ -0,0 +1,5 @@ +package app.ehrenamtskarte.backend.helper + +import app.ehrenamtskarte.backend.user.KoblenzUser + +val koblenzTestUser = KoblenzUser("Karla Koblenz", 12213, "123K") diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasherTest.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasherTest.kt index 419e1bf4c..00e452a49 100644 --- a/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasherTest.kt +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/Argon2IdHasherTest.kt @@ -2,8 +2,7 @@ package app.ehrenamtskarte.backend.verification import Argon2IdHasher import app.ehrenamtskarte.backend.common.utils.Environment import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PEPPER_SYS_ENV -import app.ehrenamtskarte.backend.helper.CardInfoTestSample -import app.ehrenamtskarte.backend.helper.ExampleCardInfo +import app.ehrenamtskarte.backend.user.KoblenzUser import io.mockk.every import io.mockk.mockkObject import kotlin.test.Test @@ -17,10 +16,8 @@ internal class Argon2IdHasherTest { assertEquals(Environment.getVariable("KOBLENZ_PEPPER"), "123456789ABC") - val userData = ExampleCardInfo.get(CardInfoTestSample.KoblenzPass) - - val hash = Argon2IdHasher.hashUserData(userData) - val expectedHash = "\$argon2id\$v=19\$m=16,t=2,p=1\$MTIzNDU2Nzg5QUJD\$xJd35mCTBZT8u+FCGWCnmOtxWzcDTb1Pnt5DHWDap7Y" // This expected output was created with https://argon2.online/ + val hash = Argon2IdHasher.hashKoblenzUserData(KoblenzUser("Karla Koblenz", 12213, "123K")) + val expectedHash = "\$argon2id\$v=19\$m=16,t=2,p=1\$MTIzNDU2Nzg5QUJD\$UIOJZIsSL8vXcuCB82xZ5E8tpH6sQd3d4U0uC02DP40" // This expected output was created with https://argon2.online/ assertEquals(expectedHash, hash) } diff --git a/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJsonTest.kt b/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJsonTest.kt index eaa1753d9..8d3a4cd75 100644 --- a/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJsonTest.kt +++ b/backend/src/test/kotlin/app/ehrenamtskarte/backend/verification/CanonicalJsonTest.kt @@ -3,6 +3,8 @@ package app.ehrenamtskarte.backend.verification import Card import app.ehrenamtskarte.backend.helper.CardInfoTestSample import app.ehrenamtskarte.backend.helper.ExampleCardInfo +import app.ehrenamtskarte.backend.helper.koblenzTestUser +import app.ehrenamtskarte.backend.verification.CanonicalJson.Companion.koblenzUserToString import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -97,20 +99,9 @@ internal class CanonicalJsonTest { } @Test - fun mapCardInfoForKoblenzPass() { - val cardInfo = ExampleCardInfo.get(CardInfoTestSample.KoblenzPass) - assertEquals( - CanonicalJson.messageToMap(cardInfo), - mapOf( - "1" to "Karla Koblenz", - "3" to - mapOf( - "1" to mapOf("1" to "95"), // Koblenz Region - "2" to mapOf("1" to "12213"), // extensionBirthday - "6" to mapOf("1" to "123K") // extensionKoblenzPassId - ) - ) - ) + fun mapUserInfoForKoblenzPass() { + val expected = koblenzUserToString(koblenzTestUser) + assertEquals("{\"1\":\"Karla\",\"2\":12213,\"3\":\"123K\"}", expected) } @Test diff --git a/docs/CreateKoblenzHash.md b/docs/CreateKoblenzHash.md index 671b491ab..fa6f45514 100644 --- a/docs/CreateKoblenzHash.md +++ b/docs/CreateKoblenzHash.md @@ -14,17 +14,10 @@ The example data is ### 1. Collect all data and merge it into an object ```agsl //Example: -full_name: "Karla Koblenz" -extensions { - extension_region { - regionId: 95 - } - extension_birthday { +{ + full_name: "Karla Koblenz" birthday: 12213 - } - extension_koblenz_pass_id { - pass_id: "123K" - } + referenceNumber: "123K" } ``` @@ -32,14 +25,13 @@ extensions { e.g. `Karla Koblenz` will match neither with `Karla Lisa Koblenz` nor with `Karlá Koblenz`. - The birthday is defined in our protobuf [card.proto](../frontend/card.proto) file: It counts the days since the birthday (calculated from 1970-01-01). All values of this field are valid, including the 0, which indicates that the birthday is on 1970-01-01. Birthdays before 1970-01-01 have negative values. -- extension_region is always 95 for Koblenz -- extension_koblenz_pass_id is set to the "Aktenzeichen" +- referenceNumber is set to the "Aktenzeichen" ### 2. Convert this object to a Canonical Json Result should be: ``` - {"1":"Karla Koblenz","3":{"1":{"1":"95"},"2":{"1":"12213"},"6":{"1":"123K"}}} + {"1":"Karla Koblenz","2":12213,"3":"123K"} ``` ### 3. Hash it with Argon2id @@ -52,13 +44,14 @@ Hash with Argon2id with the following parameters: | Iterations | 2 | | Parallellism | 1 | | Memory | 16 | +| HashLength | 32 | | Salt | Secret Salt will be shared with Koblenz
for the example use `123456789ABC` | ### 4. The result... ...for the example data and example salt must be: -`$argon2id$v=19$m=16,t=2,p=1$MTIzNDU2Nzg5QUJD$KStr3PVblyAh2bIleugv796G+p4pvRNiAON0MHVufVY` +`$argon2id$v=19$m=16,t=2,p=1$MTIzNDU2Nzg5QUJD$UIOJZIsSL8vXcuCB82xZ5E8tpH6sQd3d4U0uC02DP40` ## Additional Information diff --git a/specs/backend-api.graphql b/specs/backend-api.graphql index 07dbc6a0f..8d9953438 100644 --- a/specs/backend-api.graphql +++ b/specs/backend-api.graphql @@ -128,8 +128,6 @@ type Mutation { createAdministrator(email: String!, project: String!, regionId: Int, role: Role!, sendWelcomeMail: Boolean!): Boolean! "Creates a new digital entitlementcard and returns it" createCardsByCardInfos(applicationIdToMarkAsProcessed: Int, encodedCardInfos: [String!]!, generateStaticCodes: Boolean!, project: String!): [CardCreationResultModel!]! - "Creates a new digital koblenz card and returns it" - createCardsByUserData(encodedCardInfo: String!, project: String!): Boolean! "Deletes an existing administrator" deleteAdministrator(adminId: Int!, project: String!): Boolean! "Deletes the application with specified id" diff --git a/specs/card.proto b/specs/card.proto index 391db3c73..e35742344 100644 --- a/specs/card.proto +++ b/specs/card.proto @@ -40,8 +40,8 @@ message NuernbergPassIdExtension { optional NuernergPassIdentifier identifier = 2; } -message KoblenzPassIdExtension { - optional string pass_id = 1; +message KoblenzReferenceNumberExtension { + optional string reference_number = 1; } enum NuernergPassIdentifier { @@ -55,7 +55,7 @@ message CardExtensions { optional NuernbergPassIdExtension extension_nuernberg_pass_id = 3; optional BavariaCardTypeExtension extension_bavaria_card_type = 4; optional StartDayExtension extension_start_day = 5; - optional KoblenzPassIdExtension extension_koblenz_pass_id = 6; + optional KoblenzReferenceNumberExtension extension_koblenz_reference_number = 6; } // For our hashing approach, we require that all fields (and subfields, recursively) of CardInfo are marked 'optional'.