diff --git a/build.gradle.kts b/build.gradle.kts index 97722ec..d77b5c3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ val logback_version="1.2.10" group = "net.sergeych" -version = "0.1.2-SNAPSHOT" +version = "0.2.1-SNAPSHOT" repositories { mavenCentral() diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt index 71bfc41..8b6d3c4 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt @@ -19,7 +19,7 @@ data class RegistrationArgs( val packedACO: ByteArray, val extraData: ByteArray? = null ) { - inline fun toSuccess(loginToken: ByteArray,extraData: T): AuthenticationResult.Success { + inline fun toSuccess(loginToken: ByteArray, extraData: T): AuthenticationResult.Success { return AuthenticationResult.Success( loginName, loginToken, BossEncoder.encode(extraData) ) @@ -37,7 +37,7 @@ sealed class AuthenticationResult { data class Success( val loginName: String, val loginToken: ByteArray, - val applicationData: ByteArray? + val applicationData: ByteArray?, ): AuthenticationResult() @Serializable diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt index 3258545..68e88de 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt @@ -10,7 +10,7 @@ sealed class LoginState(val isLoggedIn: Boolean) { * User is logged in (either connected or yet not). Client application should save * updated [loginData] at this point */ - class LoggedIn(val loginData: SuperloginData) : LoginState(true) + class LoggedIn(val loginData: ClientState) : LoginState(true) /** * Login state whatever it was now is logged out, and client application should delete diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt index ad57c72..7443b81 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt @@ -35,10 +35,20 @@ data class SuperloginData( val data: T? = null, ) +@Serializable +class ClientState(val slData: SuperloginData, val dataKey: SymmetricKey) { + val loginName by slData::loginName + val loginToken by slData::loginToken + val data by slData::data + + constructor(loginName: String,loginToken: ByteArray?,data: T?, dataKey: SymmetricKey) + : this(SuperloginData(loginName, loginToken, data), dataKey) +} + class SuperloginClient( private val transport: Parsec3Transport, - savedData: SuperloginData? = null, + savedData: ClientState? = null, private val dataType: KType, override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(), ) : Parsec3Transport, Loggable by LogTag("SLCLI") { @@ -52,7 +62,7 @@ class SuperloginClient( private val _cflow = MutableStateFlow(false) override val connectedFlow: StateFlow = _cflow - private var slData: SuperloginData? = savedData + private var clientState: ClientState? = savedData set(value) { if (field != value) { field = value @@ -60,7 +70,6 @@ class SuperloginClient( // do actual disconnect work _cflow.value = false _state.value = LoginState.LoggedOut - dataKey = null } else { val v = _state.value if (v !is LoginState.LoggedIn<*> || v.loginData != value) { @@ -114,23 +123,25 @@ class SuperloginClient( private val serverApi = SuperloginServerApi() private suspend fun tryRestoreLogin() { - slData?.loginToken?.let { token -> - debug { "trying to restore login with a token" } - while (true) { - try { - val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token) - slData = if (ar is AuthenticationResult.Success) { - val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } - debug { "login restored by the token: ${ar.loginName}" } - SuperloginData(ar.loginName, ar.loginToken, data) - } else { - debug { "failed to restore login by the token: $ar" } - null + clientState?.let { clientState -> + clientState.loginToken?.let { token -> + debug { "trying to restore login with a token" } + while (true) { + try { + val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token) + this.clientState = if (ar is AuthenticationResult.Success) { + val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } + debug { "login restored by the token: ${ar.loginName}" } + ClientState(ar.loginName, ar.loginToken, data, clientState.dataKey) + } else { + debug { "failed to restore login by the token: $ar" } + null + } + break + } catch (t: Throwable) { + exception { "failed to restore login by token, will retry" to t } + delay(1500) } - break - } catch (t: Throwable) { - exception { "failed to restore login by token, will retry" to t } - delay(1500) } } } ?: warning { "tryRestoreLogin is ignored as slData is now null" } @@ -181,8 +192,7 @@ class SuperloginClient( * as it would require storing user's password which must not be done_. * Use [retrieveDataKey] to restore to with a password (when logged in) */ - var dataKey: SymmetricKey? = null - private set + val dataKey: SymmetricKey? get() = clientState?.dataKey /** * Perform registration and login attempt and return the result. It automatically caches and reuses intermediate @@ -204,8 +214,7 @@ class SuperloginClient( return rn.registerWithData(loginName, password, extraData = BossEncoder.encode(dataType, data)) .also { rr -> if (rr is Registration.Result.Success) { - slData = SuperloginData(loginName, rr.loginToken, extractData(rr.encodedData)) - dataKey = rr.dataKey + clientState = ClientState(loginName, rr.loginToken, extractData(rr.encodedData), rr.dataKey) } } } @@ -225,7 +234,7 @@ class SuperloginClient( suspend fun logout() { mustBeLoggedIn() invoke(serverApi.slLogout) - slData = null + clientState = null } /** @@ -235,24 +244,23 @@ class SuperloginClient( * @return updated login data (and new token value) or null if token is not (or not anymore) * available for logging in. */ - suspend fun loginByToken(token: ByteArray): SuperloginData? { + suspend fun loginByToken(token: ByteArray,newDataKey: SymmetricKey): ClientState? { mustBeLoggedOut() val r = invoke(serverApi.slLoginByToken, token) return when (r) { - AuthenticationResult.LoginIdUnavailable -> TODO() - AuthenticationResult.LoginUnavailable -> null - AuthenticationResult.RestoreIdUnavailable -> TODO() - is AuthenticationResult.Success -> SuperloginData( + is AuthenticationResult.Success -> ClientState( r.loginName, r.loginToken, - extractData(r.applicationData) + extractData(r.applicationData), + newDataKey ).also { - slData = it + clientState = it } + else -> null } } - suspend fun loginByPassword(loginName: String, password: String): SuperloginData? { + suspend fun loginByPassword(loginName: String, password: String): ClientState? { mustBeLoggedOut() // Request derivation params val params = invoke(serverApi.slRequestDerivationParams, loginName) @@ -276,10 +284,10 @@ class SuperloginClient( ) ) if (result is AuthenticationResult.Success) { - SuperloginData(loginName, result.loginToken, extractData(result.applicationData)) + ClientState(loginName, result.loginToken, extractData(result.applicationData), + aco.payload.dataStorageKey) .also { - slData = it - dataKey = aco.data.payload.dataStorageKey + clientState = it } } else null } @@ -290,41 +298,41 @@ class SuperloginClient( } } - /** - * Try to retrieve dataKey (usually after login by token), it is impossible - * to do without a valid password key. Should be logged in. If [dataKey] is not null - * what means is already known, returns it immediatel, otherwise uses password - * to access ACO on the server and extract data key. - * @return data key or null if it is impossible to do (no connection or wrong password) - */ - suspend fun retrieveDataKey(password: String): SymmetricKey? { - mustBeLoggedIn() - dataKey?.let { return it } - - val loginName = slData?.loginName ?: throw SLInternalException("slData: empty login name") - try { - val params = invoke( - serverApi.slRequestDerivationParams, - loginName - ) - val keys = DerivedKeys.derive(password, params) - // Request login data by derived it - return invoke( - serverApi.slRequestACOByLoginName, - RequestACOByLoginNameArgs(loginName, keys.loginId) - ).let { loginRequest -> - AccessControlObject.unpackWithKey( - loginRequest.packedACO, - keys.loginAccessKey - )?.let { aco -> - aco.data.payload.dataStorageKey.also { dataKey = it } - } - } - } catch (t: Throwable) { - t.printStackTrace() - return null - } - } +// /** +// * Try to retrieve dataKey (usually after login by token), it is impossible +// * to do without a valid password key. Should be logged in. If [dataKey] is not null +// * what means is already known, returns it immediatel, otherwise uses password +// * to access ACO on the server and extract data key. +// * @return data key or null if it is impossible to do (no connection or wrong password) +// */ +// suspend fun retrieveDataKey(password: String): SymmetricKey? { +// mustBeLoggedIn() +// dataKey?.let { return it } +// +// val loginName = slData?.loginName ?: throw SLInternalException("slData: empty login name") +// try { +// val params = invoke( +// serverApi.slRequestDerivationParams, +// loginName +// ) +// val keys = DerivedKeys.derive(password, params) +// // Request login data by derived it +// return invoke( +// serverApi.slRequestACOByLoginName, +// RequestACOByLoginNameArgs(loginName, keys.loginId) +// ).let { loginRequest -> +// AccessControlObject.unpackWithKey( +// loginRequest.packedACO, +// keys.loginAccessKey +// )?.let { aco -> +// aco.data.payload.dataStorageKey.also { dataKey = it } +// } +// } +// } catch (t: Throwable) { +// t.printStackTrace() +// return null +// } +// } /** * Resets password and log in using a `secret` string (one that wwas reported on registration. __Never store @@ -342,15 +350,14 @@ class SuperloginClient( secret: String, newPassword: String, params: PasswordDerivationParams = PasswordDerivationParams(), loginKeyStrength: Int = 2048, - ): SuperloginData? { + ): ClientState? { mustBeLoggedOut() return try { val (id, key) = RestoreKey.parse(secret) val packedACO = invoke(serverApi.slRequestACOBySecretId, id) AccessControlObject.unpackWithKey(packedACO, key)?.let { changePasswordWithACO(it, newPassword) - dataKey = it.data.payload.dataStorageKey - slData + clientState } } catch (x: RestoreKey.InvalidSecretException) { null @@ -400,7 +407,8 @@ class SuperloginClient( ) when (result) { is AuthenticationResult.Success -> { - slData = SuperloginData(result.loginName, result.loginToken, extractData(result.applicationData)) + clientState = ClientState(result.loginName, result.loginToken, extractData(result.applicationData), + aco.payload.dataStorageKey) true } @@ -428,7 +436,7 @@ class SuperloginClient( loginKeyStrength: Int = 2048, ): Boolean { mustBeLoggedIn() - val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here") + val loginName = clientState?.loginName ?: throw SLInternalException("loginName should be defined here") val dp = invoke(serverApi.slRequestDerivationParams, loginName) val keys = DerivedKeys.derive(oldPassword, dp) val data = invoke(serverApi.slRequestACOByLoginName, RequestACOByLoginNameArgs(loginName, keys.loginId)) @@ -442,7 +450,7 @@ class SuperloginClient( companion object { inline operator fun invoke( t: Parsec3Transport, - savedData: SuperloginData? = null, + savedData: ClientState? = null, ): SuperloginClient { return SuperloginClient(t, savedData, typeOf()) } diff --git a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt index b7fadd2..5da44f3 100644 --- a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt +++ b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt @@ -166,18 +166,17 @@ internal class WsServerKtTest { assertIs(slc.state.value) assertEquals(null, slc.call(api.loginName)) - var ar = slc.loginByToken(token) + var ar = slc.loginByToken(token, dk1!!) assertNotNull(ar) - assertNull(slc.dataKey) assertEquals("bar!", ar.data?.foo) assertTrue { slc.isLoggedIn } assertEquals("foo", slc.call(api.loginName)) - assertNull(slc.retrieveDataKey("badpasswd")) - assertEquals(dk1?.id, slc.retrieveDataKey("passwd")?.id) - assertEquals(dk1?.id, slc.dataKey?.id) +// assertNull(slc.retrieveDataKey("badpasswd")) +// assertEquals(dk1?.id, slc.retrieveDataKey("passwd")?.id) +// assertEquals(dk1?.id, slc.dataKey?.id) // - assertThrowsAsync { slc.loginByToken(token) } + assertThrowsAsync { slc.loginByToken(token, dk1) } } }