From 21baa083b75dec6e22e10eee4a6e55b580c81984 Mon Sep 17 00:00:00 2001 From: Tiago Nascimento Date: Fri, 7 Jun 2024 11:01:15 -0300 Subject: [PATCH] Add initial storage manager Signed-off-by: Tiago Nascimento --- MobileSdk/build.gradle.kts | 1 + .../com/spruceid/mobile/sdk/StorageManager.kt | 149 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index 830db05..62fb571 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -131,6 +131,7 @@ dependencies { implementation("androidx.camera:camera-mlkit-vision:1.3.0-alpha06") implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.0") /* End UI dependencies */ + implementation("androidx.datastore:datastore-preferences:1.1.1") testImplementation("junit:junit:4.13.2") androidTestImplementation("com.android.support.test:runner:1.0.2") androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2") diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt new file mode 100644 index 0000000..d997809 --- /dev/null +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt @@ -0,0 +1,149 @@ +import android.content.Context +import android.util.Base64 +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStoreFile +import com.spruceid.wallet.sdk.KeyManager +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map + +private class DataStoreSingleton private constructor(context: Context) { + val dataStore: DataStore = store(context, "default") + + companion object { + private const val FILENAME_PREFIX = "datastore_" + + private fun location(context: Context, file: String) = + context.preferencesDataStoreFile(FILENAME_PREFIX + file.lowercase()) + + private fun store(context: Context, file: String): DataStore = + PreferenceDataStoreFactory.create(produceFile = { location(context, file) }) + + @Volatile + private var instance: DataStoreSingleton? = null + + fun getInstance(context: Context) = + instance ?: synchronized(this) { + instance ?: DataStoreSingleton(context).also { instance = it } + } + } +} + +object StorageManager { + private val flags = Base64.URL_SAFE xor Base64.NO_PADDING xor Base64.NO_WRAP + + /// Function: encrypt + /// + /// Encrypts the given string. + /// + /// Arguments: + /// value - The string value to be encrypted + private fun encrypt(value: String): Result { + val keyManager = KeyManager() + try { + if (!keyManager.keyExists("datastore")) { + keyManager.generateEncryptionKey("datastore") + } + val encrypted = keyManager.encryptPayload("datastore", value.toByteArray()) + val iv = Base64.encodeToString(encrypted.first, flags) + val bytes = Base64.encodeToString(encrypted.second, flags) + val res = "$iv;$bytes".toByteArray() + return Result.success(res) + } catch (e: Exception) { + return Result.failure(e) + } + } + + /// Function: decrypt + /// + /// Decrypts the given byte array. + /// + /// Arguments: + /// value - The byte array to be decrypted + private fun decrypt(value: ByteArray): Result { + val keyManager = KeyManager() + try { + if (!keyManager.keyExists("datastore")) { + return Result.failure(Exception("Cannot retrieve values before creating encryption keys")) + } + val decoded = value.decodeToString().split(";") + assert(decoded.size == 2) + val iv = Base64.decode(decoded.first(), flags) + val encrypted = Base64.decode(decoded.last(), flags) + val decrypted = keyManager.decryptPayload("datastore", iv, encrypted) + ?: return Result.failure(Exception("Failed to decrypt value")) + return Result.success(decrypted.decodeToString()) + } catch (e: Exception) { + return Result.failure(e) + } + } + + /// Function: add + /// + /// Adds a key-value pair to storage. Should the key already exist, the value will be + /// replaced. + /// + /// Arguments: + /// context - The application context to be able to access the DataStore + /// key - The key to add + /// value - The value to add under the key + suspend fun add(context: Context, key: String, value: String): Result { + val storeKey = byteArrayPreferencesKey(key) + val storeValue = encrypt(value) + + if (storeValue.isFailure) { + return Result.failure(Exception("Failed to encrypt value for storage")) + } + + DataStoreSingleton.getInstance(context).dataStore.edit { store -> + store[storeKey] = storeValue.getOrThrow() + } + + return Result.success(Unit) + } + + /// Function: get + /// + /// Retrieves the value from storage identified by key. + /// + /// Arguments: + /// context - The application context to be able to access the DataStore + /// key - The key to retrieve + suspend fun get(context: Context, key: String): Result { + val storeKey = byteArrayPreferencesKey(key) + return DataStoreSingleton.getInstance(context).dataStore.data.map { store -> + try { + store[storeKey]?.let { v -> + val storeValue = decrypt(v) + when { + storeValue.isSuccess -> Result.success(storeValue.getOrThrow()) + storeValue.isFailure -> Result.failure(storeValue.exceptionOrNull()!!) + else -> Result.failure(Exception("Failed to decrypt value for storage")) + } + } ?: Result.success(null) + } catch (e: Exception) { + Result.failure(e) + } + }.catch { exception -> + emit(Result.failure(exception)) + }.first() + } + + /// Function: remove + /// + /// Removes a key-value pair from storage by key. + /// + /// Arguments: + /// context - The application context to be able to access the DataStore + /// key - The key to remove + suspend fun remove(context: Context, key: String): Result { + val storeKey = stringPreferencesKey(key) + DataStoreSingleton.getInstance(context).dataStore.edit { store -> + if (store.contains(storeKey)) { + store.remove(storeKey) + } + } + return Result.success(Unit) + } +}