Skip to content

Commit

Permalink
feat(shared-pref-crypto): improve error handling and logging
Browse files Browse the repository at this point in the history
SDK-217

Co-authored-by: davidSchuppa <[email protected]>
Co-authored-by: LasOri <[email protected]>
Co-authored-by: megamegax <[email protected]>
Co-authored-by: matusekma <[email protected]>
  • Loading branch information
5 people committed Feb 7, 2025
1 parent c1c9277 commit 1e8d589
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 191 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import android.security.keystore.KeyGenParameterSpec
import android.util.Base64
import com.emarsys.core.crypto.SharedPreferenceCrypto
package com.emarsys.core.crypto

import android.security.keystore.KeyProperties
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
Expand All @@ -17,168 +14,119 @@ 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 {
private companion object {
const val encryptedBase64 = "Base64EncryptedBase64IV123123"
class SharedPreferenceCryptoTest {
private lateinit var keyStore: KeyStore

@Before
fun setup() {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
keyStore.deleteEntry("emarsys_sdk_key_shared_pref_key_v3")
}

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
@After
fun tearDown() {
unmockkAll()
}

@Before
fun setup() {
mockkStatic(KeyStore::class)
@Test
fun init_shouldGenerateKey_ifNotPresent_inKeyStore() {
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 = SharedPreferenceCrypto()
verify { KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) }
}

@After
fun tearDown() {
unmockkAll()
@Test
fun init_shouldNotGenerateKey_ifPresent_inKeyStore() {
mockkStatic(KeyGenerator::class)

SharedPreferenceCrypto()
SharedPreferenceCrypto()

verify(exactly = 1) { KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES) }
}

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

val result = sharedPreferenceCrypto.getOrCreateSecretKey()
val testCrypto = SharedPreferenceCrypto()
val encrypted = testCrypto.encrypt(testValue)

result shouldBe mockSecretKey
verify { mockKeyStore.getKey(any(), null) }
testCrypto.decrypt(encrypted) shouldBe testValue
}

@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
fun encrypt_shouldGenerateNewSecretKey_andRetryEncrypting_once_andReturnEncryptedValue_ifSucceeds() {
val testValue = "testValue"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception") andThenAnswer { callOriginal() }

val testCrypto = SharedPreferenceCrypto()

val result = sharedPreferenceCrypto.getOrCreateSecretKey()
val result = testCrypto.encrypt(testValue)

result shouldBe mockSecretKey
verify { mockKeyGenerator.generateKey() }
verify(exactly = 2) { KeyGenerator.getInstance(any()) }

result shouldNotBe testValue
}

@Test
fun testEncrypt_Success() {
val value = "test_value"
val encryptedBytes = byteArrayOf(1, 2, 3, 4)
val iv = byteArrayOf(5, 6, 7, 8)
fun encrypt_shouldGenerateNewSecretKey_andRetryEncrypting_once_andReturnInitialValue_ifFails() {
val testValue = "testValue"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception")

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 testCrypto = SharedPreferenceCrypto()

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)
val result = testCrypto.encrypt(testValue)

result shouldNotBe value
result shouldBe "encodedStringencodedString"
verify(exactly = 2) { KeyGenerator.getInstance(any()) }
result shouldBe testValue
}

@Test
fun testEncrypt_Exception() {
val value = "test_value"
fun decrypt_shouldReturn_null_andGenerateNewSecretKey_ifGeneralSecurityExceptionHappens() {
val testValue = "dGVzdFZhbHVlU2hvdWxkQmVTaXh0ZWVuQ2hhcnNMb25n"
mockkStatic(KeyGenerator::class)
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws GeneralSecurityException("Test exception")

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

val result = sharedPreferenceCrypto.encrypt(value, mockSecretKey)
testCrypto.decrypt(testValue) shouldBe null

result shouldBe value
verify { KeyGenerator.getInstance(any()) }
}

@Test
fun testDecrypt_Success() {
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(encryptedBase64, mockSecretKey)

result shouldBe "decrypted"
fun decrypt_shouldReturn_encryptedValue_ifIllegalArgumentException_withBase64ErrorHappens() {
val testValue = "testValueShouldBeSixteenCharsLong"

val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe testValue
}

@Test
fun testDecrypt_Exception() {
val IVValue = "Base64EncryptedBase64IV123"
val decryptedBytes = encryptedBase64.toByteArray()
every {
mockCipher.init(any(), mockSecretKey, any<GCMParameterSpec>())
} just Runs
every {
mockCipher.doFinal(any())
} returns decryptedBytes
every {
Base64.decode(
IVValue,
Base64.DEFAULT
)
} throws GeneralSecurityException("Decryption failed")

val result = sharedPreferenceCrypto.decrypt(encryptedBase64, mockSecretKey)

result shouldBe null
fun decrypt_shouldReturn_null_ifIllegalArgumentExceptionHappens() {
val testValue = "dGVzdFZhbHVlU2hvdWxkQmVTaXh0ZWVuQ2hhcnNMb25n"
mockkStatic(Cipher::class)
every { Cipher.getInstance("AES/GCM/NoPadding") } throws IllegalArgumentException("Test exception")

val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe null
}

@Test
fun testDecrypt_IllegalArgumentException() {
val IVValue = "Base64EncryptedBase64IV123"
val decryptedBytes = encryptedBase64.toByteArray()
every {
mockCipher.init(any(), mockSecretKey, any<GCMParameterSpec>())
} just Runs
every {
mockCipher.doFinal(any())
} returns decryptedBytes
every {
Base64.decode(
IVValue,
Base64.DEFAULT
)
} throws IllegalArgumentException("bad base-64")

val result = sharedPreferenceCrypto.decrypt(encryptedBase64, mockSecretKey)

result shouldBe null
fun decrypt_shouldReturn_null_ifExceptionHappens() {
val testValue = "testValue"

val testCrypto = SharedPreferenceCrypto()
testCrypto.decrypt(testValue) shouldBe null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,10 @@ class EmarsysEncryptedSharedPreferencesV3Test {
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"
every { mockSharedPreferenceCrypto.encrypt(any()) } returns "encryptedValue"

emarsysEncryptedSharedPreferencesV3 = EmarsysEncryptedSharedPreferencesV3(
mockContext,
Expand All @@ -54,7 +53,6 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.all } returns encryptedMap
every {
mockSharedPreferenceCrypto.decrypt(
any(),
any()
)
} returnsMany listOf("decryptedValue1", "decryptedValue2", "decryptedValue3")
Expand All @@ -76,8 +74,7 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.getString("testKey", null) } returns "encryptedValue"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue",
mockSecretKey
"encryptedValue"
)
} returns "decryptedValue"

Expand All @@ -91,8 +88,7 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.getString("testKey", null) } returns "encryptedValue"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue",
mockSecretKey
"encryptedValue"
)
} returns null

Expand All @@ -107,14 +103,12 @@ class EmarsysEncryptedSharedPreferencesV3Test {
every { mockRealPreferences.getStringSet("testKey", null) } returns encryptedSet
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue1",
mockSecretKey
"encryptedValue1"
)
} returns "decryptedValue1"
every {
mockSharedPreferenceCrypto.decrypt(
"encryptedValue2",
mockSecretKey
"encryptedValue2"
)
} returns "decryptedValue2"

Expand Down Expand Up @@ -196,9 +190,9 @@ class EmarsysEncryptedSharedPreferencesV3Test {

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) { mockSharedPreferenceCrypto.encrypt("testValue") }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value1") }
verify(exactly = 1) { mockSharedPreferenceCrypto.encrypt("value2") }

verify(exactly = 1) { mockInternalEditor.putString("testKey", "encryptedValue") }
verify(exactly = 1) { mockInternalEditor.putInt("testIntKey", 42) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.emarsys.core.storage

import android.content.Context
import android.content.SharedPreferences
import com.emarsys.core.crypto.SharedPreferenceCrypto
import com.emarsys.core.storage.EmarsysEncryptedSharedPreferencesV3
import com.emarsys.core.storage.EncryptedSharedPreferencesToSharedPreferencesMigration
import com.emarsys.core.storage.SharedPreferencesV3Provider
import com.emarsys.testUtil.ReflectionTestUtils
import io.kotest.matchers.shouldBe
import io.mockk.Runs
Expand Down Expand Up @@ -36,7 +35,6 @@ class SharedPreferencesV3ProviderTest {
any()
)
} returns mockRealSharedPrefs
every { mockCrypto.getOrCreateSecretKey() } returns mockk()
every { mockMigration.migrate(any(), any()) } just Runs

}
Expand Down
Loading

0 comments on commit 1e8d589

Please sign in to comment.