Skip to content

Commit

Permalink
1433: Create user hashing for Koblenz
Browse files Browse the repository at this point in the history
  • Loading branch information
ztefanie committed Jun 19, 2024
1 parent 3780f9b commit 38164c7
Show file tree
Hide file tree
Showing 13 changed files with 384 additions and 94 deletions.
5 changes: 5 additions & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ dependencies {
// Use the Kotlin JUnit integration.
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")

testImplementation("io.mockk:mockk:1.13.11")

implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
Expand All @@ -69,11 +71,14 @@ dependencies {
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2")
implementation("de.grundid.opendatalab:geojson-jackson:1.14")
implementation("commons-codec:commons-codec:1.17.0")

implementation("com.eatthepath:java-otp:0.4.0") // dynamic card verification
implementation("com.auth0:java-jwt:4.4.0") // JSON web tokens
implementation("at.favre.lib:bcrypt:0.10.2")

implementation("org.bouncycastle:bcpkix-jdk18on:1.76")

implementation("com.google.zxing:core:3.5.2") // QR-Codes
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.ehrenamtskarte.backend.common.utils

// This helper class was created to enable mocking getenv in Tests
class Environment {
companion object {
fun getVariable(name: String): String? = System.getenv(name)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ package app.ehrenamtskarte.backend.common.webservice

const val EAK_BAYERN_PROJECT = "bayern.ehrenamtskarte.app"
const val NUERNBERG_PASS_PROJECT = "nuernberg.sozialpass.app"
const val KOBLENZ_PASS_PROJECT = "koblenz.pass.app"
const val KOBLENZ_PEPPER_SYS_ENV = "KOBLENZ_PEPPER"
const val SHOWCASE_PROJECT = "showcase.entitlementcard.app"
const val DEFAULT_PROJECT = EAK_BAYERN_PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@
package app.ehrenamtskarte.backend.exception.service

class NotEakProjectException() : Exception("This query can only be used for EAK project")

class NotKoblenzProjectException() : Exception("This query can only be used for Koblenz project")
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
package app.ehrenamtskarte.backend.regions.database

import app.ehrenamtskarte.backend.common.webservice.EAK_BAYERN_PROJECT
import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PASS_PROJECT
import app.ehrenamtskarte.backend.common.webservice.NUERNBERG_PASS_PROJECT
import app.ehrenamtskarte.backend.projects.database.ProjectEntity
import org.jetbrains.exposed.sql.transactions.transaction

fun insertOrUpdateRegions() {
transaction {
val projects = ProjectEntity.all()
val dbRegions = RegionEntity.all()
val projects = ProjectEntity.all()
val dbRegions = RegionEntity.all()

fun createOrUpdateRegion(
regionProjectId: String,
regionName: String,
regionPrefix: String,
regionWebsite: String
) {
val project =
projects.firstOrNull { it.project == regionProjectId }
?: throw Error("Required project '$regionProjectId' not found!")
val region = dbRegions.singleOrNull { it.projectId == project.id }
if (region == null) {
RegionEntity.new {
projectId = project.id
name = regionName
prefix = regionPrefix
regionIdentifier = null
website = regionWebsite
}
} else {
region.name = regionName
region.prefix = regionPrefix
region.website = regionWebsite
}
}

transaction {
// Create or update eak regions in database
val eakProject = projects.firstOrNull { it.project == EAK_BAYERN_PROJECT }
?: throw Error("Required project '$EAK_BAYERN_PROJECT' not found!")
val eakProject =
projects.firstOrNull { it.project == EAK_BAYERN_PROJECT }
?: throw Error("Required project '$EAK_BAYERN_PROJECT' not found!")
EAK_BAYERN_REGIONS.forEach { eakRegion ->
val dbRegion = dbRegions.find { it.regionIdentifier == eakRegion[2] && it.projectId == eakProject.id }
if (dbRegion == null) {
Expand All @@ -30,22 +57,7 @@ fun insertOrUpdateRegions() {
}
}

// Create or update nuernberg region in database
val nuernbergPassProject = projects.firstOrNull { it.project == NUERNBERG_PASS_PROJECT }
?: throw Error("Required project '$NUERNBERG_PASS_PROJECT' not found!")
val nuernbergRegion = dbRegions.singleOrNull { it.projectId == nuernbergPassProject.id }
if (nuernbergRegion == null) {
RegionEntity.new {
projectId = nuernbergPassProject.id
name = "Nürnberg"
prefix = "Stadt"
regionIdentifier = null
website = "https://nuernberg.de"
}
} else {
nuernbergRegion.name = "Nürnberg"
nuernbergRegion.prefix = "Stadt"
nuernbergRegion.website = "https://nuernberg.de"
}
createOrUpdateRegion(NUERNBERG_PASS_PROJECT, "Nürnberg", "Stadt", "https://nuernberg.de")
createOrUpdateRegion(KOBLENZ_PASS_PROJECT, "Koblenz", "Stadt", "https://koblenz.de/")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import app.ehrenamtskarte.backend.common.utils.Environment
import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PEPPER_SYS_ENV
import app.ehrenamtskarte.backend.verification.CanonicalJson
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
import org.bouncycastle.crypto.params.Argon2Parameters
import java.nio.charset.StandardCharsets
import java.util.Base64

class Argon2IdHasher {
companion object {
/**
* Copied from spring-security Argon2EncodingUtils.java licenced under Apache 2.0
*
* Encodes a raw Argon2-hash and its parameters into the standard Argon2-hash-string
* as specified in the reference implementation
* (https://github.com/P-H-C/phc-winner-argon2/blob/master/src/encoding.c#L244):
*
* {@code $argon2<T>[$v=<num>]$m=<num>,t=<num>,p=<num>$<bin>$<bin>}
**/
@Throws(IllegalArgumentException::class)
fun encode(
hash: ByteArray?,
parameters: Argon2Parameters
): String? {
val b64encoder: Base64.Encoder = Base64.getEncoder().withoutPadding()
val stringBuilder = StringBuilder()
val type =
when (parameters.type) {
Argon2Parameters.ARGON2_d -> "\$argon2d"
Argon2Parameters.ARGON2_i -> "\$argon2i"
Argon2Parameters.ARGON2_id -> "\$argon2id"
else -> throw IllegalArgumentException("Invalid algorithm type: " + parameters.type)
}
stringBuilder.append(type)
stringBuilder
.append("\$v=")
.append(parameters.version)
.append("\$m=")
.append(parameters.memory)
.append(",t=")
.append(parameters.iterations)
.append(",p=")
.append(parameters.lanes)
if (parameters.salt != null) {
stringBuilder.append("$").append(b64encoder.encodeToString(parameters.salt))
}
stringBuilder.append("$").append(b64encoder.encodeToString(hash))
return stringBuilder.toString()
}

fun hashUserData(cardInfo: Card.CardInfo): String? {
val canonicalJson = CanonicalJson.messageToMap(cardInfo)
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)
val params =
Argon2Parameters
.Builder(Argon2Parameters.ARGON2_id)
.withVersion(19)
.withIterations(2)
.withSalt(pepperByteArray)
.withParallelism(1)
.withMemoryAsKB(16)
.build()

val generator = Argon2BytesGenerator()
generator.init(params)
val result = ByteArray(hashLength)
generator.generateBytes(CanonicalJson.serializeToString(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,11 +1,14 @@
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 @@ -163,6 +166,25 @@ 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 @@ -6,24 +6,36 @@ enum class CardInfoTestSample {
Nuernberg,
NuernbergWithStartDay,
NuernbergWithPassId,
NuernbergWithPassNr
NuernbergWithPassNr,
KoblenzPass
}

object ExampleCardInfo {
private val bavarianBase = buildCardInfo(
Card.CardInfo.getDefaultInstance(),
fullName = "Max Mustermann",
regionId = 16
)
private val bavarianBase =
buildCardInfo(
Card.CardInfo.getDefaultInstance(),
fullName = "Max Mustermann",
regionId = 16
)

private val nuernbergBase = buildCardInfo(
Card.CardInfo.getDefaultInstance(),
fullName = "Max Mustermann",
regionId = 93,
nuernbergPassId = 99999999,
birthDay = -365 * 10,
expirationDay = 365 * 40 // Equals 14.600
)
private val nuernbergBase =
buildCardInfo(
Card.CardInfo.getDefaultInstance(),
fullName = "Max Mustermann",
regionId = 93,
nuernbergPassId = 99999999,
birthDay = -365 * 10,
expirationDay = 365 * 40 // Equals 14.600
)

private val koblenzBase =
buildCardInfo(
Card.CardInfo.getDefaultInstance(),
fullName = "Karla Koblenz",
regionId = 95,
koblenzPassId = "123K",
birthDay = 12213 // 10.06.2003
)

private fun buildCardInfo(
base: Card.CardInfo,
Expand All @@ -34,6 +46,7 @@ object ExampleCardInfo {
birthDay: Int? = null,
nuernbergPassId: Int? = null,
nuernbergPassIdIdentifier: Card.NuernergPassIdentifier? = null,
koblenzPassId: String? = null,
startDay: Int? = null
): Card.CardInfo {
val cardInfo = Card.CardInfo.newBuilder(base)
Expand All @@ -49,40 +62,50 @@ object ExampleCardInfo {
nuernbergPassIdIdentifier
)
}
if (koblenzPassId != null) extensions.extensionKoblenzPassIdBuilder.setPassId(koblenzPassId)
if (startDay != null) extensions.extensionStartDayBuilder.setStartDay(startDay)
return cardInfo.buildPartial()
}

fun get(cardInfoTestSample: CardInfoTestSample): Card.CardInfo {
return when (cardInfoTestSample) {
CardInfoTestSample.BavarianStandard -> buildCardInfo(
bavarianBase,
expirationDay = 365 * 40, // Equals 14.600
bavariaCardType = Card.BavariaCardType.STANDARD
)
fun get(cardInfoTestSample: CardInfoTestSample): Card.CardInfo =
when (cardInfoTestSample) {
CardInfoTestSample.BavarianStandard ->
buildCardInfo(
bavarianBase,
expirationDay = 365 * 40, // Equals 14.600
bavariaCardType = Card.BavariaCardType.STANDARD
)

CardInfoTestSample.BavarianGold -> buildCardInfo(
bavarianBase,
bavariaCardType = Card.BavariaCardType.GOLD
)
CardInfoTestSample.BavarianGold ->
buildCardInfo(
bavarianBase,
bavariaCardType = Card.BavariaCardType.GOLD
)

CardInfoTestSample.Nuernberg -> nuernbergBase
CardInfoTestSample.NuernbergWithStartDay -> buildCardInfo(
nuernbergBase,
startDay = 365 * 2
)
CardInfoTestSample.NuernbergWithStartDay ->
buildCardInfo(
nuernbergBase,
startDay = 365 * 2
)

CardInfoTestSample.NuernbergWithPassId -> buildCardInfo(
nuernbergBase,
nuernbergPassIdIdentifier = Card.NuernergPassIdentifier.passId,
startDay = 365 * 2
)
CardInfoTestSample.NuernbergWithPassId ->
buildCardInfo(
nuernbergBase,
nuernbergPassIdIdentifier = Card.NuernergPassIdentifier.passId,
startDay = 365 * 2
)

CardInfoTestSample.NuernbergWithPassNr -> buildCardInfo(
nuernbergBase,
nuernbergPassIdIdentifier = Card.NuernergPassIdentifier.passNr,
startDay = 365 * 2
)
CardInfoTestSample.NuernbergWithPassNr ->
buildCardInfo(
nuernbergBase,
nuernbergPassIdIdentifier = Card.NuernergPassIdentifier.passNr,
startDay = 365 * 2
)

CardInfoTestSample.KoblenzPass ->
buildCardInfo(
koblenzBase
)
}
}
}
Loading

0 comments on commit 38164c7

Please sign in to comment.