11package org.onebusaway.vehiclepositions.util
22
33import android.content.Context
4+ import android.content.SharedPreferences
5+ import android.util.Log
46import androidx.security.crypto.EncryptedSharedPreferences
57import androidx.security.crypto.MasterKey
68import dagger.hilt.android.qualifiers.ApplicationContext
79import javax.inject.Inject
810import javax.inject.Singleton
911
1012/* *
11- * TokenManager
12- * ─────────────────────────────────────────────────────────────────────────────
13- * Secure storage for JWT access token and refresh token using
14- * EncryptedSharedPreferences backed by Android Keystore (AES-256).
15- *
13+ * Secure storage for JWT and refresh tokens.
1614 * IMPORTANT: This class is a storage facade only.
17- * It never seeds or generates tokens.
1815 * The login flow (Milestone 2) is solely responsible for writing the
1916 * initial token via saveToken() after a successful /auth/login response.
2017 */
2118@Singleton
2219class TokenManager @Inject constructor(
2320 @ApplicationContext private val context : Context
2421) {
25- private val masterKey = MasterKey .Builder (context)
26- .setKeyScheme(MasterKey .KeyScheme .AES256_GCM )
27- .build()
28-
29- private val prefs = EncryptedSharedPreferences .create(
30- context,
31- " secure_prefs" ,
32- masterKey,
33- EncryptedSharedPreferences .PrefKeyEncryptionScheme .AES256_SIV ,
34- EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM
35- )
36-
3722 companion object {
38- private const val KEY_JWT = " jwt_token"
23+ private const val TAG = " TokenManager"
24+ private const val KEY_JWT = " jwt_token"
3925 private const val KEY_REFRESH_TOKEN = " refresh_token"
40- private const val TAG = " TokenManager"
26+ private const val PREFS_FILE = " secure_prefs"
27+ }
28+
29+ // lazy init with keystore recovery fallback
30+ // MasterKey.Builder and EncryptedSharedPreferences can throw on
31+ // corrupted keystore - a well-documented issue on many devices
32+ private val prefs: SharedPreferences by lazy {
33+ try {
34+ val masterKey = MasterKey .Builder (context)
35+ .setKeyScheme(MasterKey .KeyScheme .AES256_GCM )
36+ .build()
37+ EncryptedSharedPreferences .create(
38+ context,
39+ PREFS_FILE ,
40+ masterKey,
41+ EncryptedSharedPreferences .PrefKeyEncryptionScheme .AES256_SIV ,
42+ EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM
43+ )
44+ } catch (e: Exception ) {
45+ // Keystore corrupted - clear and retry once
46+ Log .e(TAG , " Keystore error — attempting recovery: ${e.message} " )
47+ context.deleteSharedPreferences(PREFS_FILE )
48+ val masterKey = MasterKey .Builder (context)
49+ .setKeyScheme(MasterKey .KeyScheme .AES256_GCM )
50+ .build()
51+ EncryptedSharedPreferences .create(
52+ context,
53+ PREFS_FILE ,
54+ masterKey,
55+ EncryptedSharedPreferences .PrefKeyEncryptionScheme .AES256_SIV ,
56+ EncryptedSharedPreferences .PrefValueEncryptionScheme .AES256_GCM
57+ )
58+ }
4159 }
4260
4361 fun saveToken (token : String ) {
4462 prefs.edit().putString(KEY_JWT , token).apply ()
45- // Never log the token value — it is a security credential
46- android.util.Log .d(TAG , " Access token saved" )
4763 }
4864
4965 fun getToken (): String? = prefs.getString(KEY_JWT , null )
5066
5167 fun saveRefreshToken (token : String ) {
5268 prefs.edit().putString(KEY_REFRESH_TOKEN , token).apply ()
53- android.util.Log .d(TAG , " Refresh token saved" )
5469 }
5570
5671 fun getRefreshToken (): String? = prefs.getString(KEY_REFRESH_TOKEN , null )
@@ -60,7 +75,7 @@ class TokenManager @Inject constructor(
6075 .remove(KEY_JWT )
6176 .remove(KEY_REFRESH_TOKEN )
6277 .apply ()
63- android.util. Log .d(TAG , " JWT and refresh token cleared from device" )
78+ Log .d(TAG , " JWT cleared from device" )
6479 }
6580
6681 fun isLoggedIn (): Boolean = getToken() != null
0 commit comments