From 318582cdb74938870fc516d303fac4a7f6fd17ba Mon Sep 17 00:00:00 2001 From: sergeych Date: Wed, 30 Nov 2022 20:32:22 +0100 Subject: [PATCH] working: registration with secret, login by token, logout, login by password. --- build.gradle.kts | 2 +- .../AccessControlObject.kt | 9 +- .../kotlin/net.sergeych.superlogin/api.kt | 20 ++++- .../client/Registration.kt | 11 ++- .../client/SuperloginClient.kt | 75 +++++++++++----- .../kotlin/net.sergeych.superlogin/salt.kt | 34 ++++++++ .../server/SLServerTraits.kt | 42 --------- .../superlogin/server/SLServerSession.kt | 86 +++++++++++++++++-- .../superlogin/server/SuperloginServer.kt | 71 +++++++++++---- .../kotlin/net/sergeych/WsServerKtTest.kt | 79 ++++++++++------- 10 files changed, 301 insertions(+), 128 deletions(-) create mode 100644 src/commonMain/kotlin/net.sergeych.superlogin/salt.kt delete mode 100644 src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt diff --git a/build.gradle.kts b/build.gradle.kts index fcfec9b..ee16946 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,7 +43,7 @@ kotlin { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") - api("net.sergeych:unikrypto:1.2.1-SNAPSHOT") + api("net.sergeych:unikrypto:1.2.2-SNAPSHOT") api("net.sergeych:parsec3:0.3.2-SNAPSHOT") api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT") 3 } diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt b/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt index 3028854..275b090 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt @@ -1,10 +1,9 @@ package net.sergeych.superlogin import kotlinx.serialization.Serializable +import net.sergeych.boss_serialization.BossDecoder import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.unikrypto.Container -import net.sergeych.unikrypto.DecryptingKey -import net.sergeych.unikrypto.Safe58 import net.sergeych.unikrypto.SymmetricKey import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -131,10 +130,14 @@ class AccessControlObject( */ inline fun unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject? = Container.decrypt>(packed, passwordKey)?.let { - println(it) AccessControlObject(typeOf>(), packed, passwordKey, it) } + fun unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey,payloadType: KType): AccessControlObject? = + Container.decryptAsBytes(packed, passwordKey)?.let { + AccessControlObject(payloadType, packed, passwordKey, BossDecoder.decodeFrom(payloadType,it)) + } + /** * Unpack and decrypt ACO with a [secret]. If you want to check [secret] integrity, use * [RestoreKey.checkSecretIntegrity]. diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt index 3fc3263..c962777 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt @@ -21,6 +21,7 @@ data class RegistrationArgs( sealed class AuthenticationResult { @Serializable data class Success( + val loginName: String, val loginToken: ByteArray, val applicationData: ByteArray? ): AuthenticationResult() @@ -36,19 +37,32 @@ sealed class AuthenticationResult { } @Serializable -data class LoginArgs( +data class RequestLoginDataArgs( + val loginName: String, val loginId: ByteArray, - val packedSignedRecord: ByteArray ) +@Serializable +data class RequestLoginDataResult( + val packedACO: ByteArray, + val nonce: ByteArray +) + +@Serializable +data class LoginByPasswordPayload( + val loginName: String +) class SuperloginServerApi : CommandHost() { - val slRegister by command() + val slGetNonce by command() + val slRegister by command() val slLogout by command() val slLoginByToken by command() val slRequestDerivationParams by command() + val slRequestLoginData by command() + val slLoginByKey by command() /** diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt index ca4ea0d..4027325 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/Registration.kt @@ -11,6 +11,7 @@ import net.sergeych.parsec3.WithAdapter import net.sergeych.superlogin.* import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload import net.sergeych.unikrypto.HashAlgorithm +import net.sergeych.unikrypto.SignedRecord import net.sergeych.unikrypto.SymmetricKey import net.sergeych.unikrypto.digest import kotlin.random.Random @@ -87,21 +88,25 @@ class Registration( passwordKeys = DerivedKeys.derive(password, lastDerivationParams!! ) lastPasswordHash = newPasswordHash } + val nonce = adapter.invokeCommand(api.slGetNonce) val loginPrivateKey = deferredLoginKey.await() val spl = SuperloginRestoreAccessPayload(login, loginPrivateKey, dataKey) repeat(10) { val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey, spl) try { - val result = adapter.invokeCommand( - api.slRegister, RegistrationArgs( + val packedArgs = SignedRecord.pack( + loginPrivateKey, + RegistrationArgs( login, passwordKeys!!.loginId, deferredLoginKey.await().publicKey, lastDerivationParams!!, restoreKey.restoreId, restoreData, extraData - ) + ), + nonce ) + val result = adapter.invokeCommand(api.slRegister, packedArgs) when (result) { AuthenticationResult.RestoreIdUnavailable -> { // rare situation but still possible: just repack the ACO diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt index 5669c09..3ee0493 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt @@ -17,8 +17,9 @@ import net.sergeych.parsec3.Adapter import net.sergeych.parsec3.CommandDescriptor import net.sergeych.parsec3.Parsec3Transport import net.sergeych.parsec3.WithAdapter -import net.sergeych.superlogin.AuthenticationResult -import net.sergeych.superlogin.SuperloginServerApi +import net.sergeych.superlogin.* +import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload +import net.sergeych.unikrypto.SignedRecord import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -32,6 +33,7 @@ import kotlin.reflect.typeOf */ @Serializable data class SuperloginData( + val loginName: String, val loginToken: ByteArray? = null, val data: T? = null, ) @@ -60,14 +62,6 @@ class SuperloginClient( // do actual disconnect work _cflow.value = false _state.value = LoginState.LoggedOut - if (!adapterReady.isActive) { - adapterReady.cancel() - adapterReady = CompletableDeferred() - } - globalLaunch { - transport.adapter().invokeCommand(serverApi.slLogout) - adapterReady.complete(Unit) - } } else { val v = _state.value if (v !is LoginState.LoggedIn<*> || v.loginData != value) { @@ -94,10 +88,12 @@ class SuperloginClient( } while (true) } - suspend fun call(ca: CommandDescriptor, args: A ): R = adapter().invokeCommand(ca, args) + suspend fun call(ca: CommandDescriptor, args: A): R = adapter().invokeCommand(ca, args) suspend fun call(ca: CommandDescriptor): R = adapter().invokeCommand(ca) - private suspend fun invoke(ca: CommandDescriptor, args: A ): R = transport.adapter().invokeCommand(ca, args) + private suspend fun invoke(ca: CommandDescriptor, args: A): R = + transport.adapter().invokeCommand(ca, args) + private suspend fun invoke(ca: CommandDescriptor): R = transport.adapter().invokeCommand(ca) private var jobs = listOf() @@ -110,7 +106,7 @@ class SuperloginClient( val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token) slData = if (ar is AuthenticationResult.Success) { val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } - SuperloginData(ar.loginToken, data) + SuperloginData(ar.loginName, ar.loginToken, data) } else { null } @@ -170,13 +166,12 @@ class SuperloginClient( return rn.registerWithData(loginName, password, extraData = BossEncoder.encode(dataType, data)) .also { rr -> if (rr is Registration.Result.Success) { - slData = SuperloginData(rr.loginToken, extractData(rr.encodedData)) + slData = SuperloginData(loginName, rr.loginToken, extractData(rr.encodedData)) } } } - private fun extractData(rr: ByteArray?): D? - = rr?.let { BossDecoder.decodeFrom(dataType, it) } + private fun extractData(rr: ByteArray?): D? = rr?.let { BossDecoder.decodeFrom(dataType, it) } private fun mustBeLoggedOut() { if (isLoggedIn) @@ -188,8 +183,9 @@ class SuperloginClient( throw IllegalStateException("please log in first") } - fun logout() { + suspend fun logout() { mustBeLoggedIn() + invoke(serverApi.slLogout) slData = null } @@ -202,15 +198,54 @@ class SuperloginClient( */ suspend fun loginByToken(token: ByteArray): SuperloginData? { mustBeLoggedOut() - val r = invoke(serverApi.slLoginByToken,token) - return when(r) { + val r = invoke(serverApi.slLoginByToken, token) + return when (r) { AuthenticationResult.LoginIdUnavailable -> TODO() AuthenticationResult.LoginUnavailable -> null AuthenticationResult.RestoreIdUnavailable -> TODO() is AuthenticationResult.Success -> SuperloginData( + r.loginName, r.loginToken, extractData(r.applicationData) - ) + ).also { + slData = it + } + } + } + + suspend fun loginByPassword(loginName: String, password: String): SuperloginData? { + mustBeLoggedOut() + // Request derivation params + val params = invoke(serverApi.slRequestDerivationParams, loginName) + val keys = DerivedKeys.derive(password, params) + // Request login data by derived it + return invoke( + serverApi.slRequestLoginData, + RequestLoginDataArgs(loginName, keys.loginId) + ).let { loginRequest -> + try { + AccessControlObject.unpackWithPasswordKey( + loginRequest.packedACO, + keys.loginAccessKey + )?.let { aco -> + val result = invoke( + serverApi.slLoginByKey, + SignedRecord.pack( + aco.payload.loginPrivateKey, + LoginByPasswordPayload(loginName), + loginRequest.nonce + ) + ) + if (result is AuthenticationResult.Success) { + SuperloginData(loginName, result.loginToken, extractData(result.applicationData)) + .also { slData = it } + } else null + } + } + catch (t: Throwable) { + t.printStackTrace() + throw t + } } } diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt b/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt new file mode 100644 index 0000000..ae782e5 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt @@ -0,0 +1,34 @@ +package net.sergeych.superlogin + +import net.sergeych.unikrypto.HashAlgorithm +import net.sergeych.unikrypto.digest + +/** + * Primitive to simplify create and compare salted byte arrays + */ +data class Salted(val salt: ByteArray,val data: ByteArray) { + + val salted = HashAlgorithm.SHA3_256.digest(salt, data) + + fun matches(other: ByteArray): Boolean { + return Salted(salt, other) == this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as Salted + + if (!salt.contentEquals(other.salt)) return false + if (!data.contentEquals(other.data)) return false + + return true + } + + override fun hashCode(): Int { + var result = salt.contentHashCode() + result = 31 * result + data.contentHashCode() + return result + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt b/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt deleted file mode 100644 index 88e6d01..0000000 --- a/src/commonMain/kotlin/net.sergeych.superlogin/server/SLServerTraits.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.sergeych.superlogin.server - -import net.sergeych.superlogin.AuthenticationResult -import net.sergeych.superlogin.RegistrationArgs - -/** - * Set of procedures the server implementing superlogin protocol should provide - */ -interface SLServerTraits { - /** - * Register specified user. The server should check that: - * - * - [RegistrationArgs.loginName] is not used, otherwise return [AuthenticationResult.LoginUnavailable] - * - [RegistrationArgs.loginId] is not used, otherwise return [AuthenticationResult.LoginIdUnavailable] - * - [RegistrationArgs.restoreId] is not used or return [AuthenticationResult.RestoreIdUnavailable] - * - * Then it should save permanently data from `registrationArgs` in a way tha allow fast search (indexed, - * usually) by `loginName`, `loginId` and `restoreId`, and return [AuthenticationResult.Success] with - * newly generated random bytes string `loginToken` that would optionally simplify logging in. - * - * If the implementation does not provide login token, it should still provide random bytes string - * to maintain hight security level of the serivce. - */ - suspend fun register(registrationArgs: RegistrationArgs): AuthenticationResult - - /** - * Logging out procedure does not need any extra logic unless reuired by application - * server software. Default implementation does nothing. - */ - suspend fun logout() {} - - /** - * Try to log in using an authentication token, which normally is returned in - * [AuthenticationResult.Success.loginToken]. If the server implementation - * does not support login token, don't implement it, use the default implementation. - * - * Otherwise, implement login by token and return either [AuthenticationResult.Success] - * or [AuthenticationResult.LoginUnavailable]. So not return anything else. - */ - suspend fun loginByToken(token: ByteArray): AuthenticationResult - = AuthenticationResult.LoginUnavailable -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt index b1fd252..12d7665 100644 --- a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt @@ -1,18 +1,90 @@ package net.sergeych.superlogin.server +import net.sergeych.boss_serialization_mp.decodeBoss import net.sergeych.parsec3.WithAdapter +import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.BackgroundKeyGenerator +import net.sergeych.superlogin.PasswordDerivationParams +import net.sergeych.superlogin.RegistrationArgs import net.sergeych.superlogin.client.SuperloginData +import net.sergeych.unikrypto.PublicKey /** - * The superlogin server session. Please use only [loginName] and [loginToken], the rest could be + * The superlogin server session. Please use only [currentLoginName] and [currentLoginToken], the rest could be * a subject to change. */ -open class SLServerSession: WithAdapter() { - var slData: SuperloginData? = null +abstract class SLServerSession : WithAdapter() { - val userData: T? get() = slData?.let { it.data } + val nonce = BackgroundKeyGenerator.randomBytes(32) - val loginToken get() = slData?.loginToken + var superloginData: SuperloginData? = null - var loginName: String? = null -} \ No newline at end of file + val userData: T? get() = superloginData?.data + + val currentLoginToken get() = superloginData?.loginToken + + val currentLoginName: String? get() = superloginData?.loginName + + fun requireLoggedIn() { + if (currentLoginName == null) throw IllegalStateException("must be logged in") + } + + fun requireLoggedOut() { + if (currentLoginName != null) throw IllegalStateException("must be logged out") + } + + /** + * Register specified user. The server should check that: + * + * - [RegistrationArgs.loginName] is not used, otherwise return [AuthenticationResult.LoginUnavailable] + * - [RegistrationArgs.loginId] is not used, otherwise return [AuthenticationResult.LoginIdUnavailable] + * - [RegistrationArgs.restoreId] is not used or return [AuthenticationResult.RestoreIdUnavailable] + * + * Then it should save permanently data from `registrationArgs` in a way tha allow fast search (indexed, + * usually) by `loginName`, `loginId` and `restoreId`, and return [AuthenticationResult.Success] with + * newly generated random bytes string `loginToken` that would optionally simplify logging in. + * + * If the implementation does not provide login token, it should still provide random bytes string + * to maintain hight security level of the serivce. + */ + abstract suspend fun register(registrationArgs: RegistrationArgs): AuthenticationResult + + /** + * Logging out procedure does not need any extra logic unless reuired by application + * server software. Default implementation does nothing. + */ + open suspend fun logout() {} + + /** + * Try to log in using an authentication token, which normally is returned in + * [AuthenticationResult.Success.loginToken]. If the server implementation + * does not support login token, don't implement it, use the default implementation. + * + * Otherwise, implement login by token and return either [AuthenticationResult.Success] + * or [AuthenticationResult.LoginUnavailable]. So not return anything else. + */ + open suspend fun loginByToken(token: ByteArray): AuthenticationResult = AuthenticationResult.LoginUnavailable + + /** + * Retreive exact password derivation params as were stored by registration. + * @return derivation params for the login name or null if the name is not known + */ + abstract suspend fun requestDerivationParams(login: String): PasswordDerivationParams? + + /** + * Override this method, check loginId to match loginName, and of ot os ok, return packed ACO + * @return packed ACO or null if loginName is wrong, or loginId does not match it. + */ + abstract suspend fun requestLoginData(loginName: String,loginId: ByteArray): ByteArray? + + /** + * Implementation must: find the actual user by loginName and check the publicKey is valid (for example + * matches stored key id in the database, and return + * @return [AuthenticationResult.Success] + */ + abstract suspend fun loginByKey(loginName: String,publicKey: PublicKey): AuthenticationResult +} + +inline fun > T.setSlData(it: AuthenticationResult.Success) { + superloginData = SuperloginData(it.loginName, it.loginToken, it.applicationData?.decodeBoss()) +} diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt index cf81768..74b6d54 100644 --- a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt @@ -1,31 +1,66 @@ package net.sergeych.superlogin.server -import net.sergeych.boss_serialization_mp.decodeBoss import net.sergeych.parsec3.AdapterBuilder import net.sergeych.parsec3.CommandHost import net.sergeych.parsec3.WithAdapter -import net.sergeych.superlogin.AuthenticationResult -import net.sergeych.superlogin.SuperloginServerApi -import net.sergeych.superlogin.client.SuperloginData +import net.sergeych.superlogin.* +import net.sergeych.unikrypto.SignedRecord +import kotlin.random.Random -inline fun , H : CommandHost> AdapterBuilder.superloginServer( - traits: SLServerTraits, -) { + +inline fun , H : CommandHost> AdapterBuilder.superloginServer() { val a2 = SuperloginServerApi() - on(a2.slRegister) { ra -> - traits.register(ra).also { rr -> - if( rr is AuthenticationResult.Success) - slData = SuperloginData(rr.loginToken, rr.applicationData?.let { it.decodeBoss()}) - loginName = ra.loginName + on(a2.slGetNonce) { nonce } + on(a2.slRegister) { packed -> + requireLoggedOut() + val ra = SignedRecord.unpack(packed) { sr -> + if( !(sr.nonce contentEquals nonce) ) + throw IllegalArgumentException("wrong signed record nonce") + }.decode() + + register(ra).also { rr -> + if( rr is AuthenticationResult.Success) { + setSlData(rr) + } } } on(a2.slLogout) { - println("--- logged out ---") - slData = null - loginName = null - traits.logout() + superloginData = null + logout() } on(a2.slLoginByToken) { token -> - traits.loginByToken(token) + requireLoggedOut() + loginByToken(token).also { + if( it is AuthenticationResult.Success) + setSlData(it) + } } -} \ No newline at end of file + on(a2.slRequestDerivationParams) { name -> + // If we don't know this login, we still provide derivatino params to + // slow down login scanning + requestDerivationParams(name) ?: PasswordDerivationParams() + } + on(a2.slRequestLoginData) { args -> + requestLoginData(args.loginName,args.loginId)?.let { + RequestLoginDataResult(it, nonce) + } ?: RequestLoginDataResult(Random.nextBytes(117), nonce) + } + on(a2.slLoginByKey) { packedSR -> + try { + // Check key + val sr = SignedRecord.unpack(packedSR) { + if (!(it.nonce contentEquals nonce)) throw IllegalArgumentException() + } + val loginName: String = sr.decode().loginName + loginByKey(loginName, sr.publicKey).also { + if( it is AuthenticationResult.Success) + setSlData(it) + } + } + catch(x: Exception) { + // most likely, wrong nonce, less probable bad signature + AuthenticationResult.LoginUnavailable + } + } +} + diff --git a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt index 70c34b3..5ae0cab 100644 --- a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt +++ b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt @@ -9,40 +9,32 @@ import net.sergeych.parsec3.Parsec3WSClient import net.sergeych.parsec3.WithAdapter import net.sergeych.parsec3.parsec3TransportServer import net.sergeych.superlogin.AuthenticationResult +import net.sergeych.superlogin.PasswordDerivationParams import net.sergeych.superlogin.RegistrationArgs import net.sergeych.superlogin.client.LoginState import net.sergeych.superlogin.client.Registration import net.sergeych.superlogin.client.SuperloginClient import net.sergeych.superlogin.server.SLServerSession -import net.sergeych.superlogin.server.SLServerTraits import net.sergeych.superlogin.server.superloginServer +import net.sergeych.unikrypto.PublicKey import superlogin.assertThrowsAsync import kotlin.random.Random -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotNull +import kotlin.test.* -data class TestSession(var buzz: String = "BuZZ") : SLServerSession() - - -class TestApiServer : CommandHost() { - val loginName by command() -} - - -object TestServerTraits : SLServerTraits { +data class TestSession(var buzz: String = "BuZZ") : SLServerSession() { val byLogin = mutableMapOf() val byLoginId = mutableMapOf, RegistrationArgs>() val byRestoreId = mutableMapOf, RegistrationArgs>() val byToken = mutableMapOf, RegistrationArgs>() + val tokens = mutableMapOf() override suspend fun register(ra: RegistrationArgs): AuthenticationResult { - println("ra: ${ra.loginName}") + println("ra: ${ra.loginName} : $currentLoginName : $superloginData") return when { ra.loginName in byLogin -> { AuthenticationResult.LoginUnavailable } + ra.loginId.toList() in byLoginId -> AuthenticationResult.LoginIdUnavailable ra.restoreId.toList() in byRestoreId -> AuthenticationResult.RestoreIdUnavailable else -> { @@ -51,35 +43,45 @@ object TestServerTraits : SLServerTraits { byLoginId[ra.loginId.toList()] = ra val token = Random.Default.nextBytes(32) byToken[token.toList()] = ra - AuthenticationResult.Success(token, ra.extraData) + tokens[ra.loginName] = token + AuthenticationResult.Success(ra.loginName, token, ra.extraData) } } } override suspend fun loginByToken(token: ByteArray): AuthenticationResult { return byToken[token.toList()]?.let { - AuthenticationResult.Success(token, it.extraData) - } ?: AuthenticationResult.LoginUnavailable + AuthenticationResult.Success(it.loginName, token, it.extraData) + } + ?: AuthenticationResult.LoginUnavailable } + override suspend fun requestDerivationParams(login: String): PasswordDerivationParams? = + byLogin[login]?.derivationParams + + override suspend fun requestLoginData(loginName: String, loginId: ByteArray): ByteArray? { + return byLogin[loginName]?.restoreData + } + + override suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult { + val ra = byLogin[loginName] + return if (ra != null && ra.loginPublicKey.id == publicKey.id) + AuthenticationResult.Success(ra.loginName, tokens[loginName]!!, ra.extraData) + else AuthenticationResult.LoginUnavailable + } } + +class TestApiServer : CommandHost() { + val loginName by command() +} + + @Serializable data class TestData( val foo: String, ) -//fun ,A: CommandHost> Application.superloginServer( -// traits: SLServerTraits, -// api: A, -// f: AdapterBuilder.()->Unit) { -// parsec3TransportServer(api) { -// superloginServer(traits) -// f() -// } -//} - - internal class WsServerKtTest { @@ -90,9 +92,10 @@ internal class WsServerKtTest { parsec3TransportServer(TestApiServer>()) { // superloginServer(TestServerTraits,TestApiServer>()) { newSession { TestSession() } - superloginServer(TestServerTraits) + superloginServer() on(api.loginName) { - loginName + println("login name called. now we have $currentLoginName : $superloginData") + currentLoginName } } }.start(wait = false) @@ -125,10 +128,24 @@ internal class WsServerKtTest { rt = slc.register("foo", "passwd", TestData("nobar")) assertIs(rt) + assertIs(slc.state.value) + assertEquals(null, slc.call(api.loginName)) var ar = slc.loginByToken(token) assertNotNull(ar) assertEquals("bar!", ar.data?.foo) + assertTrue { slc.isLoggedIn } + assertEquals("foo", slc.call(api.loginName)) +// + assertThrowsAsync { slc.loginByToken(token) } + slc.logout() + + assertNull(slc.loginByPassword("foo", "wrong")) + ar = slc.loginByPassword("foo", "passwd") + println(ar) + assertNotNull(ar) + assertEquals("bar!", ar.data?.foo) + assertTrue { slc.isLoggedIn } assertEquals("foo", slc.call(api.loginName)) }