From 087dd1df9fb30c862bcc399651b906e350dda19e Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 22 Oct 2024 15:34:30 +0100 Subject: [PATCH] Implement StorageManagerInterface (#44) Use File API rather than Preference Datastore --- MobileSdk/build.gradle.kts | 2 +- .../spruceid/mobile/sdk/IsoMdlPresentation.kt | 2 +- .../com/spruceid/mobile/sdk/KeyManager.kt | 2 +- .../com/spruceid/mobile/sdk/StorageManager.kt | 197 +++++++----------- 4 files changed, 74 insertions(+), 129 deletions(-) diff --git a/MobileSdk/build.gradle.kts b/MobileSdk/build.gradle.kts index a459b9f..374b73d 100644 --- a/MobileSdk/build.gradle.kts +++ b/MobileSdk/build.gradle.kts @@ -120,7 +120,7 @@ android { } dependencies { - api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.0.36") + api("com.spruceid.mobile.sdk.rs:mobilesdkrs:0.1.0") //noinspection GradleCompatible implementation("com.android.support:appcompat-v7:28.0.0") /* Begin UI dependencies */ diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt index 2151915..e36b091 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/IsoMdlPresentation.kt @@ -27,7 +27,7 @@ class IsoMdlPresentation( var itemsRequests: List = listOf() var bleManager: Transport? = null - suspend fun initialize() { + fun initialize() { try { session = initializeMdlPresentationFromBytes(this.mdoc, uuid.toString()) this.bleManager = Transport(this.bluetoothManager) diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/KeyManager.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/KeyManager.kt index 4a6c020..9b37188 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/KeyManager.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/KeyManager.kt @@ -304,7 +304,7 @@ class KeyManager { * @property payload to be encrypted. * @return the decrypted payload. */ - fun decryptPayload(id: String, iv: ByteArray, payload: ByteArray): ByteArray? { + fun decryptPayload(id: String, iv: ByteArray, payload: ByteArray): ByteArray { val secretKey = getSecretKey(id) val cipher = Cipher.getInstance("AES/GCM/NoPadding") val spec = GCMParameterSpec(128, iv) diff --git a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt index a95e5fb..369a857 100644 --- a/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt +++ b/MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt @@ -1,142 +1,32 @@ 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.mobile.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 = "sprucekit/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 const val B64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP - private const val KEY_NAME = "sprucekit/datastore" - - /// 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(KEY_NAME)) { - keyManager.generateEncryptionKey(KEY_NAME) - } - val encrypted = keyManager.encryptPayload(KEY_NAME, value.toByteArray()) - val iv = Base64.encodeToString(encrypted.first, B64_FLAGS) - val bytes = Base64.encodeToString(encrypted.second, B64_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(KEY_NAME)) { - 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(), B64_FLAGS) - val encrypted = Base64.decode(decoded.last(), B64_FLAGS) - val decrypted = - keyManager.decryptPayload(KEY_NAME, iv, encrypted) - ?: return Result.failure(Exception("Failed to decrypt value")) - return Result.success(decrypted.decodeToString()) - } catch (e: Exception) { - return Result.failure(e) - } - } +import com.spruceid.mobile.sdk.rs.StorageManagerInterface +import java.io.File +class StorageManager(val context: Context) : StorageManagerInterface { /// 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) + override fun add(key: String, value: ByteArray) = + context.openFileOutput(filename(key), 0).use { it.write(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() + override fun get(key: String): ByteArray { + val bytes = ByteArray(0) + context.openFileInput(filename(key)).use { it.read(bytes) } + return decrypt(bytes) } /// Function: remove @@ -144,15 +34,70 @@ object StorageManager { /// 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) + override fun remove(key: String) { + File(context.filesDir, filename(key)).delete() + } + + + /// Function: list + /// + /// Lists all key-value pair in storage + override fun list(): List { + val list = context.filesDir.list() ?: throw Exception("cannot list stored objects") + + return list.mapNotNull { + if (it.startsWith(FILENAME_PREFIX)) { + it.substring(FILENAME_PREFIX.length + 1) + } else { + null } } - return Result.success(Unit) + } + + + companion object { + private const val B64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + private const val KEY_NAME = "sprucekit/datastore" + + /// Function: encrypt + /// + /// Encrypts the given string. + /// + /// Arguments: + /// value - The string value to be encrypted + private fun encrypt(value: ByteArray): ByteArray { + val keyManager = KeyManager() + if (!keyManager.keyExists(KEY_NAME)) { + keyManager.generateEncryptionKey(KEY_NAME) + } + val encrypted = keyManager.encryptPayload(KEY_NAME, value) + val iv = Base64.encodeToString(encrypted.first, B64_FLAGS) + val bytes = Base64.encodeToString(encrypted.second, B64_FLAGS) + val res = "$iv;$bytes".toByteArray() + return res + } + + /// Function: decrypt + /// + /// Decrypts the given byte array. + /// + /// Arguments: + /// value - The byte array to be decrypted + private fun decrypt(value: ByteArray): ByteArray { + val keyManager = KeyManager() + if (!keyManager.keyExists(KEY_NAME)) { + throw Exception("Cannot retrieve values before creating encryption keys") + } + val decoded = value.decodeToString().split(";") + assert(decoded.size == 2) + val iv = Base64.decode(decoded.first(), B64_FLAGS) + val encrypted = Base64.decode(decoded.last(), B64_FLAGS) + return keyManager.decryptPayload(KEY_NAME, iv, encrypted) + } + + private const val FILENAME_PREFIX = "sprucekit:datastore" + + private fun filename(filename: String) = "$FILENAME_PREFIX:$filename" } }