Skip to content

Commit 3674110

Browse files
committed
server: store tokens hashed with argon2
Auth tokens aren't stored as plaintext on the server but are hashed with argon2. The salt is the creation time of the token. Temporary tokens aren't hashed because they are short-lived (10 min max from creation till redemption). https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf
1 parent 7ea6506 commit 3674110

File tree

5 files changed

+61
-14
lines changed

5 files changed

+61
-14
lines changed

server/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ dependencies {
6363
implementation("org.postgresql:postgresql:42.2.24")
6464
implementation("org.kohsuke:github-api:1.133")
6565

66+
implementation("org.bouncycastle:bcprov-jdk15on:1.69")
67+
68+
6669
implementation (platform ("software.amazon.awssdk:bom:2.17.56"))
6770
implementation("software.amazon.awssdk:s3")
6871

server/src/main/kotlin/cloud/skadi/gist/Helper.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import io.ktor.request.*
1313
import io.ktor.response.*
1414
import io.ktor.util.*
1515
import io.seruco.encoding.base62.Base62
16+
import org.bouncycastle.crypto.generators.Argon2BytesGenerator
17+
import org.bouncycastle.crypto.params.Argon2Parameters
1618
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
1719
import org.jetbrains.exposed.sql.transactions.transaction
1820
import java.nio.ByteBuffer
@@ -166,4 +168,24 @@ fun ApplicationCall.url(gist: Gist) =
166168
fun String.sha256(): ByteArray {
167169
val digest = MessageDigest.getInstance("SHA-256")
168170
return digest.digest(this.toByteArray())
171+
}
172+
private val base64Encoder = Base64.getEncoder()
173+
private val base64Decoder = Base64.getDecoder()
174+
175+
fun ByteArray.base64() = base64Encoder.encodeToString(this)
176+
fun String.decoderBase64() = base64Decoder.decode(this)
177+
178+
fun encodeWithArgon(salt: ByteArray, toEncode: String): String {
179+
val params = Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
180+
.withSalt(salt)
181+
.withParallelism(1)
182+
.withMemoryAsKB(1 shl 12)
183+
.withIterations(3)
184+
.build()
185+
val hash = ByteArray(32)
186+
187+
val generator = Argon2BytesGenerator()
188+
generator.init(params)
189+
generator.generateBytes(toEncode.toCharArray(0), hash)
190+
return base64Encoder.encodeToString(salt) +"$"+ base64Encoder.encodeToString(hash)
169191
}

server/src/main/kotlin/cloud/skadi/gist/data/Entites.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class User(id: EntityID<Int>) : IntEntity(id) {
3333
}
3434

3535
object TokenTable: LongIdTable() {
36-
val token = varchar("token", 256).uniqueIndex()
36+
val token = varchar("token", 1024).uniqueIndex()
3737
val created = datetime("created")
3838
val lastUsed = datetime("last-used").nullable()
3939
val name = varchar("name", 256)

server/src/main/kotlin/cloud/skadi/gist/data/Queries.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package cloud.skadi.gist.data
22

3+
import cloud.skadi.gist.decodeBase64
4+
import cloud.skadi.gist.encodeWithArgon
35
import cloud.skadi.gist.plugins.GistSession
46
import cloud.skadi.gist.shared.GistVisibility
57
import io.ktor.util.*
@@ -30,9 +32,17 @@ suspend fun userByEmail(email: String) = newSuspendedTransaction {
3032
User.find { UserTable.email eq email }.firstOrNull()
3133
}
3234

33-
suspend fun userByToken(token: String, updateLastUsed: Boolean = true) =
34-
newSuspendedTransaction {
35-
val dbToken = Token.find { TokenTable.token eq token and (TokenTable.isTemporary eq false) }.firstOrNull()
35+
fun userByToken(token: String, updateLastUsed: Boolean = true) =
36+
transaction {
37+
val tokenToFind = if(token.startsWith("v2_")) {
38+
val split = token.substring(3).split('$')
39+
val salt = split.first()
40+
val token = split.last()
41+
encodeWithArgon(salt.decodeBase64(), token)
42+
} else {
43+
token
44+
}
45+
val dbToken = Token.find { TokenTable.token eq tokenToFind and (TokenTable.isTemporary eq false) }.firstOrNull()
3646
if (updateLastUsed)
3747
dbToken?.lastUsed = LocalDateTime.now()
3848
dbToken?.user

server/src/main/kotlin/cloud/skadi/gist/routing/Ide.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package cloud.skadi.gist.routing
22

33
import cloud.skadi.gist.authenticated
4+
import cloud.skadi.gist.base64
45
import cloud.skadi.gist.data.Token
56
import cloud.skadi.gist.data.getTemporaryToken
7+
import cloud.skadi.gist.encodeWithArgon
68
import cloud.skadi.gist.shared.*
79
import io.ktor.application.*
810
import io.ktor.http.*
@@ -11,7 +13,10 @@ import io.ktor.response.*
1113
import io.ktor.routing.*
1214
import io.ktor.util.*
1315
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
16+
import java.nio.ByteBuffer
1417
import java.time.LocalDateTime
18+
import java.time.ZoneOffset
19+
1520

1621
fun Application.configureIdeRoutes() {
1722

@@ -26,7 +31,7 @@ fun Application.configureIdeRoutes() {
2631
val deviceName = call.parameters[PARAMETER_DEVICE_NAME]
2732
val csrfToken = call.parameters[PARAMETER_CSRF_TOKEN]
2833

29-
if(callback == null) {
34+
if (callback == null) {
3035
call.respond(HttpStatusCode.BadRequest)
3136
return@authenticated
3237
}
@@ -35,7 +40,7 @@ fun Application.configureIdeRoutes() {
3540
return@authenticated
3641
}
3742

38-
if(csrfToken == null) {
43+
if (csrfToken == null) {
3944
call.respond(HttpStatusCode.BadRequest)
4045
return@authenticated
4146
}
@@ -47,10 +52,12 @@ fun Application.configureIdeRoutes() {
4752
token = generateNonce()
4853
isTemporary = true
4954
}
50-
call.respondRedirect { takeFrom(callback)
55+
call.respondRedirect {
56+
takeFrom(callback)
5157
parameters[PARAMETER_CSRF_TOKEN] = csrfToken
5258
parameters[PARAMETER_TEMPORARY_TOKEN] = token.token
53-
parameters[PARAMETER_USER_NAME] = user.login }
59+
parameters[PARAMETER_USER_NAME] = user.login
60+
}
5461
}
5562

5663
}
@@ -59,28 +66,33 @@ fun Application.configureIdeRoutes() {
5966
val parameters = call.receiveParameters()
6067
val temporaryToken = parameters[PARAMETER_TEMPORARY_TOKEN]
6168

62-
if(temporaryToken == null) {
69+
if (temporaryToken == null) {
6370
call.respond(HttpStatusCode.NotFound)
6471
return@post
6572
}
6673

6774
val dbToken = getTemporaryToken(temporaryToken)
68-
69-
if(dbToken == null) {
75+
if (dbToken == null) {
7076
call.respond(HttpStatusCode.NotFound)
7177
return@post
7278
}
7379

80+
val nonce = generateNonce()
81+
val created = LocalDateTime.now()
82+
val salt =
83+
ByteBuffer.allocate(12).putLong(created.toEpochSecond(ZoneOffset.UTC)).putInt(created.nano).array()
84+
val encoded = encodeWithArgon(salt, nonce)
85+
7486
newSuspendedTransaction {
7587
val token = Token.new {
7688
name = dbToken.name
7789
this.user = dbToken.user
78-
created = LocalDateTime.now()
79-
token = generateNonce()
90+
this.created = created
91+
token = encoded
8092
isTemporary = false
8193
}
8294
dbToken.lastUsed = LocalDateTime.now()
83-
call.respondText(token.token)
95+
call.respondText("v2_${salt.base64()}$$nonce")
8496
}
8597
}
8698
}

0 commit comments

Comments
 (0)