Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ dependencies {
kapt(libs.google.dagger.hilt.android.compile)
implementation(libs.androidx.hilt)
implementation(libs.kotlinx.coroutines.rx3)
// TODO: Remove later. Needed only for one-time migration of old EncryptedSharedPreferences data
implementation(libs.androidx.security.crypto)

testImplementation(libs.junit)
testImplementation(libs.mockito.kotlin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
package ee.ria.DigiDoc.domain.preferences

import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import androidx.test.platform.app.InstrumentationRegistry
import ee.ria.DigiDoc.common.Constant.Defaults.DEFAULT_UUID_VALUE
Expand Down Expand Up @@ -83,12 +82,7 @@ class DataStoreTest {
preferences.edit().remove(key).apply()
}

val encryptedPreferences: SharedPreferences = EncryptedPreferences.getEncryptedPreferences(context)

encryptedPreferences.all?.clear()
encryptedPreferences.all?.forEach { (key, _) ->
encryptedPreferences.edit().remove(key).apply()
}
EncryptedPreferences.clear(context)
}

@Test
Expand Down
8 changes: 7 additions & 1 deletion app/src/main/kotlin/ee/ria/DigiDoc/RIADigiDocApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ package ee.ria.DigiDoc

import android.app.Application
import dagger.hilt.android.HiltAndroidApp
import ee.ria.DigiDoc.init.EncryptedPreferencesMigration

@HiltAndroidApp
class RIADigiDocApp : Application()
class RIADigiDocApp : Application() {
override fun onCreate() {
super.onCreate()
EncryptedPreferencesMigration.migrate(this)
}
}
79 changes: 40 additions & 39 deletions app/src/main/kotlin/ee/ria/DigiDoc/domain/preferences/DataStore.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,30 +92,15 @@ class DataStore
}
}

fun getCanNumber(): String {
val encryptedPreferences: SharedPreferences? = getEncryptedPreferences(context)
if (encryptedPreferences != null) {
return encryptedPreferences.getString(
resources.getString(R.string.main_settings_can_key),
"",
) ?: ""
}
errorLog(
logTag,
"Unable to read CAN",
)
return ""
}
fun getCanNumber(): String =
runEncrypted(context, "Unable to read CAN") {
EncryptedPreferences.getString(context, resources.getString(R.string.main_settings_can_key))
}

fun setCanNumber(can: String) {
val encryptedPreferences: SharedPreferences? = getEncryptedPreferences(context)
if (encryptedPreferences != null) {
encryptedPreferences.edit {
putString(resources.getString(R.string.main_settings_can_key), can)
}
return
runEncryptedWrite(context, "Unable to save CAN") {
EncryptedPreferences.putString(context, resources.getString(R.string.main_settings_can_key), can)
}
errorLog(logTag, "Unable to save CAN")
}

fun getPhoneNo(): String =
Expand Down Expand Up @@ -690,26 +675,22 @@ class DataStore
) ?: ""

fun setProxyPassword(password: String) {
getEncryptedPreferences(context)?.edit {
putString(
runEncryptedWrite(context, "Unable to set proxy password") {
EncryptedPreferences.putString(
context,
resources.getString(ee.ria.DigiDoc.network.R.string.main_settings_proxy_password_key),
password,
)
}
errorLog(logTag, "Unable to set proxy password")
}

fun getProxyPassword(): String {
val encryptedPreferences: SharedPreferences? = getEncryptedPreferences(context)
if (encryptedPreferences != null) {
return encryptedPreferences.getString(
fun getProxyPassword(): String =
runEncrypted(context, "Unable to get proxy password") {
EncryptedPreferences.getString(
context,
resources.getString(ee.ria.DigiDoc.network.R.string.main_settings_proxy_password_key),
"",
) ?: ""
)
}
errorLog(logTag, "Unable to get proxy password")
return ""
}

fun getLocale(): Locale? {
val locale = preferences.getString(KEY_LOCALE, null)
Expand Down Expand Up @@ -772,16 +753,36 @@ class DataStore
preferences.edit { putString(IDENTIFICATION_METHOD_SETTING, myEidIdentificationMethodSetting.methodName) }
}

private fun getEncryptedPreferences(context: Context): SharedPreferences? =
private fun runEncrypted(
context: Context,
errorMessage: String,
operation: () -> String,
): String =
try {
EncryptedPreferences.getEncryptedPreferences(context)
operation()
} catch (e: GeneralSecurityException) {
errorLog(logTag, "Unable to get encrypted preferences", e)
errorLog(logTag, errorMessage, e)
showMessage(context, R.string.error_general_client)
null
""
} catch (e: IOException) {
errorLog(logTag, "Unable to get encrypted preferences", e)
errorLog(logTag, errorMessage, e)
showMessage(context, R.string.error_general_client)
null
""
}

private fun runEncryptedWrite(
context: Context,
errorMessage: String,
operation: () -> Unit,
) {
try {
operation()
} catch (e: GeneralSecurityException) {
errorLog(logTag, errorMessage, e)
showMessage(context, R.string.error_general_client)
} catch (e: IOException) {
errorLog(logTag, errorMessage, e)
showMessage(context, R.string.error_general_client)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 2017 - 2026 Riigi Infosüsteemi Amet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/

@file:Suppress("PackageName")

package ee.ria.DigiDoc.init

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import ee.ria.DigiDoc.common.preferences.EncryptedPreferences
import ee.ria.DigiDoc.utilsLib.logging.LoggingUtil.Companion.errorLog
import java.io.File

object EncryptedPreferencesMigration {
private const val OLD_PREFS_NAME = "encryptedPreferencesStorage"

// Keys that were stored in the old EncryptedSharedPreferences
private const val KEY_CAN = "can"
private const val KEY_PROXY_PASSWORD = "main_settings_proxy_password"

@Suppress("DEPRECATION")
fun migrate(context: Context) {
if (EncryptedPreferences.isMigrated(context)) return

val oldPrefsFile = File(context.filesDir.parent, "shared_prefs/$OLD_PREFS_NAME.xml")
if (!oldPrefsFile.exists()) {
EncryptedPreferences.setMigrated(context)
return
}

try {
val masterKey = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val oldPrefs =
EncryptedSharedPreferences.create(
OLD_PREFS_NAME,
masterKey,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)

val can = oldPrefs.getString(KEY_CAN, null)
val proxyPassword = oldPrefs.getString(KEY_PROXY_PASSWORD, null)

if (!can.isNullOrEmpty()) {
EncryptedPreferences.putString(context, KEY_CAN, can)
}
if (!proxyPassword.isNullOrEmpty()) {
EncryptedPreferences.putString(context, KEY_PROXY_PASSWORD, proxyPassword)
}

context.deleteSharedPreferences(OLD_PREFS_NAME)
EncryptedPreferences.setMigrated(context)
} catch (e: Exception) {
errorLog("EncryptedPrefsMigration", "Failed to migrate encrypted preferences", e)
}
}
}
1 change: 0 additions & 1 deletion commons-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.androidx.security.crypto)
implementation(libs.guava)
implementation(libs.bouncy.castle)
implementation(libs.google.dagger.hilt.android)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
package ee.ria.DigiDoc.common.preferences

import android.content.Context
import android.util.Base64
import androidx.test.platform.app.InstrumentationRegistry
import ee.ria.DigiDoc.common.preferences.EncryptedPreferences.getEncryptedPreferences
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test

Expand All @@ -34,12 +36,84 @@ class EncryptedPreferencesTest {
@Before
fun setUp() {
context = InstrumentationRegistry.getInstrumentation().targetContext
EncryptedPreferences.clear(context)
context
.getSharedPreferences("encryptedPrefsMigration", Context.MODE_PRIVATE)
.edit()
.clear()
.apply()
}

@Test
fun encryptedPreferences_getEncryptedPreferences_success() {
val result = getEncryptedPreferences(context)
fun encryptedPreferences_putString_success() {
EncryptedPreferences.putString(context, "test_key", "test_value")
assertEquals("test_value", EncryptedPreferences.getString(context, "test_key"))
}

@Test
fun encryptedPreferences_putString_successOverwritingExistingKey() {
EncryptedPreferences.putString(context, "key", "first")
EncryptedPreferences.putString(context, "key", "second")
assertEquals("second", EncryptedPreferences.getString(context, "key"))
}

@Test
fun encryptedPreferences_putString_successWithEmptyString() {
EncryptedPreferences.putString(context, "key", "")
assertEquals("", EncryptedPreferences.getString(context, "key", "default"))
}

@Test
fun encryptedPreferences_putString_successWithMultipleKeys() {
EncryptedPreferences.putString(context, "key_a", "value_a")
EncryptedPreferences.putString(context, "key_b", "value_b")
assertEquals("value_a", EncryptedPreferences.getString(context, "key_a"))
assertEquals("value_b", EncryptedPreferences.getString(context, "key_b"))
}

@Test
fun encryptedPreferences_getString_missingKeyReturnsCallerDefault() {
assertEquals("default", EncryptedPreferences.getString(context, "missing_key", "default"))
}

@Test
fun encryptedPreferences_getString_missingKeyReturnsEmptyStringByDefault() {
assertEquals("", EncryptedPreferences.getString(context, "missing_key"))
}

@Test
fun encryptedPreferences_clear_removesStoredValues() {
EncryptedPreferences.putString(context, "key", "value")
EncryptedPreferences.clear(context)
assertEquals("default", EncryptedPreferences.getString(context, "key", "default"))
}

@Test
fun encryptedPreferences_getString_returnsDefaultValueOnTamperedCiphertext() {
EncryptedPreferences.putString(context, "tamper_key", "real_value")
val underlying = context.getSharedPreferences("encryptedPreferencesStorageV2", Context.MODE_PRIVATE)
val obfuscatedKey = underlying.all.keys.first()
val fakeData = ByteArray(32) { it.toByte() }
underlying.edit().putString(obfuscatedKey, Base64.encodeToString(fakeData, Base64.NO_WRAP)).apply()

assertEquals("default", EncryptedPreferences.getString(context, "tamper_key", "default"))
}

assertNotNull(result)
@Test
fun encryptedPreferences_isMigrated_returnsFalseInitially() {
assertFalse(EncryptedPreferences.isMigrated(context))
}

@Test
fun encryptedPreferences_setMigrated_success() {
EncryptedPreferences.setMigrated(context)
assertTrue(EncryptedPreferences.isMigrated(context))
}

@Test
fun encryptedPreferences_clear_doesNotAffectMigrationFlag() {
EncryptedPreferences.setMigrated(context)
EncryptedPreferences.clear(context)
assertTrue(EncryptedPreferences.isMigrated(context))
}
}
Loading
Loading