diff --git a/build.gradle.kts b/build.gradle.kts index e7c2ff2..377c429 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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() diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt index 3cc4b9b..ad57c72 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/client/SuperloginClient.kt @@ -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( // 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( * Call client API commands with it (uses [adapter] under the hood) */ suspend fun call(ca: CommandDescriptor, args: A): R = adapter().invokeCommand(ca, args) + /** * Call client API commands with it (uses [adapter] under the hood) */ @@ -113,7 +116,7 @@ class SuperloginClient( 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( */ 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( .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( ) 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( } } + /** + * 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( + 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( * @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? { mustBeLoggedOut() return try { @@ -297,6 +349,7 @@ class SuperloginClient( val packedACO = invoke(serverApi.slRequestACOBySecretId, id) AccessControlObject.unpackWithKey(packedACO, key)?.let { changePasswordWithACO(it, newPassword) + dataKey = it.data.payload.dataStorageKey slData } } catch (x: RestoreKey.InvalidSecretException) { @@ -338,9 +391,11 @@ class SuperloginClient( 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( * @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 + 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(data.packedACO, keys.loginAccessKey)?.let { - changePasswordWithACO(it, newPassword,passwordDerivationParams, loginKeyStrength) - } ?: false + val dp = invoke(serverApi.slRequestDerivationParams, loginName) + val keys = DerivedKeys.derive(oldPassword, dp) + val data = invoke(serverApi.slRequestACOByLoginName, RequestACOByLoginNameArgs(loginName, keys.loginId)) + return AccessControlObject.unpackWithKey(data.packedACO, keys.loginAccessKey) + ?.let { + changePasswordWithACO(it, newPassword, passwordDerivationParams, loginKeyStrength) + } ?: false } companion object { - inline operator fun invoke( + inline operator fun invoke( t: Parsec3Transport, savedData: SuperloginData? = null, ): SuperloginClient { diff --git a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt index fa99a67..8fdf48f 100644 --- a/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt +++ b/src/jvmTest/kotlin/net/sergeych/WsServerKtTest.kt @@ -136,9 +136,12 @@ internal class WsServerKtTest { runBlocking { val api = TestApiServer() val slc = SuperloginClient(client) + assertNull(slc.dataKey) assertEquals(LoginState.LoggedOut, slc.state.value) var rt = slc.register("foo", "passwd", TestData("bar!")) + val dk1 = slc.dataKey assertIs(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(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 { 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(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 }