From 40c5f03a2c7d5ea0d2097bdf5219509d14a5ced3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Mikul=C3=A1=C5=A1ek?= Date: Thu, 5 Dec 2024 17:52:25 +0100 Subject: [PATCH 1/3] fix: remove deadlock on killToken with HttpClient --- src/commonMain/kotlin/LoxoneTokenAuthenticator.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt b/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt index d009df3..1c8ff19 100644 --- a/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt +++ b/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt @@ -40,10 +40,14 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor( } } + // Websockets that are authenticated private val authWebsockets = mutableSetOf() suspend fun ensureAuthenticated(client: LoxoneClient) = - execConditionalWithMutex({ !TokenState(token).isUsable || !authWebsockets.contains(client) }) { + execConditionalWithMutex({ + // Token is not usable or websocket client is not authenticated + !TokenState(token).isUsable || (client is WebsocketLoxoneClient && !authWebsockets.contains(client)) + }) { if (hashing == null) { hashing = client.callForMsg(commandForUser(user)) } From 60c1b5d5a62c188222c3632faafb18b56501a41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Mikul=C3=A1=C5=A1ek?= Date: Fri, 6 Dec 2024 13:45:56 +0100 Subject: [PATCH 2/3] fix: try get hashing key everytime for http --- src/commonMain/kotlin/LoxoneCrypto.kt | 9 ++----- .../kotlin/LoxoneTokenAuthenticator.kt | 26 +++++++++---------- .../kotlin/ktor/KtorHttpLoxoneClient.kt | 16 ++++++++---- src/commonMain/kotlin/message/Token.kt | 5 ---- 4 files changed, 25 insertions(+), 31 deletions(-) diff --git a/src/commonMain/kotlin/LoxoneCrypto.kt b/src/commonMain/kotlin/LoxoneCrypto.kt index 3b07854..f0acd80 100644 --- a/src/commonMain/kotlin/LoxoneCrypto.kt +++ b/src/commonMain/kotlin/LoxoneCrypto.kt @@ -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 { diff --git a/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt b/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt index 1c8ff19..745dede 100644 --- a/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt +++ b/src/commonMain/kotlin/LoxoneTokenAuthenticator.kt @@ -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 @@ -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 { @@ -45,21 +42,17 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor( suspend fun ensureAuthenticated(client: LoxoneClient) = execConditionalWithMutex({ - // Token is not usable or websocket client is not authenticated - !TokenState(token).isUsable || (client is WebsocketLoxoneClient && !authWebsockets.contains(client)) + !TokenState(token).isUsable || + (client is WebsocketLoxoneClient && !authWebsockets.contains(client)) }) { - if (hashing == null) { - hashing = client.callForMsg(commandForUser(user)) - } - + 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, @@ -79,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) } @@ -89,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) @@ -101,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()) { diff --git a/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt b/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt index 448476f..fb71d3b 100644 --- a/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt +++ b/src/commonMain/kotlin/ktor/KtorHttpLoxoneClient.kt @@ -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) } } } diff --git a/src/commonMain/kotlin/message/Token.kt b/src/commonMain/kotlin/message/Token.kt index 790d60a..4a78f92 100644 --- a/src/commonMain/kotlin/message/Token.kt +++ b/src/commonMain/kotlin/message/Token.kt @@ -31,11 +31,6 @@ data class Token( */ fun secondsToExpireFromNow() = LoxoneTime.getUnixEpochSeconds(validUntil) - Clock.System.now().epochSeconds - fun 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 From 02f450e0ac782ffc21e391d86762d14f35691bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Mikul=C3=A1=C5=A1ek?= Date: Fri, 6 Dec 2024 17:08:06 +0100 Subject: [PATCH 3/3] fix: typo in kdoc --- src/commonMain/kotlin/message/Token.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/message/Token.kt b/src/commonMain/kotlin/message/Token.kt index 4a78f92..a400200 100644 --- a/src/commonMain/kotlin/message/Token.kt +++ b/src/commonMain/kotlin/message/Token.kt @@ -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]