diff --git a/infra/core/src/main/java/com/simprints/core/tools/utils/LanguageHelper.kt b/infra/core/src/main/java/com/simprints/core/tools/utils/LanguageHelper.kt index 198004b3f7..3a1c07efed 100644 --- a/infra/core/src/main/java/com/simprints/core/tools/utils/LanguageHelper.kt +++ b/infra/core/src/main/java/com/simprints/core/tools/utils/LanguageHelper.kt @@ -3,6 +3,7 @@ package com.simprints.core.tools.utils import android.content.Context import android.content.SharedPreferences import android.content.res.Configuration +import androidx.core.content.edit import com.simprints.core.ExcludedFromGeneratedTestCoverageReports import java.util.Locale @@ -15,11 +16,9 @@ object LanguageHelper { lateinit var prefs: SharedPreferences var language: String - get() { - return prefs.getString(SHARED_PREFS_LANGUAGE_KEY, SHARED_PREFS_LANGUAGE_DEFAULT)!! - } + get() = prefs.getString(SHARED_PREFS_LANGUAGE_KEY, SHARED_PREFS_LANGUAGE_DEFAULT)!! set(value) { - prefs.edit().putString(SHARED_PREFS_LANGUAGE_KEY, value).apply() + prefs.edit { putString(SHARED_PREFS_LANGUAGE_KEY, value) } } fun init(ctx: Context) { diff --git a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt index 30b7e7ed3c..9b41f26006 100644 --- a/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt +++ b/infra/enrolment-records/repository/src/main/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImpl.kt @@ -1,6 +1,5 @@ package com.simprints.infra.enrolment.records.repository -import android.content.Context import androidx.core.content.edit import com.simprints.core.DispatcherIO import com.simprints.core.domain.tokenization.TokenizableString @@ -16,14 +15,13 @@ import com.simprints.infra.enrolment.records.repository.local.SelectEnrolmentRec import com.simprints.infra.enrolment.records.repository.local.migration.InsertRecordsInRoomDuringMigrationUseCase import com.simprints.infra.enrolment.records.repository.remote.EnrolmentRecordRemoteDataSource import com.simprints.infra.logging.Simber -import dagger.hilt.android.qualifiers.ApplicationContext +import com.simprints.infra.security.SecurityManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import javax.inject.Inject internal class EnrolmentRecordRepositoryImpl @Inject constructor( - @ApplicationContext context: Context, private val remoteDataSource: EnrolmentRecordRemoteDataSource, @CommCareDataSource private val commCareDataSource: IdentityDataSource, private val tokenizationProcessor: TokenizationProcessor, @@ -31,8 +29,9 @@ internal class EnrolmentRecordRepositoryImpl @Inject constructor( @DispatcherIO private val dispatcher: CoroutineDispatcher, @EnrolmentBatchSize private val batchSize: Int, private val insertRecordsInRoomDuringMigration: InsertRecordsInRoomDuringMigrationUseCase, + securityManager: SecurityManager, ) : EnrolmentRecordRepository { - private val prefs = context.getSharedPreferences(PREF_FILE_NAME, Context.MODE_PRIVATE) + private val prefs = securityManager.buildEncryptedSharedPreferences(PREF_FILE_NAME) companion object { private const val PREF_FILE_NAME = "UPLOAD_ENROLMENT_RECORDS_PROGRESS" diff --git a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt index 6e00f4e85c..38451fccb4 100644 --- a/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt +++ b/infra/enrolment-records/repository/src/test/java/com/simprints/infra/enrolment/records/repository/EnrolmentRecordRepositoryImplTest.kt @@ -1,6 +1,5 @@ package com.simprints.infra.enrolment.records.repository -import android.content.Context import android.content.SharedPreferences import com.simprints.core.domain.tokenization.asTokenizableEncrypted import com.simprints.core.domain.tokenization.asTokenizableRaw @@ -17,6 +16,7 @@ import com.simprints.infra.enrolment.records.repository.local.EnrolmentRecordLoc import com.simprints.infra.enrolment.records.repository.local.SelectEnrolmentRecordLocalDataSourceUseCase import com.simprints.infra.enrolment.records.repository.local.migration.InsertRecordsInRoomDuringMigrationUseCase import com.simprints.infra.enrolment.records.repository.remote.EnrolmentRecordRemoteDataSource +import com.simprints.infra.security.SecurityManager import io.mockk.* import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -64,8 +64,8 @@ class EnrolmentRecordRepositoryImplTest { private val prefs = mockk { every { edit() } returns prefsEditor } - private val ctx = mockk { - every { getSharedPreferences(any(), any()) } returns prefs + private val securityManager = mockk { + every { buildEncryptedSharedPreferences(any()) } returns prefs } private lateinit var repository: EnrolmentRecordRepositoryImpl private val project = mockk() @@ -78,7 +78,6 @@ class EnrolmentRecordRepositoryImplTest { every { prefsEditor.remove(any()) } returns prefsEditor coEvery { selectEnrolmentRecordLocalDataSource() } returns localDataSource repository = EnrolmentRecordRepositoryImpl( - context = ctx, remoteDataSource = remoteDataSource, selectEnrolmentRecordLocalDataSource = selectEnrolmentRecordLocalDataSource, commCareDataSource = commCareDataSource, @@ -86,6 +85,7 @@ class EnrolmentRecordRepositoryImplTest { dispatcher = UnconfinedTestDispatcher(), batchSize = BATCH_SIZE, insertRecordsInRoomDuringMigration = insertRecordsDuringMigration, + securityManager = securityManager, ) } diff --git a/infra/network/build.gradle.kts b/infra/network/build.gradle.kts index adda95ae6d..4d7753b9a0 100644 --- a/infra/network/build.gradle.kts +++ b/infra/network/build.gradle.kts @@ -19,6 +19,7 @@ android { dependencies { implementation(project(":infra:logging")) implementation(project(":infra:logging-persistent")) + implementation(project(":infra:security")) debugImplementation(libs.chuck.debug) { exclude("androidx.lifecycle", "lifecycle-viewmodel-ktx") @@ -47,7 +48,6 @@ dependencies { testImplementation(libs.testing.mockwebserver) testImplementation(libs.chuck.release) - } configurations { diff --git a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt index c4bb34df9c..fad99ac203 100644 --- a/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt +++ b/infra/network/src/main/java/com/simprints/infra/network/url/BaseUrlProviderImpl.kt @@ -6,17 +6,19 @@ import androidx.annotation.VisibleForTesting import androidx.core.content.edit import com.simprints.infra.logging.Simber import com.simprints.infra.network.BuildConfig +import com.simprints.infra.security.SecurityManager import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton @Singleton internal class BaseUrlProviderImpl @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, + securityManager: SecurityManager, ) : BaseUrlProvider { companion object { - private const val PREF_FILE_NAME = "b3f0cf9b-4f3f-4c5b-bf85-7b1f44eddd7a" - private const val PREF_MODE = Context.MODE_PRIVATE + private const val LEGACY_PREF_FILE_NAME = "b3f0cf9b-4f3f-4c5b-bf85-7b1f44eddd7a" + private const val SECURE_PREF_FILE_NAME = "97285f18-9742-469e-accf-3ed54def7a7e" private const val API_BASE_URL_KEY = "ApiBaseUrl" private const val API_VERSION = "v2" private const val BASE_URL_SUFFIX = "/androidapi/$API_VERSION/" @@ -26,13 +28,27 @@ internal class BaseUrlProviderImpl @Inject constructor( "https://${BuildConfig.BASE_URL_PREFIX}.simprints-apis.com$BASE_URL_SUFFIX" } - val prefs: SharedPreferences = context.getSharedPreferences(PREF_FILE_NAME, PREF_MODE) + private val securePrefs = securityManager.buildEncryptedSharedPreferences(SECURE_PREF_FILE_NAME) - override fun getApiBaseUrl(): String = prefs + /** + * Ensures that data has been migrated to secure prefs before accessing it. + */ + private fun getSecurePrefs(): SharedPreferences { + // Data has been migrated to secure prefs. + // TODO Delete after there are no users below 2025.3.0 + if (!securePrefs.contains(API_BASE_URL_KEY)) { + val prefs = context.getSharedPreferences(LEGACY_PREF_FILE_NAME, Context.MODE_PRIVATE) + securePrefs.edit(commit = true) { putString(API_BASE_URL_KEY, prefs.getString(API_BASE_URL_KEY, "")) } + prefs.edit(commit = true) { clear() } + } + return securePrefs + } + + override fun getApiBaseUrl(): String = getSecurePrefs() .getString(API_BASE_URL_KEY, DEFAULT_BASE_URL)!! .also { Simber.d("API base URL is $it") } - override fun getApiBaseUrlPrefix(): String = prefs + override fun getApiBaseUrlPrefix(): String = getSecurePrefs() .getString(API_BASE_URL_KEY, DEFAULT_BASE_URL) ?.removeSuffix(BASE_URL_SUFFIX) ?.also { Simber.d("API base URL prefix is $it") }!! @@ -51,11 +67,11 @@ internal class BaseUrlProviderImpl @Inject constructor( Simber.d("Setting API base URL to $newValue") - prefs.edit(commit = true) { putString(API_BASE_URL_KEY, newValue) } + getSecurePrefs().edit(commit = true) { putString(API_BASE_URL_KEY, newValue) } } override fun resetApiBaseUrl() { Simber.d("Resetting API base") - prefs.edit(commit = true) { putString(API_BASE_URL_KEY, DEFAULT_BASE_URL) } + getSecurePrefs().edit(commit = true) { putString(API_BASE_URL_KEY, DEFAULT_BASE_URL) } } } diff --git a/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt b/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt index 8418abd838..15327fd3a2 100644 --- a/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt +++ b/infra/network/src/test/java/com/simprints/infra/network/url/BaseUrlProviderImplTest.kt @@ -2,12 +2,12 @@ package com.simprints.infra.network.url import android.content.Context import android.content.SharedPreferences -import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.* import com.simprints.infra.network.url.BaseUrlProviderImpl.Companion.DEFAULT_BASE_URL -import io.mockk.MockKAnnotations -import io.mockk.every -import io.mockk.impl.annotations.RelaxedMockK -import io.mockk.verify +import com.simprints.infra.security.SecurityManager +import io.mockk.* +import io.mockk.impl.annotations.* +import io.mockk.impl.annotations.MockK import org.junit.Before import org.junit.Test @@ -22,27 +22,54 @@ class BaseUrlProviderImplTest { @RelaxedMockK lateinit var ctx: Context - @RelaxedMockK - lateinit var sharedPreferences: SharedPreferences + @MockK + private lateinit var securityManager: SecurityManager - @RelaxedMockK - lateinit var editor: SharedPreferences.Editor + @MockK + private lateinit var legacySharedPreferences: SharedPreferences + + @MockK + private lateinit var legacyEditor: SharedPreferences.Editor + + @MockK + private lateinit var secureSharedPreferences: SharedPreferences + + @MockK + private lateinit var secureEditor: SharedPreferences.Editor private lateinit var baseUrlProviderImpl: BaseUrlProviderImpl @Before fun setup() { - MockKAnnotations.init(this) - every { ctx.getSharedPreferences(any(), any()) } returns sharedPreferences - every { sharedPreferences.edit() } returns editor - every { editor.putString(any(), any()) } returns editor + MockKAnnotations.init(this, relaxed = true) + every { ctx.getSharedPreferences(any(), any()) } returns legacySharedPreferences + every { legacySharedPreferences.edit() } returns legacyEditor - baseUrlProviderImpl = BaseUrlProviderImpl(ctx) + every { securityManager.buildEncryptedSharedPreferences(any()) } returns secureSharedPreferences + every { secureSharedPreferences.edit() } returns secureEditor + every { secureEditor.putString(any(), any()) } returns secureEditor + + baseUrlProviderImpl = BaseUrlProviderImpl(ctx, securityManager) + } + + @Test + fun `should migrate data from legacy prefs to secure prefs`() { + every { legacySharedPreferences.contains(any()) } returns true + every { legacySharedPreferences.getString(any(), any()) } returns "old-value" + + val result = baseUrlProviderImpl.getApiBaseUrl() + + verify { secureEditor.putString(any(), any()) } + verify(exactly = 1) { + legacyEditor.clear() + legacyEditor.commit() + secureEditor.commit() + } } @Test fun `get api base url should return the actual url`() { - every { sharedPreferences.getString(any(), any()) } returns URL + every { secureSharedPreferences.getString(any(), any()) } returns URL val url = baseUrlProviderImpl.getApiBaseUrl() @@ -51,7 +78,7 @@ class BaseUrlProviderImplTest { @Test fun `get api base url prefix should return the actual url`() { - every { sharedPreferences.getString(any(), any()) } returns URL_WITH_SUFFIX + every { secureSharedPreferences.getString(any(), any()) } returns URL_WITH_SUFFIX val url = baseUrlProviderImpl.getApiBaseUrlPrefix() @@ -62,7 +89,7 @@ class BaseUrlProviderImplTest { fun `set api base url should set the url to the default one when the one passed is null`() { baseUrlProviderImpl.setApiBaseUrl(null) - verify(exactly = 1) { editor.putString(any(), DEFAULT_BASE_URL) } + verify(exactly = 1) { secureEditor.putString(any(), DEFAULT_BASE_URL) } } @Test @@ -70,7 +97,7 @@ class BaseUrlProviderImplTest { val url = "https://url.com" baseUrlProviderImpl.setApiBaseUrl(url) - verify(exactly = 1) { editor.putString(any(), "$url$URL_SUFFIX") } + verify(exactly = 1) { secureEditor.putString(any(), "$url$URL_SUFFIX") } } @Test @@ -78,13 +105,13 @@ class BaseUrlProviderImplTest { val url = "url.com" baseUrlProviderImpl.setApiBaseUrl(url) - verify(exactly = 1) { editor.putString(any(), "https://$url$URL_SUFFIX") } + verify(exactly = 1) { secureEditor.putString(any(), "https://$url$URL_SUFFIX") } } @Test fun `reset api base url should set the url to the default one`() { baseUrlProviderImpl.resetApiBaseUrl() - verify(exactly = 1) { editor.putString(any(), DEFAULT_BASE_URL) } + verify(exactly = 1) { secureEditor.putString(any(), DEFAULT_BASE_URL) } } }