Skip to content

Commit

Permalink
MBL-1269: Encrypt/decript token (#1983)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arkariang authored Mar 25, 2024
1 parent df6f389 commit 037f96d
Show file tree
Hide file tree
Showing 7 changed files with 225 additions and 10 deletions.
7 changes: 4 additions & 3 deletions app/src/main/java/com/kickstarter/ApplicationModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import com.kickstarter.libs.graphql.DateTimeAdapter;
import com.kickstarter.libs.graphql.Iso8601DateTimeAdapter;
import com.kickstarter.libs.graphql.EmailAdapter;
import com.kickstarter.libs.keystore.EncryptionEngine;
import com.kickstarter.libs.preferences.BooleanPreference;
import com.kickstarter.libs.preferences.BooleanPreferenceType;
import com.kickstarter.libs.preferences.IntPreference;
Expand Down Expand Up @@ -284,7 +285,7 @@ static ApiRequestInterceptor provideApiRequestInterceptor(
@Singleton
@NonNull
static GraphQLInterceptor provideGraphQLInterceptor(final @NonNull String clientId,
final @NonNull CurrentUserType currentUser, final @NonNull Build build) {
final @NonNull CurrentUserTypeV2 currentUser, final @NonNull Build build) {
return new GraphQLInterceptor(clientId, currentUser, build);
}

Expand Down Expand Up @@ -366,8 +367,8 @@ static WebRequestInterceptor provideWebRequestInterceptor(final @NonNull Current
@Singleton
@AccessTokenPreference
@NonNull
static StringPreferenceType provideAccessTokenPreference(final @NonNull SharedPreferences sharedPreferences) {
return new StringPreference(sharedPreferences, SharedPreferenceKey.ACCESS_TOKEN);
static StringPreferenceType provideAccessTokenPreference(final @NonNull SharedPreferences sharedPreferences, final @ApplicationContext @NonNull Context context, final @NonNull FeatureFlagClientType featureFlagClient) {
return new EncryptionEngine(sharedPreferences, SharedPreferenceKey.ACCESS_TOKEN, context, featureFlagClient);
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ enum class FlagKey(val key: String) {
ANDROID_PRE_LAUNCH_SCREEN("android_pre_launch_screen"),
ANDROID_DARK_MODE_ENABLED("android_dark_mode_enabled"),
ANDROID_POST_CAMPAIGN_PLEDGES("android_post_campaign_pledges"),
ANDROID_OAUTH("android_oauth")
ANDROID_OAUTH("android_oauth"),
ANDROID_ENCRYPT("android_encrypt_token")
}

fun FeatureFlagClient.getFetchInterval(): Long =
Expand Down
100 changes: 100 additions & 0 deletions app/src/main/java/com/kickstarter/libs/keystore/EncryptionEngine.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.kickstarter.libs.keystore

import android.content.Context
import android.content.SharedPreferences
import android.security.keystore.KeyProperties
import android.security.keystore.KeyProtection
import com.kickstarter.libs.featureflag.FeatureFlagClientType
import com.kickstarter.libs.featureflag.FlagKey
import com.kickstarter.libs.preferences.StringPreferenceType
import com.kickstarter.libs.utils.extensions.decrypt
import com.kickstarter.libs.utils.extensions.encrypt
import com.kickstarter.libs.utils.extensions.isKSApplication
import timber.log.Timber
import java.security.Key
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey

interface KSKeyStore {
var ksKeyStore: KeyStore?
fun getSecretKey(keyAlias: String): Key? {
return ksKeyStore?.getKey(keyAlias, null)
}

fun generateSecretKey(keyAlias: String) {
ksKeyStore?.let {
val keyGen = KeyGenerator.getInstance("AES")
keyGen.init(256)
val secretKey: SecretKey = keyGen.generateKey()

// - Set duration/rotation for the key on the keyStorage
// val start: Calendar = Calendar.getInstance()
// val end: Calendar = Calendar.getInstance()
// end.add(Calendar.YEAR, 2)

val entry = KeyStore.SecretKeyEntry(secretKey)
val protectionParameter =
KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
// .setKeyValidityStart(start.time)
// .setKeyValidityEnd(end.time)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build()

it.setEntry(keyAlias, entry, protectionParameter)
}
}
}
class EncryptionEngine(
private val sharedPreferences: SharedPreferences,
private val keyAlias: String,
private val defaultValue: String = "",
private val context: Context,
private val featureFlagClient: FeatureFlagClientType
) : StringPreferenceType {

// - Avoid instantiating KeyStore on test Applications
var ksKeyStore = object : KSKeyStore {
override var ksKeyStore: KeyStore? =
if (context.isKSApplication()) KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
else null
}

// - Overload to be able to use kotlin named parameters from JAVA code
constructor(
sharedPreferences: SharedPreferences,
accessToken: String,
context: Context,
featureFlagClient: FeatureFlagClientType
) : this(sharedPreferences = sharedPreferences, keyAlias = accessToken, context = context, featureFlagClient = featureFlagClient) {
Timber.d("$this :Overloaded constructor")

ksKeyStore.generateSecretKey(keyAlias = keyAlias)
}

override val isSet: Boolean
get() = sharedPreferences.contains(keyAlias)

override fun get(): String {
return if (isSet) {
if (featureFlagClient.getBoolean(FlagKey.ANDROID_ENCRYPT)) {
val b64 = sharedPreferences.getString(keyAlias, defaultValue) ?: defaultValue
val secretKey = ksKeyStore.getSecretKey(keyAlias)
return b64.decrypt(secretKey) ?: defaultValue
} else sharedPreferences.getString(keyAlias, defaultValue) ?: defaultValue
} else ""
}
override fun set(value: String?) {
value?.let {
if (featureFlagClient.getBoolean(FlagKey = FlagKey.ANDROID_ENCRYPT)) {
val secretKey = ksKeyStore.getSecretKey(keyAlias)
val encryptedData = value.encrypt(secretKey = secretKey)
sharedPreferences.edit().putString(keyAlias, encryptedData).apply()
} else sharedPreferences.edit().putString(keyAlias, value).apply()
}
}
override fun delete() {
sharedPreferences.edit().remove(keyAlias).apply()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,55 @@ import android.os.Build
import android.text.Html
import android.text.Spanned
import android.text.TextUtils
import android.util.Base64
import android.util.Patterns
import com.braze.support.emptyToNull
import com.kickstarter.R
import org.jsoup.Jsoup
import java.nio.charset.StandardCharsets
import java.security.Key
import java.security.MessageDigest
import java.text.NumberFormat
import java.util.Locale
import java.util.regex.Matcher
import java.util.regex.Pattern
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec

const val MINIMUM_PASSWORD_LENGTH = 6

fun String.encrypt(secretKey: Key?): String? {
return try {
val cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)

val cipherText = Base64.encodeToString(cipher.doFinal(this.toByteArray()), Base64.DEFAULT)
val iv = Base64.encodeToString(cipher.iv, Base64.DEFAULT)

"$cipherText.$iv"
} catch (e: Exception) {
null
}
}

fun String.decrypt(secretKey: Key?): String? {
return try {
val array = this.split(".")
val cipherData = Base64.decode(array[0], Base64.DEFAULT)
val iv = Base64.decode(array[1], Base64.DEFAULT)
val cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING")
val spec = IvParameterSpec(iv)

cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)

val clearText = cipher.doFinal(cipherData)

String(clearText, 0, clearText.size, StandardCharsets.UTF_8)
} catch (e: Exception) {
null
}
}

/**
* Returns a boolean that reflects if the string is an email address
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.kickstarter.services.interceptors

import com.kickstarter.libs.Build
import com.kickstarter.libs.CurrentUserType
import com.kickstarter.libs.CurrentUserTypeV2
import com.kickstarter.libs.FirebaseHelper
import com.kickstarter.libs.utils.WebUtils
import okhttp3.Interceptor
Expand All @@ -14,17 +14,16 @@ import okhttp3.Response
*/
class GraphQLInterceptor(
private val clientId: String,
private val currentUser: CurrentUserType,
private val currentUser: CurrentUserTypeV2,
private val build: Build
) : Interceptor {
override fun intercept(chain: Chain): Response {
val original = chain.request()
val builder = original.newBuilder().method(original.method, original.body)

this.currentUser.observable()
.subscribe {
builder.addHeader("Authorization", "token " + this.currentUser.accessToken)
}
this.currentUser.accessToken?.let {
builder.addHeader("Authorization", "token $it")
}

builder.addHeader("User-Agent", WebUtils.userAgent(this.build))
.addHeader("X-KICKSTARTER-CLIENT", this.clientId)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.kickstarter.libs.preferences

import android.content.Context
import com.kickstarter.KSRobolectricTestCase
import com.kickstarter.libs.Build
import com.kickstarter.libs.MockSharedPreferences
import com.kickstarter.libs.keystore.EncryptionEngine
import com.kickstarter.libs.keystore.KSKeyStore
import com.kickstarter.mock.MockFeatureFlagClient
import org.junit.Test
import java.security.Key
import java.security.KeyStore
import javax.crypto.spec.SecretKeySpec

class EncryptionEngineTest : KSRobolectricTestCase() {

lateinit var build: Build
lateinit var context: Context

override fun setUp() {
super.setUp()
build = requireNotNull(environment().build())
context = application()
}
@Test
fun testEncryptDecrypt() {

val mockKSKeyStore = object : KSKeyStore {
override var ksKeyStore: KeyStore? = null

override fun getSecretKey(keyAlias: String): Key? {
val key = "aesEncryptionKey"
return SecretKeySpec(key.toByteArray(), "AES")
}
}
val mockffClient = MockFeatureFlagClient()
val engine = EncryptionEngine(
sharedPreferences = MockSharedPreferences(),
"Alias",
context,
mockffClient,
)

engine.ksKeyStore = mockKSKeyStore

val textForEncryption = "This my text that will be encrypted!"

engine.set(textForEncryption)
val decrypted = engine.get()

assertEquals(textForEncryption, decrypted)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,33 @@ import com.kickstarter.R
import org.junit.Test
import org.robolectric.RuntimeEnvironment
import java.util.Locale
import javax.crypto.spec.SecretKeySpec

class StringExtKtTest : KSRobolectricTestCase() {

@Test
fun testEncryptDecryptString() {
val textForEncryption = "This my text that will be encrypted!"
val key = "aesEncryptionKey"
val secretKey = SecretKeySpec(key.toByteArray(), "AES")
val encryptedString = textForEncryption.encrypt(secretKey = secretKey) ?: ""
val decrypted = encryptedString.decrypt(secretKey) ?: ""

assertEquals(textForEncryption, decrypted)
assertTrue(decrypted.isNotEmpty())
}

@Test
fun testEncryptDecryptTokenFormat() {
val textForEncryption = "003718603ff4a25887d83157bd11d39f0c7501f0"
val key = "aesEncryptionKey"
val secretKey = SecretKeySpec(key.toByteArray(), "AES")
val encryptedString = textForEncryption.encrypt(secretKey = secretKey) ?: ""
val decrypted = encryptedString.decrypt(secretKey = secretKey) ?: ""

assertEquals(textForEncryption, decrypted)
assertTrue(decrypted.isNotEmpty())
}
@Test
fun isEmail_whenGivenEmail_shouldReturnTrue() {
assertTrue(VALID_EMAIL.isEmail())
Expand Down

0 comments on commit 037f96d

Please sign in to comment.