working: registration with secret, login by token, logout, login by password.

This commit is contained in:
Sergey Chernov 2022-11-30 20:32:22 +01:00
parent 449de2e504
commit 318582cdb7
10 changed files with 301 additions and 128 deletions

View File

@ -43,7 +43,7 @@ kotlin {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3") 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:parsec3:0.3.2-SNAPSHOT")
api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT") api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT")
3 } 3 }

View File

@ -1,10 +1,9 @@
package net.sergeych.superlogin package net.sergeych.superlogin
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import net.sergeych.boss_serialization.BossDecoder
import net.sergeych.boss_serialization_mp.BossEncoder import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.unikrypto.Container import net.sergeych.unikrypto.Container
import net.sergeych.unikrypto.DecryptingKey
import net.sergeych.unikrypto.Safe58
import net.sergeych.unikrypto.SymmetricKey import net.sergeych.unikrypto.SymmetricKey
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -131,10 +130,14 @@ class AccessControlObject<T>(
*/ */
inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? = inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? =
Container.decrypt<Data<T>>(packed, passwordKey)?.let { Container.decrypt<Data<T>>(packed, passwordKey)?.let {
println(it)
AccessControlObject(typeOf<Data<T>>(), packed, passwordKey, 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 * Unpack and decrypt ACO with a [secret]. If you want to check [secret] integrity, use
* [RestoreKey.checkSecretIntegrity]. * [RestoreKey.checkSecretIntegrity].

View File

@ -21,6 +21,7 @@ data class RegistrationArgs(
sealed class AuthenticationResult { sealed class AuthenticationResult {
@Serializable @Serializable
data class Success( data class Success(
val loginName: String,
val loginToken: ByteArray, val loginToken: ByteArray,
val applicationData: ByteArray? val applicationData: ByteArray?
): AuthenticationResult() ): AuthenticationResult()
@ -36,19 +37,32 @@ sealed class AuthenticationResult {
} }
@Serializable @Serializable
data class LoginArgs( data class RequestLoginDataArgs(
val loginName: String,
val loginId: ByteArray, 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>() { 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 slLogout by command<Unit,Unit>()
val slLoginByToken by command<ByteArray,AuthenticationResult>() val slLoginByToken by command<ByteArray,AuthenticationResult>()
val slRequestDerivationParams by command<String,PasswordDerivationParams>() val slRequestDerivationParams by command<String,PasswordDerivationParams>()
val slRequestLoginData by command<RequestLoginDataArgs,RequestLoginDataResult>()
val slLoginByKey by command<ByteArray,AuthenticationResult>()
/** /**

View File

@ -11,6 +11,7 @@ import net.sergeych.parsec3.WithAdapter
import net.sergeych.superlogin.* import net.sergeych.superlogin.*
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
import net.sergeych.unikrypto.HashAlgorithm import net.sergeych.unikrypto.HashAlgorithm
import net.sergeych.unikrypto.SignedRecord
import net.sergeych.unikrypto.SymmetricKey import net.sergeych.unikrypto.SymmetricKey
import net.sergeych.unikrypto.digest import net.sergeych.unikrypto.digest
import kotlin.random.Random import kotlin.random.Random
@ -87,21 +88,25 @@ class Registration(
passwordKeys = DerivedKeys.derive(password, lastDerivationParams!! ) passwordKeys = DerivedKeys.derive(password, lastDerivationParams!! )
lastPasswordHash = newPasswordHash lastPasswordHash = newPasswordHash
} }
val nonce = adapter.invokeCommand(api.slGetNonce)
val loginPrivateKey = deferredLoginKey.await() val loginPrivateKey = deferredLoginKey.await()
val spl = SuperloginRestoreAccessPayload(login, loginPrivateKey, dataKey) val spl = SuperloginRestoreAccessPayload(login, loginPrivateKey, dataKey)
repeat(10) { repeat(10) {
val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey, spl) val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey, spl)
try { try {
val result = adapter.invokeCommand( val packedArgs = SignedRecord.pack(
api.slRegister, RegistrationArgs( loginPrivateKey,
RegistrationArgs(
login, login,
passwordKeys!!.loginId, passwordKeys!!.loginId,
deferredLoginKey.await().publicKey, deferredLoginKey.await().publicKey,
lastDerivationParams!!, lastDerivationParams!!,
restoreKey.restoreId, restoreData, restoreKey.restoreId, restoreData,
extraData extraData
),
nonce
) )
) val result = adapter.invokeCommand(api.slRegister, packedArgs)
when (result) { when (result) {
AuthenticationResult.RestoreIdUnavailable -> { AuthenticationResult.RestoreIdUnavailable -> {
// rare situation but still possible: just repack the ACO // rare situation but still possible: just repack the ACO

View File

@ -17,8 +17,9 @@ import net.sergeych.parsec3.Adapter
import net.sergeych.parsec3.CommandDescriptor import net.sergeych.parsec3.CommandDescriptor
import net.sergeych.parsec3.Parsec3Transport import net.sergeych.parsec3.Parsec3Transport
import net.sergeych.parsec3.WithAdapter import net.sergeych.parsec3.WithAdapter
import net.sergeych.superlogin.AuthenticationResult import net.sergeych.superlogin.*
import net.sergeych.superlogin.SuperloginServerApi import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
import net.sergeych.unikrypto.SignedRecord
import kotlin.reflect.KType import kotlin.reflect.KType
import kotlin.reflect.typeOf import kotlin.reflect.typeOf
@ -32,6 +33,7 @@ import kotlin.reflect.typeOf
*/ */
@Serializable @Serializable
data class SuperloginData<T>( data class SuperloginData<T>(
val loginName: String,
val loginToken: ByteArray? = null, val loginToken: ByteArray? = null,
val data: T? = null, val data: T? = null,
) )
@ -60,14 +62,6 @@ class SuperloginClient<D, S : WithAdapter>(
// do actual disconnect work // do actual disconnect work
_cflow.value = false _cflow.value = false
_state.value = LoginState.LoggedOut _state.value = LoginState.LoggedOut
if (!adapterReady.isActive) {
adapterReady.cancel()
adapterReady = CompletableDeferred()
}
globalLaunch {
transport.adapter().invokeCommand(serverApi.slLogout)
adapterReady.complete(Unit)
}
} else { } else {
val v = _state.value val v = _state.value
if (v !is LoginState.LoggedIn<*> || v.loginData != value) { if (v !is LoginState.LoggedIn<*> || v.loginData != value) {
@ -94,10 +88,12 @@ class SuperloginClient<D, S : WithAdapter>(
} while (true) } 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) 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 suspend fun <R> invoke(ca: CommandDescriptor<Unit, R>): R = transport.adapter().invokeCommand(ca)
private var jobs = listOf<Job>() private var jobs = listOf<Job>()
@ -110,7 +106,7 @@ class SuperloginClient<D, S : WithAdapter>(
val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token) val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token)
slData = if (ar is AuthenticationResult.Success) { slData = if (ar is AuthenticationResult.Success) {
val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) }
SuperloginData(ar.loginToken, data) SuperloginData(ar.loginName, ar.loginToken, data)
} else { } else {
null null
} }
@ -170,13 +166,12 @@ class SuperloginClient<D, S : WithAdapter>(
return rn.registerWithData(loginName, password, extraData = BossEncoder.encode(dataType, data)) return rn.registerWithData(loginName, password, extraData = BossEncoder.encode(dataType, data))
.also { rr -> .also { rr ->
if (rr is Registration.Result.Success) { 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? private fun extractData(rr: ByteArray?): D? = rr?.let { BossDecoder.decodeFrom(dataType, it) }
= rr?.let { BossDecoder.decodeFrom(dataType, it) }
private fun mustBeLoggedOut() { private fun mustBeLoggedOut() {
if (isLoggedIn) if (isLoggedIn)
@ -188,8 +183,9 @@ class SuperloginClient<D, S : WithAdapter>(
throw IllegalStateException("please log in first") throw IllegalStateException("please log in first")
} }
fun logout() { suspend fun logout() {
mustBeLoggedIn() mustBeLoggedIn()
invoke(serverApi.slLogout)
slData = null slData = null
} }
@ -202,15 +198,54 @@ class SuperloginClient<D, S : WithAdapter>(
*/ */
suspend fun loginByToken(token: ByteArray): SuperloginData<D>? { suspend fun loginByToken(token: ByteArray): SuperloginData<D>? {
mustBeLoggedOut() mustBeLoggedOut()
val r = invoke(serverApi.slLoginByToken,token) val r = invoke(serverApi.slLoginByToken, token)
return when(r) { return when (r) {
AuthenticationResult.LoginIdUnavailable -> TODO() AuthenticationResult.LoginIdUnavailable -> TODO()
AuthenticationResult.LoginUnavailable -> null AuthenticationResult.LoginUnavailable -> null
AuthenticationResult.RestoreIdUnavailable -> TODO() AuthenticationResult.RestoreIdUnavailable -> TODO()
is AuthenticationResult.Success -> SuperloginData( is AuthenticationResult.Success -> SuperloginData(
r.loginName,
r.loginToken, r.loginToken,
extractData(r.applicationData) 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
}
} }
} }

View 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
}
}

View File

@ -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
}

View File

@ -1,18 +1,90 @@
package net.sergeych.superlogin.server package net.sergeych.superlogin.server
import net.sergeych.boss_serialization_mp.decodeBoss
import net.sergeych.parsec3.WithAdapter 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.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. * a subject to change.
*/ */
open class SLServerSession<T>: WithAdapter() { abstract class SLServerSession<T> : WithAdapter() {
var slData: SuperloginData<T>? = null
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>())
} }

View File

@ -1,31 +1,66 @@
package net.sergeych.superlogin.server package net.sergeych.superlogin.server
import net.sergeych.boss_serialization_mp.decodeBoss
import net.sergeych.parsec3.AdapterBuilder import net.sergeych.parsec3.AdapterBuilder
import net.sergeych.parsec3.CommandHost import net.sergeych.parsec3.CommandHost
import net.sergeych.parsec3.WithAdapter import net.sergeych.parsec3.WithAdapter
import net.sergeych.superlogin.AuthenticationResult import net.sergeych.superlogin.*
import net.sergeych.superlogin.SuperloginServerApi import net.sergeych.unikrypto.SignedRecord
import net.sergeych.superlogin.client.SuperloginData 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>() val a2 = SuperloginServerApi<WithAdapter>()
on(a2.slRegister) { ra -> on(a2.slGetNonce) { nonce }
traits.register(ra).also { rr -> on(a2.slRegister) { packed ->
if( rr is AuthenticationResult.Success) requireLoggedOut()
slData = SuperloginData<D>(rr.loginToken, rr.applicationData?.let { it.decodeBoss<D>()}) val ra = SignedRecord.unpack(packed) { sr ->
loginName = ra.loginName 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) { on(a2.slLogout) {
println("--- logged out ---") superloginData = null
slData = null logout()
loginName = null
traits.logout()
} }
on(a2.slLoginByToken) { token -> 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
}
} }
} }

View File

@ -9,40 +9,32 @@ import net.sergeych.parsec3.Parsec3WSClient
import net.sergeych.parsec3.WithAdapter import net.sergeych.parsec3.WithAdapter
import net.sergeych.parsec3.parsec3TransportServer import net.sergeych.parsec3.parsec3TransportServer
import net.sergeych.superlogin.AuthenticationResult import net.sergeych.superlogin.AuthenticationResult
import net.sergeych.superlogin.PasswordDerivationParams
import net.sergeych.superlogin.RegistrationArgs import net.sergeych.superlogin.RegistrationArgs
import net.sergeych.superlogin.client.LoginState import net.sergeych.superlogin.client.LoginState
import net.sergeych.superlogin.client.Registration import net.sergeych.superlogin.client.Registration
import net.sergeych.superlogin.client.SuperloginClient import net.sergeych.superlogin.client.SuperloginClient
import net.sergeych.superlogin.server.SLServerSession import net.sergeych.superlogin.server.SLServerSession
import net.sergeych.superlogin.server.SLServerTraits
import net.sergeych.superlogin.server.superloginServer import net.sergeych.superlogin.server.superloginServer
import net.sergeych.unikrypto.PublicKey
import superlogin.assertThrowsAsync import superlogin.assertThrowsAsync
import kotlin.random.Random import kotlin.random.Random
import kotlin.test.Test import kotlin.test.*
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlin.test.assertNotNull
data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>() data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>() {
class TestApiServer<T : WithAdapter> : CommandHost<T>() {
val loginName by command<Unit, String?>()
}
object TestServerTraits : SLServerTraits {
val byLogin = mutableMapOf<String, RegistrationArgs>() val byLogin = mutableMapOf<String, RegistrationArgs>()
val byLoginId = mutableMapOf<List<Byte>, RegistrationArgs>() val byLoginId = mutableMapOf<List<Byte>, RegistrationArgs>()
val byRestoreId = mutableMapOf<List<Byte>, RegistrationArgs>() val byRestoreId = mutableMapOf<List<Byte>, RegistrationArgs>()
val byToken = mutableMapOf<List<Byte>, RegistrationArgs>() val byToken = mutableMapOf<List<Byte>, RegistrationArgs>()
val tokens = mutableMapOf<String, ByteArray>()
override suspend fun register(ra: RegistrationArgs): AuthenticationResult { override suspend fun register(ra: RegistrationArgs): AuthenticationResult {
println("ra: ${ra.loginName}") println("ra: ${ra.loginName} : $currentLoginName : $superloginData")
return when { return when {
ra.loginName in byLogin -> { ra.loginName in byLogin -> {
AuthenticationResult.LoginUnavailable AuthenticationResult.LoginUnavailable
} }
ra.loginId.toList() in byLoginId -> AuthenticationResult.LoginIdUnavailable ra.loginId.toList() in byLoginId -> AuthenticationResult.LoginIdUnavailable
ra.restoreId.toList() in byRestoreId -> AuthenticationResult.RestoreIdUnavailable ra.restoreId.toList() in byRestoreId -> AuthenticationResult.RestoreIdUnavailable
else -> { else -> {
@ -51,35 +43,45 @@ object TestServerTraits : SLServerTraits {
byLoginId[ra.loginId.toList()] = ra byLoginId[ra.loginId.toList()] = ra
val token = Random.Default.nextBytes(32) val token = Random.Default.nextBytes(32)
byToken[token.toList()] = ra 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 { override suspend fun loginByToken(token: ByteArray): AuthenticationResult {
return byToken[token.toList()]?.let { return byToken[token.toList()]?.let {
AuthenticationResult.Success(token, it.extraData) AuthenticationResult.Success(it.loginName, token, it.extraData)
} ?: AuthenticationResult.LoginUnavailable }
?: 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 @Serializable
data class TestData( data class TestData(
val foo: String, 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 { internal class WsServerKtTest {
@ -90,9 +92,10 @@ internal class WsServerKtTest {
parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) { parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) {
// superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) { // superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) {
newSession { TestSession() } newSession { TestSession() }
superloginServer(TestServerTraits) superloginServer()
on(api.loginName) { on(api.loginName) {
loginName println("login name called. now we have $currentLoginName : $superloginData")
currentLoginName
} }
} }
}.start(wait = false) }.start(wait = false)
@ -125,10 +128,24 @@ internal class WsServerKtTest {
rt = slc.register("foo", "passwd", TestData("nobar")) rt = slc.register("foo", "passwd", TestData("nobar"))
assertIs<Registration.Result.InvalidLogin>(rt) assertIs<Registration.Result.InvalidLogin>(rt)
assertIs<LoginState.LoggedOut>(slc.state.value)
assertEquals(null, slc.call(api.loginName))
var ar = slc.loginByToken(token) var ar = slc.loginByToken(token)
assertNotNull(ar) assertNotNull(ar)
assertEquals("bar!", ar.data?.foo) 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)) assertEquals("foo", slc.call(api.loginName))
} }