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

SpeziStorage - Review and API proposals #123

Merged
merged 14 commits into from
Oct 21, 2024

Conversation

eldcn
Copy link
Contributor

@eldcn eldcn commented Oct 13, 2024

SpeziStorage - Review and API proposals

What was done

We have aligned with @pauljohanneskraft that I will bring my proposals in the former PR #108 in a new PR.

  • In previous PRs we already implemented KeyValueStorage which provides an API to write encrypted data for primitive types in SharedPreferences. This component now got customized to write in shared preferences in encrypted and unencrypted way via KeyValueStorageFactory#create(fileName, KeyValueStorageType). Consumer of the API have the opportunity to either use the default storages, or create a custom one via the factory. Furthermore, since we were using data store in the previous version of LocalKeyValueStorage, it got removed now and all the functions of the key value storage are not suspending anymore - This is safe as shared preferences caches all the changes in memory first, and writes in the disc asynchronously.
  • SecureStorage -
image
  • This component was breaking single responsibility principle in my opinion. On one hand, it was providing methods to store, read, delete and update Credentials, on the other hand was providing some methods to create public and private keys from key store (key chain in iOS). Key related methods however, were solely used in the context of LocalStorage when using LocalStorageSetting.EncryptedUsingKeyStore setting to store a data in a file.
    • Removes key related from SecureStorage and solely provides CRUD methods to it to handle Credentials. Furthermore, the api got extended to retrieve all credentials of a user, and delete credentials per user and per server separately. Under the hood, the SecureStorage uses an encrypted KeyValueStorage that persists the encrypted json of Credentials object.
    • I would propose to rename this component to CredentialsSecureStorage or CredentialsStorage, but it requires alignment with iOS
    • image
    • Introduces public component AndroidKeyStore which can be used to create public/private key pairs which then can later on be used in the context of LocalStorageSetting.Encrypted(KeyPair)
    • image
    • Each method of SecureStorage was requesting server as a separate parameter, while it in my opinion belongs to the Credentials type - A hint comes also from this swiftlin disable. Hence, Credentials now receives a nullable server property as well.
    • SecureStorageItemType was indicating three cases KEY; SERVER_CREDENTIALS; NON_SERVER_CREDENTIALS;, however KEY was never used in the context of SecureStorage. Furthermore, the storage is offering a method to deleteAllCredentials(SecureStorageItemType), if KEY would be part of the types, we would be all the PrivateKey and PublicKeys of the keystore, which is a side effect and a buggy behaviour. I removed KEY from SecureStorageItemType which complies semantically with Credentials type, which can either have a server property or not (null).
  • LocalStorage - Keeps similar API as iOS by using kotlin serialization
  • image
  • Every public component of the library is provides as an interface, and the corresponding implementation as an internal component which is bound by default in hilt graph in StorageModule.kt. StorageModule (hilt module) serves at the same time also as the public api of the module itself.
  • All components have been tested
  • A recording of the public API and generated files and their content:
spezi-storage.mp4

⚙️ Release Notes

Add a bullet point list summary of the feature and possible migration guides if this is a breaking change so this section can be added to the release notes.
Include code snippets that provide examples of the feature implemented or links to the documentation if it appends or changes the public interface.

📚 Documentation

Please ensure that you properly document any additions in conformance to Spezi Documentation Guide.
You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.

✅ Testing

Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.
This section describes important information about the tests and why some elements might not be testable.

📝 Code of Conduct & Contributing Guidelines

By submitting creating this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

Copy link

codecov bot commented Oct 13, 2024

Codecov Report

Attention: Patch coverage is 77.73109% with 53 lines in your changes missing coverage. Please review.

Project coverage is 39.59%. Comparing base (6bd38de) to head (e90f910).
Report is 3 commits behind head on feature/spezi-storage.

Files with missing lines Patch % Lines
...ford/spezi/modules/storage/secure/SecureStorage.kt 0.00% 23 Missing ⚠️
...tanford/spezi/modules/storage/secure/KeyStorage.kt 75.00% 3 Missing and 8 partials ⚠️
.../spezi/modules/storage/secure/CredentialStorage.kt 80.00% 5 Missing and 2 partials ⚠️
...d/spezi/modules/storage/key/KeyValueStorageImpl.kt 90.33% 1 Missing and 2 partials ⚠️
...anford/spezi/modules/storage/local/LocalStorage.kt 94.00% 0 Missing and 3 partials ⚠️
...ezi/modules/storage/key/InMemoryKeyValueStorage.kt 71.43% 2 Missing ⚠️
...i/core/bluetooth/domain/BLEPairedDevicesStorage.kt 75.00% 0 Missing and 1 partial ⚠️
...stanford/spezi/modules/storage/di/StorageModule.kt 90.00% 1 Missing ⚠️
...d/spezi/modules/storage/key/KeyValueStorageType.kt 66.67% 1 Missing ⚠️
...ord/spezi/modules/storage/secure/CredentialType.kt 66.67% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@                     Coverage Diff                     @@
##             feature/spezi-storage     #123      +/-   ##
===========================================================
+ Coverage                    33.54%   39.59%   +6.05%     
- Complexity                     666      766     +100     
===========================================================
  Files                          234      262      +28     
  Lines                         9670    10533     +863     
  Branches                      1360     1511     +151     
===========================================================
+ Hits                          3243     4169     +926     
+ Misses                        6191     5997     -194     
- Partials                       236      367     +131     
Flag Coverage Δ
uitests 33.55% <77.78%> (?)
unittests 33.60% <75.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...pezi/modules/storage/key/KeyValueStorageFactory.kt 100.00% <100.00%> (ø)
...spezi/modules/storage/local/LocalStorageSetting.kt 100.00% <100.00%> (ø)
...tanford/spezi/modules/storage/secure/Credential.kt 100.00% <100.00%> (ø)
...i/core/bluetooth/domain/BLEPairedDevicesStorage.kt 98.42% <75.00%> (+1.23%) ⬆️
...stanford/spezi/modules/storage/di/StorageModule.kt 90.91% <90.00%> (ø)
...d/spezi/modules/storage/key/KeyValueStorageType.kt 66.67% <66.67%> (ø)
...ord/spezi/modules/storage/secure/CredentialType.kt 66.67% <66.67%> (ø)
...ezi/modules/storage/key/InMemoryKeyValueStorage.kt 93.34% <71.43%> (ø)
...d/spezi/modules/storage/key/KeyValueStorageImpl.kt 90.33% <90.33%> (ø)
...anford/spezi/modules/storage/local/LocalStorage.kt 92.73% <94.00%> (ø)
... and 3 more

... and 37 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 6bd38de...e90f910. Read the comment docs.

Copy link
Contributor

@Basler182 Basler182 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job. I like the code, I just added a few small comments. @pauljohanneskraft could certainly do a more detailed review regarding the iOS API adaptation.

putString(sharedPreferencesKey(server, credentials.username), credentials.password)
override fun retrieveUserCredentials(username: String): List<Credentials> {
return storage.allKeys().mapNotNull { key ->
if (key.substringAfter(SERVER_USERNAME_SEPARATOR) == username) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider outsourcing this functionality to a method, as it occurs several times here

private fun isUserKey(key: String, username: String): Boolean = key.substringAfter(SERVER_USERNAME_SEPARATOR) == username

}
}

private fun createCipher(mode: Int, key: Key): Cipher =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could renaming createCipher to e.g. getInitalizedCipher make its purpose of create and initalize clearer?

sharedPreferences.edit { putFloat(key, value) }
}

override fun getByteArray(key: String, default: ByteArray): ByteArray {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why getByteArray doesnt exists without the default value?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really, since it is using string under the hood and shared prefs provides nullable get for string, added the non default one for bytearray as well now 👍


override fun getByteArray(key: String, default: ByteArray): ByteArray {
val encoded = sharedPreferences.getString(key, null)
return encoded?.let { Base64.decode(it, Base64.DEFAULT) } ?: default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could it make sense to pay attention to whether an error might occur during decoding? Like:

override fun getByteArray(key: String, default: ByteArray): ByteArray {
    val encoded = sharedPreferences.getString(key, null)
    return try {
        encoded?.let { Base64.decode(it, Base64.DEFAULT) } ?: default
    } catch (e: IllegalArgumentException) {
        logger.e {}
        default
    }
}

Throws: IllegalArgumentException – if the input contains incorrect padding

)

KeyValueStorageType.ENCRYPTED -> {
val masterKey = MasterKey
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could think about lazy initalize and reuse the key, since its an expansive operation.

private val masterKey: MasterKey by lazy {
    MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()
}

)

// when
val storedLetter = localStorage.read<Letter>(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the syntax we got by those changes. 🚀

@pauljohanneskraft pauljohanneskraft merged commit 49d7f04 into feature/spezi-storage Oct 21, 2024
7 checks passed
@PSchmiedmayer
Copy link
Member

Thank you @eldcn @Basler182 and @pauljohanneskraft for working on the API alignments and merging this!

@pauljohanneskraft pauljohanneskraft deleted the storage/review branch October 21, 2024 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants