v0.2.1: data key is not in the client saved state (less secure but does not require the password all the time)

This commit is contained in:
Sergey Chernov 2022-12-19 13:44:26 +01:00
parent 9d7f421f77
commit ba7fcb947e
5 changed files with 93 additions and 86 deletions

View File

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

View File

@ -19,7 +19,7 @@ data class RegistrationArgs(
val packedACO: ByteArray, val packedACO: ByteArray,
val extraData: ByteArray? = null val extraData: ByteArray? = null
) { ) {
inline fun <reified T>toSuccess(loginToken: ByteArray,extraData: T): AuthenticationResult.Success { inline fun <reified T>toSuccess(loginToken: ByteArray, extraData: T): AuthenticationResult.Success {
return AuthenticationResult.Success( return AuthenticationResult.Success(
loginName, loginToken, BossEncoder.encode(extraData) loginName, loginToken, BossEncoder.encode(extraData)
) )
@ -37,7 +37,7 @@ sealed class AuthenticationResult {
data class Success( data class Success(
val loginName: String, val loginName: String,
val loginToken: ByteArray, val loginToken: ByteArray,
val applicationData: ByteArray? val applicationData: ByteArray?,
): AuthenticationResult() ): AuthenticationResult()
@Serializable @Serializable

View File

@ -10,7 +10,7 @@ sealed class LoginState(val isLoggedIn: Boolean) {
* User is logged in (either connected or yet not). Client application should save * User is logged in (either connected or yet not). Client application should save
* updated [loginData] at this point * updated [loginData] at this point
*/ */
class LoggedIn<D>(val loginData: SuperloginData<D>) : LoginState(true) class LoggedIn<D>(val loginData: ClientState<D>) : LoginState(true)
/** /**
* Login state whatever it was now is logged out, and client application should delete * Login state whatever it was now is logged out, and client application should delete

View File

@ -35,10 +35,20 @@ data class SuperloginData<T>(
val data: T? = null, val data: T? = null,
) )
@Serializable
class ClientState<T>(val slData: SuperloginData<T>, val dataKey: SymmetricKey) {
val loginName by slData::loginName
val loginToken by slData::loginToken
val data by slData::data
constructor(loginName: String,loginToken: ByteArray?,data: T?, dataKey: SymmetricKey)
: this(SuperloginData(loginName, loginToken, data), dataKey)
}
class SuperloginClient<D, S : WithAdapter>( class SuperloginClient<D, S : WithAdapter>(
private val transport: Parsec3Transport<S>, private val transport: Parsec3Transport<S>,
savedData: SuperloginData<D>? = null, savedData: ClientState<D>? = null,
private val dataType: KType, private val dataType: KType,
override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(), override val exceptionsRegistry: ExceptionsRegistry = ExceptionsRegistry(),
) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") { ) : Parsec3Transport<S>, Loggable by LogTag("SLCLI") {
@ -52,7 +62,7 @@ class SuperloginClient<D, S : WithAdapter>(
private val _cflow = MutableStateFlow<Boolean>(false) private val _cflow = MutableStateFlow<Boolean>(false)
override val connectedFlow: StateFlow<Boolean> = _cflow override val connectedFlow: StateFlow<Boolean> = _cflow
private var slData: SuperloginData<D>? = savedData private var clientState: ClientState<D>? = savedData
set(value) { set(value) {
if (field != value) { if (field != value) {
field = value field = value
@ -60,7 +70,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
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) {
@ -114,23 +123,25 @@ class SuperloginClient<D, S : WithAdapter>(
private val serverApi = SuperloginServerApi<WithAdapter>() private val serverApi = SuperloginServerApi<WithAdapter>()
private suspend fun tryRestoreLogin() { private suspend fun tryRestoreLogin() {
slData?.loginToken?.let { token -> clientState?.let { clientState ->
debug { "trying to restore login with a token" } clientState.loginToken?.let { token ->
while (true) { debug { "trying to restore login with a token" }
try { while (true) {
val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token) try {
slData = if (ar is AuthenticationResult.Success) { val ar = transport.adapter().invokeCommand(serverApi.slLoginByToken, token)
val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) } this.clientState = if (ar is AuthenticationResult.Success) {
debug { "login restored by the token: ${ar.loginName}" } val data: D? = ar.applicationData?.let { BossDecoder.decodeFrom(dataType, it) }
SuperloginData(ar.loginName, ar.loginToken, data) debug { "login restored by the token: ${ar.loginName}" }
} else { ClientState(ar.loginName, ar.loginToken, data, clientState.dataKey)
debug { "failed to restore login by the token: $ar" } } else {
null debug { "failed to restore login by the token: $ar" }
null
}
break
} catch (t: Throwable) {
exception { "failed to restore login by token, will retry" to t }
delay(1500)
} }
break
} catch (t: Throwable) {
exception { "failed to restore login by token, will retry" to t }
delay(1500)
} }
} }
} ?: warning { "tryRestoreLogin is ignored as slData is now null" } } ?: warning { "tryRestoreLogin is ignored as slData is now null" }
@ -181,8 +192,7 @@ class SuperloginClient<D, S : WithAdapter>(
* as it would require storing user's password which must not be done_. * as it would require storing user's password which must not be done_.
* Use [retrieveDataKey] to restore to with a password (when logged in) * Use [retrieveDataKey] to restore to with a password (when logged in)
*/ */
var dataKey: SymmetricKey? = null val dataKey: SymmetricKey? get() = clientState?.dataKey
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
@ -204,8 +214,7 @@ 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(loginName, rr.loginToken, extractData(rr.encodedData)) clientState = ClientState(loginName, rr.loginToken, extractData(rr.encodedData), rr.dataKey)
dataKey = rr.dataKey
} }
} }
} }
@ -225,7 +234,7 @@ class SuperloginClient<D, S : WithAdapter>(
suspend fun logout() { suspend fun logout() {
mustBeLoggedIn() mustBeLoggedIn()
invoke(serverApi.slLogout) invoke(serverApi.slLogout)
slData = null clientState = null
} }
/** /**
@ -235,24 +244,23 @@ class SuperloginClient<D, S : WithAdapter>(
* @return updated login data (and new token value) or null if token is not (or not anymore) * @return updated login data (and new token value) or null if token is not (or not anymore)
* available for logging in. * available for logging in.
*/ */
suspend fun loginByToken(token: ByteArray): SuperloginData<D>? { suspend fun loginByToken(token: ByteArray,newDataKey: SymmetricKey): ClientState<D>? {
mustBeLoggedOut() mustBeLoggedOut()
val r = invoke(serverApi.slLoginByToken, token) val r = invoke(serverApi.slLoginByToken, token)
return when (r) { return when (r) {
AuthenticationResult.LoginIdUnavailable -> TODO() is AuthenticationResult.Success -> ClientState(
AuthenticationResult.LoginUnavailable -> null
AuthenticationResult.RestoreIdUnavailable -> TODO()
is AuthenticationResult.Success -> SuperloginData(
r.loginName, r.loginName,
r.loginToken, r.loginToken,
extractData(r.applicationData) extractData(r.applicationData),
newDataKey
).also { ).also {
slData = it clientState = it
} }
else -> null
} }
} }
suspend fun loginByPassword(loginName: String, password: String): SuperloginData<D>? { suspend fun loginByPassword(loginName: String, password: String): ClientState<D>? {
mustBeLoggedOut() mustBeLoggedOut()
// Request derivation params // Request derivation params
val params = invoke(serverApi.slRequestDerivationParams, loginName) val params = invoke(serverApi.slRequestDerivationParams, loginName)
@ -276,10 +284,10 @@ class SuperloginClient<D, S : WithAdapter>(
) )
) )
if (result is AuthenticationResult.Success) { if (result is AuthenticationResult.Success) {
SuperloginData(loginName, result.loginToken, extractData(result.applicationData)) ClientState(loginName, result.loginToken, extractData(result.applicationData),
aco.payload.dataStorageKey)
.also { .also {
slData = it clientState = it
dataKey = aco.data.payload.dataStorageKey
} }
} else null } else null
} }
@ -290,41 +298,41 @@ class SuperloginClient<D, S : WithAdapter>(
} }
} }
/** // /**
* Try to retrieve dataKey (usually after login by token), it is impossible // * 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 // * 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 // * what means is already known, returns it immediatel, otherwise uses password
* to access ACO on the server and extract data key. // * 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) // * @return data key or null if it is impossible to do (no connection or wrong password)
*/ // */
suspend fun retrieveDataKey(password: String): SymmetricKey? { // suspend fun retrieveDataKey(password: String): SymmetricKey? {
mustBeLoggedIn() // mustBeLoggedIn()
dataKey?.let { return it } // dataKey?.let { return it }
//
val loginName = slData?.loginName ?: throw SLInternalException("slData: empty login name") // val loginName = slData?.loginName ?: throw SLInternalException("slData: empty login name")
try { // try {
val params = invoke( // val params = invoke(
serverApi.slRequestDerivationParams, // serverApi.slRequestDerivationParams,
loginName // loginName
) // )
val keys = DerivedKeys.derive(password, params) // val keys = DerivedKeys.derive(password, params)
// Request login data by derived it // // Request login data by derived it
return invoke( // return invoke(
serverApi.slRequestACOByLoginName, // serverApi.slRequestACOByLoginName,
RequestACOByLoginNameArgs(loginName, keys.loginId) // RequestACOByLoginNameArgs(loginName, keys.loginId)
).let { loginRequest -> // ).let { loginRequest ->
AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>( // AccessControlObject.unpackWithKey<SuperloginRestoreAccessPayload>(
loginRequest.packedACO, // loginRequest.packedACO,
keys.loginAccessKey // keys.loginAccessKey
)?.let { aco -> // )?.let { aco ->
aco.data.payload.dataStorageKey.also { dataKey = it } // aco.data.payload.dataStorageKey.also { dataKey = it }
} // }
} // }
} catch (t: Throwable) { // } catch (t: Throwable) {
t.printStackTrace() // t.printStackTrace()
return null // 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
@ -342,15 +350,14 @@ class SuperloginClient<D, S : WithAdapter>(
secret: String, newPassword: String, secret: String, newPassword: String,
params: PasswordDerivationParams = PasswordDerivationParams(), params: PasswordDerivationParams = PasswordDerivationParams(),
loginKeyStrength: Int = 2048, loginKeyStrength: Int = 2048,
): SuperloginData<D>? { ): ClientState<D>? {
mustBeLoggedOut() mustBeLoggedOut()
return try { return try {
val (id, key) = RestoreKey.parse(secret) val (id, key) = RestoreKey.parse(secret)
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 clientState
slData
} }
} catch (x: RestoreKey.InvalidSecretException) { } catch (x: RestoreKey.InvalidSecretException) {
null null
@ -400,7 +407,8 @@ class SuperloginClient<D, S : WithAdapter>(
) )
when (result) { when (result) {
is AuthenticationResult.Success -> { is AuthenticationResult.Success -> {
slData = SuperloginData(result.loginName, result.loginToken, extractData(result.applicationData)) clientState = ClientState(result.loginName, result.loginToken, extractData(result.applicationData),
aco.payload.dataStorageKey)
true true
} }
@ -428,7 +436,7 @@ class SuperloginClient<D, S : WithAdapter>(
loginKeyStrength: Int = 2048, loginKeyStrength: Int = 2048,
): Boolean { ): Boolean {
mustBeLoggedIn() mustBeLoggedIn()
val loginName = slData?.loginName ?: throw SLInternalException("loginName should be defined here") val loginName = clientState?.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))
@ -442,7 +450,7 @@ class SuperloginClient<D, S : WithAdapter>(
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: ClientState<D>? = null,
): SuperloginClient<D, S> { ): SuperloginClient<D, S> {
return SuperloginClient(t, savedData, typeOf<D>()) return SuperloginClient(t, savedData, typeOf<D>())
} }

View File

@ -166,18 +166,17 @@ internal class WsServerKtTest {
assertIs<LoginState.LoggedOut>(slc.state.value) assertIs<LoginState.LoggedOut>(slc.state.value)
assertEquals(null, slc.call(api.loginName)) assertEquals(null, slc.call(api.loginName))
var ar = slc.loginByToken(token) var ar = slc.loginByToken(token, dk1!!)
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")) // assertNull(slc.retrieveDataKey("badpasswd"))
assertEquals(dk1?.id, slc.retrieveDataKey("passwd")?.id) // assertEquals(dk1?.id, slc.retrieveDataKey("passwd")?.id)
assertEquals(dk1?.id, slc.dataKey?.id) // assertEquals(dk1?.id, slc.dataKey?.id)
// //
assertThrowsAsync<IllegalStateException> { slc.loginByToken(token) } assertThrowsAsync<IllegalStateException> { slc.loginByToken(token, dk1) }
} }
} }