Skip to content
Snippets Groups Projects
Commit f922d858 authored by Kamal Bramwell's avatar Kamal Bramwell
Browse files

Implemented preferences module in session

parent 3838acb5
No related branches found
No related tags found
No related merge requests found
Showing
with 214 additions and 121 deletions
...@@ -14,6 +14,12 @@ android { ...@@ -14,6 +14,12 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
buildConfigField(
"double",
"APP_VERSION",
android.defaultConfig.versionName ?: "1.0"
)
} }
buildTypes { buildTypes {
......
package io.elixxir.core.common package io.elixxir.core.common
import com.google.firebase.crashlytics.ktx.crashlytics import kotlinx.coroutines.*
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import timber.log.Timber
interface Config { interface Config {
val defaultDispatcher: CoroutineDispatcher val dispatcher: CoroutineDispatcher
fun log(message: String, export: Boolean = false) fun log(message: String, export: Boolean = false)
fun logException(e: Exception, export: Boolean = true) fun logException(e: Exception, export: Boolean = true)
}
object DefaultConfig : Config {
override val defaultDispatcher: CoroutineDispatcher
get() = Dispatchers.IO
override fun log(message: String, export: Boolean) { fun newScopeNamed(name: String): CoroutineScope =
Timber.d(message) CoroutineScope(
if (export) Firebase.crashlytics.log(message) CoroutineName(name)
+ Job()
+ Dispatchers.Default
)
} }
override fun logException(e: Exception, export: Boolean) {
Timber.e(e)
if (export) Firebase.crashlytics.recordException(e)
}
}
\ No newline at end of file
package io.elixxir.core.common
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import timber.log.Timber
object DefaultConfig : Config {
override val dispatcher: CoroutineDispatcher
get() = Dispatchers.IO
override fun log(message: String, export: Boolean) {
Timber.d(message)
if (export) Firebase.crashlytics.log(message)
}
override fun logException(e: Exception, export: Boolean) {
Timber.e(e)
if (export) Firebase.crashlytics.recordException(e)
}
}
\ No newline at end of file
package io.elixxir.core.common
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import timber.log.Timber
fun log(message: String, reportToFirebase: Boolean = false) {
Timber.d(message)
if (reportToFirebase) Firebase.crashlytics.log(message)
}
fun logException(e: Exception, reportToFirebase: Boolean = true) {
Timber.e(e)
if (reportToFirebase) Firebase.crashlytics.recordException(e)
}
\ No newline at end of file
package io.elixxir.core.common
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
fun NotExposedYet(): Nothing = TODO("Not exposed in Bindings yet")
fun newScopeNamed(name: String): CoroutineScope =
CoroutineScope(
CoroutineName(name)
+ Job()
+ Dispatchers.Default
)
\ No newline at end of file
package io.elixxir.core.common.util
import io.elixxir.core.common.Config
/**
* Convenience method for returning a Result<T> wrapped in a try/catch.
*/
inline fun <T> Config.resultOf(block: () -> T): Result<T> {
return try {
Result.success(block())
} catch (e: Exception) {
logException(e)
Result.failure(e)
}
}
package io.elixxir.core.preferences
import android.content.SharedPreferences
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
class EncrpytedPreferences {
private val masterKeyAlias = "xx_preferences_key"
private val preferencesAlias = "xx_preferences"
private val masterKeySpec = KeyGenParameterSpec.Builder(
masterKeyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setDigests(KeyProperties.DIGEST_SHA256)
.setRandomizedEncryptionRequired(true)
.setKeySize(256)
.build()
private val masterKey =
MasterKey.Builder(context, masterKeyAlias).setKeyGenParameterSpec(masterKeySpec).build()
var preferences: SharedPreferences = EncryptedSharedPreferences.create(
context,
preferencesAlias,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
\ No newline at end of file
package io.elixxir.core.preferences
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import io.elixxir.core.preferences.model.KeyStorePreferences
import io.elixxir.core.preferences.model.KeyStorePrefs
@Module
@InstallIn(SingletonComponent::class)
interface PreferencesModule {
@Binds
fun bindKeyStorePreferences(
prefs: KeyStorePrefs
): KeyStorePreferences
}
\ No newline at end of file
package io.elixxir.core.preferences package io.elixxir.core.preferences
import io.elixxir.core.preferences.model.KeyStorePreferences
interface PreferencesRepository { interface PreferencesRepository {
val keyStore: KeyStorePreferences
} }
\ No newline at end of file
//package io.elixxir.core.preferences.di
//
//import dagger.Binds
//import dagger.Module
//import dagger.hilt.InstallIn
//import dagger.hilt.android.components.ViewModelComponent
//
//@Module
//@InstallIn(ViewModelComponent::class)
//interface PreferencesModule {
//
// @Binds
// fun bindPreferencesRepository(
//
// ): PreferencesRepository
//}
\ No newline at end of file
package io.elixxir.core.preferences.model
import javax.inject.Inject
interface KeyStorePreferences {
var userSecret: String
}
class KeyStorePrefs @Inject internal constructor() : KeyStorePreferences {
override var userSecret: String = ""
}
\ No newline at end of file
...@@ -12,6 +12,6 @@ interface SessionModule { ...@@ -12,6 +12,6 @@ interface SessionModule {
@Binds @Binds
fun bindSessionRepository( fun bindSessionRepository(
repo: SessionDataSource repo: SessionDataSource,
): SessionRepository ): SessionRepository
} }
\ No newline at end of file
package io.elixxir.data.session package io.elixxir.data.session
import io.elixxir.data.session.model.SessionState import io.elixxir.data.session.model.SessionState
import io.elixxir.xxclient.cmix.CMix
interface SessionRepository { interface SessionRepository {
fun getSessionState(): SessionState fun getSessionState(): SessionState
fun createSession() suspend fun decryptSessionPassword(): Result<ByteArray>
fun restoreSession() suspend fun getOrCreateSession(): Result<CMix>
fun deleteSession() suspend fun restoreSession()
suspend fun deleteSession(): Result<Unit>
} }
\ No newline at end of file
package io.elixxir.data.session.data
interface CipherPreferences {
var userSecret: String
}
class CipherPrefs() : CipherPreferences {
override var userSecret: String = ""
}
\ No newline at end of file
package io.elixxir.data.session.data package io.elixxir.data.session.data
interface KeyStoreManager { internal interface KeyStoreManager {
suspend fun generatePassword(): Result<Unit> suspend fun generatePassword(): Result<Unit>
suspend fun decryptPassword(): Result<ByteArray> suspend fun decryptPassword(): Result<ByteArray>
} }
\ No newline at end of file
package io.elixxir.data.session.data package io.elixxir.data.session.data
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.elixxir.core.common.Config
import io.elixxir.core.common.util.resultOf
import io.elixxir.data.session.SessionRepository import io.elixxir.data.session.SessionRepository
import io.elixxir.data.session.model.SessionState import io.elixxir.data.session.model.SessionState
import kotlinx.coroutines.Dispatchers import io.elixxir.xxclient.bindings.Bindings
import kotlinx.coroutines.launch import io.elixxir.xxclient.cmix.CMix
import kotlinx.coroutines.withContext
import java.io.File
import javax.inject.Inject import javax.inject.Inject
class SessionDataSource @Inject internal constructor() : SessionRepository { class SessionDataSource @Inject internal constructor(
override fun getSessionState(): SessionState { @ApplicationContext private val context: Context,
TODO("Not yet implemented") private val bindings: Bindings,
private val keyStore: KeyStoreManager,
config: Config,
) : SessionRepository, Config by config {
private var cmix: CMix? = null
private val sessionFolder : File =
File(context.filesDir, SESSION_FOLDER_PATH).apply {
log("Session folder location: $absolutePath")
} }
override fun createSession() { override fun getSessionState(): SessionState =
TODO("Not yet implemented") if (sessionFolder.exists()) SessionState.ExistingUser
else SessionState.NewUser
override suspend fun decryptSessionPassword(): Result<ByteArray> =
keyStore.decryptPassword()
override suspend fun getOrCreateSession(): Result<CMix> = resultOf {
if (getSessionState() == SessionState.NewUser) createSession()
else getSession()
} }
override fun restoreSession() { private suspend fun createSession(): CMix {
TODO("Not yet implemented") keyStore.generatePassword().getOrThrow()
val appFolder = createSessionFolder()
return bindings.loadCmix(
sessionFileDirectory = appFolder.path,
sessionPassword = decryptSessionPassword().getOrThrow(),
cmixParams = bindings.defaultCmixParams
)
}
private suspend fun loadCmix(): CMix = bindings.loadCmix(
sessionFileDirectory = sessionFolder.path,
sessionPassword = decryptSessionPassword().getOrThrow(),
cmixParams = bindings.defaultCmixParams
).also {
cmix = it
} }
override fun deleteSession() { private suspend fun createSessionFolder(): File = withContext(dispatcher) {
deleteSession().getOrThrow()
sessionFolder.apply {
mkdir()
log("Bindings folder was successfully created at: $absolutePath")
}
}
private suspend fun getSession(): CMix = cmix ?: loadCmix()
override suspend fun restoreSession() {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
private fun getOrCreateSession() { override suspend fun deleteSession(): Result<Unit> {
scope.launch(Dispatchers.IO) { return resultOf {
val appFolder = repo.createSessionFolder(context) sessionFolder.apply {
try { if (exists()) {
repo.newClient(appFolder, sessionPassword) log("Session from previous installation was found.")
preferences.lastAppVersion = BuildConfig.VERSION_CODE log("It contains ${listFiles()?.size ?: 0} files.")
connectToCmix() log("Deleting!")
} catch (err: Exception) { deleteRecursively()
err.printStackTrace() }
displayError(err.toString())
} }
Result.success(Unit)
} }
} }
companion object {
private const val SESSION_FOLDER_PATH = "xxmessenger/session"
}
} }
\ No newline at end of file
...@@ -3,37 +3,38 @@ package io.elixxir.data.session.data ...@@ -3,37 +3,38 @@ package io.elixxir.data.session.data
import android.os.Build import android.os.Build
import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import io.elixxir.core.common.log import io.elixxir.core.common.Config
import io.elixxir.core.preferences.model.KeyStorePreferences
import io.elixxir.data.session.util.fromBase64toByteArray import io.elixxir.data.session.util.fromBase64toByteArray
import io.elixxir.data.session.util.toBase64String import io.elixxir.data.session.util.toBase64String
import io.elixxir.xxclient.bindings.Bindings import io.elixxir.xxclient.bindings.Bindings
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.IllegalStateException
import java.security.* import java.security.*
import java.security.spec.MGF1ParameterSpec import java.security.spec.MGF1ParameterSpec
import java.security.spec.RSAKeyGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec
import javax.crypto.Cipher import javax.crypto.Cipher
import javax.crypto.spec.OAEPParameterSpec import javax.crypto.spec.OAEPParameterSpec
import javax.crypto.spec.PSource import javax.crypto.spec.PSource
import javax.inject.Inject
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
internal class XxmKeystore( class XxmKeyStore @Inject internal constructor(
private val bindings: Bindings, private val bindings: Bindings,
private val preferences: CipherPreferences, private val prefs: KeyStorePreferences,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO config: Config,
) : KeyStoreManager { ) : KeyStoreManager, Config by config {
private val keystore: KeyStore by lazy { private val keyStore: KeyStore by lazy {
KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
} }
private val publicKey: PublicKey by lazy { private val publicKey: PublicKey by lazy {
keystore.getCertificate(KEY_ALIAS).publicKey keyStore.getCertificate(KEY_ALIAS).publicKey
} }
private val privateKey: PrivateKey? private val privateKey: PrivateKey?
get() = keystore.getKey(KEY_ALIAS, null) as PrivateKey? get() = keyStore.getKey(KEY_ALIAS, null) as PrivateKey?
private val keyPairGenerator: KeyPairGenerator by lazy { private val keyPairGenerator: KeyPairGenerator by lazy {
KeyPairGenerator.getInstance( KeyPairGenerator.getInstance(
...@@ -81,9 +82,9 @@ internal class XxmKeystore( ...@@ -81,9 +82,9 @@ internal class XxmKeystore(
} }
private fun deletePreviousKeys() { private fun deletePreviousKeys() {
if (keystore.containsAlias(KEY_ALIAS)) { if (keyStore.containsAlias(KEY_ALIAS)) {
log("Deleting key alias") log("Deleting key alias")
keystore.deleteEntry(KEY_ALIAS) keyStore.deleteEntry(KEY_ALIAS)
} }
} }
...@@ -111,7 +112,7 @@ internal class XxmKeystore( ...@@ -111,7 +112,7 @@ internal class XxmKeystore(
cipher.init(Cipher.ENCRYPT_MODE, publicKey, cipherMode) cipher.init(Cipher.ENCRYPT_MODE, publicKey, cipherMode)
val encryptedBytes = cipher.doFinal(pwd) val encryptedBytes = cipher.doFinal(pwd)
log("Encrypted: ${encryptedBytes.toBase64String()}") log("Encrypted: ${encryptedBytes.toBase64String()}")
preferences.userSecret = encryptedBytes.toBase64String() prefs.userSecret = encryptedBytes.toBase64String()
return encryptedBytes return encryptedBytes
} }
...@@ -125,7 +126,11 @@ internal class XxmKeystore( ...@@ -125,7 +126,11 @@ internal class XxmKeystore(
} }
private fun decryptSecret(): ByteArray { private fun decryptSecret(): ByteArray {
val encryptedBytes = preferences.userSecret.fromBase64toByteArray() if (prefs.userSecret.isBlank()) {
throw IllegalStateException("Key has not been saved yet!")
}
val encryptedBytes = prefs.userSecret.fromBase64toByteArray()
val cipher = Cipher.getInstance(KEYSTORE_ALGORITHM) val cipher = Cipher.getInstance(KEYSTORE_ALGORITHM)
log("Initializing Decrypt") log("Initializing Decrypt")
cipher.init(Cipher.DECRYPT_MODE, privateKey, cipherMode) cipher.init(Cipher.DECRYPT_MODE, privateKey, cipherMode)
......
...@@ -14,12 +14,6 @@ android { ...@@ -14,12 +14,6 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro") consumerProguardFiles("consumer-rules.pro")
buildConfigField(
"double",
"APP_VERSION",
android.defaultConfig.versionName ?: "1.0"
)
} }
buildTypes { buildTypes {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment