working: registration with secret, login by token, logout, login by password.
This commit is contained in:
		
							parent
							
								
									449de2e504
								
							
						
					
					
						commit
						318582cdb7
					
				| @ -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            } | ||||
|  | ||||
| @ -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<T>( | ||||
|          */ | ||||
|         inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? = | ||||
|             Container.decrypt<Data<T>>(packed, passwordKey)?.let { | ||||
|                 println(it) | ||||
|                 AccessControlObject(typeOf<Data<T>>(), packed, passwordKey, it) | ||||
|             } | ||||
| 
 | ||||
|         fun <T>unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey,payloadType: KType): AccessControlObject<T>? = | ||||
|             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]. | ||||
|  | ||||
| @ -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<T: WithAdapter> : CommandHost<T>() { | ||||
| 
 | ||||
|     val slRegister by command<RegistrationArgs,AuthenticationResult>() | ||||
|     val slGetNonce by command<Unit,ByteArray>() | ||||
|     val slRegister by command<ByteArray,AuthenticationResult>() | ||||
|     val slLogout by command<Unit,Unit>() | ||||
|     val slLoginByToken by command<ByteArray,AuthenticationResult>() | ||||
| 
 | ||||
|     val slRequestDerivationParams by command<String,PasswordDerivationParams>() | ||||
|     val slRequestLoginData by command<RequestLoginDataArgs,RequestLoginDataResult>() | ||||
|     val slLoginByKey by command<ByteArray,AuthenticationResult>() | ||||
| 
 | ||||
| 
 | ||||
|      /** | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<T>( | ||||
|     val loginName: String, | ||||
|     val loginToken: ByteArray? = null, | ||||
|     val data: T? = null, | ||||
| ) | ||||
| @ -60,14 +62,6 @@ class SuperloginClient<D, S : WithAdapter>( | ||||
|                     // 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<D, S : WithAdapter>( | ||||
|         } while (true) | ||||
|     } | ||||
| 
 | ||||
|     suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A ): R = adapter().invokeCommand(ca, args) | ||||
|     suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A): R = adapter().invokeCommand(ca, args) | ||||
|     suspend fun <R> call(ca: CommandDescriptor<Unit, R>): R = adapter().invokeCommand(ca) | ||||
| 
 | ||||
|     private suspend fun <A, R> invoke(ca: CommandDescriptor<A, R>, args: A ): R = transport.adapter().invokeCommand(ca, args) | ||||
|     private suspend fun <A, R> invoke(ca: CommandDescriptor<A, R>, args: A): R = | ||||
|         transport.adapter().invokeCommand(ca, args) | ||||
| 
 | ||||
|     private suspend fun <R> invoke(ca: CommandDescriptor<Unit, R>): R = transport.adapter().invokeCommand(ca) | ||||
| 
 | ||||
|     private var jobs = listOf<Job>() | ||||
| @ -110,7 +106,7 @@ class SuperloginClient<D, S : WithAdapter>( | ||||
|                 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<D, S : WithAdapter>( | ||||
|         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<D, S : WithAdapter>( | ||||
|             throw IllegalStateException("please log in first") | ||||
|     } | ||||
| 
 | ||||
|     fun logout() { | ||||
|     suspend fun logout() { | ||||
|         mustBeLoggedIn() | ||||
|         invoke(serverApi.slLogout) | ||||
|         slData = null | ||||
|     } | ||||
| 
 | ||||
| @ -202,15 +198,54 @@ class SuperloginClient<D, S : WithAdapter>( | ||||
|      */ | ||||
|     suspend fun loginByToken(token: ByteArray): SuperloginData<D>? { | ||||
|         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<D>? { | ||||
|         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<SuperloginRestoreAccessPayload>( | ||||
|                     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 | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										34
									
								
								src/commonMain/kotlin/net.sergeych.superlogin/salt.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/commonMain/kotlin/net.sergeych.superlogin/salt.kt
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
|     } | ||||
| } | ||||
| @ -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 | ||||
| } | ||||
| @ -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<T>: WithAdapter() { | ||||
|     var slData: SuperloginData<T>? = null | ||||
| abstract class SLServerSession<T> : WithAdapter() { | ||||
| 
 | ||||
|     val userData: T? get() = slData?.let { it.data } | ||||
|     val nonce = BackgroundKeyGenerator.randomBytes(32) | ||||
| 
 | ||||
|     val loginToken get() = slData?.loginToken | ||||
|     var superloginData: SuperloginData<T>? = null | ||||
| 
 | ||||
|     var loginName: String? = null | ||||
| } | ||||
|     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 <reified D, T : SLServerSession<D>> T.setSlData(it: AuthenticationResult.Success) { | ||||
|     superloginData = SuperloginData(it.loginName, it.loginToken, it.applicationData?.decodeBoss<D>()) | ||||
| } | ||||
|  | ||||
| @ -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 <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer( | ||||
|     traits: SLServerTraits, | ||||
| ) { | ||||
| 
 | ||||
| inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer() { | ||||
|     val a2 = SuperloginServerApi<WithAdapter>() | ||||
|     on(a2.slRegister) { ra -> | ||||
|         traits.register(ra).also { rr -> | ||||
|             if( rr is AuthenticationResult.Success) | ||||
|                 slData = SuperloginData<D>(rr.loginToken, rr.applicationData?.let { it.decodeBoss<D>()}) | ||||
|                 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<RegistrationArgs>() | ||||
| 
 | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|     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<LoginByPasswordPayload>().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 | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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<TestData>() | ||||
| 
 | ||||
| 
 | ||||
| class TestApiServer<T : WithAdapter> : CommandHost<T>() { | ||||
|     val loginName by command<Unit, String?>() | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| object TestServerTraits : SLServerTraits { | ||||
| data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>() { | ||||
|     val byLogin = mutableMapOf<String, RegistrationArgs>() | ||||
|     val byLoginId = mutableMapOf<List<Byte>, RegistrationArgs>() | ||||
|     val byRestoreId = mutableMapOf<List<Byte>, RegistrationArgs>() | ||||
|     val byToken = mutableMapOf<List<Byte>, RegistrationArgs>() | ||||
|     val tokens = mutableMapOf<String, ByteArray>() | ||||
| 
 | ||||
|     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<T : WithAdapter> : CommandHost<T>() { | ||||
|     val loginName by command<Unit, String?>() | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| @Serializable | ||||
| data class TestData( | ||||
|     val foo: String, | ||||
| ) | ||||
| 
 | ||||
| //fun <S: SLServerSession<*>,A: CommandHost<S>> Application.superloginServer( | ||||
| //    traits: SLServerTraits, | ||||
| //    api: A, | ||||
| //    f: AdapterBuilder<S,A>.()->Unit) { | ||||
| //    parsec3TransportServer(api) { | ||||
| //        superloginServer(traits) | ||||
| //        f() | ||||
| //    } | ||||
| //} | ||||
| 
 | ||||
| 
 | ||||
| internal class WsServerKtTest { | ||||
| 
 | ||||
| 
 | ||||
| @ -90,9 +92,10 @@ internal class WsServerKtTest { | ||||
|             parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) { | ||||
| //            superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) { | ||||
|                 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<Registration.Result.InvalidLogin>(rt) | ||||
|             assertIs<LoginState.LoggedOut>(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<IllegalStateException> { 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)) | ||||
|         } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user