Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: deadlock and hashing #41

Merged
merged 3 commits into from
Dec 9, 2024
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
9 changes: 2 additions & 7 deletions src/commonMain/kotlin/LoxoneCrypto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,8 @@ internal object LoxoneCrypto {
}
}

@OptIn(ExperimentalStdlibApi::class)
fun loxoneHmac(token: Token, operation: String): String =
token.withTokenAndKey { tokenVal, key ->
bytesToHex(HmacSHA256(key).doFinal(tokenVal.encodeToByteArray())).also { finalHash ->
logger.trace { "$operation final hash: $finalHash" }
}
}
fun loxoneHmac(token: Token, hashing: Hashing, operation: String): String =
loxoneHmac(checkNotNull(token.token) { "Can't hash non-filled token" }, hashing, operation)

@OptIn(ExperimentalStdlibApi::class)
private fun loxoneDigest(secret: String, hashing: Hashing): String {
Expand Down
28 changes: 15 additions & 13 deletions src/commonMain/kotlin/LoxoneTokenAuthenticator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package cz.smarteon.loxkt

import cz.smarteon.loxkt.LoxoneCommands.Tokens
import cz.smarteon.loxkt.LoxoneCrypto.loxoneHashing
import cz.smarteon.loxkt.message.Hashing
import cz.smarteon.loxkt.message.Hashing.Companion.commandForUser
import cz.smarteon.loxkt.message.Token
import cz.smarteon.loxkt.message.TokenState
Expand All @@ -26,8 +25,6 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor(

private val mutex = Mutex()

private var hashing: Hashing? = null

private var token: Token? by Delegates.observable(repository.getToken(profile)) { _, _, newValue ->
if (newValue != null) {
logger.info {
Expand All @@ -40,22 +37,22 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor(
}
}

// Websockets that are authenticated
private val authWebsockets = mutableSetOf<WebsocketLoxoneClient>()

suspend fun ensureAuthenticated(client: LoxoneClient) =
execConditionalWithMutex({ !TokenState(token).isUsable || !authWebsockets.contains(client) }) {
if (hashing == null) {
hashing = client.callForMsg(commandForUser(user))
}

execConditionalWithMutex({
!TokenState(token).isUsable ||
(client is WebsocketLoxoneClient && !authWebsockets.contains(client))
}) {
val hashing = client.callForMsg(commandForUser(user))
val state = TokenState(token)

when {
state.isExpired -> {
logger.debug { "Token expired, requesting new one" }
token = client.callForMsg(
Tokens.get(
loxoneHashing(profile.credentials!!.password, checkNotNull(hashing), "getttoken", user),
loxoneHashing(profile.credentials!!.password, hashing, "getttoken", user),
user,
settings.tokenPermission,
settings.clientId,
Expand All @@ -75,7 +72,7 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor(
else -> {
if (client is WebsocketLoxoneClient) {
logger.debug { "Authenticating websocket with token $token" }
val authResponse = client.callForMsg(Tokens.auth(tokenHash("authenticate"), user))
val authResponse = client.callForMsg(Tokens.auth(tokenHash(client, "authenticate"), user))
token = token!!.merge(authResponse)
authWebsockets.add(client)
}
Expand All @@ -85,7 +82,7 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor(

suspend fun killToken(client: LoxoneClient) = execConditionalWithMutex({ TokenState(token).isUsable }) {
logger.debug { "Going to kill token $token" }
client.callForMsg(Tokens.kill(tokenHash("killtoken"), user))
client.callForMsg(Tokens.kill(tokenHash(client, "killtoken"), user))
logger.info { "Token killed" }
token = null
authWebsockets.remove(client)
Expand All @@ -97,7 +94,12 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor(
}
}

fun tokenHash(operation: String) = LoxoneCrypto.loxoneHmac(checkNotNull(token), operation)
suspend fun tokenHash(client: LoxoneClient, operation: String) =
LoxoneCrypto.loxoneHmac(
checkNotNull(token) { "Token must be present for hashing" },
client.callForMsg(commandForUser(user)),
operation
)

private suspend fun execConditionalWithMutex(condition: () -> Boolean, block: suspend () -> Unit) {
if (condition()) {
Expand Down
16 changes: 11 additions & 5 deletions src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,23 @@ class KtorHttpLoxoneClient internal constructor(
httpClient.close()
}

private fun HttpRequestBuilder.commandRequest(addAuth: Boolean = true, pathBuilder: URLBuilder.() -> Unit) {
private suspend fun HttpRequestBuilder.commandRequest(addAuth: Boolean = true, pathBuilder: URLBuilder.() -> Unit) {
val userAndHash = if (addAuth && authentication is LoxoneAuth.Token) {
val authenticator = authentication.authenticator
authenticator.user to authenticator.tokenHash(this@KtorHttpLoxoneClient, "http-autht")
} else {
null
}

url {
protocol = if (endpoint.useSsl) URLProtocol.HTTPS else URLProtocol.HTTP
host = endpoint.host
port = endpoint.port
appendEncodedPathSegments(endpoint.path)
pathBuilder()
if (addAuth && authentication is LoxoneAuth.Token) {
val authenticator = authentication.authenticator
parameters.append("autht", authenticator.tokenHash("http-autht"))
parameters.append("user", authenticator.user)
if (userAndHash != null) {
parameters.append("user", userAndHash.first)
parameters.append("autht", userAndHash.second)
}
}
}
Expand Down
7 changes: 1 addition & 6 deletions src/commonMain/kotlin/message/Token.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable
/**
* Represents Loxone authentication token.
*
* @roperty[token] The actual token value. May be null in case of response to refresh token or `authwithtoken`
* @property[token] The actual token value. May be null in case of response to refresh token or `authwithtoken`
* @property[key] The token key value. May be null in case of response to refresh token or authwithtoken.
* @property[validUntil] Seconds since loxone epoch (1.1.2009) to which the token is valid.
* @property[rights]
Expand All @@ -31,11 +31,6 @@ data class Token(
*/
fun secondsToExpireFromNow() = LoxoneTime.getUnixEpochSeconds(validUntil) - Clock.System.now().epochSeconds

fun <T> withTokenAndKey(block: (String, ByteArray) -> T): T {
check(filled) { "Can't invoke block(token, key) on nonfilled token" }
return block(token!!, key!!)
}

/**
* Merges the given token to this one and returns the merged token. The [Token.token] and [Token.key] are taken
* from given token only if they are not null, otherwise the values from this token are used. Other properties are
Expand Down
Loading