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 {
|
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 }
|
||||||
|
@ -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].
|
||||||
|
@ -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>()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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
|
||||||
|
@ -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) {
|
||||||
@ -97,7 +91,9 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,9 +204,48 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
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>())
|
||||||
}
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user