From 4dbeb00128180dc93ce9c1009d0b3b184b52b5f8 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 --- WalletSdk/build.gradle.kts | 1 + .../com/spruceid/wallet/sdk/StorageManager.kt | 137 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 WalletSdk/src/main/java/com/spruceid/wallet/sdk/StorageManager.kt diff --git a/WalletSdk/build.gradle.kts b/WalletSdk/build.gradle.kts index f9d3630..2466220 100644 --- a/WalletSdk/build.gradle.kts +++ b/WalletSdk/build.gradle.kts @@ -129,6 +129,7 @@ dependencies { implementation("com.google.zxing:core:3.3.3") implementation("com.google.accompanist:accompanist-permissions:0.34.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/WalletSdk/src/main/java/com/spruceid/wallet/sdk/StorageManager.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/StorageManager.kt new file mode 100644 index 0000000..1e4494e --- /dev/null +++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/StorageManager.kt @@ -0,0 +1,137 @@ +import android.content.Context +import android.util.Base64 +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStoreFile +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 = "cadmv-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 { + + /// Function: encrypt + /// + /// Encrypts the given string. + /// + /// Arguments: + /// value - The string value to be encrypted + private fun encrypt(value: String): Result { + try { + // TODO: Use KeyManager.encrypt or equivalent + val encoded = Base64.encode( + value.toByteArray(), Base64.URL_SAFE xor Base64.NO_PADDING xor Base64.NO_WRAP + ) + return Result.success(encoded) + } 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 { + try { + // TODO: Use KeyManager.encrypt or equivalent + val decoded = Base64.decode( + value, Base64.URL_SAFE xor Base64.NO_PADDING xor Base64.NO_WRAP + ) + return Result.success(decoded.decodeToString()) + } catch (e: Exception) { + return Result.failure(Exception()) + } + } + + /// 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) + } +}