-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[MOBL-1776] Improved security by replacing SharedPreferences with Enc…
…ryptedSharedPreferences to store UserInfo (#357) * [MOBL-1811] Gradle = 8.6, AGP = 8.4.0, Kotlin = 1.9.23, targetSdkVersion = 34 * Reverting an unwanted version change * first draft of encrypted shared pref implementation is ready * Added tests for the get and save methods * Added code for migration when needed. * Added test cases to make sure the new preference is backward compatible * Deprecated the clear(context) method * Added additional test case * Added configuration variable to opt in for encryption feature
- Loading branch information
Showing
7 changed files
with
288 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
41 changes: 41 additions & 0 deletions
41
android-sdk/src/androidTest/java/com/blueshift/BlueshiftEncryptedPreferencesTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
package com.blueshift | ||
|
||
import androidx.test.platform.app.InstrumentationRegistry | ||
import org.junit.Before | ||
import org.junit.Test | ||
|
||
class BlueshiftEncryptedPreferencesTest { | ||
@Before | ||
fun setUp() { | ||
val context = InstrumentationRegistry.getInstrumentation().targetContext | ||
BlueshiftEncryptedPreferences.init(context) | ||
} | ||
|
||
@Test | ||
fun savedValueIsReturnedWhenCorrectKeyIsProvided() { | ||
BlueshiftEncryptedPreferences.putString(key = "key", value = "value") | ||
val result = BlueshiftEncryptedPreferences.getString(key = "key", null) | ||
assert(result == "value") | ||
} | ||
|
||
@Test | ||
fun defaultValueIsReturnedWhenIncorrectKeyIsProvided() { | ||
BlueshiftEncryptedPreferences.putString(key = "key", value = "value") | ||
val result = BlueshiftEncryptedPreferences.getString(key = "wrong_key", "default") | ||
assert(result == "default") | ||
} | ||
|
||
@Test | ||
fun storedValueIsRemovedWhenCallingRemoveMethod() { | ||
BlueshiftEncryptedPreferences.putString(key = "key", value = "value") | ||
|
||
val result1 = BlueshiftEncryptedPreferences.getString(key = "key", null) | ||
assert(result1 == "value") | ||
|
||
BlueshiftEncryptedPreferences.remove(key = "key") | ||
|
||
val result2 = BlueshiftEncryptedPreferences.getString(key = "key", null) | ||
assert(result2 == null) | ||
} | ||
|
||
} |
106 changes: 106 additions & 0 deletions
106
android-sdk/src/androidTest/java/com/blueshift/model/UserInfoTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package com.blueshift.model | ||
|
||
import android.content.Context | ||
import android.content.SharedPreferences | ||
import androidx.test.platform.app.InstrumentationRegistry | ||
import com.blueshift.BlueshiftEncryptedPreferences | ||
import org.junit.Before | ||
import org.junit.Test | ||
|
||
class UserInfoTest { | ||
private lateinit var context: Context | ||
private lateinit var legacyPreference: SharedPreferences | ||
|
||
private fun oldPreferenceFile(context: Context): String { | ||
return context.packageName + ".user_info_file" | ||
} | ||
|
||
private fun oldPreferenceKey(context: Context): String { | ||
return context.packageName + ".user_info_key" | ||
} | ||
|
||
@Before | ||
fun setUp() { | ||
context = InstrumentationRegistry.getInstrumentation().targetContext | ||
|
||
// Reset old preferences | ||
legacyPreference = context.getSharedPreferences( | ||
oldPreferenceFile(context), Context.MODE_PRIVATE | ||
) | ||
legacyPreference.edit().remove(oldPreferenceKey(context)).commit() | ||
|
||
// Reset new preferences | ||
BlueshiftEncryptedPreferences.init(context) | ||
BlueshiftEncryptedPreferences.remove(PREF_KEY) | ||
} | ||
|
||
@Test | ||
fun load_newInstall_returnEmptyUserInfoObject() { | ||
// For a fresh installation, the shared preferences will not contain any data. | ||
// So, the user info class should provide an instance without any value for its members. | ||
val user = UserInfo.getInstance(context) | ||
assert(user.name == null) | ||
} | ||
|
||
@Test | ||
fun load_updatedFromOldSDK_returnSameUserInfoObject() { | ||
// Mock the presence of a user object in the old preference. | ||
legacyPreference.edit().putString(oldPreferenceKey(context), USER_JSON).apply() | ||
|
||
// When encryption is not enabled, the user info class should provide the same value | ||
// for its members as we saved in the old preference. | ||
val userinfo = UserInfo.load(context, false) | ||
assert(userinfo.name == JOHN) | ||
|
||
// When encryption is not enabled, the user info class should provide the same value | ||
// for its members as we saved in the old preference. | ||
val userinfo2 = UserInfo.load(context, true) | ||
assert(userinfo2.name == JOHN) | ||
|
||
// Kill the existing instance for the next test. | ||
UserInfo.killInstance() | ||
} | ||
|
||
@Test | ||
fun load_updatedFromOldSDK_copiesTheContentOfOldPrefToNewPref() { | ||
// Mock the presence of a user object in the old preference. | ||
legacyPreference.edit().putString(oldPreferenceKey(context), USER_JSON).apply() | ||
|
||
// case1 : When encryption is not enabled. | ||
val userInfo = UserInfo.load(context, false) | ||
assert(userInfo.name == JOHN) | ||
|
||
// case2 : When encryption is enabled. | ||
UserInfo.load(context, true) | ||
val json = BlueshiftEncryptedPreferences.getString(PREF_KEY, null) | ||
assert(USER_JSON == json) | ||
|
||
// Kill the existing instance for the next test. | ||
UserInfo.killInstance() | ||
} | ||
|
||
@Test | ||
fun load_updatedFromOldSDK_deletesTheDataInOldPreference() { | ||
// Mock the presence of a user object in the old preference. | ||
legacyPreference.edit().putString(oldPreferenceKey(context), USER_JSON).apply() | ||
|
||
// case1 : When encryption is not enabled. | ||
val userInfo = UserInfo.load(context, false) | ||
assert(userInfo.name == JOHN) | ||
|
||
// case2 : When encryption is enabled. | ||
UserInfo.load(context, true) | ||
// Make sure the value stored in the old preferences is removed after copying it to the new preferences. | ||
val legacyJson = legacyPreference.getString(oldPreferenceKey(context), null) | ||
assert(legacyJson == null) | ||
|
||
// Kill the existing instance for the next test. | ||
UserInfo.killInstance() | ||
} | ||
|
||
companion object { | ||
const val JOHN = "John" | ||
const val PREF_KEY = "user_info" | ||
const val USER_JSON = "{\"joined_at\":0,\"name\":\"$JOHN\",\"unsubscribed\":false}" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
android-sdk/src/main/java/com/blueshift/BlueshiftEncryptedPreferences.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.blueshift | ||
|
||
import android.content.Context | ||
import android.content.SharedPreferences | ||
import androidx.security.crypto.EncryptedSharedPreferences | ||
import androidx.security.crypto.MasterKeys | ||
|
||
object BlueshiftEncryptedPreferences { | ||
private val PREF_NAME = "blueshift_sdk_preferences" | ||
|
||
private lateinit var sharedPreferences: SharedPreferences | ||
|
||
fun init(context: Context) { | ||
val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) | ||
|
||
sharedPreferences = EncryptedSharedPreferences.create( | ||
PREF_NAME, | ||
masterKey, | ||
context, | ||
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, | ||
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM | ||
) | ||
} | ||
|
||
fun remove(key: String) { | ||
if (::sharedPreferences.isInitialized) { | ||
sharedPreferences.edit().remove(key).apply() | ||
} | ||
} | ||
|
||
fun putString(key: String, value: String?) { | ||
if (::sharedPreferences.isInitialized) { | ||
sharedPreferences.edit().putString(key, value).apply() | ||
} | ||
} | ||
|
||
fun getString(key: String, defaultValue: String?): String? { | ||
return if (::sharedPreferences.isInitialized) { | ||
sharedPreferences.getString(key, defaultValue) | ||
} else { | ||
defaultValue | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters