-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(shared-prefs): migrate to a non jetpack-security based solution
SUITEDEV-36674 Co-authored-by: davidSchuppa <[email protected]> Co-authored-by: LasOri <[email protected]>
- Loading branch information
1 parent
6f8a878
commit 9f3c87b
Showing
18 changed files
with
886 additions
and
30 deletions.
There are no files selected for viewing
153 changes: 153 additions & 0 deletions
153
core/src/androidTest/java/com/emarsys/core/crypto/SharedPreferenceCryptoTest.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,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 | ||
} | ||
} |
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
202 changes: 202 additions & 0 deletions
202
.../src/androidTest/java/com/emarsys/core/storage/EmarsysEncryptedSharedPreferencesV3Test.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,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 | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.