Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/issue 42 local storage encryption #47

Merged
merged 26 commits into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
706998f
added storage module
Basler182 Jun 16, 2024
ceb5395
added PreferenceKey
Basler182 Jun 16, 2024
da62f80
added build.gradle.kts
Basler182 Jun 16, 2024
4dbe08d
added SecureFileStorage
Basler182 Jun 16, 2024
071bdd1
tested delete
Basler182 Jun 16, 2024
aad0b49
added Storage interface
Basler182 Jun 16, 2024
e220cf9
added coroutine and testing dependency
Basler182 Jun 16, 2024
4de8d19
added LocalStorage
Basler182 Jun 16, 2024
3fb00eb
added EncryptedSharedPreferencesStorage and tested it
Basler182 Jun 16, 2024
0a8533b
added documentation
Basler182 Jun 16, 2024
e1a109d
removed not needed lines
Basler182 Jun 16, 2024
0f0dc04
added libs.versions.toml
Basler182 Jun 16, 2024
70006b6
added libs.versions.toml version
Basler182 Jun 16, 2024
d4b703d
Merge branch 'main' into feature/issue-42-local-storage-encryption
Basler182 Jun 21, 2024
278fecd
Merge branch 'main' into feature/issue-42-local-storage-encryption
Basler182 Jun 21, 2024
47c0bc9
Merge branch 'main' into feature/issue-42-local-storage-encryption
Basler182 Jun 22, 2024
0efb34d
sorted libs.versions.toml
Basler182 Jun 22, 2024
bb38c9c
fixed README.MD
Basler182 Jun 22, 2024
ab0d4a2
refactored to UnconfinedTestDispatcher
Basler182 Jun 22, 2024
d77a989
removed no longer needed dependencies
Basler182 Jun 22, 2024
f1d3718
removed not needed runBlocking
Basler182 Jun 22, 2024
4b2f01b
refactored to firstOrNull
Basler182 Jun 22, 2024
6ee145d
caught datastore.data
Basler182 Jun 22, 2024
58b191d
added delete file after each test
Basler182 Jun 23, 2024
cf52b3e
added runCatching Wrappings to FileStorage
Basler182 Jun 23, 2024
08bbd29
removed not supported type
Basler182 Jun 23, 2024
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
8 changes: 7 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ compileSdk = "34"
composeBom = "2024.05.00"
composeNavigation = "2.8.0-alpha08"
coreKtx = "1.13.1"
coreKtxVersion = "1.5.0"
coreTestingVersion = "2.2.0"
coroutinesVersion = "1.8.0"
credentialsPlayServicesAuth = "1.2.2"
datastorePreferences = "1.1.1"
detekt = "1.23.6" # please adjust github action version as well in case of version change
credentialsPlayServicesAuth = "1.2.2"
Basler182 marked this conversation as resolved.
Show resolved Hide resolved
dokka = "1.9.20"
espressoCore = "3.5.1"
firebaseAuthKtx = "23.0.0"
Expand All @@ -38,6 +40,7 @@ mockKVersion = "1.13.10"
playServicesAuth = "21.2.0"
rulesVersion = "1.5.0"
runnerVersion = "1.5.2"
securityCryptoKtx = "1.1.0-alpha06"
targetSdk = "34"
testCoreVersion = "1.5.0"
timberVersion = "5.0.1"
Expand All @@ -54,6 +57,7 @@ androidx-health-connect-client = { module = "androidx.health.connect:connect-cli
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "coreTestingVersion" }
androidx-credentials-play-services-auth = { module = "androidx.credentials:credentials-play-services-auth", version.ref = "credentialsPlayServicesAuth" }
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
Expand All @@ -62,6 +66,7 @@ androidx-lifecycle-view-model-ktx = { group = "androidx.lifecycle", name = "life
androidx-test-core = { group = "androidx.test", name = "core", version.ref = "testCoreVersion" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "runnerVersion" }
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "rulesVersion" }
androidx-security-crypto-ktx = { group = "androidx.security", name = "security-crypto-ktx", version.ref = "securityCryptoKtx" }
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
Expand All @@ -71,6 +76,7 @@ compose-ui-test = { group = "androidx.compose.ui", name = "ui-test-junit4" }
compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
core-ktx = { group = "androidx.test", name = "core-ktx", version.ref = "coreKtxVersion" }
coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutinesVersion" }
coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutinesVersion" }
coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "coroutinesVersion" }
Expand Down
1 change: 1 addition & 0 deletions modules/storage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
80 changes: 80 additions & 0 deletions modules/storage/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Module storage

The storage module provides components for managing storage in your application. It includes classes
for handling key-value/file storage and secure storage of data.

## Usage

To use the Storage module in your project, add the following dependency to your `build.gradle` file:

```gradle
dependencies {
implementation(":core:storage")`
Basler182 marked this conversation as resolved.
Show resolved Hide resolved
}
```

and provide the wanted storage implementation with Hilt DI. There are the following storage
implementations:

- `EncryptedFileStorage` for the `FileStorage` interface
- `EncryptedSharedPreferencesStorage` for the `KeyValueStorage` interface
- `LocalStorage` for the `KeyValueStorage` interface

## Key-Value Storage

The key-value storage provides a simple interface for storing and retrieving key-value pairs:

```kotlin
interface KeyValueStorage {
suspend fun <T : Any> saveData(key: PreferenceKey<T>, data: T)
fun <T> readData(key: PreferenceKey<T>): Flow<T?>
suspend fun <T> readDataBlocking(key: PreferenceKey<T>): T?
suspend fun <T> deleteData(key: PreferenceKey<T>)
}
```

It can be used like this:

```kotlin
val stringKey = PreferenceKey.StringKey("user_name")
keyValueStorage.saveData(stringKey, "test_user_name")
keyValueStorage.readDataBlocking(stringKey)?.let {
println("Read string data blocking: $it")
}
keyValueStorage.deleteData(stringKey)
```

or you can use the `Flow` interface to observe changes:

```kotlin
val job = launch {
keyValueStorage.readData(stringKey).collect { data: String? ->
println("Read string data: $data")
}
}
```

## File Storage

The file storage provides a simple interface for storing and retrieving files:

```kotlin
interface FileStorage {
suspend fun readFile(fileName: String): ByteArray?
suspend fun deleteFile(fileName: String)
suspend fun saveFile(fileName: String, data: ByteArray)
}
```

It can be used like this:

```kotlin
val fileName = "testFile.data"
val data = "Hello, Stanford!".toByteArray()
fileStorage.saveFile(fileName, data)
val readData = fileStorage.readFile(fileName)
readData?.let {
println("Read file data: ${String(it)}")
}
fileStorage.deleteFile(fileName)
```
26 changes: 26 additions & 0 deletions modules/storage/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
plugins {
alias(libs.plugins.spezi.library)
alias(libs.plugins.spezi.hilt)
}

android {
namespace = "edu.stanford.spezi.modules.storage"
}

dependencies {
androidTestImplementation(libs.bundles.compose.androidTest)
Basler182 marked this conversation as resolved.
Show resolved Hide resolved

androidTestImplementation(project(":core:testing"))

implementation(project(":core:coroutines"))

implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.security.crypto.ktx)
implementation(libs.core.ktx)

implementation(project(":core:utils"))

testImplementation(libs.bundles.unit.testing)

testImplementation(project(":core:testing"))
Basler182 marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package edu.stanford.spezi.modules.storage.file

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import edu.stanford.spezi.core.testing.runTestUnconfined
import kotlinx.coroutines.Dispatchers
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class EncryptedFileKeyValueStorageTest {
private var context: Context = ApplicationProvider.getApplicationContext()
private var fileStorage: FileStorage =
EncryptedFileStorage(
context = context,
ioDispatcher = Dispatchers.Unconfined,
Basler182 marked this conversation as resolved.
Show resolved Hide resolved
)

@Test
fun `it should save and read file correctly`() = runTestUnconfined {
// Given
val fileName = "testFile"
val data = "Hello, World!".toByteArray()

// When
fileStorage.saveFile(fileName, data)

// Then
val readData = fileStorage.readFile(fileName)
assertThat(readData).isEqualTo(data)
}

@Test
fun `it should return null when reading non-existent file`() = runTestUnconfined {
// Given
val fileName = "nonExistentFile"

// When
val readData = fileStorage.readFile(fileName)

// Then
assertThat(readData).isNull()
}

@Test
fun `it should overwrite existing file when saving with same filename`() = runTestUnconfined {
// Given
val fileName = "testFile"
val initialData = "Hello, World!".toByteArray()
val newData = "New data".toByteArray()

// When
fileStorage.saveFile(fileName, initialData)
fileStorage.saveFile(fileName, newData)
Basler182 marked this conversation as resolved.
Show resolved Hide resolved

// Then
val readData = fileStorage.readFile(fileName)
assertThat(readData).isEqualTo(newData)
}

@Test
fun `it should delete file correctly`() = runTestUnconfined {
// Given
val fileName = "testFile"
val data = "Hello, World!".toByteArray()
fileStorage.saveFile(fileName, data)

// When
fileStorage.deleteFile(fileName)

// Then
val readData = fileStorage.readFile(fileName)
assertThat(readData).isNull()
}
}
Loading
Loading