Skip to content

Commit 975ab1e

Browse files
committed
feat: password hashing with argon2
1 parent 4dcd80d commit 975ab1e

File tree

5 files changed

+54
-7
lines changed

5 files changed

+54
-7
lines changed

.editorconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ charset = utf-8
55
end_of_line = lf
66
indent_size = 4
77
indent_style = tab
8-
insert_final_newline = false
8+
insert_final_newline = true
99
max_line_length = 120
1010
tab_width = 4
1111
# noinspection EditorConfigKeyCorrectness
@@ -20,4 +20,4 @@ indent_size = 2
2020
indent_style = space
2121

2222
[*.md]
23-
trim_trailing_whitespace = true
23+
trim_trailing_whitespace = false

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies {
3535
implementation(libs.flyway.core)
3636
implementation(libs.flyway.mysql)
3737
implementation(libs.hikaricp)
38+
implementation(libs.argon2)
3839
implementation(libs.mysql.connector.j)
3940
implementation(libs.mariadb.java.client)
4041
implementation(libs.logback.classic)

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ mysql-connector-j = { module = "com.mysql:mysql-connector-j", version = "9.2.0"
2525
mariadb-java-client = { module = "org.mariadb.jdbc:mariadb-java-client", version = "3.5.4" }
2626
hikaricp = { module = "com.zaxxer:HikariCP", version = "6.3.0" }
2727

28+
argon2 = { module = "de.mkammerer:argon2-jvm", version = "2.12" }
29+
2830
logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-version" }
2931

3032
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host", version.ref = "ktor-version" }

src/main/kotlin/org/kotatsu/resources/User.kt

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package org.kotatsu.resources
33
import org.kotatsu.database
44
import org.kotatsu.model.user.*
55
import org.kotatsu.model.users
6-
import org.kotatsu.util.md5
76
import org.kotatsu.util.withRetry
87
import org.ktorm.dsl.eq
98
import org.ktorm.dsl.insertAndGenerateKey
109
import org.ktorm.entity.find
1110
import org.ktorm.entity.singleOrNull
11+
import org.kotatsu.util.PasswordHasher
12+
import org.kotatsu.util.md5
1213

1314
suspend fun getOrCreateUser(request: AuthRequest, allowNewRegister: Boolean): UserInfo? = withRetry {
1415
require(request.password.length in 2..24) {
@@ -17,16 +18,38 @@ suspend fun getOrCreateUser(request: AuthRequest, allowNewRegister: Boolean): Us
1718
require(request.email.length in 5..320 && '@' in request.email) {
1819
"Invalid email address"
1920
}
20-
val passDigest = request.password.md5()
21+
2122
val user = database.users.find { (it.email eq request.email) }
23+
2224
when {
23-
user == null && allowNewRegister -> registerUser(request, passDigest)
25+
user == null && allowNewRegister -> {
26+
val bcryptHash = PasswordHasher.hash(request.password)
27+
registerUser(request, bcryptHash)
28+
}
2429
user == null -> null
25-
user.passwordHash == passDigest -> user.toUserInfo()
26-
else -> null
30+
else -> {
31+
val storedHash = user.passwordHash
32+
33+
when {
34+
PasswordHasher.isArgon2Hash(storedHash) -> {
35+
if (PasswordHasher.verify(request.password, storedHash)) {
36+
user.toUserInfo()
37+
} else null
38+
}
39+
else -> { // MD5 legacy
40+
if (storedHash == request.password.md5()) {
41+
// Upgrade to bcrypt
42+
user.passwordHash = PasswordHasher.hash(request.password)
43+
user.flushChanges()
44+
user.toUserInfo()
45+
} else null
46+
}
47+
}
48+
}
2749
}
2850
}
2951

52+
3053
private fun registerUser(request: AuthRequest, passwordDigest: String): UserInfo? {
3154
val userId = database.insertAndGenerateKey(UsersTable) {
3255
set(it.email, request.email)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.kotatsu.util
2+
3+
import de.mkammerer.argon2.Argon2
4+
import de.mkammerer.argon2.Argon2Factory
5+
6+
object PasswordHasher {
7+
private val argon2: Argon2 = Argon2Factory.create(Argon2Factory.Argon2Types.ARGON2id)
8+
9+
private const val ITERATIONS = 3
10+
private const val MEMORY_KB = 65536 // 64 MB
11+
private const val PARALLELISM = 1
12+
13+
fun hash(password: String): String =
14+
argon2.hash(ITERATIONS, MEMORY_KB, PARALLELISM, password.toCharArray())
15+
16+
fun verify(password: String, hash: String): Boolean =
17+
argon2.verify(hash, password.toCharArray())
18+
19+
fun isArgon2Hash(hash: String): Boolean =
20+
hash.startsWith("\$argon2id\$")
21+
}

0 commit comments

Comments
 (0)