persist mib sessions on disk instead of refreshing token and fix contact picker render issue #37
Auto Tag on Version Change / check-version (push) Failing after 15m8s
Auto Tag on Version Change / check-version (push) Failing after 15m8s
This commit is contained in:
@@ -19,7 +19,7 @@ class MibContactsClient {
|
||||
|
||||
private fun cookieHeader(session: MibSession) =
|
||||
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
|
||||
"mbnonce=${session.nonceGenerator}; time-tracker=597"
|
||||
"mbnonce=${MibNonce.generate(session.nonceGenerator)}; time-tracker=597"
|
||||
|
||||
private fun Request.Builder.withSessionHeaders(session: MibSession): Request.Builder = this
|
||||
.header("Cookie", cookieHeader(session))
|
||||
|
||||
@@ -52,6 +52,18 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
|
||||
// ─── Public entry point ───────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Restores a previously persisted session without any network call. Sets the session and
|
||||
* stored credentials so that doRequest() can do an automatic re-login on 419 if needed.
|
||||
*/
|
||||
fun restoreSession(session: MibSession, username: String, passwordHash: String, otpSeed: String) {
|
||||
loginId = username
|
||||
storedUsername = username
|
||||
storedPasswordHash = passwordHash
|
||||
storedOtpSeed = otpSeed
|
||||
lastSession = session
|
||||
}
|
||||
|
||||
/**
|
||||
* Full login flow. Automatically handles first-time device registration
|
||||
* vs. subsequent logins using stored key1/key2.
|
||||
@@ -245,10 +257,14 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
||||
"" // fall through to recovery below
|
||||
}
|
||||
|
||||
// Detect expired session: HTTP 419 (empty response) or JSON with reasonCode 505
|
||||
// Detect expired session: HTTP 419 (empty response), reasonCode 505, or digestFail
|
||||
// (digestFail / reasonCode 501 means the nonce is invalid for this session — happens
|
||||
// when a persisted session is loaded after the server has already expired it)
|
||||
val isExpired = response.isEmpty() ||
|
||||
(response.trimStart().startsWith("{") &&
|
||||
JSONObject(response).optString("reasonCode") == "505")
|
||||
(response.trimStart().startsWith("{") && run {
|
||||
val obj = JSONObject(response)
|
||||
obj.optString("reasonCode") == "505" || obj.optBoolean("digestFail", false)
|
||||
})
|
||||
|
||||
if (isExpired && inRelogin.compareAndSet(false, true)) {
|
||||
try {
|
||||
|
||||
@@ -151,7 +151,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||
|
||||
(activity as? HomeActivity)?.triggerRefresh()
|
||||
(activity as? HomeActivity)?.loadAllContacts()
|
||||
}
|
||||
|
||||
private fun attachMediator(pages: List<TabDef>) {
|
||||
|
||||
@@ -734,13 +734,75 @@ fun applyNavLabelVisibility() {
|
||||
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||
loginId to async(Dispatchers.IO) {
|
||||
val app = application as BasedBankApp
|
||||
|
||||
// Prefer in-memory session; fall back to persisted session on restart.
|
||||
var existingSession = app.mibSessions[loginId]
|
||||
var existingFlow = app.mibLoginFlows[loginId]
|
||||
val cachedProfiles = app.mibProfilesMap[loginId]?.takeIf { it.isNotEmpty() }
|
||||
?: store.loadMibProfiles(loginId)
|
||||
|
||||
if (existingSession == null) {
|
||||
val persisted = store.loadMibSession(loginId)
|
||||
if (persisted != null && cachedProfiles.isNotEmpty()) {
|
||||
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
||||
flow.restoreSession(persisted, creds.username, creds.passwordHash, creds.otpSeed)
|
||||
existingSession = persisted
|
||||
existingFlow = flow
|
||||
app.mibSessions[loginId] = persisted
|
||||
app.mibLoginFlows[loginId] = flow
|
||||
app.mibProfilesMap[loginId] = cachedProfiles
|
||||
}
|
||||
}
|
||||
|
||||
// Try the existing session first — avoids a full re-login on every refresh.
|
||||
// doRequest() inside fetchAllProfiles() handles 419 (session expired) by
|
||||
// automatically re-logging in and retrying, so this is safe to call directly.
|
||||
if (existingSession != null && existingFlow != null && cachedProfiles.isNotEmpty()) {
|
||||
existingFlow.onSessionRefreshed = { newSession, newProfiles ->
|
||||
app.mibSessions[loginId] = newSession
|
||||
app.mibProfilesMap[loginId] = newProfiles
|
||||
store.saveMibSession(loginId, newSession)
|
||||
store.saveMibProfiles(loginId, newProfiles)
|
||||
}
|
||||
try {
|
||||
val visibleProfiles = cachedProfiles.filterVisibleProfiles(loginId)
|
||||
val accounts = existingFlow.fetchAllProfiles(existingSession, visibleProfiles, "mib_${creds.username}")
|
||||
val freshSession = existingFlow.lastSession ?: existingSession
|
||||
app.mibSessions[loginId] = freshSession
|
||||
store.saveMibSession(loginId, freshSession)
|
||||
if (existingFlow.lastProfiles.isNotEmpty()) {
|
||||
app.mibProfilesMap[loginId] = existingFlow.lastProfiles
|
||||
store.saveMibProfiles(loginId, existingFlow.lastProfiles)
|
||||
}
|
||||
AccountCache.save(this@HomeActivity, accounts)
|
||||
return@async accounts
|
||||
} catch (e: java.io.IOException) {
|
||||
refreshErrors.add("NO_INTERNET")
|
||||
return@async AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||
} catch (e: BankServerException) {
|
||||
refreshErrors.add("SERVER:${e.bankName}")
|
||||
return@async AccountCache.load(this@HomeActivity).filter { it.loginTag == "mib_$loginId" }
|
||||
} catch (_: Exception) {
|
||||
// Session completely unrecoverable — fall through to full re-login
|
||||
}
|
||||
}
|
||||
|
||||
// Full re-login: first start with no persisted session, or unrecoverable above
|
||||
try {
|
||||
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
||||
flow.onSessionRefreshed = { newSession, newProfiles ->
|
||||
app.mibSessions[loginId] = newSession
|
||||
app.mibProfilesMap[loginId] = newProfiles
|
||||
store.saveMibSession(loginId, newSession)
|
||||
store.saveMibProfiles(loginId, newProfiles)
|
||||
}
|
||||
val accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||
val app = application as BasedBankApp
|
||||
app.mibSessions[loginId] = flow.lastSession!!
|
||||
val newSession = flow.lastSession!!
|
||||
app.mibSessions[loginId] = newSession
|
||||
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||
app.mibLoginFlows[loginId] = flow
|
||||
store.saveMibSession(loginId, newSession)
|
||||
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||
accounts
|
||||
} catch (e: java.io.IOException) {
|
||||
|
||||
@@ -79,9 +79,41 @@ class CredentialStore(context: Context) {
|
||||
.remove("mib_${loginId}_enc_profile")
|
||||
.remove("mib_${loginId}_enc_full_name")
|
||||
.remove("mib_${loginId}_hidden_profile_ids")
|
||||
.remove("mib_${loginId}_enc_session")
|
||||
.apply()
|
||||
}
|
||||
|
||||
// ── MIB active session (xxid + sessionKey + nonceGenerator) ─────────────────
|
||||
|
||||
fun saveMibSession(loginId: String, session: sh.sar.basedbank.api.mib.MibSession) {
|
||||
val json = JSONObject().apply {
|
||||
put("xxid", session.xxid)
|
||||
put("sessionKey", session.sessionKey)
|
||||
put("nonceGenerator", session.nonceGenerator)
|
||||
put("appId", session.appId)
|
||||
}.toString()
|
||||
val key = getOrCreateKey()
|
||||
prefs.edit().putString("mib_${loginId}_enc_session", encrypt(json, key)).apply()
|
||||
}
|
||||
|
||||
fun loadMibSession(loginId: String): sh.sar.basedbank.api.mib.MibSession? {
|
||||
val key = getOrCreateKey()
|
||||
val enc = prefs.getString("mib_${loginId}_enc_session", null) ?: return null
|
||||
return try {
|
||||
val o = JSONObject(decrypt(enc, key))
|
||||
sh.sar.basedbank.api.mib.MibSession(
|
||||
appId = o.getString("appId"),
|
||||
xxid = o.getString("xxid"),
|
||||
nonceGenerator = o.getString("nonceGenerator"),
|
||||
sessionKey = o.getString("sessionKey")
|
||||
)
|
||||
} catch (_: Exception) { null }
|
||||
}
|
||||
|
||||
fun clearMibSession(loginId: String) {
|
||||
prefs.edit().remove("mib_${loginId}_enc_session").apply()
|
||||
}
|
||||
|
||||
// ── MIB session keys (key1/key2) and app ID (per loginId) ────────────────
|
||||
|
||||
fun saveMibKeys(loginId: String, key1: String, key2: String) {
|
||||
|
||||
Reference in New Issue
Block a user