Skip to content

Commit

Permalink
1433: Hash Koblenz data without cardinfo
Browse files Browse the repository at this point in the history
  • Loading branch information
ztefanie committed Jul 2, 2024
1 parent 38164c7 commit f67524a
Show file tree
Hide file tree
Showing 11 changed files with 44 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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<String, Any>): Boolean {
val hasName = canonicalJson.get("1") != null
val hasExtensions = canonicalJson.get("3") as? Map<String, Any>
val hasKoblenzPassExtension = hasExtensions?.get("6") as? Map<String, String>
val hasKoblenzPassId = hasKoblenzPassExtension?.get("1") != null
return hasName && hasKoblenzPassId
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -89,6 +91,10 @@ class CanonicalJson {
}
}

fun koblenzUserToString(koblenzUser: KoblenzUser): String {
return ObjectMapper().writeValueAsString(koblenzUser)
}

fun serializeToString(message: GeneratedMessageV3) = serializeToString(messageToMap(message))

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<GraphQLContext>()
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ object ExampleCardInfo {
Card.CardInfo.getDefaultInstance(),
fullName = "Karla Koblenz",
regionId = 95,
koblenzPassId = "123K",
koblenzReferenceNumber = "123K",
birthDay = 12213 // 10.06.2003
)

Expand All @@ -46,7 +46,7 @@ object ExampleCardInfo {
birthDay: Int? = null,
nuernbergPassId: Int? = null,
nuernbergPassIdIdentifier: Card.NuernergPassIdentifier? = null,
koblenzPassId: String? = null,
koblenzReferenceNumber: String? = null,
startDay: Int? = null
): Card.CardInfo {
val cardInfo = Card.CardInfo.newBuilder(base)
Expand All @@ -62,7 +62,7 @@ object ExampleCardInfo {
nuernbergPassIdIdentifier
)
}
if (koblenzPassId != null) extensions.extensionKoblenzPassIdBuilder.setPassId(koblenzPassId)
if (koblenzReferenceNumber != null) extensions.extensionKoblenzReferenceNumberBuilder.setReferenceNumber(koblenzReferenceNumber)
if (startDay != null) extensions.extensionStartDayBuilder.setStartDay(startDay)
return cardInfo.buildPartial()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package app.ehrenamtskarte.backend.helper

import app.ehrenamtskarte.backend.user.KoblenzUser

val koblenzTestUser = KoblenzUser("Karla Koblenz", 12213, "123K")
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 Koblenz\",\"2\":12213,\"3\":\"123K\"}", expected)
}

@Test
Expand Down
21 changes: 7 additions & 14 deletions docs/CreateKoblenzHash.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,24 @@ 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"
}
```

- The full_name must be `FirstnameSpaceLastname`. Every char must exaclty match the user input, as otherwise there is not possiblity to match the data Koblenz transfers with the input the user makes.
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
Expand All @@ -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<br/>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
Expand Down
2 changes: 0 additions & 2 deletions specs/backend-api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
6 changes: 3 additions & 3 deletions specs/card.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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'.
Expand Down

0 comments on commit f67524a

Please sign in to comment.