Skip to content

Commit

Permalink
Add initial storage manager (#14)
Browse files Browse the repository at this point in the history
* Add initial storage manager

Signed-off-by: Tiago Nascimento <[email protected]>

* Addressing review comments

Signed-off-by: Tiago Nascimento <[email protected]>

* Fix reference to wallet.sdk.KeyManager

---------

Signed-off-by: Tiago Nascimento <[email protected]>
Co-authored-by: Ross Schulman <[email protected]>
  • Loading branch information
theosirian and rschulman authored Sep 16, 2024
1 parent 3e5df43 commit 236f0a0
Show file tree
Hide file tree
Showing 2 changed files with 159 additions and 0 deletions.
1 change: 1 addition & 0 deletions MobileSdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
158 changes: 158 additions & 0 deletions MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
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<Preferences> = 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<Preferences> =
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<ByteArray> {
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<String> {
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)
}
}

/// 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<Unit> {
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<String?> {
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<Unit> {
val storeKey = stringPreferencesKey(key)
DataStoreSingleton.getInstance(context).dataStore.edit { store ->
if (store.contains(storeKey)) {
store.remove(storeKey)
}
}
return Result.success(Unit)
}
}

0 comments on commit 236f0a0

Please sign in to comment.