From 317944b21b637ea76b425e9cdb85c52a0b9a0abd Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 31 May 2023 15:49:47 +0200 Subject: [PATCH 01/55] Minimum example for field level encryption --- CuratedSyncExamples/.gitignore | 15 ++ CuratedSyncExamples/app/.gitignore | 1 + CuratedSyncExamples/app/build.gradle.kts | 52 +++++ CuratedSyncExamples/app/proguard-rules.pro | 21 ++ .../curatedsyncexamples/RealmKeyStoreTests.kt | 43 ++++ .../io/realm/curatedsyncexamples/UserMock.kt | 60 ++++++ .../app/src/main/AndroidManifest.xml | 26 +++ .../realm/curatedsyncexamples/MainActivity.kt | 73 +++++++ .../fieldencryption/EncryptionUtils.kt | 22 +++ .../fieldencryption/Models.kt | 75 +++++++ .../fieldencryption/UserExt.kt | 16 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 18 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values-night/themes.xml | 7 + .../app/src/main/res/values/colors.xml | 5 + .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/themes.xml | 9 + .../app/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../curatedsyncexamples/ExampleUnitTest.kt | 17 ++ CuratedSyncExamples/build.gradle.kts | 6 + CuratedSyncExamples/gradle.properties | 23 +++ .../gradle/wrapper/gradle-wrapper.properties | 6 + CuratedSyncExamples/gradlew | 185 ++++++++++++++++++ CuratedSyncExamples/gradlew.bat | 89 +++++++++ CuratedSyncExamples/keystore/.gitignore | 1 + CuratedSyncExamples/keystore/build.gradle.kts | 48 +++++ .../keystore/consumer-rules.pro | 0 .../keystore/proguard-rules.pro | 21 ++ .../java/io/realm/keystore/CipherSpecTests.kt | 48 +++++ .../io/realm/keystore/RealmKeyStoreTests.kt | 62 ++++++ .../java/io/realm/keystore/Utils.kt | 69 +++++++ .../keystore/src/main/AndroidManifest.xml | 4 + .../main/java/io/realm/keystore/CipherSpec.kt | 10 + .../java/io/realm/keystore/CypherSpecExt.kt | 31 +++ .../src/main/java/io/realm/keystore/Models.kt | 15 ++ .../java/io/realm/keystore/RealmKeyStore.kt | 59 ++++++ .../RealmKeyStoreEncryptionService.kt | 54 +++++ CuratedSyncExamples/settings.gradle.kts | 17 ++ 52 files changed, 1455 insertions(+) create mode 100644 CuratedSyncExamples/.gitignore create mode 100644 CuratedSyncExamples/app/.gitignore create mode 100644 CuratedSyncExamples/app/build.gradle.kts create mode 100644 CuratedSyncExamples/app/proguard-rules.pro create mode 100644 CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt create mode 100644 CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/UserMock.kt create mode 100644 CuratedSyncExamples/app/src/main/AndroidManifest.xml create mode 100644 CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/MainActivity.kt create mode 100644 CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/EncryptionUtils.kt create mode 100644 CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/Models.kt create mode 100644 CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/UserExt.kt create mode 100644 CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 CuratedSyncExamples/app/src/main/res/layout/activity_main.xml create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 CuratedSyncExamples/app/src/main/res/values-night/themes.xml create mode 100644 CuratedSyncExamples/app/src/main/res/values/colors.xml create mode 100644 CuratedSyncExamples/app/src/main/res/values/strings.xml create mode 100644 CuratedSyncExamples/app/src/main/res/values/themes.xml create mode 100644 CuratedSyncExamples/app/src/main/res/xml/backup_rules.xml create mode 100644 CuratedSyncExamples/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 CuratedSyncExamples/app/src/test/java/io/realm/curatedsyncexamples/ExampleUnitTest.kt create mode 100644 CuratedSyncExamples/build.gradle.kts create mode 100644 CuratedSyncExamples/gradle.properties create mode 100644 CuratedSyncExamples/gradle/wrapper/gradle-wrapper.properties create mode 100755 CuratedSyncExamples/gradlew create mode 100644 CuratedSyncExamples/gradlew.bat create mode 100644 CuratedSyncExamples/keystore/.gitignore create mode 100644 CuratedSyncExamples/keystore/build.gradle.kts create mode 100644 CuratedSyncExamples/keystore/consumer-rules.pro create mode 100644 CuratedSyncExamples/keystore/proguard-rules.pro create mode 100644 CuratedSyncExamples/keystore/src/androidTest/java/io/realm/keystore/CipherSpecTests.kt create mode 100644 CuratedSyncExamples/keystore/src/androidTest/java/io/realm/keystore/RealmKeyStoreTests.kt create mode 100644 CuratedSyncExamples/keystore/src/androidTest/java/io/realm/keystore/Utils.kt create mode 100644 CuratedSyncExamples/keystore/src/main/AndroidManifest.xml create mode 100644 CuratedSyncExamples/keystore/src/main/java/io/realm/keystore/CipherSpec.kt create mode 100644 CuratedSyncExamples/keystore/src/main/java/io/realm/keystore/CypherSpecExt.kt create mode 100644 CuratedSyncExamples/keystore/src/main/java/io/realm/keystore/Models.kt create mode 100644 CuratedSyncExamples/keystore/src/main/java/io/realm/keystore/RealmKeyStore.kt create mode 100644 CuratedSyncExamples/keystore/src/main/java/io/realm/keystore/RealmKeyStoreEncryptionService.kt create mode 100644 CuratedSyncExamples/settings.gradle.kts diff --git a/CuratedSyncExamples/.gitignore b/CuratedSyncExamples/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/CuratedSyncExamples/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/CuratedSyncExamples/app/.gitignore b/CuratedSyncExamples/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/CuratedSyncExamples/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/CuratedSyncExamples/app/build.gradle.kts b/CuratedSyncExamples/app/build.gradle.kts new file mode 100644 index 0000000..f2ebdc2 --- /dev/null +++ b/CuratedSyncExamples/app/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("io.realm.kotlin") version "1.9.0" + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.21" +} + +android { + namespace = "io.realm.curatedsyncexamples" + compileSdk = 33 + + defaultConfig { + applicationId = "io.realm.curatedsyncexamples" + minSdk = 28 + targetSdk = 33 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility(JavaVersion.VERSION_1_8) + targetCompatibility(JavaVersion.VERSION_1_8) + } + kotlinOptions { + jvmTarget = "1.8" + } +} + +dependencies { + implementation(project(":keystore")) + implementation("androidx.core:core-ktx:1.10.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0") + implementation("io.realm.kotlin:library-sync:1.9.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/CuratedSyncExamples/app/proguard-rules.pro b/CuratedSyncExamples/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/CuratedSyncExamples/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt b/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt new file mode 100644 index 0000000..fe946a9 --- /dev/null +++ b/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt @@ -0,0 +1,43 @@ +package io.realm.curatedsyncexamples + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.random.Random + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class EncryptionKeySpecTests { + private val keySpec = EncryptionKeySpec( + algorithm = "PBKDF2WithHmacSHA256", + salt = Random.nextBytes(16), + iterationsCount = 100000, + keyLength = 128, + ) + + @Test + fun generatePBKDF2Key() { + // Generating two keys from the same password should result in the same value + val expectedKey = keySpec.generateKey("hello world") + val matchingKey = keySpec.generateKey("hello world") + + assertArrayEquals(expectedKey.encoded, matchingKey.encoded) + + // Two keys from two different passwords should be different + val unmatchingKey = keySpec.generateKey("hello world2") + assertArrayNotEquals(expectedKey.encoded, unmatchingKey.encoded) + } +} + +fun assertArrayNotEquals(expected: ByteArray?, actual: ByteArray?) { + try { + assertArrayEquals(expected, actual) + } catch (_: AssertionError) { + // Ignore + } +} \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/UserMock.kt b/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/UserMock.kt new file mode 100644 index 0000000..1252acc --- /dev/null +++ b/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/UserMock.kt @@ -0,0 +1,60 @@ +package io.realm.curatedsyncexamples + +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.AuthenticationProvider +import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.Functions +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.UserIdentity +import io.realm.kotlin.mongodb.auth.ApiKeyAuth + +class UserMock( + override val id: String +) : User { + override val accessToken: String + get() = TODO("Not yet implemented") + override val apiKeyAuth: ApiKeyAuth + get() = TODO("Not yet implemented") + override val app: App + get() = TODO("Not yet implemented") + override val deviceId: String + get() = TODO("Not yet implemented") + override val functions: Functions + get() = TODO("Not yet implemented") + override val identities: List + get() = TODO("Not yet implemented") + override val identity: String + get() = TODO("Not yet implemented") + override val loggedIn: Boolean + get() = TODO("Not yet implemented") + override val provider: AuthenticationProvider + get() = TODO("Not yet implemented") + override val refreshToken: String + get() = TODO("Not yet implemented") + override val state: User.State + get() = TODO("Not yet implemented") + + override suspend fun delete() { + TODO("Not yet implemented") + } + + override fun equals(other: Any?): Boolean { + TODO("Not yet implemented") + } + + override suspend fun linkCredentials(credentials: Credentials): User { + TODO("Not yet implemented") + } + + override suspend fun logOut() { + TODO("Not yet implemented") + } + + override suspend fun refreshCustomData() { + TODO("Not yet implemented") + } + + override suspend fun remove(): User { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/AndroidManifest.xml b/CuratedSyncExamples/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..416186b --- /dev/null +++ b/CuratedSyncExamples/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/MainActivity.kt b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/MainActivity.kt new file mode 100644 index 0000000..29f1fd5 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/MainActivity.kt @@ -0,0 +1,73 @@ +package io.realm.curatedsyncexamples + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import io.realm.curatedsyncexamples.fieldencryption.Dog +import io.realm.curatedsyncexamples.fieldencryption.EncryptedStringField +import io.realm.curatedsyncexamples.fieldencryption.cipherSpec +import io.realm.curatedsyncexamples.fieldencryption.computeHash +import io.realm.curatedsyncexamples.fieldencryption.fieldCipherSpec +import io.realm.curatedsyncexamples.fieldencryption.fieldEncryptionKeySpec +import io.realm.curatedsyncexamples.fieldencryption.generateKey +import io.realm.curatedsyncexamples.fieldencryption.hash +import io.realm.curatedsyncexamples.fieldencryption.key +import io.realm.keystore.RealmKeyStore +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import kotlin.random.Random + +class MainActivity : AppCompatActivity() { + lateinit var app: App + private val keyStore = RealmKeyStore() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + runBlocking { + app = App.create("cypher-scjvs") + + val email = "${Random.nextLong()}aaabb@bbcccc.com" + val password = "123456" + app.emailPasswordAuth.registerUser(email, password) + val user = app.login(Credentials.emailPassword(email, password)) + + val key = user.fieldEncryptionKeySpec().generateKey(password) + cipherSpec = user.fieldCipherSpec() + + RealmKeyStore().setFieldLevelEncryptionKey(user, key) + } + + // Get hold of the key + key = keyStore.getFieldLevelEncryptionKey(app.currentUser!!) + hash = key.computeHash() + + val realm = Realm.open( + SyncConfiguration + .Builder(app.currentUser!!, setOf(Dog::class, EncryptedStringField::class)) + .initialSubscriptions { + add(it.query()) + } + .build() + ) + + val dog = realm.writeBlocking { + val dog = copyToRealm(Dog()) + + dog.name?.let { name -> + name.value = "hello world" + } + + val ev = dog.name!!.encryptedValue + + val name = dog.name!!.value + } + } +} + + + diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/EncryptionUtils.kt b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/EncryptionUtils.kt new file mode 100644 index 0000000..518ddda --- /dev/null +++ b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/EncryptionUtils.kt @@ -0,0 +1,22 @@ +package io.realm.curatedsyncexamples.fieldencryption + +import java.security.Key +import java.security.MessageDigest +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec + +const val HASH_ALGORITHM = "SHA-256" + +fun EncryptionKeySpec.generateKey(password: String): SecretKey = + PBEKeySpec( + /* password = */ password.toCharArray(), + /* salt = */ salt, + /* iterationCount = */ iterationsCount, + /* keyLength = */ keyLength + ).let { + SecretKeyFactory.getInstance(algorithm).generateSecret(it) + } + +fun Key.computeHash(): ByteArray = + MessageDigest.getInstance(HASH_ALGORITHM).digest(encoded) diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/Models.kt b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/Models.kt new file mode 100644 index 0000000..ac936ed --- /dev/null +++ b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/Models.kt @@ -0,0 +1,75 @@ +package io.realm.curatedsyncexamples.fieldencryption + +import io.realm.keystore.CipherSpec +import io.realm.keystore.decrypt +import io.realm.keystore.encrypt +import io.realm.kotlin.types.EmbeddedRealmObject +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.Ignore +import io.realm.kotlin.types.annotations.PrimaryKey +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import org.mongodb.kbson.BsonObjectId +import java.nio.charset.StandardCharsets +import java.security.Key +import kotlin.reflect.KProperty + +lateinit var cipherSpec: CipherSpec +lateinit var key: Key +lateinit var hash: ByteArray + +@Serializable +data class EncryptionKeySpec( + val algorithm: String, + val salt: ByteArray, + @SerialName("iterations_count") + val iterationsCount: Int, + @SerialName("key_length") + val keyLength: Int, +) + +@Serializable +data class CustomData( + @SerialName("field_encryption_key_spec") + val fieldEncryptionKeySpec: EncryptionKeySpec?, + @SerialName("encryption_transformation") + val cipherSpec: CipherSpec? +) + +class Dog : RealmObject { + @PrimaryKey + var _id: BsonObjectId = BsonObjectId() + var name: EncryptedStringField? = EncryptedStringField() +} + +class EncryptedStringField : EmbeddedRealmObject { + var keyHash: ByteArray = byteArrayOf() + var encryptedValue: ByteArray = byteArrayOf() + + @Ignore + var value: String by DecryptionDelegate() + + inner class DecryptionDelegate { + private fun EncryptedStringField.isEncryptionKeyValid() = + keyHash.contentEquals(hash) + + operator fun getValue(thisRef: EncryptedStringField, property: KProperty<*>): String = + if (!thisRef.isEncryptionKeyValid()) "Wrong encryption key" + else String( + bytes = cipherSpec.decrypt(thisRef.encryptedValue, key), + charset = StandardCharsets.UTF_8 + ) + + operator fun setValue( + thisRef: EncryptedStringField, + property: KProperty<*>, + value: String + ) { + thisRef.keyHash = hash + thisRef.encryptedValue = cipherSpec.encrypt( + input = value.toByteArray(StandardCharsets.UTF_8), + key = key + ) + } + } +} \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/UserExt.kt b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/UserExt.kt new file mode 100644 index 0000000..80650f5 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/UserExt.kt @@ -0,0 +1,16 @@ +package io.realm.curatedsyncexamples.fieldencryption + +import io.realm.keystore.CipherSpec +import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.ext.customData + +@OptIn(ExperimentalRealmSerializerApi::class) +fun User.fieldEncryptionKeySpec(): EncryptionKeySpec { + return customData()?.fieldEncryptionKeySpec!! +} + +@OptIn(ExperimentalRealmSerializerApi::class) +fun User.fieldCipherSpec(): CipherSpec { + return customData()?.cipherSpec!! +} diff --git a/CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml b/CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CuratedSyncExamples/app/src/main/res/layout/activity_main.xml b/CuratedSyncExamples/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..59c0484 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/CuratedSyncExamples/app/src/main/res/values-night/themes.xml b/CuratedSyncExamples/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a866263 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/values/colors.xml b/CuratedSyncExamples/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/values/strings.xml b/CuratedSyncExamples/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..c49dc38 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Curated sync examples + \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/values/themes.xml b/CuratedSyncExamples/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..180c115 --- /dev/null +++ b/CuratedSyncExamples/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + - \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/res/values/themes.xml b/CuratedSyncExamples/app/src/main/res/values/themes.xml index 180c115..9b4464e 100644 --- a/CuratedSyncExamples/app/src/main/res/values/themes.xml +++ b/CuratedSyncExamples/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ - diff --git a/CuratedSyncExamples/build.gradle.kts b/CuratedSyncExamples/build.gradle.kts index 63081a9..f55768e 100644 --- a/CuratedSyncExamples/build.gradle.kts +++ b/CuratedSyncExamples/build.gradle.kts @@ -1,6 +1,6 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id("com.android.application") version "8.0.0" apply false - id("com.android.library") version "8.0.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.21" apply false + alias(libs.plugins.android.application) apply false + alias(libs.plugins.android.library) apply false + alias(libs.plugins.kotlin.android) apply false } \ No newline at end of file diff --git a/CuratedSyncExamples/gradle/libs.versions.toml b/CuratedSyncExamples/gradle/libs.versions.toml new file mode 100644 index 0000000..b62e928 --- /dev/null +++ b/CuratedSyncExamples/gradle/libs.versions.toml @@ -0,0 +1,44 @@ +[versions] +kotlin = "1.8.21" +androidGradlePlugin = "8.0.0" +realm = "1.9.0" +kotlinxSerializationJson="1.5.0" +androidxAppCompat="1.6.1" +androidxComposeBom = "2023.01.00" +androidxNavigation = "2.5.3" +androidxLifecycle = "2.6.1" +koin = "3.4.2" +koinCompose = "3.4.5" +junit4 = "4.13.2" +androidxEspresso = "3.5.1" +androidxTestExt = "1.1.5" +kotlinxCoroutines = "1.6.4" +androidxTestRunner = "1.5.1" +[libraries] +androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } +androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" } +androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata" } +androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } +androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidxNavigation" } +androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidxLifecycle" } +androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } +androidx-compose-ui-tooling-core = { group = "androidx.compose.ui", name = "ui-tooling" } +androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +koin-android = { group = "io.insert-koin", name = "koin-android",version.ref = "koin"} +koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose" ,version.ref = "koinCompose"} +realm-library-sync = { group = "io.realm.kotlin", name = "library-sync" ,version.ref = "realm"} +junit4 = { group = "junit", name = "junit", version.ref = "junit4" } +androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } +androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" } + +kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } +androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } + +[plugins] +android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } +android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +realm-kotlin = { id = "io.realm.kotlin", version.ref = "realm" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file From 8c316861d1a5c64e0ba4972a2ee90217fdc5d907 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 7 Jun 2023 23:30:33 +0200 Subject: [PATCH 11/55] Add server app --- .../auth/custom_user_data.json | 8 ++ .../apps/field-encryption/auth/providers.json | 24 +++++ .../data_sources/mongodb-atlas/config.json | 10 +++ .../mongodb-atlas/default_rule.json | 21 +++++ .../custom_data/relationships.json | 1 + .../field_encryption/custom_data/schema.json | 82 ++++++++++++++++++ .../secret_record/relationships.json | 1 + .../secret_record/schema.json | 28 ++++++ .../environments/development.json | 3 + .../environments/no-environment.json | 3 + .../environments/production.json | 3 + .../field-encryption/environments/qa.json | 3 + .../environments/testing.json | 3 + .../field-encryption/functions/config.json | 10 +++ .../functions/initializeEncryptionKey.js | 44 ++++++++++ .../functions/updateKeyStore.js | 23 +++++ .../apps/field-encryption/graphql/config.json | 3 + .../http_endpoints/config.json | 1 + .../apps/field-encryption/realm_config.json | 8 ++ .../apps/field-encryption/sync/config.json | 16 ++++ CuratedSyncExamples/{app => demo}/.gitignore | 0 .../{app => demo}/build.gradle.kts | 0 .../{app => demo}/proguard-rules.pro | 0 .../curatedsyncexamples/KeyHelperTests.kt | 0 .../curatedsyncexamples/RealmKeyStoreTests.kt | 0 .../src/main/AndroidManifest.xml | 0 .../CuratedSyncExamplesApp.kt | 0 .../ExampleSelectorActivity.kt | 0 .../curatedsyncexamples/fieldencryption/DI.kt | 2 +- .../FieldEncryptionActivity.kt | 0 .../fieldencryption/ext/EncryptionExt.kt | 0 .../fieldencryption/ext/UserExt.kt | 0 .../models/AndroidKeyStoreHelper.kt | 0 .../fieldencryption/models/CipherSpec.kt | 0 .../fieldencryption/models/CustomData.kt | 0 .../models/EncryptedStringField.kt | 0 .../models/EncryptionKeySpec.kt | 0 .../fieldencryption/models/SecretRecord.kt | 1 + .../models/SerializableSecretKey.kt | 0 .../fieldencryption/models/UserKeyStore.kt | 0 .../fieldencryption/ui/NavGraph.kt | 17 +--- .../ui/keystore/KeyStoreScreen.kt | 0 .../ui/keystore/KeyStoreViewModel.kt | 0 .../fieldencryption/ui/login/LoginScreen.kt | 0 .../ui/login/LoginViewModel.kt | 0 .../ui/records/SecretRecordsScreen.kt | 0 .../ui/records/SecretRecordsViewModel.kt | 0 .../curatedsyncexamples/ui/theme/Color.kt | 0 .../curatedsyncexamples/ui/theme/Theme.kt | 0 .../curatedsyncexamples/ui/theme/Type.kt | 0 .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../main/res/drawable/realmio_logo_vector.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../src/main/res/values/strings.xml | 0 .../src/main/res/values/themes.xml | 0 .../src/main/res/xml/backup_rules.xml | 0 .../main/res/xml/data_extraction_rules.xml | 0 CuratedSyncExamples/gradle/libs.versions.toml | 5 +- CuratedSyncExamples/settings.gradle.kts | 2 +- 71 files changed, 302 insertions(+), 20 deletions(-) create mode 100644 CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json create mode 100644 CuratedSyncExamples/apps/field-encryption/auth/providers.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json create mode 100644 CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json create mode 100644 CuratedSyncExamples/apps/field-encryption/environments/development.json create mode 100644 CuratedSyncExamples/apps/field-encryption/environments/no-environment.json create mode 100644 CuratedSyncExamples/apps/field-encryption/environments/production.json create mode 100644 CuratedSyncExamples/apps/field-encryption/environments/qa.json create mode 100644 CuratedSyncExamples/apps/field-encryption/environments/testing.json create mode 100644 CuratedSyncExamples/apps/field-encryption/functions/config.json create mode 100644 CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js create mode 100644 CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js create mode 100644 CuratedSyncExamples/apps/field-encryption/graphql/config.json create mode 100644 CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json create mode 100644 CuratedSyncExamples/apps/field-encryption/realm_config.json create mode 100644 CuratedSyncExamples/apps/field-encryption/sync/config.json rename CuratedSyncExamples/{app => demo}/.gitignore (100%) rename CuratedSyncExamples/{app => demo}/build.gradle.kts (100%) rename CuratedSyncExamples/{app => demo}/proguard-rules.pro (100%) rename CuratedSyncExamples/{app => demo}/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt (100%) rename CuratedSyncExamples/{app => demo}/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/AndroidManifest.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt (96%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt (97%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt (86%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/drawable/ic_launcher_background.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/drawable/realmio_logo_vector.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/values/strings.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/values/themes.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/xml/backup_rules.xml (100%) rename CuratedSyncExamples/{app => demo}/src/main/res/xml/data_extraction_rules.xml (100%) diff --git a/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json b/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json new file mode 100644 index 0000000..5b2140d --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json @@ -0,0 +1,8 @@ +{ + "enabled": true, + "mongo_service_name": "mongodb-atlas", + "database_name": "field_encryption", + "collection_name": "custom_data", + "user_id_field": "owner_id", + "on_user_creation_function_name": "initializeEncryptionKey" +} diff --git a/CuratedSyncExamples/apps/field-encryption/auth/providers.json b/CuratedSyncExamples/apps/field-encryption/auth/providers.json new file mode 100644 index 0000000..68fc5ed --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/auth/providers.json @@ -0,0 +1,24 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": true + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + }, + "local-userpass": { + "name": "local-userpass", + "type": "local-userpass", + "config": { + "autoConfirm": true, + "resetPasswordSubject": "Dummy password reset settings", + "resetPasswordUrl": "https://www.dummy-domain.com/reset", + "runConfirmationFunction": false, + "runResetFunction": false + }, + "disabled": false + } +} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json new file mode 100644 index 0000000..9913676 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json @@ -0,0 +1,10 @@ +{ + "name": "mongodb-atlas", + "type": "mongodb-atlas", + "config": { + "clusterName": "Cluster0", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json new file mode 100644 index 0000000..449d0cc --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json @@ -0,0 +1,21 @@ +{ + "roles": [ + { + "name": "readOwnWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "owner_id": "%%user.id" + }, + "read": { + "owner_id": "%%user.id" + } + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json @@ -0,0 +1 @@ +{} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json new file mode 100644 index 0000000..38869df --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json @@ -0,0 +1,82 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "field_encryption_cipher_spec": { + "bsonType": "object", + "properties": { + "algorithm": { + "bsonType": "string" + }, + "block": { + "bsonType": "string" + }, + "key_length": { + "bsonType": "long" + }, + "padding": { + "bsonType": "string" + } + } + }, + "key_store": { + "bsonType": "object", + "properties": { + "cipher_spec": { + "bsonType": "object", + "properties": { + "algorithm": { + "bsonType": "string" + }, + "block": { + "bsonType": "string" + }, + "key_length": { + "bsonType": "int" + }, + "padding": { + "bsonType": "string" + } + } + }, + "encryption_key_spec": { + "bsonType": "object", + "properties": { + "algorithm": { + "bsonType": "string" + }, + "iterations_count": { + "bsonType": "int" + }, + "key_length": { + "bsonType": "int" + }, + "salt": { + "bsonType": "array", + "items": { + "bsonType": "int" + } + } + } + }, + "key_hash": { + "bsonType": "array", + "items": { + "bsonType": "int" + } + }, + "secure_contents": { + "bsonType": "array", + "items": { + "bsonType": "int" + } + } + } + }, + "owner_id": { + "bsonType": "string" + } + }, + "title": "custom_datum" +} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json @@ -0,0 +1 @@ +{} diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json new file mode 100644 index 0000000..a8b3c87 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json @@ -0,0 +1,28 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "content": { + "properties": { + "encryptedValue": { + "bsonType": "binData" + } + }, + "required": [ + "encryptedValue" + ], + "title": "EncryptedStringField", + "type": "object" + }, + "owner_id": { + "bsonType": "string" + } + }, + "required": [ + "_id", + "owner_id" + ], + "title": "secret_record", + "type": "object" +} diff --git a/CuratedSyncExamples/apps/field-encryption/environments/development.json b/CuratedSyncExamples/apps/field-encryption/environments/development.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/environments/development.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/field-encryption/environments/no-environment.json b/CuratedSyncExamples/apps/field-encryption/environments/no-environment.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/environments/no-environment.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/field-encryption/environments/production.json b/CuratedSyncExamples/apps/field-encryption/environments/production.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/environments/production.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/field-encryption/environments/qa.json b/CuratedSyncExamples/apps/field-encryption/environments/qa.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/environments/qa.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/field-encryption/environments/testing.json b/CuratedSyncExamples/apps/field-encryption/environments/testing.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/environments/testing.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/field-encryption/functions/config.json b/CuratedSyncExamples/apps/field-encryption/functions/config.json new file mode 100644 index 0000000..bb30be9 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/functions/config.json @@ -0,0 +1,10 @@ +[ + { + "name": "initializeEncryptionKey", + "private": false + }, + { + "name": "updateKeyStore", + "private": false + } +] diff --git a/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js b/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js new file mode 100644 index 0000000..cfffb52 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js @@ -0,0 +1,44 @@ +const crypto = require('crypto'); + +// Executing this function would generate a new encryption key and store it in the user custom data +exports = async function (user) { + const salt = crypto.randomBytes(16); + + const customUserDataCollection = context.services + .get("mongodb-atlas") + .db("field_encryption") + .collection("custom_data"); + + try { + await customUserDataCollection.insertOne({ + // Save the user's account ID to your configured user_id_field + owner_id: user.id, + // Store any other user data you want + field_encryption_cipher_spec: { + algorithm: "AES", + block: "CBC", + padding: "PKCS7Padding", + key_length: 128 + }, + key_store: { + encryption_key_spec: { + algorithm: "PBKDF2WithHmacSHA256", + salt: BSON.Binary.fromHex(salt.toString('hex')), + iterations_count: 100000, + key_length: 128, + }, + cipher_spec: { + algorithm: "AES", + block: "CBC", + padding: "PKCS7Padding", + key_length: 128 + }, + secure_contents: null, + key_hash: null, + } + }); + } catch (e) { + console.error(`Failed to create custom user data document for user: ${user.id}`); + throw e + } +}; \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js new file mode 100644 index 0000000..759eb00 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js @@ -0,0 +1,23 @@ +exports = async function (arg) { + const customUserDataCollection = context.services + .get("mongodb-atlas") + .db("field_encryption") + .collection("custom_data"); + + try { + console.log(context.user.id) + await customUserDataCollection.updateOne( + { + // Save the user's account ID to your configured user_id_field + owner_id: context.user.id + }, + { + $set: {key_store: arg} + } + ); + return true; + } catch (e) { + console.error(`Failed to create custom user data document for user: ${context.user.id}`); + throw e + } +}; \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/graphql/config.json b/CuratedSyncExamples/apps/field-encryption/graphql/config.json new file mode 100644 index 0000000..c1d7285 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/graphql/config.json @@ -0,0 +1,3 @@ +{ + "use_natural_pluralization": true +} diff --git a/CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json b/CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json @@ -0,0 +1 @@ +[] diff --git a/CuratedSyncExamples/apps/field-encryption/realm_config.json b/CuratedSyncExamples/apps/field-encryption/realm_config.json new file mode 100644 index 0000000..4156d4e --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/realm_config.json @@ -0,0 +1,8 @@ +{ + "app_id": "field-encryption-fjrvt", + "config_version": 20210101, + "name": "field-encryption", + "location": "IE", + "provider_region": "aws-eu-west-1", + "deployment_model": "LOCAL" +} diff --git a/CuratedSyncExamples/apps/field-encryption/sync/config.json b/CuratedSyncExamples/apps/field-encryption/sync/config.json new file mode 100644 index 0000000..f2c7f14 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/sync/config.json @@ -0,0 +1,16 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": false, + "service_name": "mongodb-atlas", + "database_name": "field_encryption", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + }, + "queryable_fields_names": [ + "owner_id" + ] +} diff --git a/CuratedSyncExamples/app/.gitignore b/CuratedSyncExamples/demo/.gitignore similarity index 100% rename from CuratedSyncExamples/app/.gitignore rename to CuratedSyncExamples/demo/.gitignore diff --git a/CuratedSyncExamples/app/build.gradle.kts b/CuratedSyncExamples/demo/build.gradle.kts similarity index 100% rename from CuratedSyncExamples/app/build.gradle.kts rename to CuratedSyncExamples/demo/build.gradle.kts diff --git a/CuratedSyncExamples/app/proguard-rules.pro b/CuratedSyncExamples/demo/proguard-rules.pro similarity index 100% rename from CuratedSyncExamples/app/proguard-rules.pro rename to CuratedSyncExamples/demo/proguard-rules.pro diff --git a/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt similarity index 100% rename from CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt rename to CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt diff --git a/CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt similarity index 100% rename from CuratedSyncExamples/app/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt rename to CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt diff --git a/CuratedSyncExamples/app/src/main/AndroidManifest.xml b/CuratedSyncExamples/demo/src/main/AndroidManifest.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/AndroidManifest.xml rename to CuratedSyncExamples/demo/src/main/AndroidManifest.xml diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt similarity index 96% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt index 81ccfe3..f0c324e 100644 --- a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt @@ -34,5 +34,5 @@ val fieldEncryptionModule = module { viewModel { NavGraphViewModel(get(), keyAlias) } - single { App.create("cypher-scjvs") } + single { App.create("field-encryption-fjrvt") } } \ No newline at end of file diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt similarity index 97% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt index 77e546d..93065da 100644 --- a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.types.annotations.PersistedName import io.realm.kotlin.types.annotations.PrimaryKey import org.mongodb.kbson.BsonObjectId +@PersistedName("secret_record") class SecretRecord : RealmObject { @PersistedName("owner_id") var ownerId: String = "" diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt similarity index 86% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt index e1b92eb..0617494 100644 --- a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt @@ -94,32 +94,21 @@ fun NavGraph( class NavigationActions(private val navController: NavHostController) { fun navigateToKeyStoreUnlockScreen() { navController.navigate(Screens.KEYSTORE_PASSWORD_SCREEN) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } + navController.popBackStack() launchSingleTop = true } } fun navigateToSecretRecordsScreen() { navController.navigate(Screens.SECRET_RECORDS_SCREEN) { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - inclusive = true - } + navController.popBackStack() launchSingleTop = true } } fun navigateToLogin() { navController.navigate(Screens.LOGIN_SCREEN) { - popUpTo(Screens.SECRET_RECORDS_SCREEN) { - inclusive = true - saveState = true - } + navController.popBackStack() launchSingleTop = true } } diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt diff --git a/CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt similarity index 100% rename from CuratedSyncExamples/app/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt diff --git a/CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/CuratedSyncExamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to CuratedSyncExamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml b/CuratedSyncExamples/demo/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/drawable/ic_launcher_background.xml rename to CuratedSyncExamples/demo/src/main/res/drawable/ic_launcher_background.xml diff --git a/CuratedSyncExamples/app/src/main/res/drawable/realmio_logo_vector.xml b/CuratedSyncExamples/demo/src/main/res/drawable/realmio_logo_vector.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/drawable/realmio_logo_vector.xml rename to CuratedSyncExamples/demo/src/main/res/drawable/realmio_logo_vector.xml diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/app/src/main/res/values/strings.xml b/CuratedSyncExamples/demo/src/main/res/values/strings.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/values/strings.xml rename to CuratedSyncExamples/demo/src/main/res/values/strings.xml diff --git a/CuratedSyncExamples/app/src/main/res/values/themes.xml b/CuratedSyncExamples/demo/src/main/res/values/themes.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/values/themes.xml rename to CuratedSyncExamples/demo/src/main/res/values/themes.xml diff --git a/CuratedSyncExamples/app/src/main/res/xml/backup_rules.xml b/CuratedSyncExamples/demo/src/main/res/xml/backup_rules.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/xml/backup_rules.xml rename to CuratedSyncExamples/demo/src/main/res/xml/backup_rules.xml diff --git a/CuratedSyncExamples/app/src/main/res/xml/data_extraction_rules.xml b/CuratedSyncExamples/demo/src/main/res/xml/data_extraction_rules.xml similarity index 100% rename from CuratedSyncExamples/app/src/main/res/xml/data_extraction_rules.xml rename to CuratedSyncExamples/demo/src/main/res/xml/data_extraction_rules.xml diff --git a/CuratedSyncExamples/gradle/libs.versions.toml b/CuratedSyncExamples/gradle/libs.versions.toml index b62e928..4d171a8 100644 --- a/CuratedSyncExamples/gradle/libs.versions.toml +++ b/CuratedSyncExamples/gradle/libs.versions.toml @@ -30,15 +30,12 @@ koin-android = { group = "io.insert-koin", name = "koin-android",version.ref = " koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose" ,version.ref = "koinCompose"} realm-library-sync = { group = "io.realm.kotlin", name = "library-sync" ,version.ref = "realm"} junit4 = { group = "junit", name = "junit", version.ref = "junit4" } -androidx-test-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidxEspresso" } androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.ref = "androidxTestExt" } - kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" } - [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } realm-kotlin = { id = "io.realm.kotlin", version.ref = "realm" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/CuratedSyncExamples/settings.gradle.kts b/CuratedSyncExamples/settings.gradle.kts index 4a9720d..67ff8b3 100644 --- a/CuratedSyncExamples/settings.gradle.kts +++ b/CuratedSyncExamples/settings.gradle.kts @@ -13,4 +13,4 @@ dependencyResolutionManagement { } } rootProject.name = "Curated sync examples" -include(":app") +include(":demo") From 0ff408c6d8c5a9aa4abd6d08d8819c2958aadd4f Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 7 Jun 2023 23:45:09 +0200 Subject: [PATCH 12/55] Clean up --- .../curatedsyncexamples/KeyHelperTests.kt | 10 ++++----- ...oidKeyStoreHelper.kt => SystemKeyStore.kt} | 21 ++++++++++--------- .../fieldencryption/ui/NavGraph.kt | 5 ++--- .../ui/records/SecretRecordsViewModel.kt | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/{AndroidKeyStoreHelper.kt => SystemKeyStore.kt} (85%) diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt index 0164990..42c3693 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt +++ b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt @@ -19,7 +19,7 @@ package io.realm.curatedsyncexamples import android.security.keystore.KeyProperties import androidx.test.ext.junit.runners.AndroidJUnit4 import io.realm.curatedsyncexamples.fieldencryption.ext.getKeyOrGenerate -import io.realm.curatedsyncexamples.fieldencryption.models.AndroidKeyStoreHelper +import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore import io.realm.curatedsyncexamples.fieldencryption.models.CipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.EncryptionKeySpec import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord @@ -87,8 +87,8 @@ class KeyHelperTests { @Test fun storeAndRetrieveKeyInAndroidKeystore() = runTest { // Store a key - AndroidKeyStoreHelper - .getKeyFromAndroidKeyStore(KEY_ALIAS) { + SystemKeyStore + .getKey(KEY_ALIAS) { SerializableSecretKey( key = keyGenerator.generateKey(), cipherSpec = cipherSpec @@ -109,8 +109,8 @@ class KeyHelperTests { @Test fun useAndroidKeyStoreKeyToEncryptDecrypt() = runTest { - key = AndroidKeyStoreHelper - .getKeyFromAndroidKeyStore(KEY_ALIAS) { + key = SystemKeyStore + .getKey(KEY_ALIAS) { SerializableSecretKey( key = keyGenerator.generateKey(), cipherSpec = cipherSpec diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt similarity index 85% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt index d504b75..afa8725 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/AndroidKeyStoreHelper.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt @@ -26,11 +26,9 @@ import io.realm.kotlin.mongodb.User import java.security.Key import java.security.KeyStore -private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore" - -object AndroidKeyStoreHelper { +object SystemKeyStore { private val keyStore: KeyStore = - KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER) + KeyStore.getInstance("AndroidKeyStore") .apply { load(null) } @@ -39,18 +37,21 @@ object AndroidKeyStoreHelper { fun removeKey(keyAlias: String) = keyStore.deleteEntry(keyAlias) - suspend fun getKeyFromAndroidKeyStore( + /** + * Suspend as it key computation can take some time. + */ + suspend fun getKey( keyAlias: String, - generateKey: suspend AndroidKeyStoreHelper.() -> SerializableSecretKey + generateKey: suspend SystemKeyStore.() -> SerializableSecretKey ): Key { if (!keyStore.isKeyEntry(keyAlias)) - storeKeyInAndroidKeyStore(keyAlias, generateKey()) + storeKey(keyAlias, generateKey()) return keyStore .getKey(keyAlias, null) } - private fun storeKeyInAndroidKeyStore( + private fun storeKey( keyAlias: String, key: SerializableSecretKey ) { @@ -69,8 +70,8 @@ object AndroidKeyStoreHelper { } suspend fun getFieldLevelEncryptionKey(keyAlias: String, user: User, password: String) = - AndroidKeyStoreHelper - .getKeyFromAndroidKeyStore(keyAlias) { + SystemKeyStore + .getKey(keyAlias) { // Key is missing in the Android keystore, retrieve it from the keystore val keyStore = user.keyStore() diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt index 0617494..194118b 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt @@ -20,12 +20,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.ViewModel -import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import io.realm.curatedsyncexamples.fieldencryption.models.AndroidKeyStoreHelper +import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.KeyStoreScreen import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginScreen import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordScreen @@ -44,7 +43,7 @@ class NavGraphViewModel( ) : ViewModel() { fun isUserLoggedIn(): Boolean = app.currentUser != null - fun isFieldEncryptionKeyAvailable(): Boolean = AndroidKeyStoreHelper.containsKey(keyAlias) + fun isFieldEncryptionKeyAvailable(): Boolean = SystemKeyStore.containsKey(keyAlias) } @Composable diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt index 833529c..efb48c3 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt @@ -20,7 +20,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.AndroidKeyStoreHelper +import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore import io.realm.curatedsyncexamples.fieldencryption.models.EncryptedStringField import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord import io.realm.curatedsyncexamples.fieldencryption.models.cipherSpec @@ -104,7 +104,7 @@ class SecretRecordsViewModel( } viewModelScope.launch(Dispatchers.IO) { user.logOut() - AndroidKeyStoreHelper.removeKey(keyAlias) + SystemKeyStore.removeKey(keyAlias) _uiState.update { it.copy(loggedOut = true) From 9c8ddd83d89570300c6665c305249922500fd483 Mon Sep 17 00:00:00 2001 From: Clemente Date: Thu, 8 Jun 2023 13:53:07 +0200 Subject: [PATCH 13/55] Check if required apps are available --- .../io/realm/curatedsyncexamples/Constants.kt | 19 +++ .../CuratedSyncExamplesApp.kt | 2 +- .../java/io/realm/curatedsyncexamples/DI.kt | 47 ++++++++ .../ExampleSelectorActivity.kt | 91 +-------------- .../curatedsyncexamples/fieldencryption/DI.kt | 15 +-- .../curatedsyncexamples/ui/ExamplesScreen.kt | 108 ++++++++++++++++++ .../ui/ExamplesScreenViewModel.kt | 66 +++++++++++ 7 files changed, 249 insertions(+), 99 deletions(-) create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt new file mode 100644 index 0000000..9166d34 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples + +const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt index 37ffd99..d3d929a 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt @@ -32,7 +32,7 @@ class CuratedSyncExamplesApp: Application() { // Reference Android context androidContext(this@CuratedSyncExamplesApp) // Load modules - modules(fieldEncryptionModule) + modules(appsModule, fieldEncryptionModule) } } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt new file mode 100644 index 0000000..7b75ffa --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples + +import io.realm.curatedsyncexamples.ui.ExamplesScreenViewModel +import io.realm.kotlin.mongodb.App +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module + +/** + * Enum with all the required App Services App. + */ +enum class Apps(val appId: String) { + FIELD_ENCRYPTION_APP(FIELD_ENCRYPTION_APP_ID); + + val qualifier = named(appId) +} + +val appsModule = module { + for (app in Apps.values()) { + single(app.qualifier) { App.create(app.appId) } + } + + viewModel { + val apps = Apps.values() + .map { appEntry -> + get(appEntry.qualifier) + } + + ExamplesScreenViewModel(apps) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt index 3af1f03..26f72e2 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt @@ -16,29 +16,10 @@ */ package io.realm.curatedsyncexamples -import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity +import io.realm.curatedsyncexamples.ui.ExamplesScreen import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme class ExampleSelectorActivity : ComponentActivity() { @@ -47,76 +28,8 @@ class ExampleSelectorActivity : ComponentActivity() { setContent { CuratedSyncExamplesTheme { - // A surface container using the 'background' color from the theme - ExamplesScreen(entries) + ExamplesScreen() } - } } } - -val entries = arrayOf( - ExampleEntry( - name = "Field level encryption", - activity = FieldEncryptionActivity::class.java - ), -) - -data class ExampleEntry( - val name: String, - val activity: Class<*> -) - -@Composable -fun ExamplesScreen( - examplesList: Array, -) { - val context = LocalContext.current - Surface( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - color = MaterialTheme.colorScheme.background - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(48.dp) - ) { - Image( - painter = painterResource(id = R.drawable.realmio_logo_vector), - contentDescription = "Realm logo", - ) - Text( - text = "Reference app that showcases different design patterns and examples of Realm Kotlin SDK with Atlas", - modifier = Modifier.padding(vertical = 48.dp), - textAlign = TextAlign.Center - ) - examplesList.forEach { example -> - ExampleEntry(example.name) { - context.startActivity(Intent(context, example.activity)) - } - } - } - } -} - -@Composable -fun ExampleEntry(name: String, modifier: Modifier = Modifier, onClick: () -> Unit) { - ElevatedButton( - onClick = onClick, - modifier = modifier - ) { - Text( - text = name, - modifier = modifier - ) - } -} - -@Preview(showBackground = true) -@Composable -fun ExamplesScreenPreview() { - CuratedSyncExamplesTheme { - ExamplesScreen(entries) - } -} \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt index f0c324e..574d9a9 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt @@ -16,11 +16,11 @@ */ package io.realm.curatedsyncexamples.fieldencryption +import io.realm.curatedsyncexamples.Apps import io.realm.curatedsyncexamples.fieldencryption.ui.NavGraphViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.KeyStoreViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordsViewModel -import io.realm.kotlin.mongodb.App import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module @@ -28,11 +28,8 @@ import org.koin.dsl.module val fieldEncryptionModule = module { val keyAlias = "fieldLevelEncryptionKey" - viewModel { KeyStoreViewModel(get(), keyAlias) } - viewModel { LoginViewModel(get()) } - viewModel { SecretRecordsViewModel(get(), keyAlias) } - - viewModel { NavGraphViewModel(get(), keyAlias) } - - single { App.create("field-encryption-fjrvt") } -} \ No newline at end of file + viewModel { KeyStoreViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { LoginViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier)) } + viewModel { SecretRecordsViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { NavGraphViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt new file mode 100644 index 0000000..66d2525 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.ui + +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.realm.curatedsyncexamples.R +import org.koin.compose.koinInject + +@Composable +fun ExamplesScreen( + viewModel: ExamplesScreenViewModel = koinInject() +) { + val unavailableApps by viewModel.unavailableApps.observeAsState(emptyList()) + val loading by viewModel.loadingState.collectAsStateWithLifecycle() + + val context = LocalContext.current + + Surface( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + color = MaterialTheme.colorScheme.background + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(48.dp) + ) { + Image( + painter = painterResource(id = R.drawable.realmio_logo_vector), + contentDescription = "Realm logo", + ) + Text( + text = "Reference app that showcases different design patterns and examples of Realm Kotlin SDK with Atlas", + modifier = Modifier.padding(vertical = 48.dp), + textAlign = TextAlign.Center + ) + if (!loading) { + if (unavailableApps.isNotEmpty()) { + Column(modifier = Modifier.fillMaxWidth()) { + Text( + modifier = Modifier.fillMaxWidth(), + text = "⚠️", + textAlign = TextAlign.Center + ) + Text( + text = "Some App Services Apps are not available. Please enable then using the Readme instructions.", + textAlign = TextAlign.Center + ) + } + } else { + viewModel.examplesList.forEach { example -> + ExampleEntry(example.name) { + context.startActivity(Intent(context, example.activity)) + } + } + } + } + } + } +} + +@Composable +fun ExampleEntry(name: String, modifier: Modifier = Modifier, onClick: () -> Unit) { + ElevatedButton( + onClick = onClick, + modifier = modifier + ) { + Text( + text = name, + modifier = modifier + ) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt new file mode 100644 index 0000000..12b9fa3 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.ui + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.exceptions.ServiceException +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class ExamplesScreenViewModel(private val apps: List) : ViewModel() { + private val loading = MutableStateFlow(true) + val loadingState: StateFlow = loading.asStateFlow() + + val unavailableApps: MutableLiveData> by lazy { MutableLiveData>() } + + val examplesList = arrayOf( + ExampleEntry( + name = "Field level encryption", + activity = FieldEncryptionActivity::class.java + ), + ) + + private suspend fun getUnavailableApps() = + apps.filterNot { app -> + try { + app.login(Credentials.anonymous()) + true + } catch (e: ServiceException) { + e.message?.startsWith("[Service][Unknown(4351)]") != true + } + } + + init { + viewModelScope.launch { + unavailableApps.postValue(getUnavailableApps()) + loading.update { false } + } + } +} + +data class ExampleEntry( + val name: String, + val activity: Class<*> +) From 456619dfb8ebb94c83552adaae98a98929889ed9 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 9 Jun 2023 12:17:21 +0200 Subject: [PATCH 14/55] Improve showing demo statuses --- .../java/io/realm/curatedsyncexamples/DI.kt | 39 ++++++++++++++----- .../curatedsyncexamples/fieldencryption/DI.kt | 10 ++--- .../ui/records/SecretRecordsScreen.kt | 5 ++- .../curatedsyncexamples/ui/ExamplesScreen.kt | 29 +++++++++----- .../ui/ExamplesScreenViewModel.kt | 38 +++++++++--------- 5 files changed, 78 insertions(+), 43 deletions(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt index 7b75ffa..067f401 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt @@ -16,32 +16,53 @@ */ package io.realm.curatedsyncexamples +import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity import io.realm.curatedsyncexamples.ui.ExamplesScreenViewModel import io.realm.kotlin.mongodb.App import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module +import kotlin.reflect.KClass /** * Enum with all the required App Services App. */ -enum class Apps(val appId: String) { - FIELD_ENCRYPTION_APP(FIELD_ENCRYPTION_APP_ID); +enum class Demos( + val title: String, + val activity: Class<*>, + val appId: String, +) { + FIELD_ENCRYPTION_APP( + "Field level encryption", + FieldEncryptionActivity::class.java, + FIELD_ENCRYPTION_APP_ID + ), + FIELD_ENCRYPTION_APP1( + "Field level encryption", + FieldEncryptionActivity::class.java, + "FIELD_ENCRYPTION_APP_ID" + ); val qualifier = named(appId) } +typealias DemoWithApp = Pair + val appsModule = module { - for (app in Apps.values()) { + // Create singletons for each app. + for (app in Demos.values()) { single(app.qualifier) { App.create(app.appId) } } viewModel { - val apps = Apps.values() - .map { appEntry -> - get(appEntry.qualifier) - } - - ExamplesScreenViewModel(apps) + ExamplesScreenViewModel( + apps = Demos.values() + .map { demo -> + DemoWithApp( + first = demo, + second = get(demo.qualifier) + ) + } + ) } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt index 574d9a9..f733e2c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt @@ -16,7 +16,7 @@ */ package io.realm.curatedsyncexamples.fieldencryption -import io.realm.curatedsyncexamples.Apps +import io.realm.curatedsyncexamples.Demos import io.realm.curatedsyncexamples.fieldencryption.ui.NavGraphViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.KeyStoreViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginViewModel @@ -28,8 +28,8 @@ import org.koin.dsl.module val fieldEncryptionModule = module { val keyAlias = "fieldLevelEncryptionKey" - viewModel { KeyStoreViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } - viewModel { LoginViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier)) } - viewModel { SecretRecordsViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } - viewModel { NavGraphViewModel(get(qualifier = Apps.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier)) } + viewModel { SecretRecordsViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { NavGraphViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt index 0967cdc..e4c0708 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt @@ -40,6 +40,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord import org.koin.compose.koinInject +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -79,6 +81,7 @@ fun NewRecord( } } +@OptIn(ExperimentalEncodingApi::class) @Composable fun SecretRecordView( record: SecretRecord @@ -97,7 +100,7 @@ fun SecretRecordView( ) Text( fontFamily = FontFamily.Monospace, - text = "Encrypted: ${String(record.content!!.encryptedValue)}" + text = "Encrypted: ${Base64.encode(record.content!!.encryptedValue)}" ) } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt index 66d2525..11d21d5 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt @@ -45,7 +45,7 @@ import org.koin.compose.koinInject fun ExamplesScreen( viewModel: ExamplesScreenViewModel = koinInject() ) { - val unavailableApps by viewModel.unavailableApps.observeAsState(emptyList()) + val demoEntriesWithStatus by viewModel.demoEntriesWithStatus.observeAsState(emptyList()) val loading by viewModel.loadingState.collectAsStateWithLifecycle() val context = LocalContext.current @@ -70,7 +70,13 @@ fun ExamplesScreen( textAlign = TextAlign.Center ) if (!loading) { - if (unavailableApps.isNotEmpty()) { + demoEntriesWithStatus.filter { it.second }.map { it.first }.forEach { + ExampleEntry(it.title) { + context.startActivity(Intent(context, it.activity)) + } + } + + if (demoEntriesWithStatus.any { !it.second }) { Column(modifier = Modifier.fillMaxWidth()) { Text( modifier = Modifier.fillMaxWidth(), @@ -78,14 +84,13 @@ fun ExamplesScreen( textAlign = TextAlign.Center ) Text( - text = "Some App Services Apps are not available. Please enable then using the Readme instructions.", + text = "One or more App Services Apps required for this demo app are not available. Please follow the Readme instructions on how to set them up.", textAlign = TextAlign.Center ) - } - } else { - viewModel.examplesList.forEach { example -> - ExampleEntry(example.name) { - context.startActivity(Intent(context, example.activity)) + demoEntriesWithStatus.filter { !it.second }.map { it.first }.forEach { + ExampleEntry(it.title, enabled = false) { + context.startActivity(Intent(context, it.activity)) + } } } } @@ -95,9 +100,15 @@ fun ExamplesScreen( } @Composable -fun ExampleEntry(name: String, modifier: Modifier = Modifier, onClick: () -> Unit) { +fun ExampleEntry( + name: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit +) { ElevatedButton( onClick = onClick, + enabled = enabled, modifier = modifier ) { Text( diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt index 12b9fa3..9a2878c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt @@ -19,42 +19,42 @@ package io.realm.curatedsyncexamples.ui import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity +import io.realm.curatedsyncexamples.DemoWithApp +import io.realm.curatedsyncexamples.Demos import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.exceptions.ServiceException +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -class ExamplesScreenViewModel(private val apps: List) : ViewModel() { +typealias DemoWithStatus = Pair + +class ExamplesScreenViewModel(private val apps: List) : ViewModel() { private val loading = MutableStateFlow(true) val loadingState: StateFlow = loading.asStateFlow() - val unavailableApps: MutableLiveData> by lazy { MutableLiveData>() } + val demoEntriesWithStatus: MutableLiveData> by lazy { MutableLiveData>() } - val examplesList = arrayOf( - ExampleEntry( - name = "Field level encryption", - activity = FieldEncryptionActivity::class.java - ), - ) + private suspend fun App.isAvailable() = + try { + login(Credentials.anonymous()) + true + } catch (e: ServiceException) { + e.message?.startsWith("[Service][Unknown(4351)]") != true + } - private suspend fun getUnavailableApps() = - apps.filterNot { app -> - try { - app.login(Credentials.anonymous()) - true - } catch (e: ServiceException) { - e.message?.startsWith("[Service][Unknown(4351)]") != true - } + private suspend fun getDemoEntriesWithStatus() = + apps.map { demoWithApp -> + DemoWithStatus(demoWithApp.first, demoWithApp.second.isAvailable()) } init { - viewModelScope.launch { - unavailableApps.postValue(getUnavailableApps()) + viewModelScope.launch(Dispatchers.IO) { + demoEntriesWithStatus.postValue(getDemoEntriesWithStatus()) loading.update { false } } } From 6ba84b345d1212c3edab348c00a5251fb3cfdd4a Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 9 Jun 2023 13:02:14 +0200 Subject: [PATCH 15/55] Improve landing UI experience --- .../demo/src/main/AndroidManifest.xml | 2 +- ...torActivity.kt => DemoSelectorActivity.kt} | 2 +- .../{DI.kt => DependencyInjection.kt} | 31 +++- .../{DI.kt => DependencyInjection.kt} | 8 +- .../ui/DemoSelectorScreen.kt | 139 ++++++++++++++++++ ...nViewModel.kt => DemoSelectorViewModel.kt} | 0 .../curatedsyncexamples/ui/ExamplesScreen.kt | 119 --------------- 7 files changed, 170 insertions(+), 131 deletions(-) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/{ExampleSelectorActivity.kt => DemoSelectorActivity.kt} (95%) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/{DI.kt => DependencyInjection.kt} (72%) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/{DI.kt => DependencyInjection.kt} (91%) create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/{ExamplesScreenViewModel.kt => DemoSelectorViewModel.kt} (100%) delete mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt diff --git a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml b/CuratedSyncExamples/demo/src/main/AndroidManifest.xml index 1b6f434..06e8abf 100644 --- a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml +++ b/CuratedSyncExamples/demo/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ android:theme="@style/Theme.CuratedSyncExamples" tools:targetApi="31"> diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt similarity index 95% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt index 26f72e2..d4d9d19 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ExampleSelectorActivity.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt @@ -22,7 +22,7 @@ import androidx.activity.compose.setContent import io.realm.curatedsyncexamples.ui.ExamplesScreen import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme -class ExampleSelectorActivity : ComponentActivity() { +class DemoSelectorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt similarity index 72% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt index 067f401..cc59f64 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt @@ -22,7 +22,6 @@ import io.realm.kotlin.mongodb.App import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named import org.koin.dsl.module -import kotlin.reflect.KClass /** * Enum with all the required App Services App. @@ -32,15 +31,35 @@ enum class Demos( val activity: Class<*>, val appId: String, ) { - FIELD_ENCRYPTION_APP( + FIELD_ENCRYPTION( "Field level encryption", FieldEncryptionActivity::class.java, FIELD_ENCRYPTION_APP_ID ), - FIELD_ENCRYPTION_APP1( - "Field level encryption", - FieldEncryptionActivity::class.java, - "FIELD_ENCRYPTION_APP_ID" + USER_PRESENCE( + "User presence", + DemoSelectorActivity::class.java, + "" + ), + OFFLINE_LOGIN( + "Offline login", + DemoSelectorActivity::class.java, + "" + ), + ERROR_HANDLING( + "Error handling", + DemoSelectorActivity::class.java, + "" + ), + BUSINESS_LOGIC( + "Business logic", + DemoSelectorActivity::class.java, + "" + ), + PURCHASE_VERIFICATION( + "Purchase verification", + DemoSelectorActivity::class.java, + "" ); val qualifier = named(appId) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt similarity index 91% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt index f733e2c..95069ce 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DI.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt @@ -28,8 +28,8 @@ import org.koin.dsl.module val fieldEncryptionModule = module { val keyAlias = "fieldLevelEncryptionKey" - viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } - viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier)) } - viewModel { SecretRecordsViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } - viewModel { NavGraphViewModel(get(qualifier = Demos.FIELD_ENCRYPTION_APP.qualifier), keyAlias) } + viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } + viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier)) } + viewModel { SecretRecordsViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } + viewModel { NavGraphViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt new file mode 100644 index 0000000..fa02b8a --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.ui + +import android.content.Intent +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Divider +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.realm.curatedsyncexamples.R +import org.koin.compose.koinInject + +@Composable +fun ExamplesScreen( + viewModel: ExamplesScreenViewModel = koinInject() +) { + val demoEntriesWithStatus by viewModel.demoEntriesWithStatus.observeAsState(emptyList()) + val loading by viewModel.loadingState.collectAsStateWithLifecycle() + + val context = LocalContext.current + + Surface( + color = MaterialTheme.colorScheme.background + ) { + if (!loading) { + LazyColumn( + modifier = Modifier.padding(horizontal = 48.dp) + ) { + item { + Column( + modifier = Modifier.padding(top = 48.dp, bottom = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.realmio_logo_vector), + contentDescription = "Realm logo", + ) + Text( + text = "Reference app that showcases different design patterns and examples of Realm Kotlin SDK with Atlas.", + modifier = Modifier.padding(top = 48.dp), + textAlign = TextAlign.Justify + ) + } + } + items( + items = demoEntriesWithStatus.filter { it.second }.map { it.first } + ) { demo -> + DemoEntry( + title = demo.title, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + context.startActivity(Intent(context, demo.activity)) + } + } + if (demoEntriesWithStatus.any { !it.second }) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp, bottom = 24.dp) + ) { + Text( + text = "⚠️ The following demos are not available. Please follow the Readme instructions to set them up.", + textAlign = TextAlign.Justify + ) + } + } + items( + items = demoEntriesWithStatus.filter { !it.second }.map { it.first } + ) { demo -> + DemoEntry( + title = demo.title, + enabled = false, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + context.startActivity(Intent(context, demo.activity)) + } + } + } + } + } + } +} + +@Composable +fun DemoEntry( + title: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + onClick: () -> Unit +) { + ElevatedButton( + onClick = onClick, + enabled = enabled, + modifier = modifier + ) { + Text( + textAlign = TextAlign.Center, + text = title + ) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt similarity index 100% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreenViewModel.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt deleted file mode 100644 index 11d21d5..0000000 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/ExamplesScreen.kt +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2023 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.realm.curatedsyncexamples.ui - -import android.content.Intent -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.ElevatedButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.realm.curatedsyncexamples.R -import org.koin.compose.koinInject - -@Composable -fun ExamplesScreen( - viewModel: ExamplesScreenViewModel = koinInject() -) { - val demoEntriesWithStatus by viewModel.demoEntriesWithStatus.observeAsState(emptyList()) - val loading by viewModel.loadingState.collectAsStateWithLifecycle() - - val context = LocalContext.current - - Surface( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), - color = MaterialTheme.colorScheme.background - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(48.dp) - ) { - Image( - painter = painterResource(id = R.drawable.realmio_logo_vector), - contentDescription = "Realm logo", - ) - Text( - text = "Reference app that showcases different design patterns and examples of Realm Kotlin SDK with Atlas", - modifier = Modifier.padding(vertical = 48.dp), - textAlign = TextAlign.Center - ) - if (!loading) { - demoEntriesWithStatus.filter { it.second }.map { it.first }.forEach { - ExampleEntry(it.title) { - context.startActivity(Intent(context, it.activity)) - } - } - - if (demoEntriesWithStatus.any { !it.second }) { - Column(modifier = Modifier.fillMaxWidth()) { - Text( - modifier = Modifier.fillMaxWidth(), - text = "⚠️", - textAlign = TextAlign.Center - ) - Text( - text = "One or more App Services Apps required for this demo app are not available. Please follow the Readme instructions on how to set them up.", - textAlign = TextAlign.Center - ) - demoEntriesWithStatus.filter { !it.second }.map { it.first }.forEach { - ExampleEntry(it.title, enabled = false) { - context.startActivity(Intent(context, it.activity)) - } - } - } - } - } - } - } -} - -@Composable -fun ExampleEntry( - name: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit -) { - ElevatedButton( - onClick = onClick, - enabled = enabled, - modifier = modifier - ) { - Text( - text = name, - modifier = modifier - ) - } -} From 3ab4e51c56e8b5760409000a2a11eef2268f7828 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 9 Jun 2023 23:22:27 +0200 Subject: [PATCH 16/55] Ui clean up --- .../fieldencryption/DependencyInjection.kt | 2 +- .../FieldEncryptionActivity.kt | 9 +- .../fieldencryption/ui/NavGraph.kt | 18 +- .../ui/keystore/KeyStoreScreen.kt | 141 ++++++++++++--- .../ui/keystore/KeyStoreViewModel.kt | 29 ++- .../fieldencryption/ui/login/LoginScreen.kt | 169 +++++++++++++----- .../ui/records/SecretRecordsScreen.kt | 159 +++++++++++----- 7 files changed, 384 insertions(+), 143 deletions(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt index 95069ce..77df0c7 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt @@ -28,7 +28,7 @@ import org.koin.dsl.module val fieldEncryptionModule = module { val keyAlias = "fieldLevelEncryptionKey" - viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } + viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias = keyAlias) } viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier)) } viewModel { SecretRecordsViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } viewModel { NavGraphViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt index 0672c3e..769be4f 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt @@ -19,6 +19,10 @@ package io.realm.curatedsyncexamples.fieldencryption import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.ui.Modifier import io.realm.curatedsyncexamples.fieldencryption.ui.NavGraph import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme @@ -29,7 +33,10 @@ class FieldEncryptionActivity : ComponentActivity() { setContent { CuratedSyncExamplesTheme { - NavGraph() + NavGraph( + modifier = Modifier + .fillMaxSize(), + ) } } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt index 194118b..0093f17 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt @@ -16,16 +16,20 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore -import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.KeyStoreScreen +import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.UnlockUserKeyStoreScreen import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginScreen import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordScreen import io.realm.kotlin.mongodb.App @@ -69,21 +73,27 @@ fun NavGraph( composable( Screens.LOGIN_SCREEN, ) { - LoginScreen { + LoginScreen( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { navActions.navigateToKeyStoreUnlockScreen() } } composable( Screens.KEYSTORE_PASSWORD_SCREEN, ) { - KeyStoreScreen { + UnlockUserKeyStoreScreen( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { navActions.navigateToSecretRecordsScreen() } } composable( Screens.SECRET_RECORDS_SCREEN, ) { - SecretRecordScreen { + SecretRecordScreen( + modifier = modifier.padding(horizontal = 16.dp) + ) { navActions.navigateToLogin() } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt index 57fb5a1..bfc9b26 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt @@ -16,8 +16,10 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.keystore +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions @@ -38,53 +40,134 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.koin.compose.koinInject -@OptIn(ExperimentalMaterial3Api::class) @Composable -fun KeyStoreScreen( +fun UnlockUserKeyStoreScreen( viewModel: KeyStoreViewModel = koinInject(), + modifier: Modifier = Modifier, onUnlocked: () -> Unit ) { - var password by remember { mutableStateOf("") } val uiState by viewModel.uiState.collectAsStateWithLifecycle() Surface( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), + modifier = modifier, color = MaterialTheme.colorScheme.background ) { - LaunchedEffect(uiState.unlocked) { - if (uiState.unlocked) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + ) { + UserKeyUnlocker( + state = uiState, + modifier = Modifier.padding(48.dp) + ) { password -> + viewModel.unlock(password) + } + } + LaunchedEffect(uiState.isUnlocked) { + if (uiState.isUnlocked) { onUnlocked() } } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(48.dp) - ) { - Text(text = "Unlock your keystore to decrypt data") + } +} - OutlinedTextField( - value = password, - enabled = !uiState.unlocking, - isError = uiState.errorMessage != null, - onValueChange = { password = it }, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) - ElevatedButton( - enabled = !uiState.unlocking, - onClick = { viewModel.unlock(password) }) { - Text(text = "Continue") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserKeyUnlocker( + state: KeyStoreUiState, + modifier: Modifier = Modifier, + onUnlock: (String) -> Unit = {} +) { + var password: String by remember { mutableStateOf("") } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + ) { + Text( + fontSize = 24.sp, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 8.dp), + text = if (state.isInitialized) { + "Unlocking user's keystore" + } else { + "Initializing user's keystore" } - uiState.errorMessage?.let { - Text(text = it) + ) + Text( + textAlign = TextAlign.Justify, + text = if (state.isInitialized) { + """ + |The user's keystore is securely stored in Atlas and linked to your account. It can be accessed from any device. + | + |Please introduced the password used to protect your user keystore. + """.trimMargin() + } else { + """ + |The user's keystore is securely stored in Atlas and linked to your account. It can be accessed from any device. + | + |For added security, please create a password to protect your user keystore. + """.trimMargin() } + ) + + OutlinedTextField( + value = password, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + enabled = !state.isUnlocking, + isError = state.errorMessage != null, + onValueChange = { password = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + ElevatedButton( + modifier = Modifier.padding(top = 4.dp), + enabled = !state.isUnlocking, + onClick = { onUnlock(password) }) { + Text(text = "Continue") + } + state.errorMessage?.let { + Text(text = it) } + + } +} + +@Preview +@Composable +fun UninitializedState() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + UserKeyUnlocker( + state = KeyStoreUiState( + isInitialized = false + ) + ) + } +} + +@Preview +@Composable +fun InitializedState() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + UserKeyUnlocker( + state = KeyStoreUiState( + isInitialized = true + ) + ) } -} \ No newline at end of file +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt index a5795ce..ef44fff 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt @@ -18,12 +18,9 @@ package io.realm.curatedsyncexamples.fieldencryption.ui.keystore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.cipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.getFieldLevelEncryptionKey import io.realm.curatedsyncexamples.fieldencryption.models.key import io.realm.kotlin.mongodb.App -import io.realm.kotlin.mongodb.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -32,39 +29,37 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch data class KeyStoreUiState( - val unlocking: Boolean = false, - val unlocked: Boolean = false, + val isInitialized: Boolean = false, + val isUnlocking: Boolean = false, + val isUnlocked: Boolean = false, val errorMessage: String? = null, ) class KeyStoreViewModel( - app: App, - private val keyAlias: String + val app: App? = null, + private val keyAlias: String, + uiState: KeyStoreUiState = KeyStoreUiState(), ) : ViewModel() { - private var user: User - - private val _uiState = MutableStateFlow(KeyStoreUiState()) + private val _uiState = MutableStateFlow(uiState) val uiState: StateFlow = _uiState.asStateFlow() init { - user = app.currentUser!! - cipherSpec = user.fieldEncryptionCipherSpec() - } + } fun unlock(password: String) { _uiState.update { - it.copy(unlocking = true, unlocked = false, errorMessage = null) + it.copy(isUnlocking = true, isUnlocked = false, errorMessage = null) } viewModelScope.launch(Dispatchers.IO) { try { - key = getFieldLevelEncryptionKey(keyAlias, user, password) + key = getFieldLevelEncryptionKey(keyAlias, app!!.currentUser!!, password) _uiState.update { - it.copy(unlocked = true) + it.copy(isUnlocked = true) } } catch (e: Exception) { _uiState.update { - it.copy(unlocking = false, errorMessage = e.message) + it.copy(isUnlocking = false, errorMessage = e.message) } } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt index 9e05e95..0c559e8 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt @@ -16,6 +16,8 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.login +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -37,26 +39,26 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.koin.compose.koinInject -@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen( viewModel: LoginViewModel = koinInject(), + modifier: Modifier = Modifier, onLoggedIn: () -> Unit ) { - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } val uiState by viewModel.uiState.collectAsStateWithLifecycle() Surface( - modifier = Modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()), + modifier = modifier, color = MaterialTheme.colorScheme.background ) { LaunchedEffect(uiState.loggedIn) { @@ -64,47 +66,126 @@ fun LoginScreen( onLoggedIn() } } - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(48.dp) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() ) { - Text(text = "Field level encryption Demo") - - OutlinedTextField( - value = email, - isError = uiState.errorMessage != null, - enabled = !uiState.loggingIn, - onValueChange = { email = it }, - placeholder = { Text("your@email.com") }, - label = { Text("Email") }, - ) - OutlinedTextField( - value = password, - isError = uiState.errorMessage != null, - enabled = !uiState.loggingIn, - onValueChange = { password = it }, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) - Row { - ElevatedButton( - onClick = { viewModel.login(email, password, true) }, - enabled = !uiState.loggingIn - ) { - Text(text = "Register") - } - ElevatedButton( - onClick = { viewModel.login(email, password) }, - enabled = !uiState.loggingIn - ) { - Text(text = "Login") - } + LoginBox( + state = uiState, + modifier = Modifier.padding(48.dp) + ) { email, password, register -> + viewModel.login(email, password, register) } + } + } +} - uiState.errorMessage?.let { - Text(text = it) +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginBox( + state: LoginUiState, + modifier: Modifier = Modifier, + onLogin: (String, String, Boolean) -> Unit = { _, _, _ -> } +) { + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + fontSize = 24.sp, + textAlign = TextAlign.Center, + text = "Field level encryption", + modifier = Modifier + .padding(bottom = 8.dp) + ) + Text( + textAlign = TextAlign.Justify, + text = + """ +|This demo shows the process of encrypting specific fields within an object, limiting their accessibility to the user alone and preventing access on the server side. +| +|The process involves importing the required keys from the Atlas keystore to the secure keystore on the device. These imported keys are then utilized to access any of the encrypted fields. + """.trimMargin() + ) + + OutlinedTextField( + modifier = Modifier.padding(top = 16.dp), + value = email, + isError = state.errorMessage != null, + enabled = !state.loggingIn, + onValueChange = { email = it }, + placeholder = { Text("your@email.com") }, + label = { Text("Email") }, + ) + OutlinedTextField( + modifier = Modifier.padding(top = 4.dp), + value = password, + isError = state.errorMessage != null, + enabled = !state.loggingIn, + onValueChange = { password = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + Row( + modifier = Modifier.padding(top = 4.dp), + horizontalArrangement = Arrangement.End + ) { + ElevatedButton( + modifier = Modifier.padding(horizontal = 4.dp), + onClick = { onLogin(email, password, true) }, + enabled = !state.loggingIn + ) { + Text(text = "Register") + } + ElevatedButton( + modifier = Modifier.padding(horizontal = 4.dp), + onClick = { onLogin(email, password, false) }, + enabled = !state.loggingIn + ) { + Text(text = "Login") } } + + state.errorMessage?.let { + Text( + text = it, + modifier = Modifier.padding(top = 8.dp), + color = Color.Red, + ) + } + } +} + +@Preview +@Composable +fun InitialStatePreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + LoginBox(state = LoginUiState()) + } +} + +@Preview +@Composable +fun LoggingInPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + LoginBox(state = LoginUiState(loggingIn = true)) + } +} + +@Preview +@Composable +fun LoginErrorPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + LoginBox(state = LoginUiState(errorMessage = "An error occurred")) } -} \ No newline at end of file +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt index e4c0708..91087bc 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt @@ -16,16 +16,24 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.records +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Button +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -34,107 +42,164 @@ import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord import org.koin.compose.koinInject import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi @OptIn(ExperimentalMaterial3Api::class) @Composable -fun NewRecord( - uiState: SecretRecordsUiState, +fun AddSecretRecordCard( + state: SecretRecordsUiState, + modifier: Modifier = Modifier, onLogout: () -> Unit, onAddNewRecord: (String) -> Unit, ) { var name by remember { mutableStateOf("") } - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) { - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - enabled = !uiState.loading && !uiState.loggingOut, - value = name, - onValueChange = { name = it }, - label = { Text("Record content") }, - ) - Row { - Button( - enabled = !uiState.loading && !uiState.loggingOut, - onClick = onLogout - ) { - Text(text = "Logout") - } - Button( - enabled = !uiState.loading && !uiState.loggingOut, - onClick = { onAddNewRecord(name) }) { - Text(text = "Add") + ElevatedCard( + modifier = modifier.fillMaxWidth() + ) { + Box(Modifier.padding(16.dp)) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + enabled = !state.loading && !state.loggingOut, + value = name, + onValueChange = { name = it }, + label = { Text("Secret record value") }, + ) + IconButton( + enabled = !state.loading && !state.loggingOut, + modifier = Modifier.padding(start = 8.dp, top = 8.dp), + onClick = { onAddNewRecord(name) }) { + Icon( + Icons.Filled.Add, + "contentDescription", + ) + } + } + ElevatedButton( + colors = ButtonDefaults.buttonColors(), + enabled = !state.loading && !state.loggingOut, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp), + onClick = onLogout + ) { + Text(text = "Logout") + } } } } } -@OptIn(ExperimentalEncodingApi::class) @Composable -fun SecretRecordView( - record: SecretRecord +fun SecretRecordCard( + content: String, + encryptedContent: String, + modifier: Modifier = Modifier, ) { ElevatedCard( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth() + modifier = modifier ) { Column( - modifier = Modifier.padding(8.dp) + modifier = Modifier.padding(16.dp) ) { Text( - fontFamily = FontFamily.Monospace, - text = "Decrypted: ${record.content!!.value}" + text = "Decrypted: $content" ) Text( - fontFamily = FontFamily.Monospace, - text = "Encrypted: ${Base64.encode(record.content!!.encryptedValue)}" + modifier = Modifier.padding(top = 4.dp), + text = "Encrypted: $encryptedContent", + softWrap = false, + overflow = TextOverflow.Ellipsis ) } } } +@OptIn(ExperimentalEncodingApi::class) @Composable fun SecretRecordScreen( viewModel: SecretRecordsViewModel = koinInject(), + modifier: Modifier = Modifier, onLogout: () -> Unit ) { val records by viewModel.records.observeAsState(emptyList()) val uiState by viewModel.uiState.collectAsStateWithLifecycle() - Column { + Column( + modifier = modifier + ) { LaunchedEffect(uiState.loggedOut) { if (uiState.loggedOut) { onLogout() } } - NewRecord( - uiState, - onLogout = { viewModel.logout() } + LazyColumn( + modifier = Modifier.weight(1f) ) { - viewModel.addRecord(it) - } - LazyColumn { items( records, key = { it._id.toHexString() } ) { record -> - SecretRecordView(record) + with(record.content!!) { + SecretRecordCard( + content = value, + encryptedContent = Base64.encode(encryptedValue), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + ) + } } } + AddSecretRecordCard( + uiState, + modifier = Modifier.padding(bottom = 16.dp), + onLogout = { viewModel.logout() } + ) { + viewModel.addRecord(it) + } + } +} + +@Preview +@Composable +fun SecretRecordCardPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + SecretRecordCard( + content = "hello world", + encryptedContent = "ab88aa34341341231234123415673294572938475def=" + ) + } +} + +@Preview +@Composable +fun AddSecretRecordCardPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + AddSecretRecordCard( + state = SecretRecordsUiState( + loading = false + ), + onLogout = {}, + onAddNewRecord = { _ -> } + ) } } From 0a35c3a8f42ebcdd49c2c25f2aba09940f0ca194 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 9 Jun 2023 23:30:59 +0200 Subject: [PATCH 17/55] Some clean up --- .../java/io/realm/curatedsyncexamples/Constants.kt | 8 +++++++- .../realm/curatedsyncexamples/DependencyInjection.kt | 10 +++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt index 9166d34..c726d3b 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt @@ -16,4 +16,10 @@ */ package io.realm.curatedsyncexamples -const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" \ No newline at end of file +const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" + +const val USER_PRESENCE_APP_ID = "" +const val OFFLINE_LOGIN_APP_ID = "" +const val ERROR_HANDLING_APP_ID = "" +const val BUSINESS_LOGIC_APP_ID = "" +const val PURCHASE_VERIFICATION_APP_ID = "" diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt index cc59f64..3b28c41 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt @@ -39,27 +39,27 @@ enum class Demos( USER_PRESENCE( "User presence", DemoSelectorActivity::class.java, - "" + USER_PRESENCE_APP_ID, ), OFFLINE_LOGIN( "Offline login", DemoSelectorActivity::class.java, - "" + OFFLINE_LOGIN_APP_ID, ), ERROR_HANDLING( "Error handling", DemoSelectorActivity::class.java, - "" + ERROR_HANDLING_APP_ID, ), BUSINESS_LOGIC( "Business logic", DemoSelectorActivity::class.java, - "" + BUSINESS_LOGIC_APP_ID, ), PURCHASE_VERIFICATION( "Purchase verification", DemoSelectorActivity::class.java, - "" + PURCHASE_VERIFICATION_APP_ID, ); val qualifier = named(appId) From d8c973edacacf4617de286801de087ec3d041607 Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 12 Jun 2023 16:19:29 +0200 Subject: [PATCH 18/55] Add some documentation --- .../functions/initializeEncryptionKey.js | 14 ++-- .../functions/updateKeyStore.js | 3 +- CuratedSyncExamples/demo/build.gradle.kts | 5 +- .../curatedsyncexamples/KeyHelperTests.kt | 8 +-- .../curatedsyncexamples/RealmKeyStoreTests.kt | 4 +- .../CuratedSyncExamplesApp.kt | 2 +- .../DependencyInjection.kt | 7 +- .../fieldencryption/ext/EncryptionExt.kt | 4 +- .../fieldencryption/ext/UserExt.kt | 4 +- .../fieldencryption/models/CustomData.kt | 14 +++- .../models/EncryptedStringField.kt | 23 ++++++- .../fieldencryption/models/SecretRecord.kt | 3 + ...ipherSpec.kt => SerializableCipherSpec.kt} | 14 +++- ...onKeySpec.kt => SerializablePBEKeySpec.kt} | 6 +- .../models/SerializableSecretKey.kt | 7 +- .../fieldencryption/models/SystemKeyStore.kt | 9 +++ .../fieldencryption/models/UserKeyStore.kt | 13 ++-- .../fieldencryption/ui/NavGraph.kt | 4 +- .../ui/records/SecretRecordsScreen.kt | 67 +++++++++++-------- .../ui/records/SecretRecordsViewModel.kt | 2 + 20 files changed, 150 insertions(+), 63 deletions(-) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/{CipherSpec.kt => SerializableCipherSpec.kt} (87%) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/{EncryptionKeySpec.kt => SerializablePBEKeySpec.kt} (89%) diff --git a/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js b/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js index cfffb52..ea4f9e2 100644 --- a/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js +++ b/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js @@ -1,6 +1,7 @@ const crypto = require('crypto'); -// Executing this function would generate a new encryption key and store it in the user custom data +// This function initializes an empty user password protected keystore and defines the algorithm that +// would be used to encrypt any fields. exports = async function (user) { const salt = crypto.randomBytes(16); @@ -11,29 +12,34 @@ exports = async function (user) { try { await customUserDataCollection.insertOne({ - // Save the user's account ID to your configured user_id_field + // Binds this custom data to the user. owner_id: user.id, - // Store any other user data you want + // Defines the field level encryption algorithm. field_encryption_cipher_spec: { algorithm: "AES", block: "CBC", padding: "PKCS7Padding", key_length: 128 }, + // User keystore key_store: { + // Password based key specs encryption_key_spec: { algorithm: "PBKDF2WithHmacSHA256", salt: BSON.Binary.fromHex(salt.toString('hex')), iterations_count: 100000, key_length: 128, }, + // Encryption cipher spec to secure keystore contents. cipher_spec: { algorithm: "AES", block: "CBC", padding: "PKCS7Padding", key_length: 128 }, + // Secured contents, being null would tell the client that it has to initialize them. secure_contents: null, + // Null as no contents exists yet. key_hash: null, } }); @@ -41,4 +47,4 @@ exports = async function (user) { console.error(`Failed to create custom user data document for user: ${user.id}`); throw e } -}; \ No newline at end of file +}; diff --git a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js index 759eb00..b98974d 100644 --- a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js +++ b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js @@ -1,3 +1,4 @@ +// This function would replace the users keystore contents. exports = async function (arg) { const customUserDataCollection = context.services .get("mongodb-atlas") @@ -20,4 +21,4 @@ exports = async function (arg) { console.error(`Failed to create custom user data document for user: ${context.user.id}`); throw e } -}; \ No newline at end of file +}; diff --git a/CuratedSyncExamples/demo/build.gradle.kts b/CuratedSyncExamples/demo/build.gradle.kts index 232b8e1..8350844 100644 --- a/CuratedSyncExamples/demo/build.gradle.kts +++ b/CuratedSyncExamples/demo/build.gradle.kts @@ -11,7 +11,10 @@ android { defaultConfig { applicationId = "io.realm.curatedsyncexamples" - minSdk = 28 + + // Field encryption key algorithms require minSdk 26 + // https://developer.android.com/reference/javax/crypto/SecretKeyFactory + minSdk = 26 targetSdk = 33 versionCode = 1 versionName = "1.0" diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt index 42c3693..c33590d 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt +++ b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt @@ -20,8 +20,8 @@ import android.security.keystore.KeyProperties import androidx.test.ext.junit.runners.AndroidJUnit4 import io.realm.curatedsyncexamples.fieldencryption.ext.getKeyOrGenerate import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore -import io.realm.curatedsyncexamples.fieldencryption.models.CipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.EncryptionKeySpec +import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec +import io.realm.curatedsyncexamples.fieldencryption.models.SerializablePBEKeySpec import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord import io.realm.curatedsyncexamples.fieldencryption.models.SerializableSecretKey import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore @@ -58,14 +58,14 @@ class KeyHelperTests { load(null) } - private val keySpec = EncryptionKeySpec( + private val keySpec = SerializablePBEKeySpec( algorithm = "PBKDF2WithHmacSHA256", salt = Random.nextBytes(16), iterationsCount = 100000, keyLength = 128, ) - private val cipherSpec = CipherSpec( + private val cipherSpec = SerializableCipherSpec( algorithm = KeyProperties.KEY_ALGORITHM_AES, block = KeyProperties.BLOCK_MODE_CBC, padding = KeyProperties.ENCRYPTION_PADDING_PKCS7, diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt index 4b25073..dcaa88e 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt +++ b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt @@ -17,7 +17,7 @@ package io.realm.curatedsyncexamples import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.curatedsyncexamples.fieldencryption.models.EncryptionKeySpec +import io.realm.curatedsyncexamples.fieldencryption.models.SerializablePBEKeySpec import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith @@ -30,7 +30,7 @@ import kotlin.random.Random */ @RunWith(AndroidJUnit4::class) class EncryptionKeySpecTests { - private val keySpec = EncryptionKeySpec( + private val keySpec = SerializablePBEKeySpec( algorithm = "PBKDF2WithHmacSHA256", salt = Random.nextBytes(16), iterationsCount = 100000, diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt index d3d929a..686781c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt @@ -32,7 +32,7 @@ class CuratedSyncExamplesApp: Application() { // Reference Android context androidContext(this@CuratedSyncExamplesApp) // Load modules - modules(appsModule, fieldEncryptionModule) + modules(mainModule, fieldEncryptionModule) } } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt index 3b28c41..de7f476 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt @@ -24,7 +24,7 @@ import org.koin.core.qualifier.named import org.koin.dsl.module /** - * Enum with all the required App Services App. + * Enum that lists all the available demos. */ enum class Demos( val title: String, @@ -67,7 +67,10 @@ enum class Demos( typealias DemoWithApp = Pair -val appsModule = module { +/** + * Koin module for the main entry point. + */ +val mainModule = module { // Create singletons for each app. for (app in Demos.values()) { single(app.qualifier) { App.create(app.appId) } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt index ed9ecf7..926468c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt @@ -16,7 +16,7 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ext -import io.realm.curatedsyncexamples.fieldencryption.models.CipherSpec +import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.SerializableSecretKey import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore import io.realm.kotlin.mongodb.User @@ -44,7 +44,7 @@ suspend fun UserKeyStore.getKeyOrGenerate( get(alias)!! } -fun CipherSpec.newKey(): SerializableSecretKey = +fun SerializableCipherSpec.newKey(): SerializableSecretKey = KeyGenerator .getInstance( /* algorithm = */ algorithm diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt index 9ed6787..d2676bd 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt @@ -16,7 +16,7 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ext -import io.realm.curatedsyncexamples.fieldencryption.models.CipherSpec +import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.CustomData import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi @@ -37,6 +37,6 @@ suspend fun User.updateKeyStore(keyStore: UserKeyStore) { } @OptIn(ExperimentalRealmSerializerApi::class) -fun User.fieldEncryptionCipherSpec(): CipherSpec { +fun User.fieldEncryptionCipherSpec(): SerializableCipherSpec { return customData()?.fieldEncryptionCipherSpec!! } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt index 71151ae..ab67549 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt @@ -19,10 +19,22 @@ package io.realm.curatedsyncexamples.fieldencryption.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +/** + * App services user custom data. It contains the resources required to achieve field level encryption: + * 1. Encryption key, it is stored in a password protected keystore. + * 2. Field encryption cipher spec, + */ @Serializable data class CustomData( + /** + * Defines the algorithm and settings use to encrypt and decrypt fields. + */ @SerialName("field_encryption_cipher_spec") - val fieldEncryptionCipherSpec: CipherSpec?, + val fieldEncryptionCipherSpec: SerializableCipherSpec?, + + /** + * Password protected keystore, it contains the field encryption key. + */ @SerialName("key_store") val keyStore: UserKeyStore ) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt index bff1a53..794424c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt @@ -21,15 +21,34 @@ import io.realm.kotlin.types.annotations.Ignore import java.security.Key import kotlin.reflect.KProperty +/** + * Cipher spec that will be used to encrypt/decrypt fields. + * + * It is a global variable because we cannot reference the user out from a RealmObject yet. + */ +lateinit var cipherSpec: SerializableCipherSpec -lateinit var cipherSpec: CipherSpec +/** + * Key that will be used to encrypt/decrypt fields. + * + * It is a global variable because we cannot reference the user out from a RealmObject yet. + */ lateinit var key: Key +/** + * Helper object that encapsulates the logic to encrypt/decrypt a String in an EmbeddedObject. + * + * The data is accessible with [value], that would use [cipherSpec] and [key] to encrypt/decrypt the + * contents that are stored in [encryptedValue]. + */ class EncryptedStringField : EmbeddedRealmObject { + /** + * Contains the encrypted contents. + */ var encryptedValue: ByteArray = byteArrayOf() /** - * This delegated property provides seamless access to the encrypted data. + * Delegated property to provide seamless access to the encrypted data. */ @Ignore var value: String by DecryptionDelegate() diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt index 93065da..3d0593f 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt @@ -21,6 +21,9 @@ import io.realm.kotlin.types.annotations.PersistedName import io.realm.kotlin.types.annotations.PrimaryKey import org.mongodb.kbson.BsonObjectId +/** + * Object that contains an encrypted field. + */ @PersistedName("secret_record") class SecretRecord : RealmObject { @PersistedName("owner_id") diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt similarity index 87% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt index 0ef9b4a..b7252a8 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CipherSpec.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt @@ -26,8 +26,12 @@ import javax.crypto.spec.IvParameterSpec private const val IV_SIZE: Int = 16 // should be ok for most of the cases +/** + * Class that contains all the information required to instantiate a [Cipher], it also provides + * with methods to encrypt/decrypt data. + */ @Serializable -data class CipherSpec( +data class SerializableCipherSpec( val algorithm: String, val block: String, val padding: String, @@ -37,12 +41,17 @@ data class CipherSpec( @Transient private val transformation = "$algorithm/$block/$padding" + /** + * Encrypts [input] using [key] + */ fun encrypt(input: ByteArray, key: Key): ByteArray = with(Cipher.getInstance(transformation)) { init(Cipher.ENCRYPT_MODE, key) iv + doFinal(input) } - + /** + * Decrypts [encryptedData] using [key] + */ fun decrypt(encryptedData: ByteArray, key: Key): ByteArray = with(Cipher.getInstance(transformation)) { init( @@ -59,6 +68,5 @@ data class CipherSpec( } catch (e: Exception) { byteArrayOf() } - } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializablePBEKeySpec.kt similarity index 89% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializablePBEKeySpec.kt index 51ddbf9..1cf3779 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptionKeySpec.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializablePBEKeySpec.kt @@ -22,8 +22,12 @@ import javax.crypto.SecretKey import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec +/** + * Class that contains all the information required to instantiate a [PBEKeySpec]. It contains a + * helper method to generate keys. + */ @Serializable -class EncryptionKeySpec( +class SerializablePBEKeySpec( val algorithm: String, val salt: ByteArray, @SerialName("iterations_count") diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt index 7109ff4..b795c41 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt @@ -20,12 +20,15 @@ import kotlinx.serialization.Serializable import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec +/** + * Class that contains the information required to instantiate a [SecretKey]. + */ @Serializable class SerializableSecretKey( val encoded: ByteArray, - val cipherSpec: CipherSpec + val cipherSpec: SerializableCipherSpec ) { - constructor(key: SecretKey, cipherSpec: CipherSpec) : this( + constructor(key: SecretKey, cipherSpec: SerializableCipherSpec) : this( encoded = key.encoded, cipherSpec = cipherSpec ) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt index afa8725..9e63d3f 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt @@ -26,6 +26,9 @@ import io.realm.kotlin.mongodb.User import java.security.Key import java.security.KeyStore +/** + * Helper for communicating with the Android KeyStore. + */ object SystemKeyStore { private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore") @@ -69,6 +72,12 @@ object SystemKeyStore { } } +/** + * Helper method that retrieves the key that would be used to access field level encrypted data. + * + * This function would first try to retrieve the key from the system keystore, if it not available + * it would then retrieve it from the user keystore. + */ suspend fun getFieldLevelEncryptionKey(keyAlias: String, user: User, password: String) = SystemKeyStore .getKey(keyAlias) { diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt index b09d4af..7173200 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt @@ -28,22 +28,25 @@ import kotlinx.serialization.json.Json import java.nio.charset.StandardCharsets import java.security.Key +/** + * Serializable class that models a password protected key store. + */ @Serializable class UserKeyStore( /** - * Spec to generate the keystore encryption key + * Specifies the algorithm used to generate the key that would secure this keystore contents. */ @SerialName("encryption_key_spec") - val encryptionKeySpec: EncryptionKeySpec, + val encryptionKeySpec: SerializablePBEKeySpec, /** - * Cipher spec used to encrypt the key store contents + * Cipher algorithm used to secure this keystore contents. */ @SerialName("cipher_spec") - val cipherSpec: CipherSpec, + val cipherSpec: SerializableCipherSpec, /** - * Contents + * Encrypted contents. The type of the unencrypted data is a Map. */ @SerialName("secure_contents") var secureContents: ByteArray?, diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt index 0093f17..6127b27 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt @@ -91,9 +91,7 @@ fun NavGraph( composable( Screens.SECRET_RECORDS_SCREEN, ) { - SecretRecordScreen( - modifier = modifier.padding(horizontal = 16.dp) - ) { + SecretRecordScreen { navActions.navigateToLogin() } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt index 91087bc..219a2be 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt @@ -19,10 +19,13 @@ package io.realm.curatedsyncexamples.fieldencryption.ui.records import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.ButtonDefaults @@ -71,6 +74,7 @@ fun AddSecretRecordCard( verticalAlignment = Alignment.CenterVertically ) { OutlinedTextField( + modifier = Modifier.weight(1f), enabled = !state.loading && !state.loggingOut, value = name, onValueChange = { name = it }, @@ -78,7 +82,7 @@ fun AddSecretRecordCard( ) IconButton( enabled = !state.loading && !state.loggingOut, - modifier = Modifier.padding(start = 8.dp, top = 8.dp), + modifier = Modifier.padding(start = 4.dp, top = 8.dp), onClick = { onAddNewRecord(name) }) { Icon( Icons.Filled.Add, @@ -91,7 +95,7 @@ fun AddSecretRecordCard( enabled = !state.loading && !state.loggingOut, modifier = Modifier .fillMaxWidth() - .padding(top = 4.dp), + .padding(top = 8.dp), onClick = onLogout ) { Text(text = "Logout") @@ -136,38 +140,47 @@ fun SecretRecordScreen( ) { val records by viewModel.records.observeAsState(emptyList()) val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val listState = rememberLazyListState() - Column( - modifier = modifier + LaunchedEffect(uiState.loggedOut) { + if (uiState.loggedOut) { + onLogout() + } + } + LaunchedEffect(key1 = records) { + listState.scrollToItem(0) + } + LazyColumn( + state = listState ) { - LaunchedEffect(uiState.loggedOut) { - if (uiState.loggedOut) { - onLogout() + items( + records, + key = { + it._id.toHexString() } - } - LazyColumn( - modifier = Modifier.weight(1f) - ) { - items( - records, - key = { - it._id.toHexString() - } - ) { record -> - with(record.content!!) { - SecretRecordCard( - content = value, - encryptedContent = Base64.encode(encryptedValue), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - ) - } + ) { record -> + with(record.content!!) { + SecretRecordCard( + content = value, + encryptedContent = Base64.encode(encryptedValue), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + ) } } + item { + Box( + modifier = Modifier.padding(16.dp).height(144.dp) + ) + } + } + Box( + modifier = Modifier.fillMaxSize().padding(16.dp), + contentAlignment= Alignment.BottomCenter + ) { AddSecretRecordCard( uiState, - modifier = Modifier.padding(bottom = 16.dp), onLogout = { viewModel.logout() } ) { viewModel.addRecord(it) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt index efb48c3..f69eedb 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt @@ -32,6 +32,7 @@ import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.query.Sort import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableStateFlow @@ -77,6 +78,7 @@ class SecretRecordsViewModel( val job = async { realm.query() + .sort("_id", Sort.DESCENDING) .asFlow() .collect { records.postValue(it.list) From f4a2c4366c8c685edd7ed38e26bbd184a94c68b4 Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 12 Jun 2023 17:49:55 +0200 Subject: [PATCH 19/55] Add Readme --- CuratedSyncExamples/README.md | 28 +++++++++++++++++++ .../apps/field-encryption/README.md | 2 ++ 2 files changed, 30 insertions(+) create mode 100644 CuratedSyncExamples/README.md create mode 100644 CuratedSyncExamples/apps/field-encryption/README.md diff --git a/CuratedSyncExamples/README.md b/CuratedSyncExamples/README.md new file mode 100644 index 0000000..a5c8f49 --- /dev/null +++ b/CuratedSyncExamples/README.md @@ -0,0 +1,28 @@ +# Kotlin SDK Curated samples + +Reference app that show cases different design patterns and examples of Realm Kotlin SDK with Atlas. + +## Samples + +### [Field level encryption](LINK_TO_README) + +This demo shows the process of protecting users sensitive data by employing encryption techniques while guaranteeing the access from any users device. + +## Demo app structure + +The project has been structured in two main folders: + +- Demo - Android app containing the different samples. Samples have been separated in different packages. +- Apps - Atlas App services apps required by each sample. + +## Getting started + +The demos are indendepent of each other, this means that it is not required to install all the app services app samples to test an individual sample. + +To begin, locate the App services app sources that you wish to install. We have conveniently linked them in the Samples list of this document. + +Next, follow the steps outlined in the [Atlas documentation](https://www.mongodb.com/docs/atlas/app-services/apps/create/) to setup the apps. These docs will guide you through the process and help troubleshoot any issue you might encounter. + +After deploying the Atlas apps, you will need to update [Constants.kt](TBD) with the newly created app ids. + +Once you have completed these steps, you would be able to run the samples using the Kotlin demo app. \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md new file mode 100644 index 0000000..f2f13ad --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -0,0 +1,2 @@ +# Field level encryption + From d846b7beadaf2be07a2e5d85a30319768ad70417 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 13 Jun 2023 00:53:08 +0200 Subject: [PATCH 20/55] Add Readme and remove embedded object --- CuratedSyncExamples/README.md | 2 +- .../apps/field-encryption/README.md | 40 +++++++++++++++++ .../secret_record/schema.json | 18 +++----- .../curatedsyncexamples/KeyHelperTests.kt | 4 +- .../models/EncryptedStringField.kt | 45 ++++++++----------- .../fieldencryption/models/SecretRecord.kt | 6 ++- .../ui/records/SecretRecordsScreen.kt | 27 ++++++----- .../ui/records/SecretRecordsViewModel.kt | 7 ++- 8 files changed, 89 insertions(+), 60 deletions(-) diff --git a/CuratedSyncExamples/README.md b/CuratedSyncExamples/README.md index a5c8f49..ff65142 100644 --- a/CuratedSyncExamples/README.md +++ b/CuratedSyncExamples/README.md @@ -4,7 +4,7 @@ Reference app that show cases different design patterns and examples of Realm Ko ## Samples -### [Field level encryption](LINK_TO_README) +### [Field level encryption](https://github.com/realm/realm-kotlin-samples/blob/main/CuratedSyncExamples/apps/field-encryption/README.md) This demo shows the process of protecting users sensitive data by employing encryption techniques while guaranteeing the access from any users device. diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md index f2f13ad..490d732 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -1,2 +1,42 @@ # Field level encryption +This demo showcases a method to enhance the security of users sensitive data. Our goal is to ensure that only the users themselves can access their data and prevent any unauthorized access by other parties. Additionally, we enable users to access their data simultaneously from multiple devices. All of this is achieved by leveraging the powerful capabilities of MongoDB services. + +## Considerations + +The purpose of this demo is to show how can we leverage the MongoDB and resources to provide end-to-end field level encryption to users with multi-device access. + +## Overview + +The encryption would be done using the Android keystore system. It offers enhanced security in key handling, any cryptographic key stored in this container is protected from unathorized use. Once a key is in the Android Keystore it can be used for different cryptographic operations, but it is not exportable. + +If we only relied on the Android KeyStore to handle the encryption keys, the user would only had access to the data from a single device, keys won't be exportable into other devices. To overcome this issue we have introduced a user bound keystore in Atlas, it stores the user's keys, and it allows to import them into any new device. + + +## User keystore + +This keystore is stored in atlas and it is accessible to the user via `CustomData`. Although the custom data is only accessible to the user, any admin with db access would be able to access to the user keys and thus to the encrypted fields. We need to also encrypt the keystore contents. + +To facilitate device roaming the keystore encryption key is password generated. This way a user would be able to generate the key on any new device if they facilitate the right passphrase. + +## Importing and creating keys + +The user keystore is not intended to serve as the primary keystore since the Android key store provides a higher level of security. + +The system keystore serves as a repository for importing keys, which are then utilized for cryptographic operations. If a user needs to generate a new key, it will be stored not only in the system keystore but also in the user's personal keystore. This ensures that the key is accessible across different devices later on. + +## Accessing data + +Once the keys are present in the system and combined with the encryption algorithm specification defined in the user's custom data, we can secure the user's data. + +In this sample we have created the `SecureStringDelegate` a helper that provides seamless access to the secured data, as if it was a regular property. + +There are some known issues around data modelling right now. First it would convenient if we were able to access the user custom data from an object, that would facilitate accessing the cipher algorithm and key. Second, when we add support for custom type adapters we would be able to collapse the secured and accessor in a single property. + +## Vector attacks + +The algorithm used to secure the user keystore in Atlas is prone to brute force attacks, anybody with with access could attempt an attack. + +Another weak point is that during the import phase the keys are available unencrypted in users unsecured memory region. + +There are other alternatives to using a password based key, for example, the keys could be provided by an external repository or we could even implement a decentralized process where devices could exchange keystores securely using asymetric keys. This process would be a more complex, and would require at least one device online to grant access to the data. diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json index a8b3c87..b67f063 100644 --- a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json @@ -3,25 +3,17 @@ "_id": { "bsonType": "objectId" }, - "content": { - "properties": { - "encryptedValue": { - "bsonType": "binData" - } - }, - "required": [ - "encryptedValue" - ], - "title": "EncryptedStringField", - "type": "object" - }, "owner_id": { "bsonType": "string" + }, + "securedContent": { + "bsonType": "binData" } }, "required": [ "_id", - "owner_id" + "owner_id", + "securedContent" ], "title": "secret_record", "type": "object" diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt index c33590d..1b384c6 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt +++ b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt @@ -119,10 +119,10 @@ class KeyHelperTests { modelsCipherSpec = cipherSpec val record = SecretRecord().apply { - content!!.value = "testing a string" + content = "testing a string" } - assertEquals("testing a string", record.content!!.value) + assertEquals("testing a string", record.content) } @Test diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt index 794424c..40c8bbb 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt @@ -19,6 +19,7 @@ package io.realm.curatedsyncexamples.fieldencryption.models import io.realm.kotlin.types.EmbeddedRealmObject import io.realm.kotlin.types.annotations.Ignore import java.security.Key +import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KProperty /** @@ -35,39 +36,29 @@ lateinit var cipherSpec: SerializableCipherSpec */ lateinit var key: Key + /** - * Helper object that encapsulates the logic to encrypt/decrypt a String in an EmbeddedObject. + * Delegates that encapsulates the logic to encrypt/decrypt a String. * - * The data is accessible with [value], that would use [cipherSpec] and [key] to encrypt/decrypt the - * contents that are stored in [encryptedValue]. + * It uses the global variables [cipherSpec] and [key] to encrypt/decrypt the + * contents from [backingProperty]. */ -class EncryptedStringField : EmbeddedRealmObject { - /** - * Contains the encrypted contents. - */ - var encryptedValue: ByteArray = byteArrayOf() - - /** - * Delegated property to provide seamless access to the encrypted data. - */ - @Ignore - var value: String by DecryptionDelegate() - - inner class DecryptionDelegate { - operator fun getValue(thisRef: EncryptedStringField, property: KProperty<*>): String = - String( - bytes = cipherSpec.decrypt(thisRef.encryptedValue, key) - ) +class SecureStringDelegate(private val backingProperty: KMutableProperty0) { + operator fun getValue( + thisRef: Any, + property: KProperty<*> + ): String = String(bytes = cipherSpec.decrypt(backingProperty.get(), key)) - operator fun setValue( - thisRef: EncryptedStringField, - property: KProperty<*>, - value: String - ) { - thisRef.encryptedValue = cipherSpec.encrypt( + operator fun setValue( + thisRef: Any, + property: KProperty<*>, + value: String + ) { + backingProperty.set( + cipherSpec.encrypt( input = value.toByteArray(), key = key ) - } + ) } } \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt index 3d0593f..90a04f6 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt @@ -17,6 +17,7 @@ package io.realm.curatedsyncexamples.fieldencryption.models import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.Ignore import io.realm.kotlin.types.annotations.PersistedName import io.realm.kotlin.types.annotations.PrimaryKey import org.mongodb.kbson.BsonObjectId @@ -30,5 +31,8 @@ class SecretRecord : RealmObject { var ownerId: String = "" @PrimaryKey var _id: BsonObjectId = BsonObjectId() - var content: EncryptedStringField? = EncryptedStringField() + + var securedContent: ByteArray = byteArrayOf() + @Ignore + var content: String by SecureStringDelegate(::securedContent) } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt index 219a2be..6181bd4 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt @@ -159,25 +159,28 @@ fun SecretRecordScreen( it._id.toHexString() } ) { record -> - with(record.content!!) { - SecretRecordCard( - content = value, - encryptedContent = Base64.encode(encryptedValue), - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp, horizontal = 16.dp), - ) - } + SecretRecordCard( + content = record.content, + encryptedContent = Base64.encode(record.securedContent), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + ) + } item { Box( - modifier = Modifier.padding(16.dp).height(144.dp) + modifier = Modifier + .padding(16.dp) + .height(144.dp) ) } } Box( - modifier = Modifier.fillMaxSize().padding(16.dp), - contentAlignment= Alignment.BottomCenter + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.BottomCenter ) { AddSecretRecordCard( uiState, diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt index f69eedb..5b5c8da 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt @@ -20,9 +20,8 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore -import io.realm.curatedsyncexamples.fieldencryption.models.EncryptedStringField import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord +import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore import io.realm.curatedsyncexamples.fieldencryption.models.cipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.getFieldLevelEncryptionKey import io.realm.curatedsyncexamples.fieldencryption.models.key @@ -66,7 +65,7 @@ class SecretRecordsViewModel( } val syncConfig = SyncConfiguration - .Builder(app.currentUser!!, setOf(SecretRecord::class, EncryptedStringField::class)) + .Builder(app.currentUser!!, setOf(SecretRecord::class)) .initialSubscriptions { // Subscribe to all secret records add(it.query()) @@ -120,7 +119,7 @@ class SecretRecordsViewModel( copyToRealm( SecretRecord().apply { this.ownerId = user.id - this.content!!.value = content + this.content = content } ) } From 56707c52258e55c4c47f9efe3702cdacbf01a11d Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 22 Jun 2023 09:33:51 +0200 Subject: [PATCH 21/55] Make keyboard interaction better. (#38) --- .../ui/keystore/KeyStoreScreen.kt | 49 ++++++++++++++--- .../fieldencryption/ui/login/LoginScreen.kt | 53 +++++++++++++++++-- 2 files changed, 90 insertions(+), 12 deletions(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt index bfc9b26..fb2ccda 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt @@ -16,14 +16,14 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.keystore +import android.view.KeyEvent import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -37,7 +37,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusOrder +import androidx.compose.ui.focus.focusProperties +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -79,13 +89,16 @@ fun UnlockUserKeyStoreScreen( } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun UserKeyUnlocker( state: KeyStoreUiState, modifier: Modifier = Modifier, onUnlock: (String) -> Unit = {} ) { + val (passwordRef, buttonRef) = remember { FocusRequester.createRefs() } + val localFocusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current var password: String by remember { mutableStateOf("") } Column( @@ -123,16 +136,38 @@ fun UserKeyUnlocker( value = password, modifier = Modifier .fillMaxWidth() - .padding(top = 16.dp), + .padding(top = 16.dp) + .focusProperties { + next = buttonRef + } + .onKeyEvent { + if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + localFocusManager.moveFocus(FocusDirection.Next) + true + } + false + }, enabled = !state.isUnlocking, isError = state.errorMessage != null, - onValueChange = { password = it }, + onValueChange = { password = it.trim() /* Do not support whitespace in password */ }, label = { Text("Password") }, + singleLine = true, visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + localFocusManager.clearFocus() + } + ), ) ElevatedButton( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier + .padding(top = 4.dp) + .focusRequester(buttonRef), enabled = !state.isUnlocking, onClick = { onUnlock(password) }) { Text(text = "Continue") diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt index 0c559e8..6f92971 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt @@ -16,6 +16,7 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.login +import android.view.KeyEvent import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -23,6 +24,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ElevatedButton @@ -38,8 +40,16 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign @@ -81,13 +91,16 @@ fun LoginScreen( } } -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class) @Composable fun LoginBox( state: LoginUiState, modifier: Modifier = Modifier, onLogin: (String, String, Boolean) -> Unit = { _, _, _ -> } ) { + val (emailRef, passwordRef) = remember { FocusRequester.createRefs() } + val localFocusManager = LocalFocusManager.current + val keyboardController = LocalSoftwareKeyboardController.current var email by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } Column( @@ -112,23 +125,53 @@ fun LoginBox( ) OutlinedTextField( - modifier = Modifier.padding(top = 16.dp), + modifier = Modifier + .padding(top = 16.dp) + .focusRequester(emailRef) + .onKeyEvent { + if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + localFocusManager.moveFocus(FocusDirection.Next) + true + } + false + }, value = email, isError = state.errorMessage != null, enabled = !state.loggingIn, - onValueChange = { email = it }, + singleLine = true, + onValueChange = { email = it.trim() }, placeholder = { Text("your@email.com") }, label = { Text("Email") }, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next) ) OutlinedTextField( - modifier = Modifier.padding(top = 4.dp), + modifier = Modifier + .padding(top = 4.dp) + .focusRequester(passwordRef) + .onKeyEvent { + if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { + localFocusManager.moveFocus(FocusDirection.Next) + true + } + false + }, value = password, isError = state.errorMessage != null, enabled = !state.loggingIn, + singleLine = true, onValueChange = { password = it }, label = { Text("Password") }, visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + localFocusManager.clearFocus() + } + ), ) Row( modifier = Modifier.padding(top = 4.dp), From 4297c11dcb11c8f5b61ce0f060b14ff1d0793da4 Mon Sep 17 00:00:00 2001 From: Clemente Date: Thu, 22 Jun 2023 16:11:32 +0200 Subject: [PATCH 22/55] Use a BKS keystore as the remote keystore --- .../apps/field-encryption/README.md | 6 +- .../auth/custom_user_data.json | 2 +- .../field_encryption/custom_data/schema.json | 67 ++------- .../field-encryption/functions/config.json | 2 +- .../functions/initializeCustomData.js | 26 ++++ .../functions/initializeEncryptionKey.js | 50 ------- .../functions/updateKeyStore.js | 6 +- .../curatedsyncexamples/KeyHelperTests.kt | 133 ++++-------------- .../curatedsyncexamples/RealmKeyStoreTests.kt | 60 -------- .../demo/src/main/assets/certificate.crt | 16 +++ .../fieldencryption/DependencyInjection.kt | 29 +++- .../fieldencryption/ext/EncryptionExt.kt | 36 +---- .../fieldencryption/ext/UserExt.kt | 28 +++- .../fieldencryption/models/CustomData.kt | 12 +- .../models/EncryptedStringField.kt | 18 ++- .../models/SerializablePBEKeySpec.kt | 47 ------- .../models/SerializableSecretKey.kt | 37 ----- .../fieldencryption/models/SystemKeyStore.kt | 94 ------------- .../fieldencryption/models/UserKeyStore.kt | 108 -------------- .../fieldencryption/ui/NavGraph.kt | 10 +- .../ui/keystore/KeyStoreScreen.kt | 6 +- .../ui/keystore/KeyStoreViewModel.kt | 106 +++++++++++--- .../ui/login/LoginViewModel.kt | 2 +- .../ui/records/SecretRecordsViewModel.kt | 76 +++++----- .../ui/DemoSelectorViewModel.kt | 4 +- 25 files changed, 289 insertions(+), 692 deletions(-) create mode 100644 CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js delete mode 100644 CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js delete mode 100644 CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt create mode 100644 CuratedSyncExamples/demo/src/main/assets/certificate.crt delete mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializablePBEKeySpec.kt delete mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableSecretKey.kt delete mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SystemKeyStore.kt delete mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md index 490d732..a7c6832 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -4,15 +4,15 @@ This demo showcases a method to enhance the security of users sensitive data. Ou ## Considerations -The purpose of this demo is to show how can we leverage the MongoDB and resources to provide end-to-end field level encryption to users with multi-device access. +The purpose of this demo is to show how can we leverage the MongoDB and resources to provide end-to-end field level encryption to users with multi-device access. The example depicts the use of Atlas as a keystore but the user keystore could reside in a separate third-party key store server. ## Overview +Field level encryption requires of two -The encryption would be done using the Android keystore system. It offers enhanced security in key handling, any cryptographic key stored in this container is protected from unathorized use. Once a key is in the Android Keystore it can be used for different cryptographic operations, but it is not exportable. +The encryption would be done using the [Android keystore system](https://developer.android.com/training/articles/keystore). It offers enhanced security in key handling, any cryptographic key stored in this container is protected from unathorized use. Once a key is in the Android Keystore it can be used for different cryptographic operations, but it would not be exportable. If we only relied on the Android KeyStore to handle the encryption keys, the user would only had access to the data from a single device, keys won't be exportable into other devices. To overcome this issue we have introduced a user bound keystore in Atlas, it stores the user's keys, and it allows to import them into any new device. - ## User keystore This keystore is stored in atlas and it is accessible to the user via `CustomData`. Although the custom data is only accessible to the user, any admin with db access would be able to access to the user keys and thus to the encrypted fields. We need to also encrypt the keystore contents. diff --git a/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json b/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json index 5b2140d..51b904d 100644 --- a/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json +++ b/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json @@ -4,5 +4,5 @@ "database_name": "field_encryption", "collection_name": "custom_data", "user_id_field": "owner_id", - "on_user_creation_function_name": "initializeEncryptionKey" + "on_user_creation_function_name": "initializeCustomData" } diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json index 38869df..5a6c9b9 100644 --- a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json +++ b/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json @@ -1,10 +1,15 @@ { + "title": "custom_data", + "type": "object", + "required": [], "properties": { "_id": { "bsonType": "objectId" }, - "field_encryption_cipher_spec": { - "bsonType": "object", + "fle_cipher_spec": { + "title": "custom_data_fle_cipher_spec", + "type": "object", + "required": [], "properties": { "algorithm": { "bsonType": "string" @@ -21,62 +26,10 @@ } }, "key_store": { - "bsonType": "object", - "properties": { - "cipher_spec": { - "bsonType": "object", - "properties": { - "algorithm": { - "bsonType": "string" - }, - "block": { - "bsonType": "string" - }, - "key_length": { - "bsonType": "int" - }, - "padding": { - "bsonType": "string" - } - } - }, - "encryption_key_spec": { - "bsonType": "object", - "properties": { - "algorithm": { - "bsonType": "string" - }, - "iterations_count": { - "bsonType": "int" - }, - "key_length": { - "bsonType": "int" - }, - "salt": { - "bsonType": "array", - "items": { - "bsonType": "int" - } - } - } - }, - "key_hash": { - "bsonType": "array", - "items": { - "bsonType": "int" - } - }, - "secure_contents": { - "bsonType": "array", - "items": { - "bsonType": "int" - } - } - } + "bsonType": "binData" }, "owner_id": { "bsonType": "string" } - }, - "title": "custom_datum" -} + } +} \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/functions/config.json b/CuratedSyncExamples/apps/field-encryption/functions/config.json index bb30be9..7892cc6 100644 --- a/CuratedSyncExamples/apps/field-encryption/functions/config.json +++ b/CuratedSyncExamples/apps/field-encryption/functions/config.json @@ -1,6 +1,6 @@ [ { - "name": "initializeEncryptionKey", + "name": "initializeCustomData", "private": false }, { diff --git a/CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js b/CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js new file mode 100644 index 0000000..0542534 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js @@ -0,0 +1,26 @@ +// Executing this function would generate a new encryption key and store it in the user custom data +exports = async function (user) { + const customUserDataCollection = context.services + .get("mongodb-atlas") + .db("field_encryption") + .collection("custom_data"); + + try { + await customUserDataCollection.insertOne({ + // Bind this user custom data to the new user + owner_id: user.id, + // Algorithm spec for field level encryption + fle_cipher_spec: { + algorithm: "AES", + block: "CBC", + padding: "PKCS7Padding", + key_length: 128 + }, + // Uninitialized BKS keystore + key_store: null + }); + } catch (e) { + console.error(`Failed to create custom user data document for user: ${user.id}`); + throw e + } +}; diff --git a/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js b/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js deleted file mode 100644 index ea4f9e2..0000000 --- a/CuratedSyncExamples/apps/field-encryption/functions/initializeEncryptionKey.js +++ /dev/null @@ -1,50 +0,0 @@ -const crypto = require('crypto'); - -// This function initializes an empty user password protected keystore and defines the algorithm that -// would be used to encrypt any fields. -exports = async function (user) { - const salt = crypto.randomBytes(16); - - const customUserDataCollection = context.services - .get("mongodb-atlas") - .db("field_encryption") - .collection("custom_data"); - - try { - await customUserDataCollection.insertOne({ - // Binds this custom data to the user. - owner_id: user.id, - // Defines the field level encryption algorithm. - field_encryption_cipher_spec: { - algorithm: "AES", - block: "CBC", - padding: "PKCS7Padding", - key_length: 128 - }, - // User keystore - key_store: { - // Password based key specs - encryption_key_spec: { - algorithm: "PBKDF2WithHmacSHA256", - salt: BSON.Binary.fromHex(salt.toString('hex')), - iterations_count: 100000, - key_length: 128, - }, - // Encryption cipher spec to secure keystore contents. - cipher_spec: { - algorithm: "AES", - block: "CBC", - padding: "PKCS7Padding", - key_length: 128 - }, - // Secured contents, being null would tell the client that it has to initialize them. - secure_contents: null, - // Null as no contents exists yet. - key_hash: null, - } - }); - } catch (e) { - console.error(`Failed to create custom user data document for user: ${user.id}`); - throw e - } -}; diff --git a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js index b98974d..bac22f3 100644 --- a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js +++ b/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js @@ -1,4 +1,3 @@ -// This function would replace the users keystore contents. exports = async function (arg) { const customUserDataCollection = context.services .get("mongodb-atlas") @@ -6,14 +5,13 @@ exports = async function (arg) { .collection("custom_data"); try { - console.log(context.user.id) await customUserDataCollection.updateOne( { - // Save the user's account ID to your configured user_id_field + // Update the users custom data owner_id: context.user.id }, { - $set: {key_store: arg} + $set: { key_store: arg } } ); return true; diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt index 1b384c6..3c491ef 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt +++ b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt @@ -17,26 +17,18 @@ package io.realm.curatedsyncexamples import android.security.keystore.KeyProperties +import android.security.keystore.KeyProtection import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.curatedsyncexamples.fieldencryption.ext.getKeyOrGenerate -import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore +import io.realm.curatedsyncexamples.fieldencryption.ext.generateAndStoreKey import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.SerializablePBEKeySpec -import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord -import io.realm.curatedsyncexamples.fieldencryption.models.SerializableSecretKey -import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore -import io.realm.curatedsyncexamples.fieldencryption.models.key -import kotlinx.coroutines.test.runTest import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith import java.security.KeyStore +import java.security.KeyStore.SecretKeyEntry import javax.crypto.KeyGenerator import javax.crypto.SecretKey -import kotlin.random.Random import kotlin.test.BeforeTest -import kotlin.test.assertFailsWith -import io.realm.curatedsyncexamples.fieldencryption.models.cipherSpec as modelsCipherSpec /** * Instrumented test, which will execute on an Android device. @@ -51,20 +43,10 @@ private const val ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore" @RunWith(AndroidJUnit4::class) class KeyHelperTests { - private val keyGenerator = KeyGenerator.getInstance(ALGORITHM).apply { - init(128) - } - private val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER).apply { + private val androidKeyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER).apply { load(null) } - private val keySpec = SerializablePBEKeySpec( - algorithm = "PBKDF2WithHmacSHA256", - salt = Random.nextBytes(16), - iterationsCount = 100000, - keyLength = 128, - ) - private val cipherSpec = SerializableCipherSpec( algorithm = KeyProperties.KEY_ALGORITHM_AES, block = KeyProperties.BLOCK_MODE_CBC, @@ -72,97 +54,40 @@ class KeyHelperTests { keyLength = 128 ) - private val userKeyStore = UserKeyStore( - encryptionKeySpec = keySpec, - cipherSpec = cipherSpec, - secureContents = null, - keyHash = null, - ) - @BeforeTest fun begin() { - keyStore.deleteEntry(KEY_ALIAS) - } - - @Test - fun storeAndRetrieveKeyInAndroidKeystore() = runTest { - // Store a key - SystemKeyStore - .getKey(KEY_ALIAS) { - SerializableSecretKey( - key = keyGenerator.generateKey(), - cipherSpec = cipherSpec - ) - } - - // The key exists - assertTrue(keyStore.isKeyEntry(KEY_ALIAS)) - val retrievedKey = keyStore.getKey(KEY_ALIAS, null) - - // We can retrieve it - assertNotNull(retrievedKey) - // Matching algorithm - assertEquals(ALGORITHM, retrievedKey.algorithm) - // Secured contents - assertNull(retrievedKey.encoded) - } - - @Test - fun useAndroidKeyStoreKeyToEncryptDecrypt() = runTest { - key = SystemKeyStore - .getKey(KEY_ALIAS) { - SerializableSecretKey( - key = keyGenerator.generateKey(), - cipherSpec = cipherSpec - ) - } - modelsCipherSpec = cipherSpec - - val record = SecretRecord().apply { - content = "testing a string" - } - - assertEquals("testing a string", record.content) + androidKeyStore.deleteEntry(KEY_ALIAS) } @Test - fun storeUserKeyStore() = runTest { - val key: SecretKey = keyGenerator.generateKey() - - assertFalse(userKeyStore.hasChanges) - - val retrievedKey = userKeyStore - .getKeyOrGenerate(KEY_ALIAS, "password") { - SerializableSecretKey( - key = key, - cipherSpec = cipherSpec + fun importKeyToAndroidKeyStore() { + val keyAlias = "alias" + val secretKey = cipherSpec.generateAndStoreKey() + + val inMemoryKeyStore = KeyStore.getInstance("BKS") + .apply { + load(null) + setEntry( + keyAlias, + SecretKeyEntry(secretKey), + null ) } - assertEquals(key.algorithm, retrievedKey.cipherSpec.algorithm) - assertArrayEquals(key.encoded, retrievedKey.encoded) - - assertTrue(userKeyStore.hasChanges) - } + // Retrieve stored key + val key = inMemoryKeyStore.getKey(keyAlias, null) as SecretKey - @Test - fun userKeyStore_wrongPasswordThrows() = runTest { - userKeyStore - .getKeyOrGenerate(KEY_ALIAS, "password") { - SerializableSecretKey( - key = keyGenerator.generateKey(), - cipherSpec = cipherSpec + // Store the key locally + androidKeyStore.setEntry( + KEY_ALIAS, + SecretKeyEntry(key), + KeyProtection + .Builder( + /* purposes = */ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ) - } - - assertFailsWith { - userKeyStore - .getKeyOrGenerate(KEY_ALIAS, "password2") { - SerializableSecretKey( - key = keyGenerator.generateKey(), - cipherSpec = cipherSpec - ) - } - } + .setBlockModes(cipherSpec.block) + .setEncryptionPaddings(cipherSpec.padding) + .build() + ) } } \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt b/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt deleted file mode 100644 index dcaa88e..0000000 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/RealmKeyStoreTests.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.realm.curatedsyncexamples - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.curatedsyncexamples.fieldencryption.models.SerializablePBEKeySpec -import org.junit.Assert.* -import org.junit.Test -import org.junit.runner.RunWith -import kotlin.random.Random - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class EncryptionKeySpecTests { - private val keySpec = SerializablePBEKeySpec( - algorithm = "PBKDF2WithHmacSHA256", - salt = Random.nextBytes(16), - iterationsCount = 100000, - keyLength = 128, - ) - - @Test - fun generatePBKDF2Key() { - // Generating two keys from the same password should result in the same value - val expectedKey = keySpec.generateKey("hello world") - val matchingKey = keySpec.generateKey("hello world") - - assertArrayEquals(expectedKey.encoded, matchingKey.encoded) - - // Two keys from two different passwords should be different - val unmatchingKey = keySpec.generateKey("hello world2") - assertArrayNotEquals(expectedKey.encoded, unmatchingKey.encoded) - } -} - -fun assertArrayNotEquals(expected: ByteArray?, actual: ByteArray?) { - try { - assertArrayEquals(expected, actual) - } catch (_: AssertionError) { - // Ignore - } -} \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/assets/certificate.crt b/CuratedSyncExamples/demo/src/main/assets/certificate.crt new file mode 100644 index 0000000..e11a8e7 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/assets/certificate.crt @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICljCCAX4CCQCyCmupaumxZTANBgkqhkiG9w0BAQsFADANMQswCQYDVQQGEwJE +SzAeFw0yMzA2MjAyMTU1MzRaFw0yNDA2MTkyMTU1MzRaMA0xCzAJBgNVBAYTAkRL +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAt/VQdsQOskZ1ucjPeZ/1 +ka2XiRv9umVkTv7sFJkVA/pkVaB4khWanxGn92+ikqhkY0V4Fr0FbnBtjkwU+uDE +nV6E8k5yU7xm2kKZhLhIEmoBWOgF04S/MV3gMDn/lQKKtIKBxnvcVVJfm5wJmjH5 +PYNJ9viTRd98kWFCuogiSFh6GvglYIOkPsv1axffoju5QXOviNA/IFZplq5WesM4 +5lGvw8pvRbWrecE6EZb3kl/fLSNrAKBxKqPtN1GNL43uRp0+WrO8zWfOPmUK8PCH +XjZMTxNJ0tVRuroMmvmin2BV3W0kTDPaZLB2bLwv9avKRbdADMBD8Vef4OojqG/S +WwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCIEz0x7ppsiQgFxGe1lDPHdIWhzul3 +/lSyvxXn2euebIOlJv6R4g1TTGfnh6A5yR8ELIO8x0lWWW7VvVewG3M0ANIk1XDq +emYO9CLnGK58vEKfL+1cTemwMrh7Lmzq0P2q25xs/ZIKUtvgvNkhwDXF91iABe9G +SDpawDbqjn1eSA2aon1P61F+W2lpqTZlza5JoJJspRLRTqDL2Tu/xmLN/9ZnpBaz +KcG0cr309vvTBEI5NfcPpNS+YGTF+rQF0CV0yoBcsBMEa/DapE6zW3aoZKIYV6OP +Sv3N2CS6awU/Iu9t8e2SrDQVD8sRPBK9RbSUQavmKspRoCUZ1/tECfuW +-----END CERTIFICATE----- diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt index 77df0c7..31c2bb5 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt @@ -23,13 +23,36 @@ import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginViewModel import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordsViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module +import java.security.KeyStore val fieldEncryptionModule = module { val keyAlias = "fieldLevelEncryptionKey" - viewModel { KeyStoreViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias = keyAlias) } + val androidKeyStore = KeyStore.getInstance("AndroidKeyStore").apply { + load(null) + } + + viewModel { + KeyStoreViewModel( + app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + keyAlias = keyAlias, + localKeyStore = androidKeyStore + ) + } viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier)) } - viewModel { SecretRecordsViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } - viewModel { NavGraphViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), keyAlias) } + viewModel { + SecretRecordsViewModel( + app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + keyAlias = keyAlias, + localKeyStore = androidKeyStore + ) + } + viewModel { + NavGraphViewModel( + app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + localKeyStore = androidKeyStore, + keyAlias = keyAlias, + ) + } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt index 926468c..3b7dd68 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt @@ -17,34 +17,10 @@ package io.realm.curatedsyncexamples.fieldencryption.ext import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.SerializableSecretKey -import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore -import io.realm.kotlin.mongodb.User -import java.security.Key -import java.security.MessageDigest import javax.crypto.KeyGenerator +import javax.crypto.SecretKey -const val HASH_ALGORITHM = "SHA-256" - -fun Key.computeHash(): ByteArray = - MessageDigest.getInstance(HASH_ALGORITHM).digest(encoded) - -suspend fun UserKeyStore.getKeyOrGenerate( - alias: String, - password: String, - generateNewKey: suspend () -> SerializableSecretKey -): SerializableSecretKey = use(password) { - if (!contains(alias)) { - set(alias, generateNewKey()) - .also { - hasChanges = true - } - } - - get(alias)!! -} - -fun SerializableCipherSpec.newKey(): SerializableSecretKey = +fun SerializableCipherSpec.generateAndStoreKey(): SecretKey = KeyGenerator .getInstance( /* algorithm = */ algorithm @@ -52,11 +28,3 @@ fun SerializableCipherSpec.newKey(): SerializableSecretKey = init(keyLength) } .generateKey() - .let { key-> - SerializableSecretKey( - key = key, - cipherSpec = this@newKey - ) - } - -fun User.generateKey(): SerializableSecretKey = fieldEncryptionCipherSpec().newKey() diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt index d2676bd..4b470b6 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt @@ -18,25 +18,39 @@ package io.realm.curatedsyncexamples.fieldencryption.ext import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.CustomData -import io.realm.curatedsyncexamples.fieldencryption.models.UserKeyStore import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.ext.call import io.realm.kotlin.mongodb.ext.customData +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.security.KeyStore @OptIn(ExperimentalRealmSerializerApi::class) -fun User.keyStore(): UserKeyStore { - return customData()?.keyStore!! -} +fun User.hasKeyStore() = customData()?.keyStore != null + +@OptIn(ExperimentalRealmSerializerApi::class) +fun User.getRemoteKeyStore(password: String): KeyStore = + KeyStore.getInstance("BKS").apply { + // Load any user keystore if available + customData()?.keyStore?.let { keyStoreBlob -> + ByteArrayInputStream(keyStoreBlob).use { keyStoreStream -> + load(keyStoreStream, password.toCharArray()) + } + } ?: load(null) + } @OptIn(ExperimentalRealmSerializerApi::class) -suspend fun User.updateKeyStore(keyStore: UserKeyStore) { +suspend fun User.updateRemoteKeyStore(keyStore: KeyStore, password: String) { functions.call("updateKeyStore") { - add(keyStore) + ByteArrayOutputStream().use { outputStream -> + keyStore.store(outputStream, password.toCharArray()) + add(outputStream.toByteArray()) + } } } @OptIn(ExperimentalRealmSerializerApi::class) fun User.fieldEncryptionCipherSpec(): SerializableCipherSpec { - return customData()?.fieldEncryptionCipherSpec!! + return customData()?.FLECipherSpec!! } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt index ab67549..f7efa5c 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt @@ -25,16 +25,16 @@ import kotlinx.serialization.Serializable * 2. Field encryption cipher spec, */ @Serializable -data class CustomData( +class CustomData( /** - * Defines the algorithm and settings use to encrypt and decrypt fields. + * Defines the algorithm for field level encryption. */ - @SerialName("field_encryption_cipher_spec") - val fieldEncryptionCipherSpec: SerializableCipherSpec?, + @SerialName("fle_cipher_spec") + val FLECipherSpec: SerializableCipherSpec?, /** - * Password protected keystore, it contains the field encryption key. + * BKS keystore. */ @SerialName("key_store") - val keyStore: UserKeyStore + val keyStore: ByteArray? ) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt index 40c8bbb..1858852 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt @@ -16,38 +16,36 @@ */ package io.realm.curatedsyncexamples.fieldencryption.models -import io.realm.kotlin.types.EmbeddedRealmObject -import io.realm.kotlin.types.annotations.Ignore import java.security.Key import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KProperty /** - * Cipher spec that will be used to encrypt/decrypt fields. + * Field level encryption cypher spec. * * It is a global variable because we cannot reference the user out from a RealmObject yet. */ -lateinit var cipherSpec: SerializableCipherSpec +lateinit var FLECipherSpec: SerializableCipherSpec /** - * Key that will be used to encrypt/decrypt fields. + * Field level encryption key. * * It is a global variable because we cannot reference the user out from a RealmObject yet. */ -lateinit var key: Key +lateinit var FLEKey: Key /** * Delegates that encapsulates the logic to encrypt/decrypt a String. * - * It uses the global variables [cipherSpec] and [key] to encrypt/decrypt the + * It uses the global variables [FLECipherSpec] and [FLEKey] to encrypt/decrypt the * contents from [backingProperty]. */ class SecureStringDelegate(private val backingProperty: KMutableProperty0) { operator fun getValue( thisRef: Any, property: KProperty<*> - ): String = String(bytes = cipherSpec.decrypt(backingProperty.get(), key)) + ): String = String(bytes = FLECipherSpec.decrypt(backingProperty.get(), FLEKey)) operator fun setValue( thisRef: Any, @@ -55,9 +53,9 @@ class SecureStringDelegate(private val backingProperty: KMutableProperty0 SerializableSecretKey - ): Key { - if (!keyStore.isKeyEntry(keyAlias)) - storeKey(keyAlias, generateKey()) - - return keyStore - .getKey(keyAlias, null) - } - - private fun storeKey( - keyAlias: String, - key: SerializableSecretKey - ) { - keyStore.setEntry( - keyAlias, - KeyStore.SecretKeyEntry(key.asSecretKey()), - KeyProtection - .Builder( - /* purposes = */ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setBlockModes(key.cipherSpec.block) - .setEncryptionPaddings(key.cipherSpec.padding) - .build() - ) - } -} - -/** - * Helper method that retrieves the key that would be used to access field level encrypted data. - * - * This function would first try to retrieve the key from the system keystore, if it not available - * it would then retrieve it from the user keystore. - */ -suspend fun getFieldLevelEncryptionKey(keyAlias: String, user: User, password: String) = - SystemKeyStore - .getKey(keyAlias) { - // Key is missing in the Android keystore, retrieve it from the keystore - val keyStore = user.keyStore() - - keyStore.getKeyOrGenerate(keyAlias, password) { - // Key is missing in the User keystore, generate a new one - user.generateKey() - }.also { - // We might have modified the user keystore, lets propagate the changes to the server - if (keyStore.hasChanges) user.updateKeyStore(keyStore) - } - } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt deleted file mode 100644 index 7173200..0000000 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/UserKeyStore.kt +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2023 Realm Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package io.realm.curatedsyncexamples.fieldencryption.models - -import io.realm.curatedsyncexamples.fieldencryption.ext.computeHash -import io.realm.kotlin.internal.platform.runBlocking -import kotlinx.coroutines.Dispatchers -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import java.nio.charset.StandardCharsets -import java.security.Key - -/** - * Serializable class that models a password protected key store. - */ -@Serializable -class UserKeyStore( - /** - * Specifies the algorithm used to generate the key that would secure this keystore contents. - */ - @SerialName("encryption_key_spec") - val encryptionKeySpec: SerializablePBEKeySpec, - - /** - * Cipher algorithm used to secure this keystore contents. - */ - @SerialName("cipher_spec") - val cipherSpec: SerializableCipherSpec, - - /** - * Encrypted contents. The type of the unencrypted data is a Map. - */ - @SerialName("secure_contents") - var secureContents: ByteArray?, - - /** - * Computed hash for the encryption key used to encode the contents. It allows to - * identify if the right key was used to decode the contents. - */ - @SerialName("key_hash") - var keyHash: ByteArray?, - - /** - * Indicates if the keystore has been modified. - */ - @Transient - var hasChanges: Boolean = false -) { - - private fun loadContents(key: Key): MutableMap = - secureContents?.let { byteArray -> - keyHash?.let { - require(keyHash.contentEquals(key.computeHash())) { "Wrong password" } - } - - val serializedKeyStore = String( - cipherSpec.decrypt(byteArray, key), - StandardCharsets.UTF_8 - ) - - Json.decodeFromString>(serializedKeyStore) - .toMutableMap() - } ?: mutableMapOf() - - private fun saveContents(contents: MutableMap, key: Key) { - val updatedKeyStore = Json.encodeToString(contents) - .toByteArray(StandardCharsets.UTF_8) - - keyHash = key.computeHash() - - secureContents = cipherSpec.encrypt(updatedKeyStore, key) - } - - suspend fun use( - password: String, - update: suspend MutableMap.() -> T - ): T = - // Encryption/decryption can take a while - runBlocking(Dispatchers.IO) { - val key = encryptionKeySpec.generateKey(password) - - loadContents(key).let { contents -> - try { - contents.update() - } finally { - saveContents(contents, key) - } - } - } -} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt index 6127b27..61f6fe4 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt @@ -16,24 +16,23 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.UnlockUserKeyStoreScreen import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginScreen import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordScreen import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.Credentials import org.koin.compose.koinInject +import java.security.KeyStore object Screens { const val LOGIN_SCREEN = "LOGIN_SCREEN" @@ -43,11 +42,12 @@ object Screens { class NavGraphViewModel( private val app: App, - private val keyAlias: String + private val keyAlias: String, + private val localKeyStore: KeyStore ) : ViewModel() { fun isUserLoggedIn(): Boolean = app.currentUser != null - fun isFieldEncryptionKeyAvailable(): Boolean = SystemKeyStore.containsKey(keyAlias) + fun isFieldEncryptionKeyAvailable(): Boolean = localKeyStore.isKeyEntry(keyAlias) } @Composable diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt index fb2ccda..2ed4b1b 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusOrder import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.onKeyEvent @@ -78,7 +77,7 @@ fun UnlockUserKeyStoreScreen( state = uiState, modifier = Modifier.padding(48.dp) ) { password -> - viewModel.unlock(password) + viewModel.importRemoteKey(password) } } LaunchedEffect(uiState.isUnlocked) { @@ -144,8 +143,9 @@ fun UserKeyUnlocker( if (it.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_ENTER) { localFocusManager.moveFocus(FocusDirection.Next) true + } else { + false } - false }, enabled = !state.isUnlocking, isError = state.errorMessage != null, diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt index ef44fff..df1a697 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt @@ -16,17 +16,27 @@ */ package io.realm.curatedsyncexamples.fieldencryption.ui.keystore +import android.security.keystore.KeyProperties +import android.security.keystore.KeyProtection import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.fieldencryption.models.getFieldLevelEncryptionKey -import io.realm.curatedsyncexamples.fieldencryption.models.key +import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec +import io.realm.curatedsyncexamples.fieldencryption.ext.generateAndStoreKey +import io.realm.curatedsyncexamples.fieldencryption.ext.getRemoteKeyStore +import io.realm.curatedsyncexamples.fieldencryption.ext.hasKeyStore +import io.realm.curatedsyncexamples.fieldencryption.ext.updateRemoteKeyStore import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.User import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.io.IOException +import java.security.KeyStore +import java.security.KeyStore.SecretKeyEntry +import javax.crypto.SecretKey data class KeyStoreUiState( val isInitialized: Boolean = false, @@ -36,32 +46,90 @@ data class KeyStoreUiState( ) class KeyStoreViewModel( - val app: App? = null, + val app: App, private val keyAlias: String, - uiState: KeyStoreUiState = KeyStoreUiState(), + private val localKeyStore: KeyStore, ) : ViewModel() { - private val _uiState = MutableStateFlow(uiState) + private val _uiState = MutableStateFlow( + KeyStoreUiState( + isInitialized = app.currentUser?.hasKeyStore() == true + ) + ) val uiState: StateFlow = _uiState.asStateFlow() - init { - - } - fun unlock(password: String) { + fun importRemoteKey(password: String) { _uiState.update { it.copy(isUnlocking = true, isUnlocked = false, errorMessage = null) } viewModelScope.launch(Dispatchers.IO) { try { - key = getFieldLevelEncryptionKey(keyAlias, app!!.currentUser!!, password) - - _uiState.update { - it.copy(isUnlocked = true) - } - } catch (e: Exception) { - _uiState.update { - it.copy(isUnlocking = false, errorMessage = e.message) - } + doImportRemoteKey(password) + } catch (exception: IOException) { + notifyError("Wrong password") } } } -} \ No newline at end of file + + private fun notifyError(errorMessage: String) { + _uiState.update { + it.copy( + isUnlocking = false, + isUnlocked = false, + errorMessage = errorMessage + ) + } + } + + /** + * Imports a key from a remote keystore to a local + */ + private suspend fun doImportRemoteKey( + password: String + ) { + app.currentUser?.let { user -> + val remoteKeyStore = user.getRemoteKeyStore(password) + + if (!remoteKeyStore.isKeyEntry(keyAlias)) { + // key is missing, generate and store a new key + generateAndStoreKey(user, remoteKeyStore, password) + } + // Now we can safely retrieve the key from the remote keystore + val remoteKey = remoteKeyStore.getKey(keyAlias, null) as SecretKey + val cipherSpec = user.fieldEncryptionCipherSpec() + + // now we can add it in the secure local keystore + localKeyStore.setEntry( + /* alias = */ keyAlias, + /* entry = */ SecretKeyEntry(remoteKey), + /* protParam = */ KeyProtection + .Builder( + /* purposes = */ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT + ) + .setBlockModes(cipherSpec.block) + .setEncryptionPaddings(cipherSpec.padding) + .build() + ) + + _uiState.update { + it.copy(isUnlocked = true) + } + } ?: notifyError("No user found") // TODO navigate to login page? + } + + private suspend fun generateAndStoreKey(user: User, keyStore: KeyStore, password: String) { + val cipherSpec = user.fieldEncryptionCipherSpec() + val key = cipherSpec.generateAndStoreKey() + + keyStore.setEntry( + keyAlias, + SecretKeyEntry(key), + null + ) + + // update the remote keystore with the new key + user.updateRemoteKeyStore( + keyStore = keyStore, + password = password + ) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt index 0ad66c0..47cf581 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt @@ -65,4 +65,4 @@ class LoginViewModel( } } } -} \ No newline at end of file +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt index 5b5c8da..71c5e9e 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt @@ -21,13 +21,10 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord -import io.realm.curatedsyncexamples.fieldencryption.models.SystemKeyStore -import io.realm.curatedsyncexamples.fieldencryption.models.cipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.getFieldLevelEncryptionKey -import io.realm.curatedsyncexamples.fieldencryption.models.key +import io.realm.curatedsyncexamples.fieldencryption.models.FLECipherSpec +import io.realm.curatedsyncexamples.fieldencryption.models.FLEKey import io.realm.kotlin.Realm import io.realm.kotlin.ext.query -import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.sync.SyncConfiguration @@ -39,6 +36,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import java.security.KeyStore data class SecretRecordsUiState( val loading: Boolean = true, @@ -48,7 +46,8 @@ data class SecretRecordsUiState( class SecretRecordsViewModel( private val app: App, - private val keyAlias: String + private val keyAlias: String, + private val localKeyStore: KeyStore, ) : ViewModel() { private lateinit var realm: Realm private lateinit var user: User @@ -58,39 +57,41 @@ class SecretRecordsViewModel( init { viewModelScope.launch(Dispatchers.IO) { - user = app.currentUser!! - cipherSpec = user.fieldEncryptionCipherSpec() - runBlocking { - key = getFieldLevelEncryptionKey(keyAlias, user, "password") - } - - val syncConfig = SyncConfiguration - .Builder(app.currentUser!!, setOf(SecretRecord::class)) - .initialSubscriptions { - // Subscribe to all secret records - add(it.query()) - } - .waitForInitialRemoteData() - .build() + app.currentUser?.let { user -> + this@SecretRecordsViewModel.user = user - realm = Realm.open(syncConfig) + // Import key and cipher spec required to access the secret contents + FLECipherSpec = user.fieldEncryptionCipherSpec() + FLEKey = localKeyStore.getKey(keyAlias, null) - val job = async { - realm.query() - .sort("_id", Sort.DESCENDING) - .asFlow() - .collect { - records.postValue(it.list) + val syncConfig = SyncConfiguration + .Builder(app.currentUser!!, setOf(SecretRecord::class)) + .initialSubscriptions { + // Subscribe to all secret records + add(it.query()) } - } + .waitForInitialRemoteData() + .build() - addCloseable { - job.cancel() - realm.close() - } + realm = Realm.open(syncConfig) - _uiState.update { - it.copy(loading = false) + val job = async { + realm.query() + .sort("_id", Sort.DESCENDING) + .asFlow() + .collect { + records.postValue(it.list) + } + } + + addCloseable { + job.cancel() + realm.close() + } + + _uiState.update { + it.copy(loading = false) + } } } } @@ -104,8 +105,9 @@ class SecretRecordsViewModel( it.copy(loggingOut = true) } viewModelScope.launch(Dispatchers.IO) { - user.logOut() - SystemKeyStore.removeKey(keyAlias) + app.currentUser?.logOut() + + localKeyStore.deleteEntry(keyAlias) _uiState.update { it.copy(loggedOut = true) @@ -125,4 +127,4 @@ class SecretRecordsViewModel( } } } -} \ No newline at end of file +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt index 9a2878c..88a04df 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt @@ -22,6 +22,7 @@ import androidx.lifecycle.viewModelScope import io.realm.curatedsyncexamples.DemoWithApp import io.realm.curatedsyncexamples.Demos import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.AuthenticationProvider import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.exceptions.ServiceException import kotlinx.coroutines.Dispatchers @@ -41,7 +42,8 @@ class ExamplesScreenViewModel(private val apps: List) : ViewModel() private suspend fun App.isAvailable() = try { - login(Credentials.anonymous()) + // Try to login an anonymous user to see if the app is active. + login(Credentials.anonymous(reuseExisting = false)).logOut() true } catch (e: ServiceException) { e.message?.startsWith("[Service][Unknown(4351)]") != true From 769322f72f96a23fda3945de3374d66c9066273d Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 26 Jun 2023 12:26:48 +0200 Subject: [PATCH 23/55] Clean up --- .../fieldencryption/models/CustomData.kt | 6 +++--- .../fieldencryption/models/EncryptedStringField.kt | 12 +++++++++--- .../fieldencryption/models/SecretRecord.kt | 9 ++++++++- .../models/SerializableCipherSpec.kt | 14 +++++--------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt index f7efa5c..509937a 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt @@ -20,20 +20,20 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** - * App services user custom data. It contains the resources required to achieve field level encryption: + * App services user custom data. It contains the resources required for FLE: * 1. Encryption key, it is stored in a password protected keystore. * 2. Field encryption cipher spec, */ @Serializable class CustomData( /** - * Defines the algorithm for field level encryption. + * Defines the FLE algorithm. */ @SerialName("fle_cipher_spec") val FLECipherSpec: SerializableCipherSpec?, /** - * BKS keystore. + * BKS keystore containing the key. */ @SerialName("key_store") val keyStore: ByteArray? diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt index 1858852..e606c83 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt @@ -16,6 +16,7 @@ */ package io.realm.curatedsyncexamples.fieldencryption.models +import java.lang.Exception import java.security.Key import kotlin.reflect.KMutableProperty0 import kotlin.reflect.KProperty @@ -44,13 +45,18 @@ lateinit var FLEKey: Key class SecureStringDelegate(private val backingProperty: KMutableProperty0) { operator fun getValue( thisRef: Any, - property: KProperty<*> - ): String = String(bytes = FLECipherSpec.decrypt(backingProperty.get(), FLEKey)) + property: KProperty<*>, + ): String = + try { + String(bytes = FLECipherSpec.decrypt(backingProperty.get(), FLEKey)) + } catch (e: Exception) { + "Data could not be decrypted" + } operator fun setValue( thisRef: Any, property: KProperty<*>, - value: String + value: String, ) { backingProperty.set( FLECipherSpec.encrypt( diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt index 90a04f6..3642f47 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt @@ -23,7 +23,7 @@ import io.realm.kotlin.types.annotations.PrimaryKey import org.mongodb.kbson.BsonObjectId /** - * Object that contains an encrypted field. + * Object with an encrypted field. */ @PersistedName("secret_record") class SecretRecord : RealmObject { @@ -32,7 +32,14 @@ class SecretRecord : RealmObject { @PrimaryKey var _id: BsonObjectId = BsonObjectId() + /** + * Contains encrypted data. + */ var securedContent: ByteArray = byteArrayOf() + + /** + * Helper that automatically encrypts/decrypts data. + */ @Ignore var content: String by SecureStringDelegate(::securedContent) } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt index b7252a8..7e253e1 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt @@ -59,14 +59,10 @@ data class SerializableCipherSpec( /* key = */ key, /* params = */ IvParameterSpec(encryptedData, 0, IV_SIZE) ) - try { - doFinal( - /* input = */ encryptedData, - /* inputOffset = */ IV_SIZE, - /* inputLen = */ encryptedData.size - IV_SIZE - ) - } catch (e: Exception) { - byteArrayOf() - } + doFinal( + /* input = */ encryptedData, + /* inputOffset = */ IV_SIZE, + /* inputLen = */ encryptedData.size - IV_SIZE + ) } } From 80cac2e33acd60ec123a151d49716aef8ad48bbe Mon Sep 17 00:00:00 2001 From: Clemente Date: Mon, 26 Jun 2023 23:32:07 +0200 Subject: [PATCH 24/55] Presence detection sample --- .../apps/field-encryption/realm_config.json | 2 +- .../apps/presence-detection/README.md | 0 .../auth/custom_user_data.json | 3 + .../presence-detection/auth/providers.json | 12 + .../data_sources/mongodb-atlas/config.json | 10 + .../mongodb-atlas/default_rule.json | 17 ++ .../user_status/relationships.json | 1 + .../user_status/schema.json | 20 ++ .../environments/development.json | 3 + .../environments/no-environment.json | 3 + .../environments/production.json | 3 + .../presence-detection/environments/qa.json | 3 + .../environments/testing.json | 3 + .../presence-detection/functions/config.json | 6 + .../functions/logPresenceDetector.js | 43 +++ .../presence-detection/graphql/config.json | 3 + .../http_endpoints/config.json | 1 + .../log_forwarders/presenceDetector.json | 17 ++ .../apps/presence-detection/realm_config.json | 8 + .../apps/presence-detection/sync/config.json | 13 + .../demo/src/main/AndroidManifest.xml | 1 + .../io/realm/curatedsyncexamples/Constants.kt | 2 +- .../CuratedSyncExamplesApp.kt | 9 +- .../DependencyInjection.kt | 3 +- .../presence/DependencyInjection.kt | 30 +++ .../presence/PresenceDetectionActivity.kt | 44 ++++ .../presence/models/UserStatus.kt | 16 ++ .../presence/ui/UserStatusListScreen.kt | 245 ++++++++++++++++++ .../presence/ui/UserStatusListViewModel.kt | 144 ++++++++++ .../ui/DemoSelectorViewModel.kt | 6 +- .../main/res/drawable/baseline_cancel_24.xml | 5 + .../res/drawable/baseline_check_circle_24.xml | 5 + 32 files changed, 673 insertions(+), 8 deletions(-) create mode 100644 CuratedSyncExamples/apps/presence-detection/README.md create mode 100644 CuratedSyncExamples/apps/presence-detection/auth/custom_user_data.json create mode 100644 CuratedSyncExamples/apps/presence-detection/auth/providers.json create mode 100644 CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/config.json create mode 100644 CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/default_rule.json create mode 100644 CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/relationships.json create mode 100644 CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/schema.json create mode 100644 CuratedSyncExamples/apps/presence-detection/environments/development.json create mode 100644 CuratedSyncExamples/apps/presence-detection/environments/no-environment.json create mode 100644 CuratedSyncExamples/apps/presence-detection/environments/production.json create mode 100644 CuratedSyncExamples/apps/presence-detection/environments/qa.json create mode 100644 CuratedSyncExamples/apps/presence-detection/environments/testing.json create mode 100644 CuratedSyncExamples/apps/presence-detection/functions/config.json create mode 100644 CuratedSyncExamples/apps/presence-detection/functions/logPresenceDetector.js create mode 100644 CuratedSyncExamples/apps/presence-detection/graphql/config.json create mode 100644 CuratedSyncExamples/apps/presence-detection/http_endpoints/config.json create mode 100644 CuratedSyncExamples/apps/presence-detection/log_forwarders/presenceDetector.json create mode 100644 CuratedSyncExamples/apps/presence-detection/realm_config.json create mode 100644 CuratedSyncExamples/apps/presence-detection/sync/config.json create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/DependencyInjection.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/PresenceDetectionActivity.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/models/UserStatus.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListScreen.kt create mode 100644 CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListViewModel.kt create mode 100644 CuratedSyncExamples/demo/src/main/res/drawable/baseline_cancel_24.xml create mode 100644 CuratedSyncExamples/demo/src/main/res/drawable/baseline_check_circle_24.xml diff --git a/CuratedSyncExamples/apps/field-encryption/realm_config.json b/CuratedSyncExamples/apps/field-encryption/realm_config.json index 4156d4e..6f436b4 100644 --- a/CuratedSyncExamples/apps/field-encryption/realm_config.json +++ b/CuratedSyncExamples/apps/field-encryption/realm_config.json @@ -1,5 +1,5 @@ { - "app_id": "field-encryption-fjrvt", + "app_id": "field-encryption", "config_version": 20210101, "name": "field-encryption", "location": "IE", diff --git a/CuratedSyncExamples/apps/presence-detection/README.md b/CuratedSyncExamples/apps/presence-detection/README.md new file mode 100644 index 0000000..e69de29 diff --git a/CuratedSyncExamples/apps/presence-detection/auth/custom_user_data.json b/CuratedSyncExamples/apps/presence-detection/auth/custom_user_data.json new file mode 100644 index 0000000..a82d0fb --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/auth/custom_user_data.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/CuratedSyncExamples/apps/presence-detection/auth/providers.json b/CuratedSyncExamples/apps/presence-detection/auth/providers.json new file mode 100644 index 0000000..dff103b --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/auth/providers.json @@ -0,0 +1,12 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": false + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + } +} diff --git a/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/config.json b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/config.json new file mode 100644 index 0000000..9913676 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/config.json @@ -0,0 +1,10 @@ +{ + "name": "mongodb-atlas", + "type": "mongodb-atlas", + "config": { + "clusterName": "Cluster0", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/default_rule.json b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/default_rule.json new file mode 100644 index 0000000..ea47d16 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/default_rule.json @@ -0,0 +1,17 @@ +{ + "roles": [ + { + "name": "readAll", + "apply_when": {}, + "document_filters": { + "write": false, + "read": true + }, + "read": true, + "write": false, + "insert": false, + "delete": false, + "search": true + } + ] +} diff --git a/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/relationships.json b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/relationships.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/relationships.json @@ -0,0 +1 @@ +{} diff --git a/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/schema.json b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/schema.json new file mode 100644 index 0000000..ec0a37a --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/data_sources/mongodb-atlas/presence-detection/user_status/schema.json @@ -0,0 +1,20 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "owner_id": { + "bsonType": "string" + }, + "present": { + "bsonType": "bool" + } + }, + "required": [ + "_id", + "present", + "owner_id" + ], + "title": "user_status", + "type": "object" +} diff --git a/CuratedSyncExamples/apps/presence-detection/environments/development.json b/CuratedSyncExamples/apps/presence-detection/environments/development.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/environments/development.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/presence-detection/environments/no-environment.json b/CuratedSyncExamples/apps/presence-detection/environments/no-environment.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/environments/no-environment.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/presence-detection/environments/production.json b/CuratedSyncExamples/apps/presence-detection/environments/production.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/environments/production.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/presence-detection/environments/qa.json b/CuratedSyncExamples/apps/presence-detection/environments/qa.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/environments/qa.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/presence-detection/environments/testing.json b/CuratedSyncExamples/apps/presence-detection/environments/testing.json new file mode 100644 index 0000000..ad7e98e --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/environments/testing.json @@ -0,0 +1,3 @@ +{ + "values": {} +} diff --git a/CuratedSyncExamples/apps/presence-detection/functions/config.json b/CuratedSyncExamples/apps/presence-detection/functions/config.json new file mode 100644 index 0000000..fb3fa8a --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/functions/config.json @@ -0,0 +1,6 @@ +[ + { + "name": "logPresenceDetector", + "private": false + } +] diff --git a/CuratedSyncExamples/apps/presence-detection/functions/logPresenceDetector.js b/CuratedSyncExamples/apps/presence-detection/functions/logPresenceDetector.js new file mode 100644 index 0000000..6fb3ce5 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/functions/logPresenceDetector.js @@ -0,0 +1,43 @@ +exports = async function (logs) { + // logs appear in ascending order + for (let i = logs.length - 1; i >= 0; i--) { + extract_presence(logs[i]) + } +}; + +async function extract_presence(log) { + let type = log.type + if (type !== "SYNC_SESSION_START" && type !== "SYNC_SESSION_END") return; + + let user_id = log.user_id; + let present = type === "SYNC_SESSION_START"; + + console.log(`User ${user_id} present: ${present}`); + + update_presence(user_id, present); +} + +async function update_presence(user_id, present) { + const customUserDataCollection = context.services + .get("mongodb-atlas") + .db("presence-detection") + .collection("user_status"); + + try { + await customUserDataCollection.updateOne( + { + owner_id: user_id, + }, + { + owner_id: user_id, + present: present + }, + { + upsert: true + } + ); + } catch (e) { + console.error(`Failed to create custom user data document for user: ${user_id}`); + throw e + } +} diff --git a/CuratedSyncExamples/apps/presence-detection/graphql/config.json b/CuratedSyncExamples/apps/presence-detection/graphql/config.json new file mode 100644 index 0000000..c1d7285 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/graphql/config.json @@ -0,0 +1,3 @@ +{ + "use_natural_pluralization": true +} diff --git a/CuratedSyncExamples/apps/presence-detection/http_endpoints/config.json b/CuratedSyncExamples/apps/presence-detection/http_endpoints/config.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/http_endpoints/config.json @@ -0,0 +1 @@ +[] diff --git a/CuratedSyncExamples/apps/presence-detection/log_forwarders/presenceDetector.json b/CuratedSyncExamples/apps/presence-detection/log_forwarders/presenceDetector.json new file mode 100644 index 0000000..3199484 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/log_forwarders/presenceDetector.json @@ -0,0 +1,17 @@ +{ + "name": "presenceDetector", + "log_types": [ + "sync" + ], + "log_statuses": [ + "success" + ], + "policy": { + "type": "batch" + }, + "action": { + "type": "function", + "name": "logPresenceDetector" + }, + "disabled": false +} diff --git a/CuratedSyncExamples/apps/presence-detection/realm_config.json b/CuratedSyncExamples/apps/presence-detection/realm_config.json new file mode 100644 index 0000000..1648fd3 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/realm_config.json @@ -0,0 +1,8 @@ +{ + "app_id": "presence-detection", + "config_version": 20210101, + "name": "presence-detection", + "location": "IE", + "provider_region": "aws-eu-west-1", + "deployment_model": "LOCAL" +} diff --git a/CuratedSyncExamples/apps/presence-detection/sync/config.json b/CuratedSyncExamples/apps/presence-detection/sync/config.json new file mode 100644 index 0000000..72c55c5 --- /dev/null +++ b/CuratedSyncExamples/apps/presence-detection/sync/config.json @@ -0,0 +1,13 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": false, + "service_name": "mongodb-atlas", + "database_name": "presence-detection", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + } +} diff --git a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml b/CuratedSyncExamples/demo/src/main/AndroidManifest.xml index 06e8abf..b549b97 100644 --- a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml +++ b/CuratedSyncExamples/demo/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ + \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt index c726d3b..13fa57b 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt @@ -18,7 +18,7 @@ package io.realm.curatedsyncexamples const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" -const val USER_PRESENCE_APP_ID = "" +const val USER_PRESENCE_APP_ID = "presence-detection-bcsii" const val OFFLINE_LOGIN_APP_ID = "" const val ERROR_HANDLING_APP_ID = "" const val BUSINESS_LOGIC_APP_ID = "" diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt index 686781c..1278bfd 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt @@ -18,11 +18,12 @@ package io.realm.curatedsyncexamples import android.app.Application import io.realm.curatedsyncexamples.fieldencryption.fieldEncryptionModule +import io.realm.curatedsyncexamples.presence.presenceDetectionModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.GlobalContext.startKoin -class CuratedSyncExamplesApp: Application() { +class CuratedSyncExamplesApp : Application() { override fun onCreate() { super.onCreate() @@ -32,7 +33,11 @@ class CuratedSyncExamplesApp: Application() { // Reference Android context androidContext(this@CuratedSyncExamplesApp) // Load modules - modules(mainModule, fieldEncryptionModule) + modules( + mainModule, + fieldEncryptionModule, + presenceDetectionModule, + ) } } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt index de7f476..c3ede16 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt @@ -17,6 +17,7 @@ package io.realm.curatedsyncexamples import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity +import io.realm.curatedsyncexamples.presence.PresenceDetectionActivity import io.realm.curatedsyncexamples.ui.ExamplesScreenViewModel import io.realm.kotlin.mongodb.App import org.koin.androidx.viewmodel.dsl.viewModel @@ -38,7 +39,7 @@ enum class Demos( ), USER_PRESENCE( "User presence", - DemoSelectorActivity::class.java, + PresenceDetectionActivity::class.java, USER_PRESENCE_APP_ID, ), OFFLINE_LOGIN( diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/DependencyInjection.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/DependencyInjection.kt new file mode 100644 index 0000000..c324eb1 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/DependencyInjection.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.presence + +import io.realm.curatedsyncexamples.Demos +import io.realm.curatedsyncexamples.presence.ui.UserStatusListViewModel +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val presenceDetectionModule = module { + viewModel { + UserStatusListViewModel( + app = get(qualifier = Demos.USER_PRESENCE.qualifier), + ) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/PresenceDetectionActivity.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/PresenceDetectionActivity.kt new file mode 100644 index 0000000..e62c049 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/PresenceDetectionActivity.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.presence + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import io.realm.curatedsyncexamples.presence.ui.UserStatusListScreen +import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme + +class PresenceDetectionActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + CuratedSyncExamplesTheme { + UserStatusListScreen( + modifier = Modifier + .fillMaxSize(), + ) { + // close the app + finish() + } + } + } + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/models/UserStatus.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/models/UserStatus.kt new file mode 100644 index 0000000..2b33c7d --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/models/UserStatus.kt @@ -0,0 +1,16 @@ +package io.realm.curatedsyncexamples.presence.models + +import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.annotations.PersistedName +import io.realm.kotlin.types.annotations.PrimaryKey +import org.mongodb.kbson.ObjectId + +@PersistedName("user_status") +class UserStatus : RealmObject { + @PersistedName("_id") + @PrimaryKey + var id: ObjectId = ObjectId() + @PersistedName("owner_id") + var ownerId: String = "" + var present: Boolean = false +} \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListScreen.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListScreen.kt new file mode 100644 index 0000000..0d3cc7d --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListScreen.kt @@ -0,0 +1,245 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.presence.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import io.realm.curatedsyncexamples.R +import io.realm.kotlin.mongodb.sync.ConnectionState +import org.koin.compose.koinInject + +@Composable +fun ControlsCard( + state: UserListUiStatus, + modifier: Modifier = Modifier, + onLogout: () -> Unit, + onConnect: () -> Unit, + onDisconnect: () -> Unit, +) { + + ElevatedCard( + modifier = modifier.fillMaxWidth() + ) { + Box(Modifier.padding(16.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + ElevatedButton( + colors = ButtonDefaults.buttonColors(), + enabled = state.connectionState != ConnectionState.CONNECTING, + modifier = Modifier + .weight(1f) + .padding(end = 4.dp), + onClick = { + if (state.connectionState == ConnectionState.DISCONNECTED) onConnect() + else if (state.connectionState == ConnectionState.CONNECTED) onDisconnect() + } + ) { + Text( + text = when (state.connectionState) { + ConnectionState.DISCONNECTED -> "Connect" + ConnectionState.CONNECTING -> "Connecting..." + ConnectionState.CONNECTED -> "Disconnect" + } + ) + } + ElevatedButton( + colors = ButtonDefaults.buttonColors(), + enabled = !state.loading && !state.loggingOut, + modifier = Modifier + .weight(1f) + .padding(start = 4.dp), + onClick = onLogout + ) { + Text(text = "Logout") + } + } + } + } +} + +@Composable +fun StatusCard( + connected: Boolean, + message: String, + modifier: Modifier = Modifier, +) { + ElevatedCard( + modifier = modifier + ) { + Row( + modifier = Modifier.padding(16.dp) + ) { + if (connected) { + Icon( + painter = painterResource(R.drawable.baseline_check_circle_24), + contentDescription = "Connected", + tint = Color(0, 200, 0) + ) + } else { + Icon( + painter = painterResource(R.drawable.baseline_cancel_24), + contentDescription = "Disconnected", + tint = Color(200, 0, 0) + ) + } + Text( + modifier = Modifier + .padding(start = 4.dp) + .align(Alignment.CenterVertically), + text = message, + softWrap = false, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +fun UserStatusListScreen( + viewModel: UserStatusListViewModel = koinInject(), + modifier: Modifier = Modifier, + onLogout: () -> Unit, +) { + val users by viewModel.userStatus.observeAsState(emptyList()) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val listState = rememberLazyListState() + + LaunchedEffect(uiState.loggedOut) { + if (uiState.loggedOut) { + onLogout() + } + } + LaunchedEffect(key1 = users) { + listState.scrollToItem(0) + } + LazyColumn( + state = listState + ) { + if (uiState.connectionState == ConnectionState.CONNECTED) { + items( + users, + key = { + it.id.toHexString() + } + ) { userStatus -> + StatusCard( + connected = userStatus.present, + message = if (uiState.userId == userStatus.ownerId) { + "You" + } else { + "User id: ${userStatus.ownerId}" + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp, horizontal = 16.dp), + ) + + } + } + item { + Box( + modifier = Modifier + .padding(16.dp) + .height(144.dp) + ) + } + } + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.BottomCenter + ) { + ControlsCard( + uiState, + onLogout = { viewModel.logout() }, + onConnect = { viewModel.connect() }, + onDisconnect = { viewModel.disconnect() }, + ) + } +} + +@Preview +@Composable +fun ConnectedUserStatusCardPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + StatusCard( + connected = true, + message = "hello world" + ) + } +} + + +@Preview +@Composable +fun DisconnectedUserStatusCardPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + StatusCard( + connected = false, + message = "hello world" + ) + } +} + +@Preview +@Composable +fun ControlsCardPreview() { + Surface( + color = MaterialTheme.colorScheme.background + ) { + ControlsCard( + state = UserListUiStatus( + loading = false + ), + onLogout = {}, + onDisconnect = {}, + onConnect = {}, + ) + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListViewModel.kt new file mode 100644 index 0000000..3e04ae2 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/presence/ui/UserStatusListViewModel.kt @@ -0,0 +1,144 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.curatedsyncexamples.presence.ui + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.realm.curatedsyncexamples.presence.models.UserStatus +import io.realm.kotlin.Realm +import io.realm.kotlin.ext.query +import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.Credentials +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.ConnectionState +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.mongodb.syncSession +import io.realm.kotlin.query.Sort +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class UserListUiStatus( + val loading: Boolean = true, + val loggingOut: Boolean = false, + val loggedOut: Boolean = false, + val userId: String = "", + val connectionState: ConnectionState = ConnectionState.CONNECTING, +) + +class UserStatusListViewModel( + private val app: App, +) : ViewModel() { + private lateinit var realm: Realm + private lateinit var user: User + + private val _uiState = MutableStateFlow(UserListUiStatus()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + app.login(Credentials.anonymous()) + .let { user -> + this@UserStatusListViewModel.user = user + + val syncConfig = SyncConfiguration + .Builder(app.currentUser!!, setOf(UserStatus::class)) + .initialSubscriptions { + // Subscribe to all user statuses + add(it.query()) + } + .waitForInitialRemoteData() + .build() + + realm = Realm.open(syncConfig) + + val job = async { + realm.query() + .sort("present", Sort.DESCENDING) + .asFlow() + .collect { + userStatus.postValue(it.list) + } + } + + val job2 = async { + realm.syncSession + .connectionStateAsFlow() + .collect {connectionStateChange -> + _uiState.update { + it.copy( + connectionState = connectionStateChange.newState + ) + } + } + } + + addCloseable { + job.cancel() + job2.cancel() + realm.close() + } + + _uiState.update { + it.copy( + userId = user.id, + loading = false, + connectionState = realm.syncSession.connectionState + ) + } + } + } + } + + val userStatus: MutableLiveData> by lazy { + MutableLiveData>() + } + + fun connect() { + _uiState.update { + it.copy(connectionState = ConnectionState.CONNECTING) + } + + realm.syncSession.resume() + } + + fun disconnect() { + _uiState.update { + it.copy(connectionState = ConnectionState.DISCONNECTED) + } + + realm.syncSession.pause() + } + + fun logout() { + _uiState.update { + it.copy(loggingOut = true) + } + viewModelScope.launch(Dispatchers.IO) { + app.currentUser?.logOut() + + _uiState.update { + it.copy(loggedOut = true) + } + } + } +} diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt index 88a04df..5cb6dea 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt @@ -42,11 +42,11 @@ class ExamplesScreenViewModel(private val apps: List) : ViewModel() private suspend fun App.isAvailable() = try { - // Try to login an anonymous user to see if the app is active. - login(Credentials.anonymous(reuseExisting = false)).logOut() + // Try to perform some action to validate that the app exists + emailPasswordAuth.resendConfirmationEmail("realm") true } catch (e: ServiceException) { - e.message?.startsWith("[Service][Unknown(4351)]") != true + e.message?.startsWith("[Service][Unknown(4351)] cannot find app") != true } private suspend fun getDemoEntriesWithStatus() = diff --git a/CuratedSyncExamples/demo/src/main/res/drawable/baseline_cancel_24.xml b/CuratedSyncExamples/demo/src/main/res/drawable/baseline_cancel_24.xml new file mode 100644 index 0000000..a0a94e3 --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/res/drawable/baseline_cancel_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/CuratedSyncExamples/demo/src/main/res/drawable/baseline_check_circle_24.xml b/CuratedSyncExamples/demo/src/main/res/drawable/baseline_check_circle_24.xml new file mode 100644 index 0000000..b83d1bc --- /dev/null +++ b/CuratedSyncExamples/demo/src/main/res/drawable/baseline_check_circle_24.xml @@ -0,0 +1,5 @@ + + + From 2e497555f588bdb63290c3c0321c82a1166a9125 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 27 Jun 2023 10:39:42 +0200 Subject: [PATCH 25/55] Push readme updates --- .../apps/field-encryption/README.md | 85 +++- .../apps/field-encryption/diagram1.svg | 365 +++++++++++++++ .../apps/field-encryption/diagram2.svg | 434 +++++++++++++++++ .../apps/field-encryption/diagram3.svg | 443 ++++++++++++++++++ 4 files changed, 1306 insertions(+), 21 deletions(-) create mode 100644 CuratedSyncExamples/apps/field-encryption/diagram1.svg create mode 100644 CuratedSyncExamples/apps/field-encryption/diagram2.svg create mode 100644 CuratedSyncExamples/apps/field-encryption/diagram3.svg diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md index a7c6832..8ba7654 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -1,42 +1,85 @@ -# Field level encryption +# Property level encryption +The goal of property-level encryption is to ensure that only the users themselves can access their data and prevent any unauthorized access by other parties, including MongoDB. -This demo showcases a method to enhance the security of users sensitive data. Our goal is to ensure that only the users themselves can access their data and prevent any unauthorized access by other parties. Additionally, we enable users to access their data simultaneously from multiple devices. All of this is achieved by leveraging the powerful capabilities of MongoDB services. +Realm does not have built-in property-level encryption support. However, with this sample we will demonstrate how to add this feature with the Realm Sync and Android Keystore APIs. Furthermore, this solution would enable users to access their data simultaneously from multiple devices. All of this is achieved by leveraging the powerful capabilities of MongoDB services. + +## Introduction +Property-level encryption restricts access to sensitive data by applying a ciphering algorithm with a unique key that will encrypt the data on the device. The encrypted data will be synced to Atlas, thus only allowing people with the proper key to be able to read it again. This also includes MonogDB itself. + +It is crucial to ensure that only the rightful users have access to the keys. ## Considerations +The purpose of this demo is to show how can we leverage MongoDB and other tools to provide end-to-end property-level encryption to users with multi-device access. + +There are different algorithms available to achieve encryption, the ones selected in this sample are for demonstrative purposes. Any production development must select them based on their security requirements. + +## Key handling +Keys are stored in a key store on the device, a secure repository that safe-keeps the keys and guarantees that only the rightful users have access to them. It normally has some hardware-enhanced security features. + +For this demo, we will use the Android Keystore available from SDK version > 18. It offers hardware-enhanced security like biometric authentication and guarantees that the cryptographic material stored in it is not exportable. + +We will use the symmetric encryption algorithm AES, as it provides great performance when encrypting files and data. + +## Multi-device support +As we mentioned, the local device keystore does not support exporting keys, this means that a user would not be able to access their data from a new device as the encryption keys would only be available from the original one. To overcome this we need a shared keystore that would allow accessing these keys from any device. + +![alt text](diagram1.svg "Deployment") + +For this sample, we store the user keys in a password-protected Keystore in the user's `CustomData`. This allows you to control access to these (encrypted) keys based on the permission system in App Services. -The purpose of this demo is to show how can we leverage the MongoDB and resources to provide end-to-end field level encryption to users with multi-device access. The example depicts the use of Atlas as a keystore but the user keystore could reside in a separate third-party key store server. +We have chosen `BKS` as the remote keystore format as it supports storing symmetric keys. It is also supported by the Android SDK and allows us to show the interaction between the remote and local keystores in the sample code. -## Overview -Field level encryption requires of two +## Key import +After the user logs in on a new device, we need to import any required keys from the remote keystore into the local. If no key were available in the remote keystore, we would generate a new one that will be stored both remotely and locally. -The encryption would be done using the [Android keystore system](https://developer.android.com/training/articles/keystore). It offers enhanced security in key handling, any cryptographic key stored in this container is protected from unathorized use. Once a key is in the Android Keystore it can be used for different cryptographic operations, but it would not be exportable. +![alt text](diagram2.svg "Key import") -If we only relied on the Android KeyStore to handle the encryption keys, the user would only had access to the data from a single device, keys won't be exportable into other devices. To overcome this issue we have introduced a user bound keystore in Atlas, it stores the user's keys, and it allows to import them into any new device. +This process would ensure that the data is accessible anytime anywhere, even offline as the keys would be stored in the user's devices. While password protected in the server, the keys stored in the device keystore would be hardware protected. -## User keystore +The process of importing the keys can be found here [link to the code]. -This keystore is stored in atlas and it is accessible to the user via `CustomData`. Although the custom data is only accessible to the user, any admin with db access would be able to access to the user keys and thus to the encrypted fields. We need to also encrypt the keystore contents. +## Encrypting data +As we mentioned before, to be able to encrypt data we need an algorithm along with the key to encrypt and decrypt. In the `CustomData` we have included a property that contains the specification for the algorithm that would be used for FLE. -To facilitate device roaming the keystore encryption key is password generated. This way a user would be able to generate the key on any new device if they facilitate the right passphrase. +AES cipher specification is defined in the users custom data. +```kotlin +{ + fle_cipher_spec: { + algorithm: "AES", + block: "CBC", + padding: "PKCS7Padding", + key_length: 128 + } +} +``` -## Importing and creating keys +Java Crypto APIs work with binary data, before encrypting we would have to convert the value into a byte array, and then after decrypting we would convert the byte array into the actual type. -The user keystore is not intended to serve as the primary keystore since the Android key store provides a higher level of security. +Realm has support for ByteArrays so we would be able to store any encrypted data in a RealmObject. -The system keystore serves as a repository for importing keys, which are then utilized for cryptographic operations. If a user needs to generate a new key, it will be stored not only in the system keystore but also in the user's personal keystore. This ensures that the key is accessible across different devices later on. +```kotlin +class Person: RealmObject { + var securedContent: ByteArray? = null +} +``` -## Accessing data +These operations can be encapsulated into a helper delegate, for example [insert link] to seamlessly provide access to the data. -Once the keys are present in the system and combined with the encryption algorithm specification defined in the user's custom data, we can secure the user's data. +```kotlin +class Person: RealmObject { + var securedContent: ByteArray? = null + var content String by SecureStringDelegate(::securedSSN) +} +``` -In this sample we have created the `SecureStringDelegate` a helper that provides seamless access to the secured data, as if it was a regular property. +Currently, there are some limitations on how we can associate a key to an object or property because RealmObjects don't provide any information about its source realm or user yet. In this demo we have opted for storing such data in global variables. -There are some known issues around data modelling right now. First it would convenient if we were able to access the user custom data from an object, that would facilitate accessing the cipher algorithm and key. Second, when we add support for custom type adapters we would be able to collapse the secured and accessor in a single property. +![alt text](diagram3.svg "Flow") -## Vector attacks +## Attack vectors +Nothing is 100% secure, there is always a tradeoffs associated with security. The implementation outlined here also suffers from the vulnerabilities. Please evaluate them against your security needs before using the approach outlined: -The algorithm used to secure the user keystore in Atlas is prone to brute force attacks, anybody with with access could attempt an attack. +The remote keystore stored on the server is only password protected and thus prone to brute-force attacks if anyone gets access to it. Choosing a secure password is very important. +During the import phase, the keys reside in unprotected user memory. Android apps are operating in secure sandboxes, but if a device is rooted this guarantee might be broken and memory will be susceptible to be read by malicious programs. -Another weak point is that during the import phase the keys are available unencrypted in users unsecured memory region. -There are other alternatives to using a password based key, for example, the keys could be provided by an external repository or we could even implement a decentralized process where devices could exchange keystores securely using asymetric keys. This process would be a more complex, and would require at least one device online to grant access to the data. diff --git a/CuratedSyncExamples/apps/field-encryption/diagram1.svg b/CuratedSyncExamples/apps/field-encryption/diagram1.svg new file mode 100644 index 0000000..bc51d52 --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/diagram1.svg @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CuratedSyncExamples/apps/field-encryption/diagram2.svg b/CuratedSyncExamples/apps/field-encryption/diagram2.svg new file mode 100644 index 0000000..0781b2f --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/diagram2.svg @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CuratedSyncExamples/apps/field-encryption/diagram3.svg b/CuratedSyncExamples/apps/field-encryption/diagram3.svg new file mode 100644 index 0000000..3e194cf --- /dev/null +++ b/CuratedSyncExamples/apps/field-encryption/diagram3.svg @@ -0,0 +1,443 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 298300af0dbcad65c855f561b9e9175692cdb220 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 27 Jun 2023 10:52:03 +0200 Subject: [PATCH 26/55] Add links --- CuratedSyncExamples/README.md | 4 ++-- CuratedSyncExamples/apps/field-encryption/README.md | 8 ++++---- .../{EncryptedStringField.kt => SecureStringDelegate.kt} | 0 3 files changed, 6 insertions(+), 6 deletions(-) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/{EncryptedStringField.kt => SecureStringDelegate.kt} (100%) diff --git a/CuratedSyncExamples/README.md b/CuratedSyncExamples/README.md index ff65142..33b49ca 100644 --- a/CuratedSyncExamples/README.md +++ b/CuratedSyncExamples/README.md @@ -4,7 +4,7 @@ Reference app that show cases different design patterns and examples of Realm Ko ## Samples -### [Field level encryption](https://github.com/realm/realm-kotlin-samples/blob/main/CuratedSyncExamples/apps/field-encryption/README.md) +### [Field level encryption](apps/field-encryption/README.md) This demo shows the process of protecting users sensitive data by employing encryption techniques while guaranteeing the access from any users device. @@ -23,6 +23,6 @@ To begin, locate the App services app sources that you wish to install. We have Next, follow the steps outlined in the [Atlas documentation](https://www.mongodb.com/docs/atlas/app-services/apps/create/) to setup the apps. These docs will guide you through the process and help troubleshoot any issue you might encounter. -After deploying the Atlas apps, you will need to update [Constants.kt](TBD) with the newly created app ids. +After deploying the Atlas apps, you will need to update [Constants.kt](demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt) with the newly created app ids. Once you have completed these steps, you would be able to run the samples using the Kotlin demo app. \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md index 8ba7654..7dcb2ea 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -36,13 +36,13 @@ After the user logs in on a new device, we need to import any required keys from This process would ensure that the data is accessible anytime anywhere, even offline as the keys would be stored in the user's devices. While password protected in the server, the keys stored in the device keystore would be hardware protected. -The process of importing the keys can be found here [link to the code]. +The process of importing the keys can be found [here](demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt#L86). ## Encrypting data As we mentioned before, to be able to encrypt data we need an algorithm along with the key to encrypt and decrypt. In the `CustomData` we have included a property that contains the specification for the algorithm that would be used for FLE. AES cipher specification is defined in the users custom data. -```kotlin +```json { fle_cipher_spec: { algorithm: "AES", @@ -63,12 +63,12 @@ class Person: RealmObject { } ``` -These operations can be encapsulated into a helper delegate, for example [insert link] to seamlessly provide access to the data. +These operations can be encapsulated into a helper delegate, for example [SecureStringDelegate](demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt#45) to seamlessly provide access to the data. ```kotlin class Person: RealmObject { var securedContent: ByteArray? = null - var content String by SecureStringDelegate(::securedSSN) + var content String by SecureStringDelegate(::securedContent) } ``` diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt similarity index 100% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/EncryptedStringField.kt rename to CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt From c98d0c074bed8f5fc35f07f4aa4dc4a25f435a02 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 10:55:00 +0200 Subject: [PATCH 27/55] Update CuratedSyncExamples/README.md Co-authored-by: Christian Melchior --- CuratedSyncExamples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CuratedSyncExamples/README.md b/CuratedSyncExamples/README.md index 33b49ca..999c3df 100644 --- a/CuratedSyncExamples/README.md +++ b/CuratedSyncExamples/README.md @@ -1,6 +1,6 @@ # Kotlin SDK Curated samples -Reference app that show cases different design patterns and examples of Realm Kotlin SDK with Atlas. +Reference app that show cases different design patterns and examples of using the Realm Kotlin SDK with Atlas App Services. ## Samples From d28393cb9f28f58df7bd73324d1335e08e7f6537 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 10:55:07 +0200 Subject: [PATCH 28/55] Update CuratedSyncExamples/README.md Co-authored-by: Christian Melchior --- CuratedSyncExamples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CuratedSyncExamples/README.md b/CuratedSyncExamples/README.md index 999c3df..11320f7 100644 --- a/CuratedSyncExamples/README.md +++ b/CuratedSyncExamples/README.md @@ -6,7 +6,7 @@ Reference app that show cases different design patterns and examples of using th ### [Field level encryption](apps/field-encryption/README.md) -This demo shows the process of protecting users sensitive data by employing encryption techniques while guaranteeing the access from any users device. +This demo shows the process of protecting users' sensitive data by employing end-to-end encryption techniques while guaranteeing access from any user's device. ## Demo app structure From 40ac767a3d520d5eb4b53fb1c3cb2a4486324f32 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 11:04:21 +0200 Subject: [PATCH 29/55] Update CuratedSyncExamples/demo/.gitignore Co-authored-by: Christian Melchior --- CuratedSyncExamples/demo/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CuratedSyncExamples/demo/.gitignore b/CuratedSyncExamples/demo/.gitignore index 42afabf..796b96d 100644 --- a/CuratedSyncExamples/demo/.gitignore +++ b/CuratedSyncExamples/demo/.gitignore @@ -1 +1 @@ -/build \ No newline at end of file +/build From c03ed27222aaf80e555d345d41e064452bbc1e9a Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 11:04:34 +0200 Subject: [PATCH 30/55] Update CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt Co-authored-by: Christian Melchior --- .../demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt index c726d3b..c8dc5d0 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt +++ b/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt @@ -17,7 +17,6 @@ package io.realm.curatedsyncexamples const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" - const val USER_PRESENCE_APP_ID = "" const val OFFLINE_LOGIN_APP_ID = "" const val ERROR_HANDLING_APP_ID = "" From 62a7557bbcfb83d6995a5564d6ae5e33a09c4f09 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 11:04:47 +0200 Subject: [PATCH 31/55] Update CuratedSyncExamples/build.gradle.kts Co-authored-by: Christian Melchior --- CuratedSyncExamples/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CuratedSyncExamples/build.gradle.kts b/CuratedSyncExamples/build.gradle.kts index f55768e..67f437e 100644 --- a/CuratedSyncExamples/build.gradle.kts +++ b/CuratedSyncExamples/build.gradle.kts @@ -3,4 +3,4 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.kotlin.android) apply false -} \ No newline at end of file +} From aee024468be6ac3b21bbd74a434b634120bb4f86 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 27 Jun 2023 11:04:55 +0200 Subject: [PATCH 32/55] Update CuratedSyncExamples/demo/build.gradle.kts Co-authored-by: Christian Melchior --- CuratedSyncExamples/demo/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CuratedSyncExamples/demo/build.gradle.kts b/CuratedSyncExamples/demo/build.gradle.kts index 8350844..864b887 100644 --- a/CuratedSyncExamples/demo/build.gradle.kts +++ b/CuratedSyncExamples/demo/build.gradle.kts @@ -87,4 +87,4 @@ dependencies { androidTestImplementation(libs.androidx.test.runner) androidTestImplementation(libs.androidx.test.ext) androidTestImplementation(libs.kotlinx.coroutines.test) -} \ No newline at end of file +} From 8a147be0fda4d33aaf309ee4af04a432993e77f5 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 27 Jun 2023 11:09:34 +0200 Subject: [PATCH 33/55] PR change requests --- CuratedSyncExamples/.gitignore | 7 +------ CuratedSyncExamples/apps/field-encryption/README.md | 4 +--- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/CuratedSyncExamples/.gitignore b/CuratedSyncExamples/.gitignore index aa724b7..878c2cd 100644 --- a/CuratedSyncExamples/.gitignore +++ b/CuratedSyncExamples/.gitignore @@ -1,12 +1,7 @@ *.iml .gradle /local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml +.idea .DS_Store /build /captures diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/CuratedSyncExamples/apps/field-encryption/README.md index 7dcb2ea..5844e14 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/CuratedSyncExamples/apps/field-encryption/README.md @@ -76,10 +76,8 @@ Currently, there are some limitations on how we can associate a key to an object ![alt text](diagram3.svg "Flow") -## Attack vectors +## Vectors attacks Nothing is 100% secure, there is always a tradeoffs associated with security. The implementation outlined here also suffers from the vulnerabilities. Please evaluate them against your security needs before using the approach outlined: The remote keystore stored on the server is only password protected and thus prone to brute-force attacks if anyone gets access to it. Choosing a secure password is very important. During the import phase, the keys reside in unprotected user memory. Android apps are operating in secure sandboxes, but if a device is rooted this guarantee might be broken and memory will be susceptible to be read by malicious programs. - - From ac3c27e83b3f881a176042da9b2c357d4c75f138 Mon Sep 17 00:00:00 2001 From: Clemente Date: Tue, 27 Jun 2023 11:34:38 +0200 Subject: [PATCH 34/55] Rename to App Services Usage Samples --- .../.gitignore | 0 .../README.md | 4 +- .../apps/field-encryption/README.md | 4 +- .../auth/custom_user_data.json | 0 .../apps/field-encryption/auth/providers.json | 0 .../data_sources/mongodb-atlas/config.json | 0 .../mongodb-atlas/default_rule.json | 0 .../custom_data/relationships.json | 0 .../field_encryption/custom_data/schema.json | 0 .../secret_record/relationships.json | 0 .../secret_record/schema.json | 0 .../apps/field-encryption/diagram1.svg | 0 .../apps/field-encryption/diagram2.svg | 0 .../apps/field-encryption/diagram3.svg | 0 .../environments/development.json | 0 .../environments/no-environment.json | 0 .../environments/production.json | 0 .../field-encryption/environments/qa.json | 0 .../environments/testing.json | 0 .../field-encryption/functions/config.json | 0 .../functions/initializeCustomData.js | 0 .../functions/updateKeyStore.js | 0 .../apps/field-encryption/graphql/config.json | 0 .../http_endpoints/config.json | 0 .../apps/field-encryption/realm_config.json | 0 .../apps/field-encryption/sync/config.json | 0 .../build.gradle.kts | 0 .../demo/.gitignore | 0 .../demo/build.gradle.kts | 4 +- .../demo/proguard-rules.pro | 0 .../curatedsyncexamples/KeyHelperTests.kt | 6 +-- .../demo/src/main/AndroidManifest.xml | 6 +-- .../demo/src/main/assets/certificate.crt | 0 .../AppServicesUsageSamplesApp.kt | 8 ++-- .../appservicesusagesamples}/Constants.kt | 2 +- .../DemoSelectorActivity.kt | 8 ++-- .../DependencyInjection.kt | 36 +++++++++--------- .../fieldencryption/DependencyInjection.kt | 20 +++++----- .../FieldEncryptionActivity.kt | 10 ++--- .../fieldencryption/ext/EncryptionExt.kt | 4 +- .../fieldencryption/ext/UserExt.kt | 6 +-- .../fieldencryption/models/CustomData.kt | 2 +- .../fieldencryption/models/SecretRecord.kt | 2 +- .../models/SecureStringDelegate.kt | 2 +- .../models/SerializableCipherSpec.kt | 2 +- .../fieldencryption/ui/NavGraph.kt | 8 ++-- .../ui/keystore/KeyStoreScreen.kt | 2 +- .../ui/keystore/KeyStoreViewModel.kt | 12 +++--- .../fieldencryption/ui/login/LoginScreen.kt | 2 +- .../ui/login/LoginViewModel.kt | 2 +- .../ui/records/SecretRecordsScreen.kt | 2 +- .../ui/records/SecretRecordsViewModel.kt | 10 ++--- .../ui/DemoSelectorScreen.kt | 4 +- .../ui/DemoSelectorViewModel.kt | 10 ++--- .../ui/theme/Color.kt | 2 +- .../ui/theme/Theme.kt | 4 +- .../appservicesusagesamples}/ui/theme/Type.kt | 2 +- .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../main/res/drawable/realmio_logo_vector.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin .../res/mipmap-hdpi/ic_launcher_round.webp | Bin .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin .../res/mipmap-mdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin .../demo/src/main/res/values/strings.xml | 4 ++ .../demo/src/main/res/values/themes.xml | 4 +- .../demo/src/main/res/xml/backup_rules.xml | 0 .../main/res/xml/data_extraction_rules.xml | 0 .../gradle.properties | 0 .../gradle/libs.versions.toml | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../settings.gradle.kts | 2 +- .../demo/src/main/res/values/strings.xml | 4 -- 83 files changed, 99 insertions(+), 101 deletions(-) rename {CuratedSyncExamples => AppServicesUsageSamples}/.gitignore (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/README.md (90%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/README.md (93%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/auth/custom_user_data.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/auth/providers.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/diagram1.svg (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/diagram2.svg (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/diagram3.svg (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/environments/development.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/environments/no-environment.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/environments/production.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/environments/qa.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/environments/testing.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/functions/config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/functions/initializeCustomData.js (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/functions/updateKeyStore.js (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/graphql/config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/http_endpoints/config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/realm_config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/apps/field-encryption/sync/config.json (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/build.gradle.kts (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/.gitignore (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/build.gradle.kts (95%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/proguard-rules.pro (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt (92%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/AndroidManifest.xml (84%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/assets/certificate.crt (100%) rename CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt (82%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/Constants.kt (95%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/DemoSelectorActivity.kt (81%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/DependencyInjection.kt (58%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/DependencyInjection.kt (60%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/FieldEncryptionActivity.kt (77%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ext/EncryptionExt.kt (85%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ext/UserExt.kt (89%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/models/CustomData.kt (94%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/models/SecretRecord.kt (95%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/models/SecureStringDelegate.kt (96%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/models/SerializableCipherSpec.kt (97%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/NavGraph.kt (92%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/keystore/KeyStoreScreen.kt (99%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/keystore/KeyStoreViewModel.kt (90%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/login/LoginScreen.kt (99%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/login/LoginViewModel.kt (97%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/records/SecretRecordsScreen.kt (99%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/fieldencryption/ui/records/SecretRecordsViewModel.kt (91%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/ui/DemoSelectorScreen.kt (98%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/ui/DemoSelectorViewModel.kt (86%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/ui/theme/Color.kt (94%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/ui/theme/Theme.kt (97%) rename {CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples => AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples}/ui/theme/Type.kt (96%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/drawable/ic_launcher_background.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/drawable/realmio_logo_vector.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-hdpi/ic_launcher.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-mdpi/ic_launcher.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp (100%) create mode 100644 AppServicesUsageSamples/demo/src/main/res/values/strings.xml rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/values/themes.xml (53%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/xml/backup_rules.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/demo/src/main/res/xml/data_extraction_rules.xml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/gradle.properties (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/gradle/libs.versions.toml (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/gradle/wrapper/gradle-wrapper.properties (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/gradlew (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/gradlew.bat (100%) rename {CuratedSyncExamples => AppServicesUsageSamples}/settings.gradle.kts (86%) delete mode 100644 CuratedSyncExamples/demo/src/main/res/values/strings.xml diff --git a/CuratedSyncExamples/.gitignore b/AppServicesUsageSamples/.gitignore similarity index 100% rename from CuratedSyncExamples/.gitignore rename to AppServicesUsageSamples/.gitignore diff --git a/CuratedSyncExamples/README.md b/AppServicesUsageSamples/README.md similarity index 90% rename from CuratedSyncExamples/README.md rename to AppServicesUsageSamples/README.md index 11320f7..84d7faf 100644 --- a/CuratedSyncExamples/README.md +++ b/AppServicesUsageSamples/README.md @@ -1,4 +1,4 @@ -# Kotlin SDK Curated samples +# App Services Usage Samples Reference app that show cases different design patterns and examples of using the Realm Kotlin SDK with Atlas App Services. @@ -23,6 +23,6 @@ To begin, locate the App services app sources that you wish to install. We have Next, follow the steps outlined in the [Atlas documentation](https://www.mongodb.com/docs/atlas/app-services/apps/create/) to setup the apps. These docs will guide you through the process and help troubleshoot any issue you might encounter. -After deploying the Atlas apps, you will need to update [Constants.kt](demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt) with the newly created app ids. +After deploying the Atlas apps, you will need to update [Constants.kt](demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt) with the newly created app ids. Once you have completed these steps, you would be able to run the samples using the Kotlin demo app. \ No newline at end of file diff --git a/CuratedSyncExamples/apps/field-encryption/README.md b/AppServicesUsageSamples/apps/field-encryption/README.md similarity index 93% rename from CuratedSyncExamples/apps/field-encryption/README.md rename to AppServicesUsageSamples/apps/field-encryption/README.md index 5844e14..0925ace 100644 --- a/CuratedSyncExamples/apps/field-encryption/README.md +++ b/AppServicesUsageSamples/apps/field-encryption/README.md @@ -36,7 +36,7 @@ After the user logs in on a new device, we need to import any required keys from This process would ensure that the data is accessible anytime anywhere, even offline as the keys would be stored in the user's devices. While password protected in the server, the keys stored in the device keystore would be hardware protected. -The process of importing the keys can be found [here](demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt#L86). +The process of importing the keys can be found [here](../../demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt#L86). ## Encrypting data As we mentioned before, to be able to encrypt data we need an algorithm along with the key to encrypt and decrypt. In the `CustomData` we have included a property that contains the specification for the algorithm that would be used for FLE. @@ -63,7 +63,7 @@ class Person: RealmObject { } ``` -These operations can be encapsulated into a helper delegate, for example [SecureStringDelegate](demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt#45) to seamlessly provide access to the data. +These operations can be encapsulated into a helper delegate, for example [SecureStringDelegate](../../demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecureStringDelegate.kt#L45) to seamlessly provide access to the data. ```kotlin class Person: RealmObject { diff --git a/CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json b/AppServicesUsageSamples/apps/field-encryption/auth/custom_user_data.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/auth/custom_user_data.json rename to AppServicesUsageSamples/apps/field-encryption/auth/custom_user_data.json diff --git a/CuratedSyncExamples/apps/field-encryption/auth/providers.json b/AppServicesUsageSamples/apps/field-encryption/auth/providers.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/auth/providers.json rename to AppServicesUsageSamples/apps/field-encryption/auth/providers.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/config.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/config.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/default_rule.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/relationships.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/custom_data/schema.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/relationships.json diff --git a/CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json b/AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json rename to AppServicesUsageSamples/apps/field-encryption/data_sources/mongodb-atlas/field_encryption/secret_record/schema.json diff --git a/CuratedSyncExamples/apps/field-encryption/diagram1.svg b/AppServicesUsageSamples/apps/field-encryption/diagram1.svg similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/diagram1.svg rename to AppServicesUsageSamples/apps/field-encryption/diagram1.svg diff --git a/CuratedSyncExamples/apps/field-encryption/diagram2.svg b/AppServicesUsageSamples/apps/field-encryption/diagram2.svg similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/diagram2.svg rename to AppServicesUsageSamples/apps/field-encryption/diagram2.svg diff --git a/CuratedSyncExamples/apps/field-encryption/diagram3.svg b/AppServicesUsageSamples/apps/field-encryption/diagram3.svg similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/diagram3.svg rename to AppServicesUsageSamples/apps/field-encryption/diagram3.svg diff --git a/CuratedSyncExamples/apps/field-encryption/environments/development.json b/AppServicesUsageSamples/apps/field-encryption/environments/development.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/environments/development.json rename to AppServicesUsageSamples/apps/field-encryption/environments/development.json diff --git a/CuratedSyncExamples/apps/field-encryption/environments/no-environment.json b/AppServicesUsageSamples/apps/field-encryption/environments/no-environment.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/environments/no-environment.json rename to AppServicesUsageSamples/apps/field-encryption/environments/no-environment.json diff --git a/CuratedSyncExamples/apps/field-encryption/environments/production.json b/AppServicesUsageSamples/apps/field-encryption/environments/production.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/environments/production.json rename to AppServicesUsageSamples/apps/field-encryption/environments/production.json diff --git a/CuratedSyncExamples/apps/field-encryption/environments/qa.json b/AppServicesUsageSamples/apps/field-encryption/environments/qa.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/environments/qa.json rename to AppServicesUsageSamples/apps/field-encryption/environments/qa.json diff --git a/CuratedSyncExamples/apps/field-encryption/environments/testing.json b/AppServicesUsageSamples/apps/field-encryption/environments/testing.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/environments/testing.json rename to AppServicesUsageSamples/apps/field-encryption/environments/testing.json diff --git a/CuratedSyncExamples/apps/field-encryption/functions/config.json b/AppServicesUsageSamples/apps/field-encryption/functions/config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/functions/config.json rename to AppServicesUsageSamples/apps/field-encryption/functions/config.json diff --git a/CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js b/AppServicesUsageSamples/apps/field-encryption/functions/initializeCustomData.js similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/functions/initializeCustomData.js rename to AppServicesUsageSamples/apps/field-encryption/functions/initializeCustomData.js diff --git a/CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js b/AppServicesUsageSamples/apps/field-encryption/functions/updateKeyStore.js similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/functions/updateKeyStore.js rename to AppServicesUsageSamples/apps/field-encryption/functions/updateKeyStore.js diff --git a/CuratedSyncExamples/apps/field-encryption/graphql/config.json b/AppServicesUsageSamples/apps/field-encryption/graphql/config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/graphql/config.json rename to AppServicesUsageSamples/apps/field-encryption/graphql/config.json diff --git a/CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json b/AppServicesUsageSamples/apps/field-encryption/http_endpoints/config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/http_endpoints/config.json rename to AppServicesUsageSamples/apps/field-encryption/http_endpoints/config.json diff --git a/CuratedSyncExamples/apps/field-encryption/realm_config.json b/AppServicesUsageSamples/apps/field-encryption/realm_config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/realm_config.json rename to AppServicesUsageSamples/apps/field-encryption/realm_config.json diff --git a/CuratedSyncExamples/apps/field-encryption/sync/config.json b/AppServicesUsageSamples/apps/field-encryption/sync/config.json similarity index 100% rename from CuratedSyncExamples/apps/field-encryption/sync/config.json rename to AppServicesUsageSamples/apps/field-encryption/sync/config.json diff --git a/CuratedSyncExamples/build.gradle.kts b/AppServicesUsageSamples/build.gradle.kts similarity index 100% rename from CuratedSyncExamples/build.gradle.kts rename to AppServicesUsageSamples/build.gradle.kts diff --git a/CuratedSyncExamples/demo/.gitignore b/AppServicesUsageSamples/demo/.gitignore similarity index 100% rename from CuratedSyncExamples/demo/.gitignore rename to AppServicesUsageSamples/demo/.gitignore diff --git a/CuratedSyncExamples/demo/build.gradle.kts b/AppServicesUsageSamples/demo/build.gradle.kts similarity index 95% rename from CuratedSyncExamples/demo/build.gradle.kts rename to AppServicesUsageSamples/demo/build.gradle.kts index 864b887..8d86880 100644 --- a/CuratedSyncExamples/demo/build.gradle.kts +++ b/AppServicesUsageSamples/demo/build.gradle.kts @@ -6,11 +6,11 @@ plugins { } android { - namespace = "io.realm.curatedsyncexamples" + namespace = "io.realm.appservicesusagesamples" compileSdk = 33 defaultConfig { - applicationId = "io.realm.curatedsyncexamples" + applicationId = "io.realm.appservicesusagesamples" // Field encryption key algorithms require minSdk 26 // https://developer.android.com/reference/javax/crypto/SecretKeyFactory diff --git a/CuratedSyncExamples/demo/proguard-rules.pro b/AppServicesUsageSamples/demo/proguard-rules.pro similarity index 100% rename from CuratedSyncExamples/demo/proguard-rules.pro rename to AppServicesUsageSamples/demo/proguard-rules.pro diff --git a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt b/AppServicesUsageSamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt similarity index 92% rename from CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt rename to AppServicesUsageSamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt index 3c491ef..c121626 100644 --- a/CuratedSyncExamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt +++ b/AppServicesUsageSamples/demo/src/androidTest/java/io/realm/curatedsyncexamples/KeyHelperTests.kt @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples +package io.realm.appservicesusagesamples import android.security.keystore.KeyProperties import android.security.keystore.KeyProtection import androidx.test.ext.junit.runners.AndroidJUnit4 -import io.realm.curatedsyncexamples.fieldencryption.ext.generateAndStoreKey -import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec +import io.realm.appservicesusagesamples.fieldencryption.ext.generateAndStoreKey +import io.realm.appservicesusagesamples.fieldencryption.models.SerializableCipherSpec import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith diff --git a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml b/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml similarity index 84% rename from CuratedSyncExamples/demo/src/main/AndroidManifest.xml rename to AppServicesUsageSamples/demo/src/main/AndroidManifest.xml index 06e8abf..46553d6 100644 --- a/CuratedSyncExamples/demo/src/main/AndroidManifest.xml +++ b/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools"> + android:theme="@style/Theme.AppServicesUsageSamplesTheme"> diff --git a/CuratedSyncExamples/demo/src/main/assets/certificate.crt b/AppServicesUsageSamples/demo/src/main/assets/certificate.crt similarity index 100% rename from CuratedSyncExamples/demo/src/main/assets/certificate.crt rename to AppServicesUsageSamples/demo/src/main/assets/certificate.crt diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt similarity index 82% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt index 686781c..d0587b0 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/CuratedSyncExamplesApp.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples +package io.realm.appservicesusagesamples import android.app.Application -import io.realm.curatedsyncexamples.fieldencryption.fieldEncryptionModule +import io.realm.appservicesusagesamples.fieldencryption.fieldEncryptionModule import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger import org.koin.core.context.GlobalContext.startKoin -class CuratedSyncExamplesApp: Application() { +class AppServicesUsageSamplesApp: Application() { override fun onCreate() { super.onCreate() @@ -30,7 +30,7 @@ class CuratedSyncExamplesApp: Application() { // Log Koin into Android logger androidLogger() // Reference Android context - androidContext(this@CuratedSyncExamplesApp) + androidContext(this@AppServicesUsageSamplesApp) // Load modules modules(mainModule, fieldEncryptionModule) } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt similarity index 95% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt index c8dc5d0..0cad5f4 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/Constants.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples +package io.realm.appservicesusagesamples const val FIELD_ENCRYPTION_APP_ID = "field-encryption-fjrvt" const val USER_PRESENCE_APP_ID = "" diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DemoSelectorActivity.kt similarity index 81% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DemoSelectorActivity.kt index d4d9d19..328d995 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DemoSelectorActivity.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DemoSelectorActivity.kt @@ -14,20 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples +package io.realm.appservicesusagesamples import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import io.realm.curatedsyncexamples.ui.ExamplesScreen -import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme +import io.realm.appservicesusagesamples.ui.ExamplesScreen +import io.realm.appservicesusagesamples.ui.theme.AppServicesUsageSamplesTheme class DemoSelectorActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - CuratedSyncExamplesTheme { + AppServicesUsageSamplesTheme { ExamplesScreen() } } diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt similarity index 58% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt index de7f476..7dbda1a 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/DependencyInjection.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt @@ -14,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples +package io.realm.appservicesusagesamples -import io.realm.curatedsyncexamples.fieldencryption.FieldEncryptionActivity -import io.realm.curatedsyncexamples.ui.ExamplesScreenViewModel +import io.realm.appservicesusagesamples.fieldencryption.FieldEncryptionActivity +import io.realm.appservicesusagesamples.ui.ExamplesScreenViewModel import io.realm.kotlin.mongodb.App import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.core.qualifier.named @@ -34,53 +34,53 @@ enum class Demos( FIELD_ENCRYPTION( "Field level encryption", FieldEncryptionActivity::class.java, - FIELD_ENCRYPTION_APP_ID + io.realm.appservicesusagesamples.FIELD_ENCRYPTION_APP_ID ), USER_PRESENCE( "User presence", - DemoSelectorActivity::class.java, - USER_PRESENCE_APP_ID, + io.realm.appservicesusagesamples.DemoSelectorActivity::class.java, + io.realm.appservicesusagesamples.USER_PRESENCE_APP_ID, ), OFFLINE_LOGIN( "Offline login", - DemoSelectorActivity::class.java, - OFFLINE_LOGIN_APP_ID, + io.realm.appservicesusagesamples.DemoSelectorActivity::class.java, + io.realm.appservicesusagesamples.OFFLINE_LOGIN_APP_ID, ), ERROR_HANDLING( "Error handling", - DemoSelectorActivity::class.java, - ERROR_HANDLING_APP_ID, + io.realm.appservicesusagesamples.DemoSelectorActivity::class.java, + io.realm.appservicesusagesamples.ERROR_HANDLING_APP_ID, ), BUSINESS_LOGIC( "Business logic", - DemoSelectorActivity::class.java, - BUSINESS_LOGIC_APP_ID, + io.realm.appservicesusagesamples.DemoSelectorActivity::class.java, + io.realm.appservicesusagesamples.BUSINESS_LOGIC_APP_ID, ), PURCHASE_VERIFICATION( "Purchase verification", - DemoSelectorActivity::class.java, - PURCHASE_VERIFICATION_APP_ID, + io.realm.appservicesusagesamples.DemoSelectorActivity::class.java, + io.realm.appservicesusagesamples.PURCHASE_VERIFICATION_APP_ID, ); val qualifier = named(appId) } -typealias DemoWithApp = Pair +typealias DemoWithApp = Pair /** * Koin module for the main entry point. */ val mainModule = module { // Create singletons for each app. - for (app in Demos.values()) { + for (app in io.realm.appservicesusagesamples.Demos.values()) { single(app.qualifier) { App.create(app.appId) } } viewModel { ExamplesScreenViewModel( - apps = Demos.values() + apps = io.realm.appservicesusagesamples.Demos.values() .map { demo -> - DemoWithApp( + io.realm.appservicesusagesamples.DemoWithApp( first = demo, second = get(demo.qualifier) ) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/DependencyInjection.kt similarity index 60% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/DependencyInjection.kt index 31c2bb5..8210620 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/DependencyInjection.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/DependencyInjection.kt @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption +package io.realm.appservicesusagesamples.fieldencryption -import io.realm.curatedsyncexamples.Demos -import io.realm.curatedsyncexamples.fieldencryption.ui.NavGraphViewModel -import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.KeyStoreViewModel -import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginViewModel -import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordsViewModel +import io.realm.appservicesusagesamples.Demos +import io.realm.appservicesusagesamples.fieldencryption.ui.NavGraphViewModel +import io.realm.appservicesusagesamples.fieldencryption.ui.keystore.KeyStoreViewModel +import io.realm.appservicesusagesamples.fieldencryption.ui.login.LoginViewModel +import io.realm.appservicesusagesamples.fieldencryption.ui.records.SecretRecordsViewModel import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module import java.security.KeyStore @@ -35,22 +35,22 @@ val fieldEncryptionModule = module { viewModel { KeyStoreViewModel( - app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + app = get(qualifier = io.realm.appservicesusagesamples.Demos.FIELD_ENCRYPTION.qualifier), keyAlias = keyAlias, localKeyStore = androidKeyStore ) } - viewModel { LoginViewModel(get(qualifier = Demos.FIELD_ENCRYPTION.qualifier)) } + viewModel { LoginViewModel(get(qualifier = io.realm.appservicesusagesamples.Demos.FIELD_ENCRYPTION.qualifier)) } viewModel { SecretRecordsViewModel( - app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + app = get(qualifier = io.realm.appservicesusagesamples.Demos.FIELD_ENCRYPTION.qualifier), keyAlias = keyAlias, localKeyStore = androidKeyStore ) } viewModel { NavGraphViewModel( - app = get(qualifier = Demos.FIELD_ENCRYPTION.qualifier), + app = get(qualifier = io.realm.appservicesusagesamples.Demos.FIELD_ENCRYPTION.qualifier), localKeyStore = androidKeyStore, keyAlias = keyAlias, ) diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/FieldEncryptionActivity.kt similarity index 77% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/FieldEncryptionActivity.kt index 769be4f..dbb5001 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/FieldEncryptionActivity.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/FieldEncryptionActivity.kt @@ -14,17 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption +package io.realm.appservicesusagesamples.fieldencryption import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.ui.Modifier -import io.realm.curatedsyncexamples.fieldencryption.ui.NavGraph -import io.realm.curatedsyncexamples.ui.theme.CuratedSyncExamplesTheme +import io.realm.appservicesusagesamples.fieldencryption.ui.NavGraph +import io.realm.appservicesusagesamples.ui.theme.AppServicesUsageSamplesTheme class FieldEncryptionActivity : ComponentActivity() { @@ -32,7 +30,7 @@ class FieldEncryptionActivity : ComponentActivity() { super.onCreate(savedInstanceState) setContent { - CuratedSyncExamplesTheme { + AppServicesUsageSamplesTheme { NavGraph( modifier = Modifier .fillMaxSize(), diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/EncryptionExt.kt similarity index 85% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/EncryptionExt.kt index 3b7dd68..7ac9b2f 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/EncryptionExt.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/EncryptionExt.kt @@ -14,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ext +package io.realm.appservicesusagesamples.fieldencryption.ext -import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec +import io.realm.appservicesusagesamples.fieldencryption.models.SerializableCipherSpec import javax.crypto.KeyGenerator import javax.crypto.SecretKey diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/UserExt.kt similarity index 89% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/UserExt.kt index 4b470b6..7d17f9b 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ext/UserExt.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ext/UserExt.kt @@ -14,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ext +package io.realm.appservicesusagesamples.fieldencryption.ext -import io.realm.curatedsyncexamples.fieldencryption.models.SerializableCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.CustomData +import io.realm.appservicesusagesamples.fieldencryption.models.SerializableCipherSpec +import io.realm.appservicesusagesamples.fieldencryption.models.CustomData import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi import io.realm.kotlin.mongodb.User import io.realm.kotlin.mongodb.ext.call diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/CustomData.kt similarity index 94% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/CustomData.kt index 509937a..e8d8c22 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/CustomData.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/CustomData.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.models +package io.realm.appservicesusagesamples.fieldencryption.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecretRecord.kt similarity index 95% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecretRecord.kt index 3642f47..5f545de 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecretRecord.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecretRecord.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.models +package io.realm.appservicesusagesamples.fieldencryption.models import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.Ignore diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecureStringDelegate.kt similarity index 96% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecureStringDelegate.kt index e606c83..ff3f1a4 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SecureStringDelegate.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SecureStringDelegate.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.models +package io.realm.appservicesusagesamples.fieldencryption.models import java.lang.Exception import java.security.Key diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SerializableCipherSpec.kt similarity index 97% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SerializableCipherSpec.kt index 7e253e1..3b7c172 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/models/SerializableCipherSpec.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/models/SerializableCipherSpec.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.models +package io.realm.appservicesusagesamples.fieldencryption.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/NavGraph.kt similarity index 92% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/NavGraph.kt index 61f6fe4..698a5ad 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/NavGraph.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/NavGraph.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui +package io.realm.appservicesusagesamples.fieldencryption.ui import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -26,9 +26,9 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController -import io.realm.curatedsyncexamples.fieldencryption.ui.keystore.UnlockUserKeyStoreScreen -import io.realm.curatedsyncexamples.fieldencryption.ui.login.LoginScreen -import io.realm.curatedsyncexamples.fieldencryption.ui.records.SecretRecordScreen +import io.realm.appservicesusagesamples.fieldencryption.ui.keystore.UnlockUserKeyStoreScreen +import io.realm.appservicesusagesamples.fieldencryption.ui.login.LoginScreen +import io.realm.appservicesusagesamples.fieldencryption.ui.records.SecretRecordScreen import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.Credentials import org.koin.compose.koinInject diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreScreen.kt similarity index 99% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreScreen.kt index 2ed4b1b..632212d 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreScreen.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreScreen.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.keystore +package io.realm.appservicesusagesamples.fieldencryption.ui.keystore import android.view.KeyEvent import androidx.compose.foundation.layout.Box diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt similarity index 90% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt index df1a697..6a4f5af 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/keystore/KeyStoreViewModel.kt @@ -14,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.keystore +package io.realm.appservicesusagesamples.fieldencryption.ui.keystore import android.security.keystore.KeyProperties import android.security.keystore.KeyProtection import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.ext.generateAndStoreKey -import io.realm.curatedsyncexamples.fieldencryption.ext.getRemoteKeyStore -import io.realm.curatedsyncexamples.fieldencryption.ext.hasKeyStore -import io.realm.curatedsyncexamples.fieldencryption.ext.updateRemoteKeyStore +import io.realm.appservicesusagesamples.fieldencryption.ext.fieldEncryptionCipherSpec +import io.realm.appservicesusagesamples.fieldencryption.ext.generateAndStoreKey +import io.realm.appservicesusagesamples.fieldencryption.ext.getRemoteKeyStore +import io.realm.appservicesusagesamples.fieldencryption.ext.hasKeyStore +import io.realm.appservicesusagesamples.fieldencryption.ext.updateRemoteKeyStore import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.User import kotlinx.coroutines.Dispatchers diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginScreen.kt similarity index 99% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginScreen.kt index 6f92971..c3903b0 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginScreen.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginScreen.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.login +package io.realm.appservicesusagesamples.fieldencryption.ui.login import android.view.KeyEvent import androidx.compose.foundation.layout.Arrangement diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginViewModel.kt similarity index 97% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginViewModel.kt index 47cf581..3fb3df2 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/login/LoginViewModel.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/login/LoginViewModel.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.login +package io.realm.appservicesusagesamples.fieldencryption.ui.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsScreen.kt similarity index 99% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsScreen.kt index 6181bd4..adff7b9 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsScreen.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsScreen.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.records +package io.realm.appservicesusagesamples.fieldencryption.ui.records import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsViewModel.kt similarity index 91% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsViewModel.kt index 71c5e9e..498cd3d 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/fieldencryption/ui/records/SecretRecordsViewModel.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/fieldencryption/ui/records/SecretRecordsViewModel.kt @@ -14,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.fieldencryption.ui.records +package io.realm.appservicesusagesamples.fieldencryption.ui.records import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.fieldencryption.ext.fieldEncryptionCipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.SecretRecord -import io.realm.curatedsyncexamples.fieldencryption.models.FLECipherSpec -import io.realm.curatedsyncexamples.fieldencryption.models.FLEKey +import io.realm.appservicesusagesamples.fieldencryption.ext.fieldEncryptionCipherSpec +import io.realm.appservicesusagesamples.fieldencryption.models.SecretRecord +import io.realm.appservicesusagesamples.fieldencryption.models.FLECipherSpec +import io.realm.appservicesusagesamples.fieldencryption.models.FLEKey import io.realm.kotlin.Realm import io.realm.kotlin.ext.query import io.realm.kotlin.mongodb.App diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorScreen.kt similarity index 98% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorScreen.kt index fa02b8a..102ffe0 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorScreen.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorScreen.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.ui +package io.realm.appservicesusagesamples.ui import android.content.Intent import androidx.compose.foundation.Image @@ -40,7 +40,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import io.realm.curatedsyncexamples.R +import io.realm.appservicesusagesamples.R import org.koin.compose.koinInject @Composable diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorViewModel.kt similarity index 86% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorViewModel.kt index 88a04df..8a2f54e 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/DemoSelectorViewModel.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/DemoSelectorViewModel.kt @@ -14,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.ui +package io.realm.appservicesusagesamples.ui import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.realm.curatedsyncexamples.DemoWithApp -import io.realm.curatedsyncexamples.Demos +import io.realm.appservicesusagesamples.DemoWithApp +import io.realm.appservicesusagesamples.Demos import io.realm.kotlin.mongodb.App import io.realm.kotlin.mongodb.AuthenticationProvider import io.realm.kotlin.mongodb.Credentials @@ -32,9 +32,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -typealias DemoWithStatus = Pair +typealias DemoWithStatus = Pair -class ExamplesScreenViewModel(private val apps: List) : ViewModel() { +class ExamplesScreenViewModel(private val apps: List) : ViewModel() { private val loading = MutableStateFlow(true) val loadingState: StateFlow = loading.asStateFlow() diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Color.kt similarity index 94% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Color.kt index 8402d79..472f468 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Color.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Color.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.ui.theme +package io.realm.appservicesusagesamples.ui.theme import androidx.compose.ui.graphics.Color diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Theme.kt similarity index 97% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Theme.kt index cc15842..310d5d9 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Theme.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Theme.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.ui.theme +package io.realm.appservicesusagesamples.ui.theme import android.app.Activity import android.os.Build @@ -54,7 +54,7 @@ private val LightColorScheme = lightColorScheme( ) @Composable -fun CuratedSyncExamplesTheme( +fun AppServicesUsageSamplesTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, diff --git a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Type.kt similarity index 96% rename from CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt rename to AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Type.kt index 083cdfd..948deb8 100644 --- a/CuratedSyncExamples/demo/src/main/java/io/realm/curatedsyncexamples/ui/theme/Type.kt +++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/ui/theme/Type.kt @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.realm.curatedsyncexamples.ui.theme +package io.realm.appservicesusagesamples.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/CuratedSyncExamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml b/AppServicesUsageSamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to AppServicesUsageSamples/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/CuratedSyncExamples/demo/src/main/res/drawable/ic_launcher_background.xml b/AppServicesUsageSamples/demo/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/drawable/ic_launcher_background.xml rename to AppServicesUsageSamples/demo/src/main/res/drawable/ic_launcher_background.xml diff --git a/CuratedSyncExamples/demo/src/main/res/drawable/realmio_logo_vector.xml b/AppServicesUsageSamples/demo/src/main/res/drawable/realmio_logo_vector.xml similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/drawable/realmio_logo_vector.xml rename to AppServicesUsageSamples/demo/src/main/res/drawable/realmio_logo_vector.xml diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/AppServicesUsageSamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to AppServicesUsageSamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/AppServicesUsageSamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to AppServicesUsageSamples/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-hdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-mdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp diff --git a/CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/AppServicesUsageSamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp similarity index 100% rename from CuratedSyncExamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp rename to AppServicesUsageSamples/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp diff --git a/AppServicesUsageSamples/demo/src/main/res/values/strings.xml b/AppServicesUsageSamples/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..1c84be8 --- /dev/null +++ b/AppServicesUsageSamples/demo/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + App Services Usage Samples + DemoSelectorActivity + \ No newline at end of file diff --git a/CuratedSyncExamples/demo/src/main/res/values/themes.xml b/AppServicesUsageSamples/demo/src/main/res/values/themes.xml similarity index 53% rename from CuratedSyncExamples/demo/src/main/res/values/themes.xml rename to AppServicesUsageSamples/demo/src/main/res/values/themes.xml index 9b4464e..613a609 100644 --- a/CuratedSyncExamples/demo/src/main/res/values/themes.xml +++ b/AppServicesUsageSamples/demo/src/main/res/values/themes.xml @@ -1,9 +1,9 @@ - -