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…
Reference in New Issue
Block a user