forked from shihaam/thijooree
persist mib sessions on disk instead of refreshing token and fix contact picker render issue #37
This commit is contained in:
@@ -19,7 +19,7 @@ class MibContactsClient {
|
|||||||
|
|
||||||
private fun cookieHeader(session: MibSession) =
|
private fun cookieHeader(session: MibSession) =
|
||||||
"mbmodel=IOS-1.0; xxid=${session.xxid}; IBSID=${session.xxid}; " +
|
"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
|
private fun Request.Builder.withSessionHeaders(session: MibSession): Request.Builder = this
|
||||||
.header("Cookie", cookieHeader(session))
|
.header("Cookie", cookieHeader(session))
|
||||||
|
|||||||
@@ -52,6 +52,18 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
|
|
||||||
// ─── Public entry point ───────────────────────────────────────────────────
|
// ─── 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
|
* Full login flow. Automatically handles first-time device registration
|
||||||
* vs. subsequent logins using stored key1/key2.
|
* vs. subsequent logins using stored key1/key2.
|
||||||
@@ -245,10 +257,14 @@ class MibLoginFlow(private val credentialStore: CredentialStore) {
|
|||||||
"" // fall through to recovery below
|
"" // 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() ||
|
val isExpired = response.isEmpty() ||
|
||||||
(response.trimStart().startsWith("{") &&
|
(response.trimStart().startsWith("{") && run {
|
||||||
JSONObject(response).optString("reasonCode") == "505")
|
val obj = JSONObject(response)
|
||||||
|
obj.optString("reasonCode") == "505" || obj.optBoolean("digestFail", false)
|
||||||
|
})
|
||||||
|
|
||||||
if (isExpired && inRelogin.compareAndSet(false, true)) {
|
if (isExpired && inRelogin.compareAndSet(false, true)) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ class ContactPickerSheetFragment : BottomSheetDialogFragment() {
|
|||||||
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
viewModel.accounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
viewModel.hideAmounts.observe(viewLifecycleOwner) { pagerAdapter.rebuildAll() }
|
||||||
|
|
||||||
(activity as? HomeActivity)?.triggerRefresh()
|
(activity as? HomeActivity)?.loadAllContacts()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun attachMediator(pages: List<TabDef>) {
|
private fun attachMediator(pages: List<TabDef>) {
|
||||||
|
|||||||
@@ -734,13 +734,75 @@ fun applyNavLabelVisibility() {
|
|||||||
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
val mibJobs = mibLoginIds.mapNotNull { loginId ->
|
||||||
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
val creds = store.loadMibCredentials(loginId) ?: return@mapNotNull null
|
||||||
loginId to async(Dispatchers.IO) {
|
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 {
|
try {
|
||||||
val flow = MibLoginFlow(CredentialStore(this@HomeActivity))
|
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 accounts = flow.login(creds.username, creds.passwordHash, creds.otpSeed)
|
||||||
val app = application as BasedBankApp
|
val newSession = flow.lastSession!!
|
||||||
app.mibSessions[loginId] = flow.lastSession!!
|
app.mibSessions[loginId] = newSession
|
||||||
app.mibProfilesMap[loginId] = flow.lastProfiles
|
app.mibProfilesMap[loginId] = flow.lastProfiles
|
||||||
app.mibLoginFlows[loginId] = flow
|
app.mibLoginFlows[loginId] = flow
|
||||||
|
store.saveMibSession(loginId, newSession)
|
||||||
store.saveMibProfiles(loginId, flow.lastProfiles)
|
store.saveMibProfiles(loginId, flow.lastProfiles)
|
||||||
accounts
|
accounts
|
||||||
} catch (e: java.io.IOException) {
|
} catch (e: java.io.IOException) {
|
||||||
|
|||||||
@@ -79,9 +79,41 @@ class CredentialStore(context: Context) {
|
|||||||
.remove("mib_${loginId}_enc_profile")
|
.remove("mib_${loginId}_enc_profile")
|
||||||
.remove("mib_${loginId}_enc_full_name")
|
.remove("mib_${loginId}_enc_full_name")
|
||||||
.remove("mib_${loginId}_hidden_profile_ids")
|
.remove("mib_${loginId}_hidden_profile_ids")
|
||||||
|
.remove("mib_${loginId}_enc_session")
|
||||||
.apply()
|
.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) ────────────────
|
// ── MIB session keys (key1/key2) and app ID (per loginId) ────────────────
|
||||||
|
|
||||||
fun saveMibKeys(loginId: String, key1: String, key2: String) {
|
fun saveMibKeys(loginId: String, key1: String, key2: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user