Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Implement StorageManagerInterface #44

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
}
}
Loading