From d12b392ed09d3f44a99b93e45a01efb6b4055247 Mon Sep 17 00:00:00 2001 From: sergeych Date: Fri, 2 Dec 2022 20:05:09 +0100 Subject: [PATCH] RC0: full planned functionality (+change password, +reset password) --- build.gradle.kts | 2 +- .../AccessControlObject.kt | 18 +-- .../kotlin/net.sergeych.superlogin/api.kt | 37 +++-- .../client/LoginState.kt | 5 + .../client/SuperloginClient.kt | 148 +++++++++++++++--- .../net.sergeych.superlogin/exceptions.kt | 12 ++ .../kotlin/net.sergeych.superlogin/salt.kt | 34 ---- .../superlogin/AccessControlObjectTest.kt | 16 +- .../kotlin/superlogin/assert_throws.kt | 2 +- .../superlogin/server/SLServerSession.kt | 21 ++- .../superlogin/server/SuperloginServer.kt | 58 +++++-- .../kotlin/net/sergeych/WsServerKtTest.kt | 123 +++++++++++---- 12 files changed, 350 insertions(+), 126 deletions(-) create mode 100644 src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt delete mode 100644 src/commonMain/kotlin/net.sergeych.superlogin/salt.kt diff --git a/build.gradle.kts b/build.gradle.kts index ee16946..5e3f570 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,7 +44,7 @@ kotlin { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") api("net.sergeych:unikrypto:1.2.2-SNAPSHOT") - api("net.sergeych:parsec3:0.3.2-SNAPSHOT") + api("net.sergeych:parsec3:0.3.3-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 275b090..55afbc8 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/AccessControlObject.kt @@ -26,7 +26,7 @@ import kotlin.reflect.typeOf * To construct it please use one of:_ * * - [AccessControlObject.pack] to generate new - * - [AccessControlObject.unpackWithPasswordKey] to decrypt it with a password key + * - [AccessControlObject.unpackWithKey] to decrypt it with a password key * - [AccessControlObject.unpackWithSecret] to decrypt it with a `secret` * * @param payloadType used to properly serialize application=specific data for [payload] @@ -125,15 +125,16 @@ class AccessControlObject( } /** - * Unpack and decrypt ACO with a password key + * Unpack and decrypt ACO with a password key or secret-based key (this once can be obtained from `secret` + * with [RestoreKey.parse]. * @return decrypted ACO or null if the key is wrong. */ - inline fun unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject? = - Container.decrypt>(packed, passwordKey)?.let { - AccessControlObject(typeOf>(), packed, passwordKey, it) + inline fun unpackWithKey(packed: ByteArray, key: SymmetricKey): AccessControlObject? = + Container.decrypt>(packed, key)?.let { + AccessControlObject(typeOf>(), packed, key, it) } - fun unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey,payloadType: KType): AccessControlObject? = + fun unpackWithKey(packed: ByteArray, passwordKey: SymmetricKey, payloadType: KType): AccessControlObject? = Container.decryptAsBytes(packed, passwordKey)?.let { AccessControlObject(payloadType, packed, passwordKey, BossDecoder.decodeFrom(payloadType,it)) } @@ -146,14 +147,11 @@ class AccessControlObject( suspend inline fun unpackWithSecret(packed: ByteArray, secret: String): AccessControlObject? { try { val (id, key) = RestoreKey.parse(secret) - return Container.decrypt>(packed, key)?.let { data -> - AccessControlObject(typeOf>(), packed, data.passwordKey, data) - } + return unpackWithKey(packed, key) } catch(_: RestoreKey.InvalidSecretException) { return null } } - } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt index c962777..d39f5c7 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt @@ -1,5 +1,6 @@ package net.sergeych.superlogin +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import net.sergeych.parsec3.CommandHost import net.sergeych.parsec3.WithAdapter @@ -13,13 +14,14 @@ data class RegistrationArgs( val loginPublicKey: PublicKey, val derivationParams: PasswordDerivationParams, val restoreId: ByteArray, - val restoreData: ByteArray, + val packedACO: ByteArray, val extraData: ByteArray? = null ) @Serializable sealed class AuthenticationResult { @Serializable + @SerialName("Success") data class Success( val loginName: String, val loginToken: ByteArray, @@ -27,32 +29,49 @@ sealed class AuthenticationResult { ): AuthenticationResult() @Serializable + @SerialName("LoginUnavailable") object LoginUnavailable: AuthenticationResult() @Serializable + @SerialName("LoginIdUnavailable") object LoginIdUnavailable: AuthenticationResult() @Serializable + @SerialName("RestoreIdUnavailable") object RestoreIdUnavailable: AuthenticationResult() } @Serializable -data class RequestLoginDataArgs( +class RequestACOByLoginNameArgs( val loginName: String, val loginId: ByteArray, ) @Serializable -data class RequestLoginDataResult( +class RequestACOResult( val packedACO: ByteArray, val nonce: ByteArray ) @Serializable -data class LoginByPasswordPayload( +class LoginByPasswordPayload( val loginName: String ) +@Serializable +class ChangePasswordArgs( + val loginName: String, + val packedSignedRecord: ByteArray +) + +@Serializable +class ChangePasswordPayload( + val packedACO: ByteArray, + val passwordDerivationParams: PasswordDerivationParams, + val newLoginKey: PublicKey +) + + class SuperloginServerApi : CommandHost() { val slGetNonce by command() @@ -61,13 +80,11 @@ class SuperloginServerApi : CommandHost() { val slLoginByToken by command() val slRequestDerivationParams by command() - val slRequestLoginData by command() + val slRequestACOByLoginName by command() val slLoginByKey by command() + val slRequestACOBySecretId by command() + val slChangePasswordAndLogin by command () - /** - * Get resstoreData by restoreId: password reset procedure start. - */ -// val requestUserLogin by command() -// val performLogin by command() } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt index d12af8b..3258545 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/LoginState.kt @@ -1,5 +1,10 @@ package net.sergeych.superlogin.client +/** + * Login client has a _login state_ which represents known state of the log-in protocol. + * It can properly process offline state and reconnection and report state bu mean + * of the state flow pf instanses of this class. See [SuperloginClient.state]. + */ sealed class LoginState(val isLoggedIn: Boolean) { /** * User is logged in (either connected or yet not). Client application should save diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt index 3ee0493..9a16635 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt @@ -1,8 +1,6 @@ package net.sergeych.superlogin.client -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable @@ -13,10 +11,7 @@ import net.sergeych.mp_logger.Loggable import net.sergeych.mp_logger.exception import net.sergeych.mp_logger.warning import net.sergeych.mp_tools.globalLaunch -import net.sergeych.parsec3.Adapter -import net.sergeych.parsec3.CommandDescriptor -import net.sergeych.parsec3.Parsec3Transport -import net.sergeych.parsec3.WithAdapter +import net.sergeych.parsec3.* import net.sergeych.superlogin.* import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload import net.sergeych.unikrypto.SignedRecord @@ -43,6 +38,7 @@ class SuperloginClient( private val transport: Parsec3Transport, savedData: SuperloginData? = null, private val dataType: KType, + override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(), ) : Parsec3Transport, Loggable by LogTag("SLCLI") { private val _state = MutableStateFlow( @@ -77,16 +73,16 @@ class SuperloginClient( private var adapterReady = CompletableDeferred() - override suspend fun adapter(): Adapter { - do { - try { - adapterReady.await() - return transport.adapter() - } catch (x: Throwable) { - exception { "failed to get adapter" to x } - } - } while (true) - } + override suspend fun adapter(): Adapter = transport.adapter() +// do { +// try { +// adapterReady.await() +// return transport.adapter() +// } catch (x: Throwable) { +// exception { "failed to get adapter" to x } +// } +// } while (true) +// } suspend fun call(ca: CommandDescriptor, args: A): R = adapter().invokeCommand(ca, args) suspend fun call(ca: CommandDescriptor): R = adapter().invokeCommand(ca) @@ -119,6 +115,7 @@ class SuperloginClient( } init { + transport.registerExceptinos(SuperloginExceptionsRegistry) jobs += globalLaunch { transport.connectedFlow.collect { on -> if (on) tryRestoreLogin() @@ -220,11 +217,11 @@ class SuperloginClient( val keys = DerivedKeys.derive(password, params) // Request login data by derived it return invoke( - serverApi.slRequestLoginData, - RequestLoginDataArgs(loginName, keys.loginId) + serverApi.slRequestACOByLoginName, + RequestACOByLoginNameArgs(loginName, keys.loginId) ).let { loginRequest -> try { - AccessControlObject.unpackWithPasswordKey( + AccessControlObject.unpackWithKey( loginRequest.packedACO, keys.loginAccessKey )?.let { aco -> @@ -241,14 +238,121 @@ class SuperloginClient( .also { slData = it } } else null } - } - catch (t: Throwable) { + } catch (t: Throwable) { t.printStackTrace() throw t } } } + /** + * Resets password and log in using a `secret` string (one that wwas reported on registration. __Never store + * secrt string in your app__. Always ask user to enter it just before the operation and wipe it out + * immediately after. It is a time-consuming procedure. Note that on success the client state changes + * to [LoginState.LoggedIn]. + * + * @param secret the secret string as was reported when registering + * @param newPassword new password (apply strength checks, it is not checked here) + * @param loginKeyStrength desired login key strength (it will be generated there) + * @param params password derivation params: it is possible to change its strength here + * @return login data instance on success or null + */ + suspend fun resetPasswordAndLogin( + secret: String, newPassword: String, + params: PasswordDerivationParams = PasswordDerivationParams(), + loginKeyStrength: Int = 2048 + ): SuperloginData? { + mustBeLoggedOut() + return try { + val (id, key) = RestoreKey.parse(secret) + val packedACO = invoke(serverApi.slRequestACOBySecretId, id) + AccessControlObject.unpackWithKey(packedACO, key)?.let { + changePasswordWithACO(it, newPassword) + slData + } + } catch (x: RestoreKey.InvalidSecretException) { + null + } catch (x: Exception) { + x.printStackTrace() + null + } + } + + /** + * Changes the password (which includes generating new login key). Does not require any particular + * [state]. This is a long operation. On success, it changes (updates) [state] to [LoginState.LoggedIn] + * with new data whatever it was before. Be aware of it. + */ + protected suspend fun changePasswordWithACO( + aco: AccessControlObject, + newPassword: String, + params: PasswordDerivationParams = PasswordDerivationParams(), + loginKeyStrength: Int = 2048, + ): Boolean { + return coroutineScope { + // Get current nonce in parallel + val deferredNonce = async { invoke(serverApi.slGetNonce) } + // get login key in parallel + val newLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength) + // derive keys in main scope + val keys = DerivedKeys.derive(newPassword, params) + + // new ACO payload: new login key, old data storage key and login + val newSlp = SuperloginRestoreAccessPayload( + aco.payload.login, + newLoginKey.await(), + aco.payload.dataStorageKey + ) + // new ACO with a new password key and payload (but the same secret!) + var newAco = aco.updatePasswordKey(keys.loginAccessKey).updatePayload(newSlp) + // trying to update + val result = invoke( + serverApi.slChangePasswordAndLogin, ChangePasswordArgs( + aco.payload.login, + SignedRecord.pack(aco.payload.loginPrivateKey, + ChangePasswordPayload(newAco.packed,params,newLoginKey.await().publicKey), + deferredNonce.await()) + ) + ) + when (result) { + is AuthenticationResult.Success -> { + slData = SuperloginData(result.loginName, result.loginToken, extractData(result.applicationData)) + true + } + + else -> { + warning { "Change password result: $result" } + false + } + } + } + } + + /** + * Change password for a logged-in user using its known password. It is a long operation + * @param oldPassword existing password (re-request it from a user!) + * @param newPassword new password. we do not chek it but it should be strong - check it on your end + * for example with [net.sergeych.unikrypto.Passwords] tools + * @param passwordDerivationParams at this point derivation parameters are alwaus updated so it is possible + * to set it to desired + * @param loginKeyStrength login key is regenerateed so its strength could be updated here + * @return true if the password has been successfully changed + */ + suspend fun changePassword(oldPassword: String, newPassword: String, + passwordDerivationParams: PasswordDerivationParams = PasswordDerivationParams(), + loginKeyStrength: Int = 2048 + ): Boolean { + mustBeLoggedIn() + val loginName = slData?.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)) + return AccessControlObject.unpackWithKey(data.packedACO, keys.loginAccessKey)?.let { + changePasswordWithACO(it, newPassword,passwordDerivationParams, loginKeyStrength) + } ?: false + } + + companion object { inline operator fun invoke( t: Parsec3Transport, diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt b/src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt new file mode 100644 index 0000000..96fcd48 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.superlogin/exceptions.kt @@ -0,0 +1,12 @@ +package net.sergeych.superlogin + +import net.sergeych.parsec3.ExceptionsRegistry + +class SLInternalException(reason: String?="superlogin internal exception (a bug)",cause: Throwable?=null): + Exception(reason, cause) + +fun addSuperloginExceptions(er: ExceptionsRegistry) { + er.register { SLInternalException(it) } +} + +val SuperloginExceptionsRegistry = ExceptionsRegistry().also { addSuperloginExceptions(it) } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt b/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt deleted file mode 100644 index ae782e5..0000000 --- a/src/commonMain/kotlin/net.sergeych.superlogin/salt.kt +++ /dev/null @@ -1,34 +0,0 @@ -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/commonTest/kotlin/superlogin/AccessControlObjectTest.kt b/src/commonTest/kotlin/superlogin/AccessControlObjectTest.kt index 9b4d6bb..7488947 100644 --- a/src/commonTest/kotlin/superlogin/AccessControlObjectTest.kt +++ b/src/commonTest/kotlin/superlogin/AccessControlObjectTest.kt @@ -2,9 +2,11 @@ package superlogin import kotlinx.coroutines.test.runTest import net.sergeych.superlogin.AccessControlObject -import net.sergeych.superlogin.RestoreKey import net.sergeych.unikrypto.SymmetricKeys -import kotlin.test.* +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull internal class AccessControlObjectTest { @@ -14,29 +16,29 @@ internal class AccessControlObjectTest { val pk2 = SymmetricKeys.random() val (rk, packed1) = AccessControlObject.pack(pk1, 117) println(rk.secret) - val ac1 = AccessControlObject.unpackWithPasswordKey(packed1,pk1) + val ac1 = AccessControlObject.unpackWithKey(packed1,pk1) assertNotNull(ac1) assertEquals(117, ac1.payload) val ac2 = AccessControlObject.unpackWithSecret(packed1,rk.secret) assertNotNull(ac2) assertEquals(117, ac2.payload) - assertNull(AccessControlObject.unpackWithPasswordKey(packed1,pk2)) + assertNull(AccessControlObject.unpackWithKey(packed1,pk2)) assertNull(AccessControlObject.unpackWithSecret(packed1,"the_-wrong-secret-yess")) val (rk2, packed2) = AccessControlObject.pack(pk2, 107) assertNull(AccessControlObject.unpackWithSecret(packed1,rk2.secret)) - var ac21 = AccessControlObject.unpackWithPasswordKey(packed2,pk2) + var ac21 = AccessControlObject.unpackWithKey(packed2,pk2) assertNotNull(ac21) assertEquals(107, ac21.payload) var packed3 = ac1.updatePayload(121).packed - ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk1) + ac21 = AccessControlObject.unpackWithKey(packed3,pk1) assertNotNull(ac21) assertEquals(121, ac21.payload) packed3 = ac1.updatePasswordKey(pk2).packed println("-------") - ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk2) + ac21 = AccessControlObject.unpackWithKey(packed3,pk2) assertNotNull(ac21) assertEquals(117, ac21.payload) diff --git a/src/commonTest/kotlin/superlogin/assert_throws.kt b/src/commonTest/kotlin/superlogin/assert_throws.kt index d2d5eee..5887af0 100644 --- a/src/commonTest/kotlin/superlogin/assert_throws.kt +++ b/src/commonTest/kotlin/superlogin/assert_throws.kt @@ -9,6 +9,6 @@ suspend inline fun assertThrowsAsync(f: suspend () -> Uni } catch(x: Throwable) { if( x !is T ) - fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}") + fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}: $x") } } diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt index 12d7665..3da5200 100644 --- a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SLServerSession.kt @@ -69,20 +69,35 @@ abstract class SLServerSession : WithAdapter() { * 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? + abstract suspend fun requestDerivationParams(loginName: 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? + abstract suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray? + + /** + * Implement retrieving ACO object by restoreId. Return found object ot null. + */ + abstract suspend fun requestACOByRestoreId(restoreId: 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 + abstract suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult + + /** + * Update access control object (resotre data) to the specified. + */ + abstract suspend fun updateAccessControlData( + loginName: String, + packedData: ByteArray, + passwordDerivationParams: PasswordDerivationParams, + newLoginKey: PublicKey + ) } inline fun > T.setSlData(it: AuthenticationResult.Success) { diff --git a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt index 74b6d54..ff37f51 100644 --- a/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt +++ b/src/jvmMain/kotlin/net/sergeych/superlogin/server/SuperloginServer.kt @@ -7,19 +7,22 @@ import net.sergeych.superlogin.* import net.sergeych.unikrypto.SignedRecord import kotlin.random.Random - +fun randomACOLike(): ByteArray { + return Random.nextBytes(117) +} inline fun , H : CommandHost> AdapterBuilder.superloginServer() { + addErrors(SuperloginExceptionsRegistry) val a2 = SuperloginServerApi() on(a2.slGetNonce) { nonce } on(a2.slRegister) { packed -> requireLoggedOut() val ra = SignedRecord.unpack(packed) { sr -> - if( !(sr.nonce contentEquals nonce) ) + if (!(sr.nonce contentEquals nonce)) throw IllegalArgumentException("wrong signed record nonce") }.decode() register(ra).also { rr -> - if( rr is AuthenticationResult.Success) { + if (rr is AuthenticationResult.Success) { setSlData(rr) } } @@ -31,7 +34,7 @@ inline fun , H : CommandHost> AdapterBuilde on(a2.slLoginByToken) { token -> requireLoggedOut() loginByToken(token).also { - if( it is AuthenticationResult.Success) + if (it is AuthenticationResult.Success) setSlData(it) } } @@ -40,10 +43,10 @@ inline fun , H : CommandHost> AdapterBuilde // 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.slRequestACOByLoginName) { args -> + requestACOByLoginName(args.loginName, args.loginId)?.let { + RequestACOResult(it, nonce) + } ?: RequestACOResult(randomACOLike(), nonce) } on(a2.slLoginByKey) { packedSR -> try { @@ -53,14 +56,47 @@ inline fun , H : CommandHost> AdapterBuilde } val loginName: String = sr.decode().loginName loginByKey(loginName, sr.publicKey).also { - if( it is AuthenticationResult.Success) + if (it is AuthenticationResult.Success) setSlData(it) } - } - catch(x: Exception) { + } catch (x: Exception) { // most likely, wrong nonce, less probable bad signature AuthenticationResult.LoginUnavailable } } + on(a2.slChangePasswordAndLogin) { args -> + val currentSlData = superloginData + try { + val sr = SignedRecord.unpack(args.packedSignedRecord) { + if (!(it.nonce contentEquals nonce)) throw IllegalArgumentException() + } + val payload = sr.decode() + val loginResult = loginByKey(args.loginName, sr.publicKey) + if (loginResult is AuthenticationResult.Success) { + setSlData(loginResult) + updateAccessControlData( + args.loginName, + payload.packedACO, + payload.passwordDerivationParams, + payload.newLoginKey + ) + println(">> ${loginResult.loginToken} -- !") + } + loginResult + } catch (_: IllegalArgumentException) { + superloginData = currentSlData + AuthenticationResult.LoginUnavailable + } catch (x: Throwable) { + x.printStackTrace() + superloginData = currentSlData + AuthenticationResult.LoginUnavailable + } + } + on(a2.slRequestACOBySecretId) { + requestACOByRestoreId(it) ?: randomACOLike() + } + on(a2.slSendTestException) { + throw SLInternalException("test") + } } diff --git a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt index 5ae0cab..ea1f2e7 100644 --- a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt +++ b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt @@ -1,5 +1,6 @@ package net.sergeych +import io.ktor.server.application.* import io.ktor.server.engine.* import io.ktor.server.netty.* import kotlinx.coroutines.runBlocking @@ -8,9 +9,7 @@ import net.sergeych.parsec3.CommandHost 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.* import net.sergeych.superlogin.client.LoginState import net.sergeych.superlogin.client.Registration import net.sergeych.superlogin.client.SuperloginClient @@ -56,11 +55,15 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession() ?: AuthenticationResult.LoginUnavailable } - override suspend fun requestDerivationParams(login: String): PasswordDerivationParams? = - byLogin[login]?.derivationParams + override suspend fun requestDerivationParams(loginName: String): PasswordDerivationParams? = + byLogin[loginName]?.derivationParams - override suspend fun requestLoginData(loginName: String, loginId: ByteArray): ByteArray? { - return byLogin[loginName]?.restoreData + override suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray? { + return byLogin[loginName]?.packedACO + } + + override suspend fun requestACOByRestoreId(restoreId: ByteArray): ByteArray? { + return byRestoreId[restoreId.toList()]?.packedACO } override suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult { @@ -69,6 +72,25 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession() AuthenticationResult.Success(ra.loginName, tokens[loginName]!!, ra.extraData) else AuthenticationResult.LoginUnavailable } + + override suspend fun updateAccessControlData( + loginName: String, + packedData: ByteArray, + passwordDerivationParams: PasswordDerivationParams, + newLoginKey: PublicKey, + ) { + val r = byLogin[loginName]?.copy( + packedACO = packedData, + derivationParams = passwordDerivationParams, + loginPublicKey = newLoginKey + ) + ?: throw RuntimeException("login not found") + byLogin[loginName] = r + byLoginId[r.loginId.toList()] = r + byToken[currentLoginToken!!.toList()] = r + byRestoreId[r.restoreId.toList()] = r + } + } @@ -88,17 +110,7 @@ internal class WsServerKtTest { @Test fun testWsServer() { - embeddedServer(Netty, port = 8080) { - parsec3TransportServer(TestApiServer>()) { -// superloginServer(TestServerTraits,TestApiServer>()) { - newSession { TestSession() } - superloginServer() - on(api.loginName) { - println("login name called. now we have $currentLoginName : $superloginData") - currentLoginName - } - } - }.start(wait = false) + embeddedServer(Netty, port = 8080, module = Application::testServerModule).start(wait = false) val client = Parsec3WSClient("ws://localhost:8080/api/p3") @@ -138,18 +150,75 @@ internal class WsServerKtTest { 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)) } } + @Test + fun changePasswordTest() { + embeddedServer(Netty, port = 8081, module = Application::testServerModule).start(wait = false) + runBlocking { + val client = Parsec3WSClient("ws://localhost:8081/api/p3") + + val api = TestApiServer() + val slc = SuperloginClient(client) + assertEquals(LoginState.LoggedOut, slc.state.value) + var rt = slc.register("foo", "passwd", TestData("bar!")) + assertIs(rt) + val secret = rt.secret + var token = rt.loginToken + + assertFalse(slc.changePassword("wrong", "new")) + assertTrue(slc.changePassword("passwd", "newpass1")) + assertTrue { slc.isLoggedIn } + assertEquals("foo", slc.call(api.loginName)) + + slc.logout() + assertNull(slc.loginByPassword("foo", "passwd")) + var ar = slc.loginByPassword("foo", "newpass1") + assertNotNull(ar) + assertEquals("bar!", ar.data?.foo) + assertTrue { slc.isLoggedIn } + assertEquals("foo", slc.call(api.loginName)) + + slc.logout() + println(secret) + assertNull(slc.resetPasswordAndLogin("bad_secret", "newpass2")) + assertNull(slc.resetPasswordAndLogin("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2")) + ar = slc.resetPasswordAndLogin(secret,"newpass2") + assertNotNull(ar) + assertEquals("bar!", ar.data?.foo) + assertTrue { slc.isLoggedIn } + assertEquals("foo", slc.call(api.loginName)) + + } + } + + @Test + fun testExceptions() { + embeddedServer(Netty, port = 8082, module = Application::testServerModule).start(wait = false) + val client = Parsec3WSClient("ws://localhost:8082/api/p3") + runBlocking { + val slc = SuperloginClient(client) + val serverApi = SuperloginServerApi() + assertThrowsAsync { + slc.call(serverApi.slSendTestException,Unit) + } + } + + } + +} + +fun Application.testServerModule() { + parsec3TransportServer(TestApiServer>()) { +// superloginServer(TestServerTraits,TestApiServer>()) { + newSession { TestSession() } + superloginServer() + on(api.loginName) { + println("login name called. now we have $currentLoginName : $superloginData") + currentLoginName + } + } } \ No newline at end of file