Skip to content

Commit f936864

Browse files
hermannakosclaude
andcommitted
[All] Encrypt SignedInUser blobs in previous-users store
PreviousUsersUtils persists a JSON SignedInUser per known account in a plain SharedPreferences file. The blob includes token, accessToken, refreshToken, and clientSecret, so simply encrypting ApiPrefs left those credentials in plaintext at rest as soon as the user had ever signed in. Wrap the JSON with the Keystore AES-GCM codec on write, unwrap on read, and lazy-migrate any legacy plaintext entries the first time get() loads them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e1f4691 commit f936864

1 file changed

Lines changed: 25 additions & 8 deletions

File tree

libs/login-api-2/src/main/java/com/instructure/loginapi/login/util/PreviousUsersUtils.kt

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.google.gson.Gson
2121
import com.instructure.canvasapi2.managers.OAuthManager
2222
import com.instructure.canvasapi2.models.User
2323
import com.instructure.canvasapi2.utils.ApiPrefs
24+
import com.instructure.canvasapi2.utils.SecureCredentialCodec
2425
import com.instructure.loginapi.login.model.SignedInUser
2526

2627
object PreviousUsersUtils {
@@ -33,21 +34,33 @@ object PreviousUsersUtils {
3334
val signedInUsers = ArrayList<SignedInUser>()
3435

3536
val sharedPreferences = context.getSharedPreferences(SIGNED_IN_USERS_PREF_NAME, Context.MODE_PRIVATE)
36-
val keys = sharedPreferences.all
37-
for ((_, value) in keys) {
38-
var signedInUser: SignedInUser? = null
37+
val migrationEditor = sharedPreferences.edit()
38+
var migrated = false
3939

40-
try {
41-
signedInUser = Gson().fromJson(value.toString(), SignedInUser::class.java)
40+
for ((key, value) in sharedPreferences.all) {
41+
val raw = value?.toString() ?: continue
42+
val isEncrypted = raw.startsWith(SecureCredentialCodec.ENCRYPTED_PREFIX)
43+
val json = if (isEncrypted) SecureCredentialCodec.decrypt(raw) ?: continue else raw
44+
45+
val signedInUser = try {
46+
Gson().fromJson(json, SignedInUser::class.java)
4247
} catch (ignore: Exception) {
43-
//Do Nothing
48+
null
4449
}
4550

4651
if (signedInUser != null) {
4752
signedInUsers.add(signedInUser)
53+
if (!isEncrypted) {
54+
SecureCredentialCodec.encrypt(json)?.let {
55+
migrationEditor.putString(key, it)
56+
migrated = true
57+
}
58+
}
4859
}
4960
}
5061

62+
if (migrated) migrationEditor.apply()
63+
5164
//Sort by last signed in date.
5265
signedInUsers.sort()
5366
return signedInUsers
@@ -90,17 +103,21 @@ object PreviousUsersUtils {
90103
): Boolean {
91104

92105
val signedInUserJSON = Gson().toJson(signedInUser)
106+
val storedValue = SecureCredentialCodec.encrypt(signedInUserJSON) ?: signedInUserJSON
93107

94108
//Save Signed In User to sharedPreferences
95109
val sharedPreferences = context.getSharedPreferences(SIGNED_IN_USERS_PREF_NAME, Context.MODE_PRIVATE)
96110
val editor = sharedPreferences.edit()
97-
editor.putString(getGlobalUserId(domain, user), signedInUserJSON)
111+
editor.putString(getGlobalUserId(domain, user), storedValue)
98112
return editor.commit()
99113
}
100114

101115
fun getSignedInUser(context: Context, domain: String, userId: Long): SignedInUser? {
102116
val prefs = context.getSharedPreferences(SIGNED_IN_USERS_PREF_NAME, Context.MODE_PRIVATE)
103-
val userJson = prefs.getString(getGlobalUserId(domain, userId), null)
117+
val raw = prefs.getString(getGlobalUserId(domain, userId), null) ?: return null
118+
val userJson = if (raw.startsWith(SecureCredentialCodec.ENCRYPTED_PREFIX)) {
119+
SecureCredentialCodec.decrypt(raw) ?: return null
120+
} else raw
104121
return try {
105122
Gson().fromJson(userJson, SignedInUser::class.java)
106123
} catch (e: Exception) {

0 commit comments

Comments
 (0)