Skip to content

Commit

Permalink
#54 Add cryptography stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
vityaman committed Nov 12, 2024
1 parent f8e61f3 commit 2734637
Show file tree
Hide file tree
Showing 14 changed files with 238 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.ifmo.se.dating.authik.security.auth

import io.jsonwebtoken.Jwts
import ru.ifmo.se.dating.security.auth.AccessToken
import ru.ifmo.se.dating.security.auth.Jwt
import java.security.PrivateKey
import java.time.Clock
import java.util.*
import kotlin.time.toJavaDuration
import java.time.Duration as JavaDuration
import kotlin.time.Duration as KotlinDuration

class JwtTokenIssuer(
private val clock: Clock,
private val privateSignKey: PrivateKey,
duration: KotlinDuration,
) : TokenIssuer {
private val duration: JavaDuration = duration.toJavaDuration()

override fun issue(payload: AccessToken.Payload): AccessToken {
val now = clock.instant()
return Jwts.builder()
.claims(Jwt.serialize(payload))
.issuedAt(Date.from(now))
.expiration(Date.from(now + duration))
.signWith(privateSignKey)
.compact()
.let { AccessToken(it) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package ru.ifmo.se.dating.authik.security.auth

import ru.ifmo.se.dating.security.auth.AccessToken

interface TokenIssuer {
fun issue(payload: AccessToken.Payload): AccessToken
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.ifmo.se.dating.authik.security.auth

import org.junit.Assert.assertEquals
import org.junit.Test
import ru.ifmo.se.dating.security.auth.AccessToken
import ru.ifmo.se.dating.security.auth.JwtTokenDecoder
import ru.ifmo.se.dating.security.auth.User
import java.security.KeyPairGenerator
import java.time.Clock
import kotlin.time.Duration.Companion.hours

class JwtTest {
@Test
fun jwtLifecycle() {
val rsa = KeyPairGenerator.getInstance("RSA").genKeyPair()

val clock = Clock.systemUTC()

val issuer = JwtTokenIssuer(
clock = clock,
privateSignKey = rsa.private,
duration = 10.hours,
)

val decoder = JwtTokenDecoder(
clock = clock,
publicSignKey = rsa.public,
)

val expected = AccessToken.Payload(User.Id(123))
val actual = issuer.issue(expected).let { decoder.decode(it) }

assertEquals(expected, actual)
}
}
7 changes: 6 additions & 1 deletion backend/foundation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ dependencies {
api(libs.io.swagger.core.v3.swagger.models)
api(libs.org.openapitools.jackson.databind.nullable)

api(libs.io.jsonwebtoken.jjwt.api)
runtimeOnly(libs.io.jsonwebtoken.jjwt.impl)
runtimeOnly(libs.io.jsonwebtoken.jjwt.jackson)

api(libs.jakarta.validation.jakarta.validation.api)
api(libs.com.fasterxml.jackson.core.jackson.databind)

Expand All @@ -31,5 +35,6 @@ dependencies {
api(libs.org.liquibase.liquibase.core)
api(libs.org.postgresql.postgresql)
api(libs.org.postgresql.r2dbc.postgresql)
}

testImplementation(libs.junit.junit)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ru.ifmo.se.dating.exception

class AuthenticationException(message: String, cause: Throwable? = null) :
SecurityException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package ru.ifmo.se.dating.exception

abstract class SecurityException(message: String, cause: Throwable? = null) :
GenericException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.ifmo.se.dating.security.auth

@JvmInline
value class AccessToken(val text: String) {
data class Payload(val userId: User.Id)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ru.ifmo.se.dating.security.auth

import io.jsonwebtoken.Claims

object Jwt {
private const val USER_ID = "user_id"

fun serialize(payload: AccessToken.Payload): Map<String, Any> = mapOf(
USER_ID to payload.userId.number,
)

fun deserialize(claims: Claims): AccessToken.Payload {
val userId = User.Id(claims[USER_ID]!! as Int)
return AccessToken.Payload(userId)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package ru.ifmo.se.dating.security.auth

import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import ru.ifmo.se.dating.exception.AuthenticationException
import java.security.PublicKey
import java.time.Clock
import java.util.*
import javax.crypto.SecretKey

class JwtTokenDecoder(
private val clock: Clock,
private val publicSignKey: PublicKey,
) : TokenDecoder {
override fun decode(token: AccessToken): AccessToken.Payload =
try {
Jwts.parser()
.verifyWith(publicSignKey)
.clock { Date.from(clock.instant()) }
.build()
.parseSignedClaims(token.text)
.payload
.let { Jwt.deserialize(it) }
} catch (e: ExpiredJwtException) {
throw AuthenticationException(e.message!!, e)
} catch (e: MalformedJwtException) {
throw AuthenticationException(e.message!!, e)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.ifmo.se.dating.security.auth

interface TokenDecoder {
fun decode(token: AccessToken): AccessToken.Payload
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package ru.ifmo.se.dating.security.auth

import ru.ifmo.se.dating.validation.expectId

object User {
@JvmInline
value class Id(val number: Int) {
init {
expectId(number)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package ru.ifmo.se.dating.security.key

import java.security.Key
import java.security.KeyFactory
import java.security.PrivateKey
import java.security.PublicKey
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec

object Keys {
fun serialize(key: Key): String =
"${key.algorithm}:${Base64.getEncoder().encodeToString(key.encoded)}"

fun deserializeSecret(string: String): SecretKey {
val (algorithm, encodedKey) = split(string)
return SecretKeySpec(encodedKey, algorithm)
}

fun deserializePublic(string: String): PublicKey {
val (algorithm, encodedKey) = split(string)
return KeyFactory.getInstance(algorithm).generatePublic(X509EncodedKeySpec(encodedKey))
}

fun deserializePrivate(string: String): PrivateKey {
val (algorithm, encodedKey) = split(string)
return KeyFactory.getInstance(algorithm).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
}

private fun split(string: String): Pair<String, ByteArray> {
val parts = string.split(":")
if (parts.size != 2) {
throw IllegalArgumentException("Invalid serialized key format")
}
val algorithm = parts[0]
val encodedKey = parts[1]
return algorithm to Base64.getDecoder().decode(encodedKey)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package ru.ifmo.se.dating.security.key

import org.junit.Assert.assertEquals
import org.junit.Test
import java.security.KeyPairGenerator
import javax.crypto.KeyGenerator

class KeysTest {
@Test
fun secretRoundTrip() {
for (i in 0..32) {
val key = KeyGenerator.getInstance("AES").generateKey()
assertEquals(key, Keys.serialize(key).let { Keys.deserializeSecret(it) })
}
}

@Test
fun publicRoundTrip() {
for (i in 0..8) {
val pair = KeyPairGenerator.getInstance("RSA").genKeyPair()
assertEquals(pair.public, Keys.serialize(pair.public).let { Keys.deserializePublic(it) })
assertEquals(pair.private, Keys.serialize(pair.private).let { Keys.deserializePrivate(it) })
}
}

@Test
fun generateKeys() {
val aes = KeyGenerator.getInstance("AES").generateKey()
val rsa = KeyPairGenerator.getInstance("RSA").genKeyPair()

println("AES Secret: '${Keys.serialize(aes)}'")
println("RSA Public: '${Keys.serialize(rsa.public)}'")
println("RSA Private: '${Keys.serialize(rsa.private)}'")
}
}
8 changes: 7 additions & 1 deletion backend/gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ io-swagger-core-v3-swagger = "2.2.25"
org-openapitools-jackson-databind-nullable = "0.2.6"
org-springdoc-springdoc-openapi-starter = "2.6.0"

io-jsonwebtoken = "0.12.6"

org-jooq = "3.19.15"
org-liquibase-liquibase-core = "4.29.2"
org-postgresql-postgresql = "42.7.4"
Expand All @@ -16,11 +18,11 @@ jakarta-validation-jakarta-validation-api = "3.1.0"

org-jetbrains-kotlinx-kotlinx-coroutines = "1.9.0"
io-projectreactor-kotlin-reactor-kotlin-extensions = "1.2.3"

io-projectreactor-reactor-test = "3.6.11"
junit-junit = "4.13.2"
org-testcontainers = "1.20.3"


[libraries]
org-springframework-boot-spring-boot = { module = "org.springframework.boot:spring-boot", version.ref = "org-springframework-boot-spring-boot" }
org-springframework-boot-spring-boot-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux", version.ref = "org-springframework-boot-spring-boot" }
Expand All @@ -36,6 +38,10 @@ io-swagger-core-v3-swagger-annotations = { module = "io.swagger.core.v3:swagger-
io-swagger-core-v3-swagger-models = { module = "io.swagger.core.v3:swagger-models", version.ref = "io-swagger-core-v3-swagger" }
org-openapitools-jackson-databind-nullable = { module = "org.openapitools:jackson-databind-nullable", version.ref = "org-openapitools-jackson-databind-nullable" }

io-jsonwebtoken-jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "io-jsonwebtoken" }
io-jsonwebtoken-jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "io-jsonwebtoken" }
io-jsonwebtoken-jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "io-jsonwebtoken" }

jakarta-validation-jakarta-validation-api = { module = "jakarta.validation:jakarta.validation-api", version.ref = "jakarta-validation-jakarta-validation-api" }
com-fasterxml-jackson-core-jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "com-fasterxml-jackson-core-jackson-databind" }

Expand Down

0 comments on commit 2734637

Please sign in to comment.