Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1433: Create user hashing for Koblenz #1499

Merged
merged 12 commits into from
Jul 30, 2024
Merged
4 changes: 4 additions & 0 deletions backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,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 @@ -79,6 +81,8 @@ dependencies {
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,73 @@

import app.ehrenamtskarte.backend.cards.CanonicalJson
import app.ehrenamtskarte.backend.common.utils.Environment
import app.ehrenamtskarte.backend.common.webservice.KOBLENZ_PEPPER_SYS_ENV
import app.ehrenamtskarte.backend.userdata.KoblenzUser
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, removed the salt from the result
*
* 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)
private fun encodeWithoutSalt(
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)
stringBuilder.append("$").append(b64encoder.encodeToString(hash))
return stringBuilder.toString()
}

fun hashKoblenzUserData(userData: KoblenzUser): String? {
val canonicalJson = CanonicalJson.koblenzUserToString(userData)
val hashLength = 32

val pepper = Environment.getVariable(KOBLENZ_PEPPER_SYS_ENV) ?: throw Exception("No koblenz pepper found")
val pepperByteArray = pepper.toByteArray(StandardCharsets.UTF_8)
val params =
Argon2Parameters
.Builder(Argon2Parameters.ARGON2_id)
.withVersion(19)
.withIterations(2)
.withSalt(pepperByteArray)
.withParallelism(1)
.withMemoryAsKB(19456)
.build()

val generator = Argon2BytesGenerator()
generator.init(params)
val result = ByteArray(hashLength)
generator.generateBytes(canonicalJson.toByteArray(), result)
return encodeWithoutSalt(result, params)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package app.ehrenamtskarte.backend.cards

import app.ehrenamtskarte.backend.userdata.KoblenzUser
import com.fasterxml.jackson.core.type.TypeReference
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 +92,11 @@ class CanonicalJson {
}
}

fun koblenzUserToString(koblenzUser: KoblenzUser): String {
val map = ObjectMapper().convertValue(koblenzUser, object : TypeReference<Map<String, Any>>() {})
return serializeToString(map)
}

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

/**
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
f1sh1918 marked this conversation as resolved.
Show resolved Hide resolved
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.sozialpass.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
@@ -0,0 +1,6 @@

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")

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
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,
regionKey: 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 {
michael-markl marked this conversation as resolved.
Show resolved Hide resolved
projectId = project.id
name = regionName
prefix = regionPrefix
regionIdentifier = regionKey
website = regionWebsite
}
} else {
region.name = regionName
region.prefix = regionPrefix
region.website = regionWebsite
region.regionIdentifier = regionKey
}
}

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 @@ -29,23 +58,8 @@ fun insertOrUpdateRegions() {
dbRegion.website = eakRegion[3]
}
}

// 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"
}
// TODO #1551: Adjust regionidentifier_unique constraint
createOrUpdateRegion(NUERNBERG_PASS_PROJECT, "Nürnberg", "Stadt", null, "https://nuernberg.de")
createOrUpdateRegion(KOBLENZ_PASS_PROJECT, "Koblenz", "Stadt", "07111", "https://koblenz.de/")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package app.ehrenamtskarte.backend.userdata

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)
11 changes: 11 additions & 0 deletions backend/src/main/resources/config/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ projects:
port: 587
username: OVERRIDE_IN_LOCAL_CONFIG
password: OVERRIDE_IN_LOCAL_CONFIG
- id: koblenz.sozialpass.app
importUrl: ""
pipelineName: SozialpassKoblenz
administrationBaseUrl: https://koblenz.sozialpass.app
administrationName: Koblenz-Pass-Verwaltung
timezone: "Europe/Berlin"
smtp:
host: mail.sozialpass.app
port: 587
username: OVERRIDE_IN_LOCAL_CONFIG
password: OVERRIDE_IN_LOCAL_CONFIG
- id: showcase.entitlementcard.app
importUrl: https://example.com
pipelineName: BerechtigungskarteShowcase
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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.userdata.KoblenzUser
import io.mockk.every
import io.mockk.mockkObject
import kotlin.test.Test
import kotlin.test.assertEquals

internal class Argon2IdHasherTest {
@Test
fun isHashingCorrectly() {
mockkObject(Environment)
every { Environment.getVariable(KOBLENZ_PEPPER_SYS_ENV) } returns "123456789ABC"

assertEquals(Environment.getVariable("KOBLENZ_PEPPER"), "123456789ABC")

val hash = Argon2IdHasher.hashKoblenzUserData(KoblenzUser("Karla Koblenz", 12213, "123K"))
val expectedHash = "\$argon2id\$v=19\$m=19456,t=2,p=1\$57YPIKvU/XE9h7/JA0tZFT2TzpwBQfYAW6K+ojXBh5w" // This expected output was created with https://argon2.online/
assertEquals(expectedHash, hash)
}
}
Loading