-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement StorageManagerInterface (#44)
Use File API rather than Preference Datastore
- Loading branch information
Showing
4 changed files
with
74 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
197 changes: 71 additions & 126 deletions
197
MobileSdk/src/main/java/com/spruceid/mobile/sdk/StorageManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |