Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions src/main/kotlin/com/wire/github/EventsHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ import com.wire.sdk.WireEventsHandlerSuspending
import com.wire.sdk.model.Conversation
import com.wire.sdk.model.ConversationMember
import com.wire.sdk.model.WireMessage
import io.lettuce.core.api.StatefulRedisConnection
import org.koin.core.context.GlobalContext
import org.slf4j.LoggerFactory

class EventsHandler : WireEventsHandlerSuspending() {
private val logger = LoggerFactory.getLogger(this::class.java)
private val redisConnection = GlobalContext.get().get<StatefulRedisConnection<String, String>>()
private val redisService = GlobalContext.get().get<RedisService>()
private val usageMetrics = GlobalContext.get().get<UsageMetrics>()
private val commandHandler = GitHubCommandHandler(
storage = redisConnection.sync(),
redisService = redisService,
usageMetrics = usageMetrics
)

Expand Down
20 changes: 9 additions & 11 deletions src/main/kotlin/com/wire/github/GitHubCommandHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ import com.wire.github.metrics.UsageMetrics
import com.wire.github.util.ENV_VAR_HOST
import com.wire.github.util.ActionsTokenGenerator
import com.wire.github.util.SessionIdentifierGenerator
import com.wire.github.util.toActionsTokenStorageKey
import com.wire.github.util.toStorageKey
import com.wire.sdk.model.QualifiedId
import io.lettuce.core.api.sync.RedisCommands

internal class GitHubCommandHandler(
private val storage: RedisCommands<String, String>,
private val redisService: RedisService,
private val usageMetrics: UsageMetrics,
private val host: String = ENV_VAR_HOST,
private val generateSecret: () -> String = SessionIdentifierGenerator::generate,
Expand Down Expand Up @@ -64,15 +61,16 @@ internal class GitHubCommandHandler(
response.also { usageMetrics.onHelpCommand() }

private fun getOrCreateSecret(conversationId: QualifiedId): String =
getOrCreate(conversationId.toStorageKey(), generateSecret)
redisService.getWebhookSecret(conversationId)
?: generateSecret().also {
redisService.setWebhookSecret(conversationId, it)
}

private fun getOrCreateActionsToken(conversationId: QualifiedId): String =
getOrCreate(conversationId.toActionsTokenStorageKey(), generateActionsToken)

private fun getOrCreate(
storageKey: String,
generate: () -> String
): String = storage.get(storageKey) ?: generate().also { storage.set(storageKey, it) }
redisService.getActionSecret(conversationId)
?: generateActionsToken().also {
redisService.setActionSecret(conversationId, it)
}

private val normalizedHost: String
get() = host.trimEnd('/')
Expand Down
88 changes: 88 additions & 0 deletions src/main/kotlin/com/wire/github/RedisService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.wire.github

import com.wire.sdk.model.QualifiedId
import io.lettuce.core.api.sync.RedisCommands
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.security.MessageDigest
import java.util.Locale
import java.util.UUID
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class RedisService(
private val storage: RedisCommands<String, String>
) {
fun getWebhookSecret(conversationId: QualifiedId): String? =
storage.get(conversationId.toWebhookStorageKey())

fun setWebhookSecret(
conversationId: QualifiedId,
secret: String
) {
storage.set(conversationId.toWebhookStorageKey(), secret)
}

fun getActionSecret(conversationId: QualifiedId): String? =
storage.get(conversationId.toActionStorageKey())

fun setActionSecret(
conversationId: QualifiedId,
secret: String
) {
storage.set(conversationId.toActionStorageKey(), secret)
}

fun isActionsTokenValid(
conversationId: QualifiedId,
token: String
): Boolean {
val storedToken = getActionSecret(conversationId) ?: return false
return MessageDigest.isEqual(
storedToken.toByteArray(StandardCharsets.UTF_8),
token.toByteArray(StandardCharsets.UTF_8)
)
}

@Throws(Exception::class)
fun isWebhookSignatureValid(
conversationId: String,
conversationDomain: String,
signature: String,
payload: String
): Boolean {
val qualifiedConversationId = QualifiedId(
UUID.fromString(conversationId),
conversationDomain
)
val secret = getWebhookSecret(qualifiedConversationId) ?: throw IOException(
"Missing secret for Conversation: $conversationId"
)
val generatedHmacSha1 = generateHmacSha1(payload, secret)
val challenge = String.format(Locale.getDefault(), "sha1=%s", generatedHmacSha1)

return challenge == signature
}

internal fun generateHmacSha1(
data: String,
secret: String
): String {
val keySpec = SecretKeySpec(secret.toByteArray(), HMAC_ALGORITHM)
val mac = Mac.getInstance(HMAC_ALGORITHM)
mac.init(keySpec)
val digest = mac.doFinal(data.toByteArray())

return digest.joinToString("") { "%02x".format(it) }
}

private fun QualifiedId.toWebhookStorageKey() = "$STORAGE_KEY_PREFIX$id@$domain"

private fun QualifiedId.toActionStorageKey() = "${toWebhookStorageKey()}$ACTION_TOKEN_SUFFIX"

private companion object {
const val STORAGE_KEY_PREFIX = "github-app:"
const val ACTION_TOKEN_SUFFIX = ":actions-token"
const val HMAC_ALGORITHM = "HmacSHA1"
}
}
13 changes: 5 additions & 8 deletions src/main/kotlin/com/wire/github/Routing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ package com.wire.github
import com.wire.github.metrics.UsageMetrics
import com.wire.github.request.model.ActionsNotificationRequest
import com.wire.github.response.model.GitHubResponse
import com.wire.github.util.ActionsTokenValidator
import com.wire.github.util.KtxSerializer
import com.wire.github.util.SignatureValidator
import com.wire.github.util.TemplateHandler
import com.wire.sdk.WireAppSdk
import com.wire.sdk.model.QualifiedId
Expand Down Expand Up @@ -44,8 +42,7 @@ fun Application.configureRouting() {
}

val wireAppSdk = GlobalContext.get().get<WireAppSdk>()
val actionsTokenValidator = GlobalContext.get().get<ActionsTokenValidator>()
val signatureValidator = GlobalContext.get().get<SignatureValidator>()
val redisService = GlobalContext.get().get<RedisService>()
val templateHandler = GlobalContext.get().get<TemplateHandler>()
val usageMetrics = GlobalContext.get().get<UsageMetrics>()

Expand Down Expand Up @@ -86,7 +83,7 @@ fun Application.configureRouting() {

// Validation of received signature
val isSignatureValid = try {
signatureValidator.isValid(
redisService.isWebhookSignatureValid(
conversationId = conversationId,
conversationDomain = conversationDomain,
signature = signature,
Expand Down Expand Up @@ -156,7 +153,7 @@ fun Application.configureRouting() {

configureActionsRoute(
wireAppSdk = wireAppSdk,
actionsTokenValidator = actionsTokenValidator
redisService = redisService
)
}
}
Expand All @@ -166,7 +163,7 @@ private const val PARAM_CONVERSATION_DOMAIN = "conversationDomain"

private fun Route.configureActionsRoute(
wireAppSdk: WireAppSdk,
actionsTokenValidator: ActionsTokenValidator
redisService: RedisService
) {
post("/actions/{$PARAM_CONVERSATION_ID}/{$PARAM_CONVERSATION_DOMAIN}") {
val conversationId = call.parameters[PARAM_CONVERSATION_ID]
Expand Down Expand Up @@ -199,7 +196,7 @@ private fun Route.configureActionsRoute(
"Bearer authorization is required"
)

if (!actionsTokenValidator.isValid(qualifiedConversationId, bearerToken)) {
if (!redisService.isActionsTokenValid(qualifiedConversationId, bearerToken)) {
application.log.warn(
"Invalid Actions token for conversation $conversationId@$conversationDomain"
)
Expand Down
10 changes: 2 additions & 8 deletions src/main/kotlin/com/wire/github/config/Modules.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.wire.github.config

import com.wire.github.EventsHandler
import com.wire.github.RedisService
import com.wire.github.metrics.UsageMetrics
import com.wire.github.util.ActionsTokenValidator
import com.wire.github.util.ENV_VAR_API_HOST
import com.wire.github.util.ENV_VAR_API_TOKEN
import com.wire.github.util.ENV_VAR_CRYPTOGRAPHY_STORAGE_KEY
import com.wire.github.util.ENV_VAR_REDIS_URL
import com.wire.github.util.SignatureValidator
import com.wire.github.util.TemplateHandler
import com.wire.sdk.WireAppSdk
import io.ktor.utils.io.core.toByteArray
Expand All @@ -23,15 +22,10 @@ val projectModules = module {
startListening()
}
}
single { SignatureValidator() }
single {
ActionsTokenValidator(
storage = get<StatefulRedisConnection<String, String>>().sync()
)
}
single { TemplateHandler() }
single { RedisClient.create(ENV_VAR_REDIS_URL) }
single<StatefulRedisConnection<String, String>> { get<RedisClient>().connect() }
single { RedisService(storage = get<StatefulRedisConnection<String, String>>().sync()) }
single { PrometheusMeterRegistry(PrometheusConfig.DEFAULT) }
single { UsageMetrics(registry = get<PrometheusMeterRegistry>()) }
}
Expand Down
22 changes: 0 additions & 22 deletions src/main/kotlin/com/wire/github/util/ActionsTokenValidator.kt

This file was deleted.

12 changes: 0 additions & 12 deletions src/main/kotlin/com/wire/github/util/Extensions.kt

This file was deleted.

50 changes: 0 additions & 50 deletions src/main/kotlin/com/wire/github/util/SignatureValidator.kt

This file was deleted.

Loading