started 0.1.*:
- keep an retrieve data key by password - renamed resetPasswordAndSignIn to avoid confusion
This commit is contained in:
parent
f5cd7a3819
commit
71a5f3c8f1
@ -9,7 +9,7 @@ val logback_version="1.2.10"
|
|||||||
|
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.0.2-SNAPSHOT"
|
version = "0.1.0-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
@ -16,6 +16,7 @@ import net.sergeych.parsec3.*
|
|||||||
import net.sergeych.superlogin.*
|
import net.sergeych.superlogin.*
|
||||||
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
|
import net.sergeych.superlogin.server.SuperloginRestoreAccessPayload
|
||||||
import net.sergeych.unikrypto.SignedRecord
|
import net.sergeych.unikrypto.SignedRecord
|
||||||
|
import net.sergeych.unikrypto.SymmetricKey
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
import kotlin.reflect.typeOf
|
import kotlin.reflect.typeOf
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ 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
|
||||||
|
dataKey = null
|
||||||
} 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) {
|
||||||
@ -96,6 +98,7 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
* Call client API commands with it (uses [adapter] under the hood)
|
* 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)
|
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)
|
* Call client API commands with it (uses [adapter] under the hood)
|
||||||
*/
|
*/
|
||||||
@ -113,7 +116,7 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
private suspend fun tryRestoreLogin() {
|
private suspend fun tryRestoreLogin() {
|
||||||
slData?.loginToken?.let { token ->
|
slData?.loginToken?.let { token ->
|
||||||
debug { "trying to restore login with a token" }
|
debug { "trying to restore login with a token" }
|
||||||
while( true ) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
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) {
|
||||||
@ -172,6 +175,15 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
*/
|
*/
|
||||||
val isLoggedIn get() = state.value.isLoggedIn
|
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
|
* 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.
|
* 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 ->
|
.also { rr ->
|
||||||
if (rr is Registration.Result.Success) {
|
if (rr is Registration.Result.Success) {
|
||||||
slData = SuperloginData(loginName, rr.loginToken, extractData(rr.encodedData))
|
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) {
|
if (result is AuthenticationResult.Success) {
|
||||||
SuperloginData(loginName, result.loginToken, extractData(result.applicationData))
|
SuperloginData(loginName, result.loginToken, extractData(result.applicationData))
|
||||||
.also { slData = it }
|
.also {
|
||||||
|
slData = it
|
||||||
|
dataKey = aco.data.payload.dataStorageKey
|
||||||
|
}
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
} catch (t: Throwable) {
|
} 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
|
* 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
|
* 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
|
* @param params password derivation params: it is possible to change its strength here
|
||||||
* @return login data instance on success or null
|
* @return login data instance on success or null
|
||||||
*/
|
*/
|
||||||
suspend fun resetPasswordAndLogin(
|
suspend fun resetPasswordAndSignIn(
|
||||||
secret: String, newPassword: String,
|
secret: String, newPassword: String,
|
||||||
params: PasswordDerivationParams = PasswordDerivationParams(),
|
params: PasswordDerivationParams = PasswordDerivationParams(),
|
||||||
loginKeyStrength: Int = 2048
|
loginKeyStrength: Int = 2048,
|
||||||
): SuperloginData<D>? {
|
): SuperloginData<D>? {
|
||||||
mustBeLoggedOut()
|
mustBeLoggedOut()
|
||||||
return try {
|
return try {
|
||||||
@ -297,6 +349,7 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
val packedACO = invoke(serverApi.slRequestACOBySecretId, id)
|
val packedACO = invoke(serverApi.slRequestACOBySecretId, id)
|
||||||
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(packedACO, key)?.let {
|
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(packedACO, key)?.let {
|
||||||
changePasswordWithACO(it, newPassword)
|
changePasswordWithACO(it, newPassword)
|
||||||
|
dataKey = it.data.payload.dataStorageKey
|
||||||
slData
|
slData
|
||||||
}
|
}
|
||||||
} catch (x: RestoreKey.InvalidSecretException) {
|
} catch (x: RestoreKey.InvalidSecretException) {
|
||||||
@ -338,9 +391,11 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
val result = invoke(
|
val result = invoke(
|
||||||
serverApi.slChangePasswordAndLogin, ChangePasswordArgs(
|
serverApi.slChangePasswordAndLogin, ChangePasswordArgs(
|
||||||
aco.payload.login,
|
aco.payload.login,
|
||||||
SignedRecord.pack(aco.payload.loginPrivateKey,
|
SignedRecord.pack(
|
||||||
ChangePasswordPayload(newAco.packed,params,newLoginKey.await().publicKey,keys.loginId),
|
aco.payload.loginPrivateKey,
|
||||||
deferredNonce.await())
|
ChangePasswordPayload(newAco.packed, params, newLoginKey.await().publicKey, keys.loginId),
|
||||||
|
deferredNonce.await()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
when (result) {
|
when (result) {
|
||||||
@ -367,23 +422,25 @@ class SuperloginClient<D, S : WithAdapter>(
|
|||||||
* @param loginKeyStrength login key is regenerateed so its strength could be updated here
|
* @param loginKeyStrength login key is regenerateed so its strength could be updated here
|
||||||
* @return true if the password has been successfully changed
|
* @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(),
|
passwordDerivationParams: PasswordDerivationParams = PasswordDerivationParams(),
|
||||||
loginKeyStrength: Int = 2048
|
loginKeyStrength: Int = 2048,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
mustBeLoggedIn()
|
mustBeLoggedIn()
|
||||||
val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here")
|
val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here")
|
||||||
val dp = invoke(serverApi.slRequestDerivationParams,loginName)
|
val dp = invoke(serverApi.slRequestDerivationParams, loginName)
|
||||||
val keys = DerivedKeys.derive(oldPassword,dp)
|
val keys = DerivedKeys.derive(oldPassword, dp)
|
||||||
val data = invoke(serverApi.slRequestACOByLoginName,RequestACOByLoginNameArgs(loginName,keys.loginId))
|
val data = invoke(serverApi.slRequestACOByLoginName, RequestACOByLoginNameArgs(loginName, keys.loginId))
|
||||||
return AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(data.packedACO, keys.loginAccessKey)?.let {
|
return AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(data.packedACO, keys.loginAccessKey)
|
||||||
changePasswordWithACO(it, newPassword,passwordDerivationParams, loginKeyStrength)
|
?.let {
|
||||||
|
changePasswordWithACO(it, newPassword, passwordDerivationParams, loginKeyStrength)
|
||||||
} ?: false
|
} ?: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
inline operator fun <reified D,S : WithAdapter> invoke(
|
inline operator fun <reified D, S : WithAdapter> invoke(
|
||||||
t: Parsec3Transport<S>,
|
t: Parsec3Transport<S>,
|
||||||
savedData: SuperloginData<D>? = null,
|
savedData: SuperloginData<D>? = null,
|
||||||
): SuperloginClient<D, S> {
|
): SuperloginClient<D, S> {
|
||||||
|
@ -136,9 +136,12 @@ internal class WsServerKtTest {
|
|||||||
runBlocking {
|
runBlocking {
|
||||||
val api = TestApiServer<WithAdapter>()
|
val api = TestApiServer<WithAdapter>()
|
||||||
val slc = SuperloginClient<TestData, WithAdapter>(client)
|
val slc = SuperloginClient<TestData, WithAdapter>(client)
|
||||||
|
assertNull(slc.dataKey)
|
||||||
assertEquals(LoginState.LoggedOut, slc.state.value)
|
assertEquals(LoginState.LoggedOut, slc.state.value)
|
||||||
var rt = slc.register("foo", "passwd", TestData("bar!"))
|
var rt = slc.register("foo", "passwd", TestData("bar!"))
|
||||||
|
val dk1 = slc.dataKey
|
||||||
assertIs<Registration.Result.Success>(rt)
|
assertIs<Registration.Result.Success>(rt)
|
||||||
|
assertEquals(dk1, rt.dataKey)
|
||||||
val secret = rt.secret
|
val secret = rt.secret
|
||||||
var token = rt.loginToken
|
var token = rt.loginToken
|
||||||
println(rt.secret)
|
println(rt.secret)
|
||||||
@ -154,6 +157,7 @@ internal class WsServerKtTest {
|
|||||||
slc.register("foo", "passwd", TestData("nobar"))
|
slc.register("foo", "passwd", TestData("nobar"))
|
||||||
}
|
}
|
||||||
slc.logout()
|
slc.logout()
|
||||||
|
assertNull(slc.dataKey)
|
||||||
assertIs<LoginState.LoggedOut>(slc.state.value)
|
assertIs<LoginState.LoggedOut>(slc.state.value)
|
||||||
assertEquals(null, slc.call(api.loginName))
|
assertEquals(null, slc.call(api.loginName))
|
||||||
|
|
||||||
@ -164,9 +168,14 @@ internal class WsServerKtTest {
|
|||||||
|
|
||||||
var ar = slc.loginByToken(token)
|
var ar = slc.loginByToken(token)
|
||||||
assertNotNull(ar)
|
assertNotNull(ar)
|
||||||
|
assertNull(slc.dataKey)
|
||||||
assertEquals("bar!", ar.data?.foo)
|
assertEquals("bar!", ar.data?.foo)
|
||||||
assertTrue { slc.isLoggedIn }
|
assertTrue { slc.isLoggedIn }
|
||||||
assertEquals("foo", slc.call(api.loginName))
|
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) }
|
assertThrowsAsync<IllegalStateException> { slc.loginByToken(token) }
|
||||||
}
|
}
|
||||||
@ -187,6 +196,7 @@ internal class WsServerKtTest {
|
|||||||
assertEquals(LoginState.LoggedOut, slc.state.value)
|
assertEquals(LoginState.LoggedOut, slc.state.value)
|
||||||
var rt = slc.register("foo", "passwd", TestData("bar!"))
|
var rt = slc.register("foo", "passwd", TestData("bar!"))
|
||||||
assertIs<Registration.Result.Success>(rt)
|
assertIs<Registration.Result.Success>(rt)
|
||||||
|
val dk1 = slc.dataKey!!
|
||||||
slc.logout()
|
slc.logout()
|
||||||
assertNull(slc.loginByPassword("foo", "passwd2"))
|
assertNull(slc.loginByPassword("foo", "passwd2"))
|
||||||
var ar = slc.loginByPassword("foo", "passwd")
|
var ar = slc.loginByPassword("foo", "passwd")
|
||||||
@ -194,6 +204,7 @@ internal class WsServerKtTest {
|
|||||||
assertEquals("bar!", ar.data?.foo)
|
assertEquals("bar!", ar.data?.foo)
|
||||||
assertTrue { slc.isLoggedIn }
|
assertTrue { slc.isLoggedIn }
|
||||||
assertEquals("foo", slc.call(api.loginName))
|
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))
|
assertEquals("foo", slc.call(api.loginName))
|
||||||
|
|
||||||
slc.logout()
|
slc.logout()
|
||||||
assertNull(slc.resetPasswordAndLogin("bad_secret", "newpass2"))
|
assertNull(slc.resetPasswordAndSignIn("bad_secret", "newpass2"))
|
||||||
assertNull(slc.resetPasswordAndLogin("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
|
assertNull(slc.resetPasswordAndSignIn("3PBpp-Aris5-ogdV7-Abz36-ggGH5", "newpass2"))
|
||||||
ar = slc.resetPasswordAndLogin(secret,"newpass2")
|
ar = slc.resetPasswordAndSignIn(secret,"newpass2")
|
||||||
assertNotNull(ar)
|
assertNotNull(ar)
|
||||||
assertEquals("bar!", ar.data?.foo)
|
assertEquals("bar!", ar.data?.foo)
|
||||||
assertTrue { slc.isLoggedIn }
|
assertTrue { slc.isLoggedIn }
|
||||||
|
Loading…
Reference in New Issue
Block a user