Skip to content

Commit

Permalink
Merge pull request #1499 from digitalfabrik/1433-create-argon2-user-h…
Browse files Browse the repository at this point in the history
…ashing-for-koblenz

1433: Create user hashing for Koblenz
  • Loading branch information
f1sh1918 committed Jul 30, 2024
2 parents 231d182 + 47ea4d2 commit 10a234e
Show file tree
Hide file tree
Showing 16 changed files with 364 additions and 99 deletions.
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
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 {
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

0 comments on commit 10a234e

Please sign in to comment.