RC0: full planned functionality (+change password, +reset password)

This commit is contained in:
Sergey Chernov 2022-12-02 20:05:09 +01:00
parent f321a82b8c
commit d12b392ed0
12 changed files with 350 additions and 126 deletions

View File

@ -44,7 +44,7 @@ kotlin {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3")
api("net.sergeych:unikrypto:1.2.2-SNAPSHOT")
api("net.sergeych:parsec3:0.3.2-SNAPSHOT")
api("net.sergeych:parsec3:0.3.3-SNAPSHOT")
api("net.sergeych:boss-serialization-mp:0.2.4-SNAPSHOT")
3 }
}

View File

@ -26,7 +26,7 @@ import kotlin.reflect.typeOf
* To construct it please use one of:_
*
* - [AccessControlObject.pack] to generate new
* - [AccessControlObject.unpackWithPasswordKey] to decrypt it with a password key
* - [AccessControlObject.unpackWithKey] to decrypt it with a password key
* - [AccessControlObject.unpackWithSecret] to decrypt it with a `secret`
*
* @param payloadType used to properly serialize application=specific data for [payload]
@ -125,15 +125,16 @@ class AccessControlObject<T>(
}
/**
* Unpack and decrypt ACO with a password key
* Unpack and decrypt ACO with a password key or secret-based key (this once can be obtained from `secret`
* with [RestoreKey.parse].
* @return decrypted ACO or null if the key is wrong.
*/
inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? =
Container.decrypt<Data<T>>(packed, passwordKey)?.let {
AccessControlObject(typeOf<Data<T>>(), packed, passwordKey, it)
inline fun <reified T> unpackWithKey(packed: ByteArray, key: SymmetricKey): AccessControlObject<T>? =
Container.decrypt<Data<T>>(packed, key)?.let {
AccessControlObject(typeOf<Data<T>>(), packed, key, it)
}
fun <T>unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey,payloadType: KType): AccessControlObject<T>? =
fun <T>unpackWithKey(packed: ByteArray, passwordKey: SymmetricKey, payloadType: KType): AccessControlObject<T>? =
Container.decryptAsBytes(packed, passwordKey)?.let {
AccessControlObject(payloadType, packed, passwordKey, BossDecoder.decodeFrom(payloadType,it))
}
@ -146,14 +147,11 @@ class AccessControlObject<T>(
suspend inline fun <reified T> unpackWithSecret(packed: ByteArray, secret: String): AccessControlObject<T>? {
try {
val (id, key) = RestoreKey.parse(secret)
return Container.decrypt<Data<T>>(packed, key)?.let { data ->
AccessControlObject(typeOf<Data<T>>(), packed, data.passwordKey, data)
}
return unpackWithKey(packed, key)
}
catch(_: RestoreKey.InvalidSecretException) {
return null
}
}
}
}

View File

@ -1,5 +1,6 @@
package net.sergeych.superlogin
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import net.sergeych.parsec3.CommandHost
import net.sergeych.parsec3.WithAdapter
@ -13,13 +14,14 @@ data class RegistrationArgs(
val loginPublicKey: PublicKey,
val derivationParams: PasswordDerivationParams,
val restoreId: ByteArray,
val restoreData: ByteArray,
val packedACO: ByteArray,
val extraData: ByteArray? = null
)
@Serializable
sealed class AuthenticationResult {
@Serializable
@SerialName("Success")
data class Success(
val loginName: String,
val loginToken: ByteArray,
@ -27,32 +29,49 @@ sealed class AuthenticationResult {
): AuthenticationResult()
@Serializable
@SerialName("LoginUnavailable")
object LoginUnavailable: AuthenticationResult()
@Serializable
@SerialName("LoginIdUnavailable")
object LoginIdUnavailable: AuthenticationResult()
@Serializable
@SerialName("RestoreIdUnavailable")
object RestoreIdUnavailable: AuthenticationResult()
}
@Serializable
data class RequestLoginDataArgs(
class RequestACOByLoginNameArgs(
val loginName: String,
val loginId: ByteArray,
)
@Serializable
data class RequestLoginDataResult(
class RequestACOResult(
val packedACO: ByteArray,
val nonce: ByteArray
)
@Serializable
data class LoginByPasswordPayload(
class LoginByPasswordPayload(
val loginName: String
)
@Serializable
class ChangePasswordArgs(
val loginName: String,
val packedSignedRecord: ByteArray
)
@Serializable
class ChangePasswordPayload(
val packedACO: ByteArray,
val passwordDerivationParams: PasswordDerivationParams,
val newLoginKey: PublicKey
)
class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
val slGetNonce by command<Unit,ByteArray>()
@ -61,13 +80,11 @@ class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
val slLoginByToken by command<ByteArray,AuthenticationResult>()
val slRequestDerivationParams by command<String,PasswordDerivationParams>()
val slRequestLoginData by command<RequestLoginDataArgs,RequestLoginDataResult>()
val slRequestACOByLoginName by command<RequestACOByLoginNameArgs,RequestACOResult>()
val slLoginByKey by command<ByteArray,AuthenticationResult>()
val slRequestACOBySecretId by command<ByteArray,ByteArray>()
val slChangePasswordAndLogin by command <ChangePasswordArgs,AuthenticationResult>()
/**
* Get resstoreData by restoreId: password reset procedure start.
*/
// val requestUserLogin by command<ByteArray,ByteArray>()
// val performLogin by command<LoginArgs
val slSendTestException by command<Unit,Unit>()
}

View File

@ -1,5 +1,10 @@
package net.sergeych.superlogin.client
/**
* Login client has a _login state_ which represents known state of the log-in protocol.
* It can properly process offline state and reconnection and report state bu mean
* of the state flow pf instanses of this class. See [SuperloginClient.state].
*/
sealed class LoginState(val isLoggedIn: Boolean) {
/**
* User is logged in (either connected or yet not). Client application should save

View File

@ -1,8 +1,6 @@
package net.sergeych.superlogin.client
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable
@ -13,10 +11,7 @@ import net.sergeych.mp_logger.Loggable
import net.sergeych.mp_logger.exception
import net.sergeych.mp_logger.warning
import net.sergeych.mp_tools.globalLaunch
import net.sergeych.parsec3.Adapter
import net.sergeych.parsec3.CommandDescriptor
import net.sergeych.parsec3.Parsec3Transport
import net.sergeych.parsec3.WithAdapter
import net.sergeych.parsec3.*
import net.sergeych.superlogin.*
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
import net.sergeych.unikrypto.SignedRecord
@ -43,6 +38,7 @@ class SuperloginClient<D, S : WithAdapter>(
private val transport: Parsec3Transport<S>,
savedData: SuperloginData<D>? = null,
private val dataType: KType,
override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(),
) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") {
private val _state = MutableStateFlow<LoginState>(
@ -77,16 +73,16 @@ class SuperloginClient<D, S : WithAdapter>(
private var adapterReady = CompletableDeferred<Unit>()
override suspend fun adapter(): Adapter<S> {
do {
try {
adapterReady.await()
return transport.adapter()
} catch (x: Throwable) {
exception { "failed to get adapter" to x }
}
} while (true)
}
override suspend fun adapter(): Adapter<S> = transport.adapter()
// do {
// try {
// adapterReady.await()
// return transport.adapter()
// } catch (x: Throwable) {
// exception { "failed to get adapter" to x }
// }
// } while (true)
// }
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)
@ -119,6 +115,7 @@ class SuperloginClient<D, S : WithAdapter>(
}
init {
transport.registerExceptinos(SuperloginExceptionsRegistry)
jobs += globalLaunch {
transport.connectedFlow.collect { on ->
if (on) tryRestoreLogin()
@ -220,11 +217,11 @@ class SuperloginClient<D, S : WithAdapter>(
val keys = DerivedKeys.derive(password, params)
// Request login data by derived it
return invoke(
serverApi.slRequestLoginData,
RequestLoginDataArgs(loginName, keys.loginId)
serverApi.slRequestACOByLoginName,
RequestACOByLoginNameArgs(loginName, keys.loginId)
).let { loginRequest ->
try {
AccessControlObject.unpackWithPasswordKey<SuperloginRestoreAccessPayload>(
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(
loginRequest.packedACO,
keys.loginAccessKey
)?.let { aco ->
@ -241,14 +238,121 @@ class SuperloginClient<D, S : WithAdapter>(
.also { slData = it }
} else null
}
}
catch (t: Throwable) {
} catch (t: Throwable) {
t.printStackTrace()
throw t
}
}
}
/**
* Resets password and log in using a `secret` string (one that wwas reported on registration. __Never store
* secrt string in your app__. Always ask user to enter it just before the operation and wipe it out
* immediately after. It is a time-consuming procedure. Note that on success the client state changes
* to [LoginState.LoggedIn].
*
* @param secret the secret string as was reported when registering
* @param newPassword new password (apply strength checks, it is not checked here)
* @param loginKeyStrength desired login key strength (it will be generated there)
* @param params password derivation params: it is possible to change its strength here
* @return login data instance on success or null
*/
suspend fun resetPasswordAndLogin(
secret: String, newPassword: String,
params: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048
): SuperloginData<D>? {
mustBeLoggedOut()
return try {
val (id, key) = RestoreKey.parse(secret)
val packedACO = invoke(serverApi.slRequestACOBySecretId, id)
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(packedACO, key)?.let {
changePasswordWithACO(it, newPassword)
slData
}
} catch (x: RestoreKey.InvalidSecretException) {
null
} catch (x: Exception) {
x.printStackTrace()
null
}
}
/**
* Changes the password (which includes generating new login key). Does not require any particular
* [state]. This is a long operation. On success, it changes (updates) [state] to [LoginState.LoggedIn]
* with new data whatever it was before. Be aware of it.
*/
protected suspend fun changePasswordWithACO(
aco: AccessControlObject<SuperloginRestoreAccessPayload>,
newPassword: String,
params: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048,
): Boolean {
return coroutineScope {
// Get current nonce in parallel
val deferredNonce = async { invoke(serverApi.slGetNonce) }
// get login key in parallel
val newLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength)
// derive keys in main scope
val keys = DerivedKeys.derive(newPassword, params)
// new ACO payload: new login key, old data storage key and login
val newSlp = SuperloginRestoreAccessPayload(
aco.payload.login,
newLoginKey.await(),
aco.payload.dataStorageKey
)
// new ACO with a new password key and payload (but the same secret!)
var newAco = aco.updatePasswordKey(keys.loginAccessKey).updatePayload(newSlp)
// trying to update
val result = invoke(
serverApi.slChangePasswordAndLogin, ChangePasswordArgs(
aco.payload.login,
SignedRecord.pack(aco.payload.loginPrivateKey,
ChangePasswordPayload(newAco.packed,params,newLoginKey.await().publicKey),
deferredNonce.await())
)
)
when (result) {
is AuthenticationResult.Success -> {
slData = SuperloginData(result.loginName, result.loginToken, extractData(result.applicationData))
true
}
else -> {
warning { "Change password result: $result" }
false
}
}
}
}
/**
* Change password for a logged-in user using its known password. It is a long operation
* @param oldPassword existing password (re-request it from a user!)
* @param newPassword new password. we do not chek it but it should be strong - check it on your end
* for example with [net.sergeych.unikrypto.Passwords] tools
* @param passwordDerivationParams at this point derivation parameters are alwaus updated so it is possible
* to set it to desired
* @param loginKeyStrength login key is regenerateed so its strength could be updated here
* @return true if the password has been successfully changed
*/
suspend fun changePassword(oldPassword: String, newPassword: String,
passwordDerivationParams: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048
): Boolean {
mustBeLoggedIn()
val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here")
val dp = invoke(serverApi.slRequestDerivationParams,loginName)
val keys = DerivedKeys.derive(oldPassword,dp)
val data = invoke(serverApi.slRequestACOByLoginName,RequestACOByLoginNameArgs(loginName,keys.loginId))
return AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(data.packedACO, keys.loginAccessKey)?.let {
changePasswordWithACO(it, newPassword,passwordDerivationParams, loginKeyStrength)
} ?: false
}
companion object {
inline operator fun <reified D, S : WithAdapter> invoke(
t: Parsec3Transport<S>,

View File

@ -0,0 +1,12 @@
package net.sergeych.superlogin
import net.sergeych.parsec3.ExceptionsRegistry
class SLInternalException(reason: String?="superlogin internal exception (a bug)",cause: Throwable?=null):
Exception(reason, cause)
fun addSuperloginExceptions(er: ExceptionsRegistry) {
er.register { SLInternalException(it) }
}
val SuperloginExceptionsRegistry = ExceptionsRegistry().also { addSuperloginExceptions(it) }

View File

@ -1,34 +0,0 @@
package net.sergeych.superlogin
import net.sergeych.unikrypto.HashAlgorithm
import net.sergeych.unikrypto.digest
/**
* Primitive to simplify create and compare salted byte arrays
*/
data class Salted(val salt: ByteArray,val data: ByteArray) {
val salted = HashAlgorithm.SHA3_256.digest(salt, data)
fun matches(other: ByteArray): Boolean {
return Salted(salt, other) == this
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as Salted
if (!salt.contentEquals(other.salt)) return false
if (!data.contentEquals(other.data)) return false
return true
}
override fun hashCode(): Int {
var result = salt.contentHashCode()
result = 31 * result + data.contentHashCode()
return result
}
}

View File

@ -2,9 +2,11 @@ package superlogin
import kotlinx.coroutines.test.runTest
import net.sergeych.superlogin.AccessControlObject
import net.sergeych.superlogin.RestoreKey
import net.sergeych.unikrypto.SymmetricKeys
import kotlin.test.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
internal class AccessControlObjectTest {
@ -14,29 +16,29 @@ internal class AccessControlObjectTest {
val pk2 = SymmetricKeys.random()
val (rk, packed1) = AccessControlObject.pack(pk1, 117)
println(rk.secret)
val ac1 = AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk1)
val ac1 = AccessControlObject.unpackWithKey<Int>(packed1,pk1)
assertNotNull(ac1)
assertEquals(117, ac1.payload)
val ac2 = AccessControlObject.unpackWithSecret<Int>(packed1,rk.secret)
assertNotNull(ac2)
assertEquals(117, ac2.payload)
assertNull(AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk2))
assertNull(AccessControlObject.unpackWithKey<Int>(packed1,pk2))
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,"the_-wrong-secret-yess"))
val (rk2, packed2) = AccessControlObject.pack(pk2, 107)
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,rk2.secret))
var ac21 = AccessControlObject.unpackWithPasswordKey<Int>(packed2,pk2)
var ac21 = AccessControlObject.unpackWithKey<Int>(packed2,pk2)
assertNotNull(ac21)
assertEquals(107, ac21.payload)
var packed3 = ac1.updatePayload(121).packed
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk1)
ac21 = AccessControlObject.unpackWithKey(packed3,pk1)
assertNotNull(ac21)
assertEquals(121, ac21.payload)
packed3 = ac1.updatePasswordKey(pk2).packed
println("-------")
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk2)
ac21 = AccessControlObject.unpackWithKey(packed3,pk2)
assertNotNull(ac21)
assertEquals(117, ac21.payload)

View File

@ -9,6 +9,6 @@ suspend inline fun <reified T: Throwable> assertThrowsAsync(f: suspend () -> Uni
}
catch(x: Throwable) {
if( x !is T )
fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}")
fail("${x::class.simpleName} was thrown instead of ${T::class.simpleName}: $x")
}
}

View File

@ -69,20 +69,35 @@ abstract class SLServerSession<T> : WithAdapter() {
* Retreive exact password derivation params as were stored by registration.
* @return derivation params for the login name or null if the name is not known
*/
abstract suspend fun requestDerivationParams(login: String): PasswordDerivationParams?
abstract suspend fun requestDerivationParams(loginName: String): PasswordDerivationParams?
/**
* Override this method, check loginId to match loginName, and of ot os ok, return packed ACO
* @return packed ACO or null if loginName is wrong, or loginId does not match it.
*/
abstract suspend fun requestLoginData(loginName: String,loginId: ByteArray): ByteArray?
abstract suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray?
/**
* Implement retrieving ACO object by restoreId. Return found object ot null.
*/
abstract suspend fun requestACOByRestoreId(restoreId: ByteArray): ByteArray?
/**
* Implementation must: find the actual user by loginName and check the publicKey is valid (for example
* matches stored key id in the database, and return
* @return [AuthenticationResult.Success]
*/
abstract suspend fun loginByKey(loginName: String,publicKey: PublicKey): AuthenticationResult
abstract suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult
/**
* Update access control object (resotre data) to the specified.
*/
abstract suspend fun updateAccessControlData(
loginName: String,
packedData: ByteArray,
passwordDerivationParams: PasswordDerivationParams,
newLoginKey: PublicKey
)
}
inline fun <reified D, T : SLServerSession<D>> T.setSlData(it: AuthenticationResult.Success) {

View File

@ -7,19 +7,22 @@ import net.sergeych.superlogin.*
import net.sergeych.unikrypto.SignedRecord
import kotlin.random.Random
fun randomACOLike(): ByteArray {
return Random.nextBytes(117)
}
inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilder<T, H>.superloginServer() {
addErrors(SuperloginExceptionsRegistry)
val a2 = SuperloginServerApi<WithAdapter>()
on(a2.slGetNonce) { nonce }
on(a2.slRegister) { packed ->
requireLoggedOut()
val ra = SignedRecord.unpack(packed) { sr ->
if( !(sr.nonce contentEquals nonce) )
if (!(sr.nonce contentEquals nonce))
throw IllegalArgumentException("wrong signed record nonce")
}.decode<RegistrationArgs>()
register(ra).also { rr ->
if( rr is AuthenticationResult.Success) {
if (rr is AuthenticationResult.Success) {
setSlData(rr)
}
}
@ -31,7 +34,7 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
on(a2.slLoginByToken) { token ->
requireLoggedOut()
loginByToken(token).also {
if( it is AuthenticationResult.Success)
if (it is AuthenticationResult.Success)
setSlData(it)
}
}
@ -40,10 +43,10 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
// slow down login scanning
requestDerivationParams(name) ?: PasswordDerivationParams()
}
on(a2.slRequestLoginData) { args ->
requestLoginData(args.loginName,args.loginId)?.let {
RequestLoginDataResult(it, nonce)
} ?: RequestLoginDataResult(Random.nextBytes(117), nonce)
on(a2.slRequestACOByLoginName) { args ->
requestACOByLoginName(args.loginName, args.loginId)?.let {
RequestACOResult(it, nonce)
} ?: RequestACOResult(randomACOLike(), nonce)
}
on(a2.slLoginByKey) { packedSR ->
try {
@ -53,14 +56,47 @@ inline fun <reified D, T : SLServerSession<D>, H : CommandHost<T>> AdapterBuilde
}
val loginName: String = sr.decode<LoginByPasswordPayload>().loginName
loginByKey(loginName, sr.publicKey).also {
if( it is AuthenticationResult.Success)
if (it is AuthenticationResult.Success)
setSlData(it)
}
}
catch(x: Exception) {
} catch (x: Exception) {
// most likely, wrong nonce, less probable bad signature
AuthenticationResult.LoginUnavailable
}
}
on(a2.slChangePasswordAndLogin) { args ->
val currentSlData = superloginData
try {
val sr = SignedRecord.unpack(args.packedSignedRecord) {
if (!(it.nonce contentEquals nonce)) throw IllegalArgumentException()
}
val payload = sr.decode<ChangePasswordPayload>()
val loginResult = loginByKey(args.loginName, sr.publicKey)
if (loginResult is AuthenticationResult.Success) {
setSlData(loginResult)
updateAccessControlData(
args.loginName,
payload.packedACO,
payload.passwordDerivationParams,
payload.newLoginKey
)
println(">> ${loginResult.loginToken} -- !")
}
loginResult
} catch (_: IllegalArgumentException) {
superloginData = currentSlData
AuthenticationResult.LoginUnavailable
} catch (x: Throwable) {
x.printStackTrace()
superloginData = currentSlData
AuthenticationResult.LoginUnavailable
}
}
on(a2.slRequestACOBySecretId) {
requestACOByRestoreId(it) ?: randomACOLike()
}
on(a2.slSendTestException) {
throw SLInternalException("test")
}
}

View File

@ -1,5 +1,6 @@
package net.sergeych
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.coroutines.runBlocking
@ -8,9 +9,7 @@ import net.sergeych.parsec3.CommandHost
import net.sergeych.parsec3.Parsec3WSClient
import net.sergeych.parsec3.WithAdapter
import net.sergeych.parsec3.parsec3TransportServer
import net.sergeych.superlogin.AuthenticationResult
import net.sergeych.superlogin.PasswordDerivationParams
import net.sergeych.superlogin.RegistrationArgs
import net.sergeych.superlogin.*
import net.sergeych.superlogin.client.LoginState
import net.sergeych.superlogin.client.Registration
import net.sergeych.superlogin.client.SuperloginClient
@ -56,11 +55,15 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>()
?: AuthenticationResult.LoginUnavailable
}
override suspend fun requestDerivationParams(login: String): PasswordDerivationParams? =
byLogin[login]?.derivationParams
override suspend fun requestDerivationParams(loginName: String): PasswordDerivationParams? =
byLogin[loginName]?.derivationParams
override suspend fun requestLoginData(loginName: String, loginId: ByteArray): ByteArray? {
return byLogin[loginName]?.restoreData
override suspend fun requestACOByLoginName(loginName: String, loginId: ByteArray): ByteArray? {
return byLogin[loginName]?.packedACO
}
override suspend fun requestACOByRestoreId(restoreId: ByteArray): ByteArray? {
return byRestoreId[restoreId.toList()]?.packedACO
}
override suspend fun loginByKey(loginName: String, publicKey: PublicKey): AuthenticationResult {
@ -69,6 +72,25 @@ data class TestSession(var buzz: String = "BuZZ") : SLServerSession<TestData>()
AuthenticationResult.Success(ra.loginName, tokens[loginName]!!, ra.extraData)
else AuthenticationResult.LoginUnavailable
}
override suspend fun updateAccessControlData(
loginName: String,
packedData: ByteArray,
passwordDerivationParams: PasswordDerivationParams,
newLoginKey: PublicKey,
) {
val r = byLogin[loginName]?.copy(
packedACO = packedData,
derivationParams = passwordDerivationParams,
loginPublicKey = newLoginKey
)
?: throw RuntimeException("login not found")
byLogin[loginName] = r
byLoginId[r.loginId.toList()] = r
byToken[currentLoginToken!!.toList()] = r
byRestoreId[r.restoreId.toList()] = r
}
}
@ -88,17 +110,7 @@ internal class WsServerKtTest {
@Test
fun testWsServer() {
embeddedServer(Netty, port = 8080) {
parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) {
// superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) {
newSession { TestSession() }
superloginServer()
on(api.loginName) {
println("login name called. now we have $currentLoginName : $superloginData")
currentLoginName
}
}
}.start(wait = false)
embeddedServer(Netty, port = 8080, module = Application::testServerModule).start(wait = false)
val client = Parsec3WSClient("ws://localhost:8080/api/p3")
@ -138,18 +150,75 @@ internal class WsServerKtTest {
assertEquals("foo", slc.call(api.loginName))
//
assertThrowsAsync<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))
}
}
@Test
fun changePasswordTest() {
embeddedServer(Netty, port = 8081, module = Application::testServerModule).start(wait = false)
runBlocking {
val client = Parsec3WSClient("ws://localhost:8081/api/p3")
val api = TestApiServer<WithAdapter>()
val slc = SuperloginClient<TestData, WithAdapter>(client)
assertEquals(LoginState.LoggedOut, slc.state.value)
var rt = slc.register("foo", "passwd", TestData("bar!"))
assertIs<Registration.Result.Success>(rt)
val secret = rt.secret
var token = rt.loginToken
assertFalse(slc.changePassword("wrong", "new"))
assertTrue(slc.changePassword("passwd", "newpass1"))
assertTrue { slc.isLoggedIn }
assertEquals("foo", slc.call(api.loginName))
slc.logout()
assertNull(slc.loginByPassword("foo", "passwd"))
var ar = slc.loginByPassword("foo", "newpass1")
assertNotNull(ar)
assertEquals("bar!", ar.data?.foo)
assertTrue { slc.isLoggedIn }
assertEquals("foo", slc.call(api.loginName))
slc.logout()
println(secret)
assertNull(slc.resetPasswordAndLogin("bad_secret", "newpass2"))
assertNull(slc.resetPasswordAndLogin("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
ar = slc.resetPasswordAndLogin(secret,"newpass2")
assertNotNull(ar)
assertEquals("bar!", ar.data?.foo)
assertTrue { slc.isLoggedIn }
assertEquals("foo", slc.call(api.loginName))
}
}
@Test
fun testExceptions() {
embeddedServer(Netty, port = 8082, module = Application::testServerModule).start(wait = false)
val client = Parsec3WSClient("ws://localhost:8082/api/p3")
runBlocking {
val slc = SuperloginClient<TestData, WithAdapter>(client)
val serverApi = SuperloginServerApi<WithAdapter>()
assertThrowsAsync<SLInternalException> {
slc.call(serverApi.slSendTestException,Unit)
}
}
}
}
fun Application.testServerModule() {
parsec3TransportServer(TestApiServer<SLServerSession<TestData>>()) {
// superloginServer(TestServerTraits,TestApiServer<SLServerSession<TestData>>()) {
newSession { TestSession() }
superloginServer()
on(api.loginName) {
println("login name called. now we have $currentLoginName : $superloginData")
currentLoginName
}
}
}