Skip to content

Commit

Permalink
feat(shared-prefs): migrate to a non jetpack-security based solution
Browse files Browse the repository at this point in the history
SUITEDEV-36674

Co-authored-by: davidSchuppa <[email protected]>
Co-authored-by: LasOri <[email protected]>
  • Loading branch information
3 people committed Oct 10, 2024
1 parent 6f8a878 commit 9f3c87b
Show file tree
Hide file tree
Showing 18 changed files with 886 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import android.security.keystore.KeyGenParameterSpec
import android.util.Base64
import com.emarsys.core.crypto.SharedPreferenceCrypto
import com.emarsys.testUtil.AnnotationSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.unmockkAll
import io.mockk.verify
import java.security.GeneralSecurityException
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec

class SharedPreferenceCryptoTest : AnnotationSpec() {

private lateinit var sharedPreferenceCrypto: SharedPreferenceCrypto
private lateinit var mockKeyStore: KeyStore
private lateinit var mockKeyGenerator: KeyGenerator
private lateinit var mockSecretKey: SecretKey
private lateinit var mockCipher: Cipher

@BeforeEach
fun setup() {
mockkStatic(KeyStore::class)
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
mockkStatic(Base64::class)

mockKeyStore = mockk()
mockKeyGenerator = mockk()
mockSecretKey = mockk()
mockCipher = mockk()

every { KeyStore.getInstance(any()) } returns mockKeyStore
every { KeyGenerator.getInstance(any(), any<String>()) } returns mockKeyGenerator
every { Cipher.getInstance(any()) } returns mockCipher

sharedPreferenceCrypto = SharedPreferenceCrypto()
}

@AfterEach
fun tearDown() {
unmockkAll()
}

@Test
fun testGetOrCreateSecretKey_KeyExists() {
every { mockKeyStore.load(null) } just Runs
every { mockKeyStore.containsAlias(any()) } returns true
every { mockKeyStore.getKey(any(), null) } returns mockSecretKey

val result = sharedPreferenceCrypto.getOrCreateSecretKey()

result shouldBe mockSecretKey
verify { mockKeyStore.getKey(any(), null) }
}

@Test
fun testGetOrCreateSecretKey_KeyDoesNotExist() {
every { mockKeyStore.load(null) } just Runs
every { mockKeyStore.containsAlias(any()) } returns false
every { mockKeyGenerator.init(any<KeyGenParameterSpec>()) } just Runs
every { mockKeyGenerator.generateKey() } returns mockSecretKey
every { mockKeyStore.setEntry(any(), any(), null) } just Runs

val result = sharedPreferenceCrypto.getOrCreateSecretKey()

result shouldBe mockSecretKey
verify { mockKeyGenerator.generateKey() }
verify { mockKeyStore.setEntry(any(), any(), null) }
}

@Test
fun testEncrypt_Success() {
val value = "test_value"
val encryptedBytes = byteArrayOf(1, 2, 3, 4)
val iv = byteArrayOf(5, 6, 7, 8)

every { mockCipher.init(Cipher.ENCRYPT_MODE, mockSecretKey) } just Runs
every { mockCipher.doFinal(any<ByteArray>()) } returns encryptedBytes
every { mockCipher.iv } returns iv
every { Base64.encodeToString(any(), Base64.DEFAULT) } returns "encodedString"

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)

result shouldNotBe value
result shouldBe "encodedStringencodedString"
}

@Test
fun testEncrypt_Exception() {
val value = "test_value"

every {
mockCipher.init(
Cipher.ENCRYPT_MODE,
mockSecretKey
)
} throws GeneralSecurityException("Encryption failed")

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)

result shouldBe value
}

@Test
fun testDecrypt_Success() {
val value = "IVBase64EncryptedBase64"
val ivBytes = byteArrayOf(1, 2, 3, 4)
val encryptedBytes = byteArrayOf(5, 6, 7, 8)
val decryptedBytes = "decrypted".toByteArray()

every { Base64.decode(any<String>(), Base64.DEFAULT) } returnsMany listOf(
ivBytes,
encryptedBytes
)
every {
mockCipher.init(
Cipher.DECRYPT_MODE,
mockSecretKey,
any<GCMParameterSpec>()
)
} just Runs
every { mockCipher.doFinal(encryptedBytes) } returns decryptedBytes

val result = sharedPreferenceCrypto.decrypt(value, mockSecretKey)

result shouldBe "decrypted"
}

@Test
fun testDecrypt_Exception() {
val value = "IVBase64EncryptedBase64"

every {
Base64.decode(
any<String>(),
Base64.DEFAULT
)
} throws GeneralSecurityException("Decryption failed")

val result = sharedPreferenceCrypto.decrypt(value, mockSecretKey)

result shouldBe value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class FakeCoreDependencyContainer(
override val fileDownloader: FileDownloader = mock(),
override val keyValueStore: KeyValueStore = mock(),
override val sharedPreferences: SharedPreferences = mock(),
override val sharedPreferencesV3: SharedPreferences = mock(),
override val hardwareIdProvider: HardwareIdProvider = mock(),
override val coreDbHelper: CoreDbHelper = mock(),
override val hardwareIdStorage: Storage<String?> = mock(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package com.emarsys.core.storage

import android.content.Context
import android.content.SharedPreferences
import com.emarsys.core.crypto.SharedPreferenceCrypto
import com.emarsys.testUtil.AnnotationSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import javax.crypto.SecretKey

class EmarsysEncryptedSharedPreferencesV3Test : AnnotationSpec() {

private lateinit var mockContext: Context
private lateinit var mockSharedPreferenceCrypto: SharedPreferenceCrypto
private lateinit var mockRealPreferences: SharedPreferences
private lateinit var mockSecretKey: SecretKey
private lateinit var emarsysEncryptedSharedPreferencesV3: EmarsysEncryptedSharedPreferencesV3
private lateinit var mockInternalEditor: SharedPreferences.Editor

@BeforeEach
fun setup() {
mockContext = mockk()
mockSharedPreferenceCrypto = mockk()
mockRealPreferences = mockk(relaxed = true)
mockSecretKey = mockk()

every { mockContext.getSharedPreferences(any(), any()) } returns mockRealPreferences
every { mockSharedPreferenceCrypto.getOrCreateSecretKey() } returns mockSecretKey
mockInternalEditor = mockk<SharedPreferences.Editor>(relaxed = true)

every { mockRealPreferences.edit() } returns mockInternalEditor
every { mockSharedPreferenceCrypto.encrypt(any(), any()) } returns "encryptedValue"

emarsysEncryptedSharedPreferencesV3 = EmarsysEncryptedSharedPreferencesV3(
mockContext,
"test_file",
mockSharedPreferenceCrypto
)
}

@Test
fun testGetAll() {
val encryptedMap = mapOf(
"key1" to "encryptedValue1",
"key2" to 42,
"key3" to true,
"key4" to 3.14f,
"key5" to 1234L,
"key6" to setOf("encryptedValue2", "encryptedValue3")
)
every { mockRealPreferences.all } returns encryptedMap
every {
mockSharedPreferenceCrypto.decrypt(
any(),
any()
)
} returnsMany listOf("decryptedValue1", "decryptedValue2", "decryptedValue3")

val result = emarsysEncryptedSharedPreferencesV3.getAll()

result shouldBe mapOf(
"key1" to "decryptedValue1",
"key2" to 42,
"key3" to true,
"key4" to 3.14f,
"key5" to 1234L,
"key6" to setOf("decryptedValue2", "decryptedValue3")
)
}

@Test
fun testGetString() {
every { mockRealPreferences.getString("testKey", null) } returns "encryptedValue"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue",
mockSecretKey
)
} returns "decryptedValue"

val result = emarsysEncryptedSharedPreferencesV3.getString("testKey", "defaultValue")

result shouldBe "decryptedValue"
}

@Test
fun testGetStringSet() {
val encryptedSet = setOf("encryptedValue1", "encryptedValue2")
every { mockRealPreferences.getStringSet("testKey", null) } returns encryptedSet
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue1",
mockSecretKey
)
} returns "decryptedValue1"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue2",
mockSecretKey
)
} returns "decryptedValue2"

val result = emarsysEncryptedSharedPreferencesV3.getStringSet("testKey", mutableSetOf())

result shouldBe mutableSetOf("decryptedValue1", "decryptedValue2")
}

@Test
fun testGetInt() {
every { mockRealPreferences.getInt("testKey", 0) } returns 42

val result = emarsysEncryptedSharedPreferencesV3.getInt("testKey", 0)

result shouldBe 42
}

@Test
fun testGetLong() {
every { mockRealPreferences.getLong("testKey", 0L) } returns 1234L

val result = emarsysEncryptedSharedPreferencesV3.getLong("testKey", 0L)

result shouldBe 1234L
}

@Test
fun testGetFloat() {
every { mockRealPreferences.getFloat("testKey", 0f) } returns 3.14f

val result = emarsysEncryptedSharedPreferencesV3.getFloat("testKey", 0f)

result shouldBe 3.14f
}

@Test
fun testGetBoolean() {
every { mockRealPreferences.getBoolean("testKey", false) } returns true

val result = emarsysEncryptedSharedPreferencesV3.getBoolean("testKey", false)

result shouldBe true
}

@Test
fun testContains() {
every { mockRealPreferences.contains("testKey") } returns true

val result = emarsysEncryptedSharedPreferencesV3.contains("testKey")

result shouldBe true
}

@Test
fun testEdit() {
every { mockRealPreferences.edit() } returns mockInternalEditor

val editor = emarsysEncryptedSharedPreferencesV3.edit()

editor.putString("testKey", "testValue")
editor.putInt("testIntKey", 42)
editor.putBoolean("testBoolKey", true)
editor.putFloat("testFloatKey", 3.14f)
editor.putLong("testLongKey", 1234L)
editor.putStringSet("testSetKey", mutableSetOf("value1", "value2"))

editor.commit()

verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("testValue", mockSecretKey) }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value1", mockSecretKey) }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value2", mockSecretKey) }

verify(exactly = 1) { mockInternalEditor.putString("testKey", "encryptedValue") }
verify(exactly = 1) { mockInternalEditor.putInt("testIntKey", 42) }
verify(exactly = 1) { mockInternalEditor.putBoolean("testBoolKey", true) }
verify(exactly = 1) { mockInternalEditor.putFloat("testFloatKey", 3.14f) }
verify(exactly = 1) { mockInternalEditor.putLong("testLongKey", 1234L) }
verify(exactly = 1) {
mockInternalEditor.putStringSet(
"testSetKey",
setOf("encryptedValue", "encryptedValue")
)
}
verify(exactly = 1) { mockInternalEditor.commit() }
}

@Test
fun testRegisterAndUnregisterOnSharedPreferenceChangeListener() {
val listener: SharedPreferences.OnSharedPreferenceChangeListener = mockk()

emarsysEncryptedSharedPreferencesV3.registerOnSharedPreferenceChangeListener(listener)
verify(exactly = 1) { mockRealPreferences.registerOnSharedPreferenceChangeListener(listener) }

emarsysEncryptedSharedPreferencesV3.unregisterOnSharedPreferenceChangeListener(listener)
verify(exactly = 1) {
mockRealPreferences.unregisterOnSharedPreferenceChangeListener(
listener
)
}
}
}
Loading

0 comments on commit 9f3c87b

Please sign in to comment.