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:
parent
9d7f421f77
commit
ba7fcb947e
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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>())
|
||||||
}
|
}
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user