Skip to content

Commit

Permalink
Implement StorageManagerInterface (#44)
Browse files Browse the repository at this point in the history
Use File API rather than Preference Datastore
  • Loading branch information
cobward authored Oct 22, 2024
1 parent 6b039d4 commit 087dd1d
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 129 deletions.
2 changes: 1 addition & 1 deletion MobileSdk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class IsoMdlPresentation(
var itemsRequests: List<ItemsRequest> = listOf()
var bleManager: Transport? = null

suspend fun initialize() {
fun initialize() {
try {
session = initializeMdlPresentationFromBytes(this.mdoc, uuid.toString())
this.bleManager = Transport(this.bluetoothManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
197 changes: 71 additions & 126 deletions MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt
Original file line number Diff line number Diff line change
@@ -1,158 +1,103 @@
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)
}
}
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<Unit> {
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<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()
override fun get(key: String): ByteArray {
val bytes = ByteArray(0)
context.openFileInput(filename(key)).use { it.read(bytes) }
return decrypt(bytes)
}

/// 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)
override fun remove(key: String) {
File(context.filesDir, filename(key)).delete()
}


/// Function: list
///
/// Lists all key-value pair in storage
override fun list(): List<String> {
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"
}
}

0 comments on commit 087dd1d

Please sign in to comment.