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 d009df3..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 { @@ -40,22 +37,22 @@ class LoxoneTokenAuthenticator @JvmOverloads constructor( } } + // Websockets that are authenticated private val authWebsockets = mutableSetOf() 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, @@ -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) } @@ -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) @@ -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()) { 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..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] @@ -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