Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
284fe2e
[MOB-9235] Use shared prefs and try to migrate date from encrypted to…
sumeruchat Dec 18, 2024
db78138
[MOB-9235 Always encrypting
sumeruchat Dec 18, 2024
e383da3
[MOB-9235 Always encrypting
sumeruchat Dec 18, 2024
880e822
[MOB-9235 Always encrypting
sumeruchat Dec 18, 2024
8c890c9
[MOB-9235 Always encrypting
sumeruchat Dec 18, 2024
9db02a0
[MOB-9235] Always encrypting
sumeruchat Dec 18, 2024
29d3f09
[MOB-9235] Always encrypting
sumeruchat Dec 18, 2024
f939ec8
[MOB-9235] Added decryption failure handler to IterableConfig
sumeruchat Dec 24, 2024
0e767d8
[MOB-9235] Added tests for encryptor
sumeruchat Dec 25, 2024
4b3dbe8
[MOB-9235] added some more tests for encryptor
sumeruchat Dec 25, 2024
5971aaa
[MOB-9235] added some more tests for encryptor
sumeruchat Dec 25, 2024
36cf793
[MOB-9235] added some more tests for encryptor
sumeruchat Dec 25, 2024
d58d403
[MOB-9235] added some more tests for migrator
sumeruchat Dec 25, 2024
a31cb09
[MOB-9235] added some more tests for keychain
sumeruchat Dec 25, 2024
239fa84
[MOB-9235] added some more tests for keychain
sumeruchat Dec 25, 2024
57965a0
[MOB-9235] Cleanup
sumeruchat Dec 25, 2024
c292958
[MOB-9235] Cleanup
sumeruchat Dec 25, 2024
1f2d741
[MOB-9235] Cleanup
sumeruchat Dec 25, 2024
d4cf70e
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
290c481
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
bc59c86
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
05e2285
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
1e31fc0
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
f182e36
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
5cbcd5c
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
d097b05
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
212ac2f
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
6418566
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
9d30528
[MOB-10402] no migration in tests
sumeruchat Dec 26, 2024
9f73e14
[MOB-10402] no migration in tests
sumeruchat Dec 26, 2024
aff0d9b
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
0b53dc9
[MOB-9235] Cleanup
sumeruchat Dec 26, 2024
8e42cfd
[MOB-9235] Better Logging
sumeruchat Dec 26, 2024
4df332e
[MOB-9235] Fixes
sumeruchat Dec 26, 2024
d790d33
[MOB-9234] Make the migration blocking
sumeruchat Dec 26, 2024
2593a47
[MOB-9235] Fix
sumeruchat Dec 26, 2024
845a364
[MOB-9235] Fix
sumeruchat Dec 26, 2024
7955bcf
[MOB-9235] Fix Test
sumeruchat Dec 26, 2024
0747152
[MOB-9235] Fix
sumeruchat Dec 26, 2024
1b4dfb2
[MOB-9235] Lint
sumeruchat Dec 26, 2024
1fd62fb
[MOB-9235] Lint
sumeruchat Dec 27, 2024
8717fc0
[MOB-9235] Cleanup
sumeruchat Dec 27, 2024
10adec8
[MOB-9235] Handle older versions
sumeruchat Dec 27, 2024
f228ad1
[MOB-9235] Cleanup
sumeruchat Dec 28, 2024
33da97d
[MOB-9235] Cleanup
sumeruchat Dec 28, 2024
343becd
[MOB-9235] Cleanup
sumeruchat Dec 30, 2024
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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

#### Added
- nothing yet
- - Added `IterableDecryptionFailureHandler` interface to handle decryption failures of PII information.

#### Removed
- nothing yet
- Removed `encryptionEnforced` parameter from `IterableConfig` as data is now always encoded for security

#### Changed
- nothing yet
- Migrated from EncryptedSharedPreferences to regular SharedPreferences to prevent ANRs while EncryptedSharedPreferences was created on the main thread. We are now using our own encryption library to encrypt PII information before storing it in SharedPreferences.

## [3.5.4]
#### Fixed
Expand Down
2 changes: 1 addition & 1 deletion iterableapi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ dependencies {
testImplementation 'androidx.test.ext:junit:1.1.5'
testImplementation 'androidx.test:rules:1.5.0'
testImplementation 'org.mockito:mockito-core:3.3.3'
testImplementation 'org.mockito:mockito-inline:2.8.47'
testImplementation 'org.mockito:mockito-inline:5.2.0'
testImplementation 'org.robolectric:robolectric:4.9.2'
testImplementation 'org.robolectric:shadows-playservices:4.9.2'
testImplementation 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ IterableKeychain getKeychain() {
}
if (keychain == null) {
try {
keychain = new IterableKeychain(getMainActivityContext(), config.encryptionEnforced);
keychain = new IterableKeychain(getMainActivityContext(), config.decryptionFailureHandler);
} catch (Exception e) {
IterableLogger.e(TAG, "Failed to create IterableKeychain", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,17 @@ public class IterableConfig {
* By default, the SDK will save in-apps to disk.
*/
final boolean useInMemoryStorageForInApps;

final boolean encryptionEnforced;

/**
* Allows for fetching embedded messages.
*/
final boolean enableEmbeddedMessaging;

/**
* Handler for decryption failures of PII information.
* Before calling this handler, the SDK will clear the PII information and create new encryption keys
*/
final IterableDecryptionFailureHandler decryptionFailureHandler;

private IterableConfig(Builder builder) {
pushIntegrationName = builder.pushIntegrationName;
urlHandler = builder.urlHandler;
Expand All @@ -109,8 +112,8 @@ private IterableConfig(Builder builder) {
allowedProtocols = builder.allowedProtocols;
dataRegion = builder.dataRegion;
useInMemoryStorageForInApps = builder.useInMemoryStorageForInApps;
encryptionEnforced = builder.encryptionEnforced;
enableEmbeddedMessaging = builder.enableEmbeddedMessaging;
decryptionFailureHandler = builder.decryptionFailureHandler;
}

public static class Builder {
Expand All @@ -128,8 +131,8 @@ public static class Builder {
private String[] allowedProtocols = new String[0];
private IterableDataRegion dataRegion = IterableDataRegion.US;
private boolean useInMemoryStorageForInApps = false;
private boolean encryptionEnforced = false;
private boolean enableEmbeddedMessaging = false;
private IterableDecryptionFailureHandler decryptionFailureHandler;

public Builder() {}

Expand Down Expand Up @@ -261,17 +264,6 @@ public Builder setAllowedProtocols(@NonNull String[] allowedProtocols) {
return this;
}

/**
* Set whether the SDK should enforce encryption. If set to `true`, the SDK will not use fallback mechanism
* of storing data in un-encrypted shared preferences if encrypted database is not available. Set this to `true`
* if PII confidentiality is a concern for your app.
* @param encryptionEnforced `true` will have the SDK enforce encryption.
*/
public Builder setEncryptionEnforced(boolean encryptionEnforced) {
this.encryptionEnforced = encryptionEnforced;
return this;
}

/**
* Set the data region used by the SDK
* @param dataRegion enum value that determines which endpoint to use, defaults to IterableDataRegion.US
Expand Down Expand Up @@ -302,6 +294,16 @@ public Builder setEnableEmbeddedMessaging(boolean enableEmbeddedMessaging) {
return this;
}

/**
* Set a handler for decryption failures that can be used to handle data recovery
* @param handler Decryption failure handler provided by the app
*/
@NonNull
public Builder setDecryptionFailureHandler(@NonNull IterableDecryptionFailureHandler handler) {
this.decryptionFailureHandler = handler;
return this;
}

@NonNull
public IterableConfig build() {
return new IterableConfig(this);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.iterable.iterableapi

import android.util.Base64
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.os.Build
import java.security.KeyStore.PasswordProtection
import androidx.annotation.VisibleForTesting

class IterableDataEncryptor {
companion object {
private const val TAG = "IterableDataEncryptor"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val ITERABLE_KEY_ALIAS = "iterable_encryption_key"
private const val GCM_IV_LENGTH = 12
private const val GCM_TAG_LENGTH = 128
private val TEST_KEYSTORE_PASSWORD = "test_password".toCharArray()

// Make keyStore static so it's shared across instances
private val keyStore: KeyStore by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
try {
KeyStore.getInstance(ANDROID_KEYSTORE).apply {
load(null)
}
} catch (e: Exception) {
IterableLogger.e(TAG, "Failed to initialize AndroidKeyStore", e)
KeyStore.getInstance("PKCS12").apply {
load(null, TEST_KEYSTORE_PASSWORD)
}
}
} else {
KeyStore.getInstance("PKCS12").apply {
load(null, TEST_KEYSTORE_PASSWORD)
}
}
}
}

init {
if (!keyStore.containsAlias(ITERABLE_KEY_ALIAS)) {
generateKey()
}
}

private fun generateKey() {
try {
if (canUseAndroidKeyStore()) {
generateAndroidKeyStoreKey()?.let { return }
}
generateFallbackKey()
} catch (e: Exception) {
IterableLogger.e(TAG, "Failed to generate key", e)
throw e
}
}

private fun canUseAndroidKeyStore(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 &&
keyStore.type == ANDROID_KEYSTORE
}

private fun generateAndroidKeyStoreKey(): Unit? {
return try {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEYSTORE
)

val keySpec = KeyGenParameterSpec.Builder(
ITERABLE_KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()

keyGenerator.init(keySpec)
keyGenerator.generateKey()
Unit
} catch (e: Exception) {
IterableLogger.e(TAG, "Failed to generate key using AndroidKeyStore", e)
null
}
}

private fun generateFallbackKey() {
val keyGenerator = KeyGenerator.getInstance("AES")
keyGenerator.init(256) // 256-bit AES key
val secretKey = keyGenerator.generateKey()

val keyEntry = KeyStore.SecretKeyEntry(secretKey)
val protParam = if (keyStore.type == "PKCS12") {
PasswordProtection(TEST_KEYSTORE_PASSWORD)
} else {
null
}
keyStore.setEntry(ITERABLE_KEY_ALIAS, keyEntry, protParam)
}

private fun getKey(): SecretKey {
val protParam = if (keyStore.type == "PKCS12") {
PasswordProtection(TEST_KEYSTORE_PASSWORD)
} else {
null
}
return (keyStore.getEntry(ITERABLE_KEY_ALIAS, protParam) as KeyStore.SecretKeyEntry).secretKey
}

class DecryptionException(message: String, cause: Throwable? = null) : Exception(message, cause)

fun resetKeys() {
try {
keyStore.deleteEntry(ITERABLE_KEY_ALIAS)
generateKey()
} catch (e: Exception) {
IterableLogger.e(TAG, "Failed to regenerate key", e)
}
}

fun encrypt(value: String?): String? {
if (value == null) return null

try {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getKey())

val iv = cipher.iv
val encrypted = cipher.doFinal(value.toByteArray(Charsets.UTF_8))

// Combine IV and encrypted data
val combined = ByteArray(iv.size + encrypted.size)
System.arraycopy(iv, 0, combined, 0, iv.size)
System.arraycopy(encrypted, 0, combined, iv.size, encrypted.size)

return Base64.encodeToString(combined, Base64.NO_WRAP)
} catch (e: Exception) {
IterableLogger.e(TAG, "Encryption failed", e)
throw e
}
}

fun decrypt(value: String?): String? {
if (value == null) return null

try {
val combined = Base64.decode(value, Base64.NO_WRAP)

// Extract IV
val iv = combined.copyOfRange(0, GCM_IV_LENGTH)
val encrypted = combined.copyOfRange(GCM_IV_LENGTH, combined.size)

val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(GCM_TAG_LENGTH, iv)
cipher.init(Cipher.DECRYPT_MODE, getKey(), spec)

return String(cipher.doFinal(encrypted), Charsets.UTF_8)
} catch (e: Exception) {
IterableLogger.e(TAG, "Decryption failed", e)
throw DecryptionException("Failed to decrypt data", e)
}
}

// Add this method for testing purposes
@VisibleForTesting
fun getKeyStore(): KeyStore = keyStore
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.iterable.iterableapi;

/**
* Interface for handling decryption failures
*/
public interface IterableDecryptionFailureHandler {
/**
* Called when a decryption failure occurs
* @param exception The exception that caused the decryption failure
*/
void onDecryptionFailed(Exception exception);
}
Loading
Loading