started 0.1.*:

- keep an retrieve data key by password
- renamed resetPasswordAndSignIn to avoid confusion
This commit is contained in:
Sergey Chernov 2022-12-18 23:16:39 +01:00
parent f5cd7a3819
commit 71a5f3c8f1
3 changed files with 89 additions and 21 deletions

View File

@ -9,7 +9,7 @@ val logback_version="1.2.10"
group = "net.sergeych"
version = "0.0.2-SNAPSHOT"
version = "0.1.0-SNAPSHOT"
repositories {
mavenCentral()

View File

@ -16,6 +16,7 @@ import net.sergeych.parsec3.*
import net.sergeych.superlogin.*
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
import net.sergeych.unikrypto.SignedRecord
import net.sergeych.unikrypto.SymmetricKey
import kotlin.reflect.KType
import kotlin.reflect.typeOf
@ -59,6 +60,7 @@ class SuperloginClient<D, S : WithAdapter>(
// do actual disconnect work
_cflow.value = false
_state.value = LoginState.LoggedOut
dataKey = null
} else {
val v = _state.value
if (v !is LoginState.LoggedIn<*> || v.loginData != value) {
@ -96,6 +98,7 @@ class SuperloginClient<D, S : WithAdapter>(
* Call client API commands with it (uses [adapter] under the hood)
*/
suspend fun <A, R> call(ca: CommandDescriptor<A, R>, args: A): R = adapter().invokeCommand(ca, args)
/**
* Call client API commands with it (uses [adapter] under the hood)
*/
@ -113,7 +116,7 @@ class SuperloginClient<D, S : WithAdapter>(
private suspend fun tryRestoreLogin() {
slData?.loginToken?.let { token ->
debug { "trying to restore login with a token" }
while( true ) {
while (true) {
try {
val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token)
slData = if (ar is AuthenticationResult.Success) {
@ -172,6 +175,15 @@ class SuperloginClient<D, S : WithAdapter>(
*/
val isLoggedIn get() = state.value.isLoggedIn
/**
* The data storage key, a random key created when registering. It is retrieved automatically
* on login by password and on registration, _It does not restore when logged in by key
* as it would require storing user's password which must not be done_.
* Use [retrieveDataKey] to restore to with a password (when logged in)
*/
var dataKey: SymmetricKey? = null
private set
/**
* Perform registration and login attempt and return the result. It automatically caches and reuses intermediate
* long-calculated keys so it runs much fatser when called again with the same password.
@ -193,6 +205,7 @@ class SuperloginClient<D, S : WithAdapter>(
.also { rr ->
if (rr is Registration.Result.Success) {
slData = SuperloginData(loginName, rr.loginToken, extractData(rr.encodedData))
dataKey = rr.dataKey
}
}
}
@ -264,7 +277,10 @@ class SuperloginClient<D, S : WithAdapter>(
)
if (result is AuthenticationResult.Success) {
SuperloginData(loginName, result.loginToken, extractData(result.applicationData))
.also { slData = it }
.also {
slData = it
dataKey = aco.data.payload.dataStorageKey
}
} else null
}
} catch (t: Throwable) {
@ -274,6 +290,42 @@ class SuperloginClient<D, S : WithAdapter>(
}
}
/**
* Try to retrieve dataKey (usually after login by token), it is impossible
* to do without a valid password key. Should be logged in. If [dataKey] is not null
* what means is already known, returns it immediatel, otherwise uses password
* to access ACO on the server and extract data key.
* @return data key or null if it is impossible to do (no connection or wrong password)
*/
suspend fun retrieveDataKey(password: String): SymmetricKey? {
mustBeLoggedIn()
dataKey?.let { return it }
val loginName = slData?.loginName ?: throw SLInternalException("slData: empty login name")
try {
val params = invoke(
serverApi.slRequestDerivationParams,
loginName
)
val keys = DerivedKeys.derive(password, params)
// Request login data by derived it
return invoke(
serverApi.slRequestACOByLoginName,
RequestACOByLoginNameArgs(loginName, keys.loginId)
).let { loginRequest ->
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(
loginRequest.packedACO,
keys.loginAccessKey
)?.let { aco ->
aco.data.payload.dataStorageKey.also { dataKey = it }
}
}
} catch (t: Throwable) {
t.printStackTrace()
return null
}
}
/**
* 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
@ -286,10 +338,10 @@ class SuperloginClient<D, S : WithAdapter>(
* @param params password derivation params: it is possible to change its strength here
* @return login data instance on success or null
*/
suspend fun resetPasswordAndLogin(
suspend fun resetPasswordAndSignIn(
secret: String, newPassword: String,
params: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048
loginKeyStrength: Int = 2048,
): SuperloginData<D>? {
mustBeLoggedOut()
return try {
@ -297,6 +349,7 @@ class SuperloginClient<D, S : WithAdapter>(
val packedACO = invoke(serverApi.slRequestACOBySecretId, id)
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(packedACO, key)?.let {
changePasswordWithACO(it, newPassword)
dataKey = it.data.payload.dataStorageKey
slData
}
} catch (x: RestoreKey.InvalidSecretException) {
@ -338,9 +391,11 @@ class SuperloginClient<D, S : WithAdapter>(
val result = invoke(
serverApi.slChangePasswordAndLogin, ChangePasswordArgs(
aco.payload.login,
SignedRecord.pack(aco.payload.loginPrivateKey,
ChangePasswordPayload(newAco.packed,params,newLoginKey.await().publicKey,keys.loginId),
deferredNonce.await())
SignedRecord.pack(
aco.payload.loginPrivateKey,
ChangePasswordPayload(newAco.packed, params, newLoginKey.await().publicKey, keys.loginId),
deferredNonce.await()
)
)
)
when (result) {
@ -367,23 +422,25 @@ class SuperloginClient<D, S : WithAdapter>(
* @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,
suspend fun changePassword(
oldPassword: String, newPassword: String,
passwordDerivationParams: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048
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)
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(
inline operator fun <reified D, S : WithAdapter> invoke(
t: Parsec3Transport<S>,
savedData: SuperloginData<D>? = null,
): SuperloginClient<D, S> {

View File

@ -136,9 +136,12 @@ internal class WsServerKtTest {
runBlocking {
val api = TestApiServer<WithAdapter>()
val slc = SuperloginClient<TestData, WithAdapter>(client)
assertNull(slc.dataKey)
assertEquals(LoginState.LoggedOut, slc.state.value)
var rt = slc.register("foo", "passwd", TestData("bar!"))
val dk1 = slc.dataKey
assertIs<Registration.Result.Success>(rt)
assertEquals(dk1, rt.dataKey)
val secret = rt.secret
var token = rt.loginToken
println(rt.secret)
@ -154,6 +157,7 @@ internal class WsServerKtTest {
slc.register("foo", "passwd", TestData("nobar"))
}
slc.logout()
assertNull(slc.dataKey)
assertIs<LoginState.LoggedOut>(slc.state.value)
assertEquals(null, slc.call(api.loginName))
@ -164,9 +168,14 @@ internal class WsServerKtTest {
var ar = slc.loginByToken(token)
assertNotNull(ar)
assertNull(slc.dataKey)
assertEquals("bar!", ar.data?.foo)
assertTrue { slc.isLoggedIn }
assertEquals("foo", slc.call(api.loginName))
assertNull(slc.retrieveDataKey("badpasswd"))
assertEquals(dk1?.id, slc.retrieveDataKey("passwd")?.id)
assertEquals(dk1?.id, slc.dataKey?.id)
//
assertThrowsAsync<IllegalStateException> { slc.loginByToken(token) }
}
@ -187,6 +196,7 @@ internal class WsServerKtTest {
assertEquals(LoginState.LoggedOut, slc.state.value)
var rt = slc.register("foo", "passwd", TestData("bar!"))
assertIs<Registration.Result.Success>(rt)
val dk1 = slc.dataKey!!
slc.logout()
assertNull(slc.loginByPassword("foo", "passwd2"))
var ar = slc.loginByPassword("foo", "passwd")
@ -194,6 +204,7 @@ internal class WsServerKtTest {
assertEquals("bar!", ar.data?.foo)
assertTrue { slc.isLoggedIn }
assertEquals("foo", slc.call(api.loginName))
assertEquals(dk1.id, slc.dataKey!!.id)
}
}
@ -226,9 +237,9 @@ internal class WsServerKtTest {
assertEquals("foo", slc.call(api.loginName))
slc.logout()
assertNull(slc.resetPasswordAndLogin("bad_secret", "newpass2"))
assertNull(slc.resetPasswordAndLogin("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
ar = slc.resetPasswordAndLogin(secret,"newpass2")
assertNull(slc.resetPasswordAndSignIn("bad_secret", "newpass2"))
assertNull(slc.resetPasswordAndSignIn("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
ar = slc.resetPasswordAndSignIn(secret,"newpass2")
assertNotNull(ar)
assertEquals("bar!", ar.data?.foo)
assertTrue { slc.isLoggedIn }