From 1adb381633ebda982c913d4e383cba771c6c2483 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 18 Jan 2024 11:23:28 +0000 Subject: [PATCH 01/11] Adding EncryptionKeyCallback to pass in the AES key via a native memory region that can be reset later to enhance security --- .../kotlin/internal/interop/RealmInterop.kt | 1 + .../kotlin/internal/interop/RealmInterop.kt | 4 ++ .../kotlin/internal/interop/RealmInterop.kt | 12 ++++ .../src/main/jni/realm_api_helpers.cpp | 6 ++ .../src/main/jni/realm_api_helpers.h | 1 + .../kotlin/io/realm/kotlin/Configuration.kt | 65 +++++++++++++++++++ .../io/realm/kotlin/RealmConfiguration.kt | 1 + .../kotlin/internal/ConfigurationImpl.kt | 8 ++- .../kotlin/internal/RealmConfigurationImpl.kt | 3 + .../io/realm/kotlin/internal/RealmImpl.kt | 16 ++++- .../kotlin/mongodb/sync/SyncConfiguration.kt | 1 + .../kotlin/test/platform/PlatformUtils.kt | 11 ++++ .../kotlin/test/platform/PlatformUtils.kt | 13 ++++ .../kotlin/test/common/EncryptionTests.kt | 59 +++++++++++++++++ .../kotlin/test/platform/PlatformUtils.kt | 24 +++++++ .../kotlin/test/platform/PlatformUtils.kt | 24 +++++++ 16 files changed, 246 insertions(+), 3 deletions(-) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 6a846f05f0..51358903d1 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -192,6 +192,7 @@ expect object RealmInterop { fun realm_config_set_schema(config: RealmConfigurationPointer, schema: RealmSchemaPointer) fun realm_config_set_max_number_of_active_versions(config: RealmConfigurationPointer, maxNumberOfVersions: Long) fun realm_config_set_encryption_key(config: RealmConfigurationPointer, encryptionKey: ByteArray) + fun realm_config_set_encryption_key_from_pointer(config: RealmConfigurationPointer, aesEncryptionKeyAddress: Long) fun realm_config_get_encryption_key(config: RealmConfigurationPointer): ByteArray? fun realm_config_set_should_compact_on_launch_function(config: RealmConfigurationPointer, callback: CompactOnLaunchCallback) fun realm_config_set_migration_function(config: RealmConfigurationPointer, callback: MigrationCallback) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index b79c41f4f4..8e922b7ec0 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -184,6 +184,10 @@ actual object RealmInterop { realmc.realm_config_set_encryption_key(config.cptr(), encryptionKey, encryptionKey.size.toLong()) } + actual fun realm_config_set_encryption_key_from_pointer(config: RealmConfigurationPointer, aesEncryptionKeyAddress: Long) { + realmc.realm_config_set_encryption_key_from_pointer(config.cptr(), aesEncryptionKeyAddress) + } + actual fun realm_config_get_encryption_key(config: RealmConfigurationPointer): ByteArray? { val key = ByteArray(ENCRYPTION_KEY_LENGTH) val keyLength: Long = realmc.realm_config_get_encryption_key(config.cptr(), key) diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 3b0faf5100..5e7befd34d 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -54,6 +54,7 @@ import kotlinx.cinterop.CPointerVar import kotlinx.cinterop.CPointerVarOf import kotlinx.cinterop.CValue import kotlinx.cinterop.CVariable +import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.LongVar import kotlinx.cinterop.MemScope import kotlinx.cinterop.StableRef @@ -77,6 +78,7 @@ import kotlinx.cinterop.readValue import kotlinx.cinterop.refTo import kotlinx.cinterop.set import kotlinx.cinterop.staticCFunction +import kotlinx.cinterop.toCPointer import kotlinx.cinterop.toCStringArray import kotlinx.cinterop.toCValues import kotlinx.cinterop.toKString @@ -419,6 +421,16 @@ actual object RealmInterop { } } + @OptIn(ExperimentalForeignApi::class) + actual fun realm_config_set_encryption_key_from_pointer(config: RealmConfigurationPointer, aesEncryptionKeyAddress: Long) { + memScoped { // Ensure memory cleanup + val ptr = aesEncryptionKeyAddress.toCPointer>() + val encryptionKey = ByteArray(64) + memcpy(encryptionKey.refTo(0), ptr, 64u) + realm_config_set_encryption_key(config, encryptionKey) + } + } + actual fun realm_config_get_encryption_key(config: RealmConfigurationPointer): ByteArray? { memScoped { val encryptionKey = ByteArray(ENCRYPTION_KEY_LENGTH) diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 54522828ba..323421596c 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -937,6 +937,12 @@ void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error realm_sync_socket_websocket_closed(reinterpret_cast(observer_ptr), was_clean, static_cast(error_code), reason); } +void realm_config_set_encryption_key_from_pointer(realm_config_t* config, int64_t aesKeyAddress) { + uint8_t key_array[64]; + std::memcpy(key_array, reinterpret_cast(aesKeyAddress), 64); + realm_config_set_encryption_key(config, key_array, 64); +} + realm_sync_socket_t* realm_sync_websocket_new(int64_t sync_client_config_ptr, jobject websocket_transport) { auto jenv = get_env(false); // Always called from JVM realm_sync_socket_t* socket_provider = realm_sync_socket_new(jenv->NewGlobalRef(websocket_transport), /*userdata*/ diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 84caf586dc..16baf7aa5e 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -161,4 +161,5 @@ bool realm_sync_websocket_message(int64_t observer_ptr, jbyteArray data, size_t void realm_sync_websocket_closed(int64_t observer_ptr, bool was_clean, int error_code, const char* reason); +void realm_config_set_encryption_key_from_pointer(realm_config_t* config, int64_t aesKeyAddress); #endif //TEST_REALM_API_HELPERS_H diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index daf8e6a42f..fdefddc2cf 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -106,6 +106,18 @@ public data class InitialRealmFileConfiguration( val checksum: String? ) +public interface EncryptionKeyCallback { + /** + * Provides the native memory address of the 64 byte array containing the key used to encrypt and decrypt the Realm file. + */ + public fun keyPointer(): Long + + /** + * This callback will be invoked by Realm after it's open. This hint to the user that the key provided in [keyPointer] can now be released. + */ + public fun releaseKey() +} + /** * Base configuration options shared between all realm configuration types. */ @@ -153,6 +165,13 @@ public interface Configuration { */ public val encryptionKey: ByteArray? + /** + * Native memory address of the 64 byte array containing the key used to encrypt and decrypt the Realm file. + * + * @return null on unencrypted Realms. + */ + public val encryptionKeyAsCallback: EncryptionKeyCallback? + /** * Callback that determines if the realm file should be compacted as part of opening it. * @@ -234,6 +253,7 @@ public interface Configuration { protected var writeDispatcher: CoroutineDispatcher? = null protected var schemaVersion: Long = 0 protected var encryptionKey: ByteArray? = null + protected var encryptionKeyAsCallback: EncryptionKeyCallback? = null protected var compactOnLaunchCallback: CompactOnLaunchCallback? = null protected var initialDataCallback: InitialDataCallback? = null protected var inMemory: Boolean = false @@ -354,6 +374,51 @@ public interface Configuration { public fun encryptionKey(encryptionKey: ByteArray): S = apply { this.encryptionKey = validateEncryptionKey(encryptionKey) } as S + /** + * Similar to [encryptionKey] but instead this will read the encryption key from native memory. + * This can enhance the security of the app, since it reduces the window where the key is available in clear + * in memory (avoid memory dump attack). Once the Realm is open, one can zero-out the memory region holding the key + * as it will be already passed to the C++ storage engine. + * + * There's also extra protection for JVM Windows target, where the underlying storage engine uses the Windows Kernel + * to encrypt/decrypt the Realm's encryption key before each usage. + * + * + * Note: The RealmConfiguration doesn't take ownership of this native memory, the caller is responsible of disposing it + * appropriately after the Realm is open using the [EncryptionKeyCallback.releaseKey]. + * + * @param encryptionKeyAsCallback Callback providing address/pointer to a 64-byte array containing the AES encryption key. + * This array should be in native memory to avoid copying the key into garbage collected heap memory (for JVM targets). + * + * One way to create such an array in JVM is to use JNI or use `sun.misc.Unsafe` as follow: + * + *``` + * import sun.misc.Unsafe + * + * val field = Unsafe::class.java.getDeclaredField("theUnsafe") + * field.isAccessible = true + * val unsafe: Unsafe = field.get(null) as Unsafe + * + * val key = Random.nextBytes(64) // Replace with your actual AES key + * val keyPointer: Long = unsafe.allocateMemory(key.size.toLong()) + * for (i in key.indices) { // Write the key bytes to native memory + * unsafe.putByte(keyPointer + i, key[i]) + * } + * + * val encryptedConf = RealmConfiguration + * .Builder(schema = setOf(Sample::class)) + * .encryptionKey(object : EncryptionKeyCallback { + * override fun keyPointer() = keyPointer + * override fun releaseKey() = unsafe.freeMemory(keyPointer) + * }) + * .build() + * + * val realm = Realm.open(encryptedConf) + *``` + */ + public fun encryptionKey(encryptionKeyAsCallback: EncryptionKeyCallback): S = + apply { this.encryptionKeyAsCallback = encryptionKeyAsCallback } as S + /** * Sets a callback for controlling whether the realm should be compacted when opened. * diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt index 4b46f62364..873efc8cb0 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt @@ -185,6 +185,7 @@ public interface RealmConfiguration : Configuration { writerDispatcherFactory, schemaVersion, encryptionKey, + encryptionKeyAsCallback, deleteRealmIfMigrationNeeded, compactOnLaunchCallback, migration, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index 1c5fae2b73..ee9f285a37 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.CompactOnLaunchCallback +import io.realm.kotlin.EncryptionKeyCallback import io.realm.kotlin.InitialDataCallback import io.realm.kotlin.InitialRealmFileConfiguration import io.realm.kotlin.LogConfiguration @@ -60,8 +61,9 @@ public open class ConfigurationImpl( schemaVersion: Long, schemaMode: SchemaMode, private val userEncryptionKey: ByteArray?, + override val encryptionKeyAsCallback: EncryptionKeyCallback?, compactOnLaunchCallback: CompactOnLaunchCallback?, - private val userMigration: RealmMigration?, + userMigration: RealmMigration?, automaticBacklinkHandling: Boolean, initialDataCallback: InitialDataCallback?, override val isFlexibleSyncConfiguration: Boolean, @@ -230,6 +232,10 @@ public open class ConfigurationImpl( RealmInterop.realm_config_set_encryption_key(nativeConfig, key) } + encryptionKeyAsCallback?.let { + RealmInterop.realm_config_set_encryption_key_from_pointer(nativeConfig, it.keyPointer()) + } + RealmInterop.realm_config_set_in_memory(nativeConfig, inMemory) nativeConfig diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt index 8b0e620e2d..3468f6b2ee 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt @@ -17,6 +17,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.CompactOnLaunchCallback +import io.realm.kotlin.EncryptionKeyCallback import io.realm.kotlin.InitialDataCallback import io.realm.kotlin.InitialRealmFileConfiguration import io.realm.kotlin.LogConfiguration @@ -40,6 +41,7 @@ internal class RealmConfigurationImpl( writeDispatcherFactory: CoroutineDispatcherFactory, schemaVersion: Long, encryptionKey: ByteArray?, + encryptionKeyAsCallback: EncryptionKeyCallback?, override val deleteRealmIfMigrationNeeded: Boolean, compactOnLaunchCallback: CompactOnLaunchCallback?, migration: RealmMigration?, @@ -62,6 +64,7 @@ internal class RealmConfigurationImpl( false -> SchemaMode.RLM_SCHEMA_MODE_AUTOMATIC }, encryptionKey, + encryptionKeyAsCallback, compactOnLaunchCallback, migration, automaticBacklinkHandling, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 463d604eea..ef26560c6f 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -46,7 +46,6 @@ import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.launch @@ -138,6 +137,20 @@ public class RealmImpl private constructor( } realmScope.launch { + configuration.encryptionKeyAsCallback?.let { + // if we're using an encryption key as a callback, we preemptively open the notifier and writer Realm + // with the given configuration because the key might be deleted from memory after the Realm is open. + + // These touches the notifier and writer lazy initialised Realms to open them with the provided configuration. + launch(notificationScheduler.dispatcher) { + notifier.realm.version().version + } + launch(writeScheduler.dispatcher) { + writer.realm.version().version + it.releaseKey() + } + } + notifier.realmChanged().collect { removeInitialRealmReference() // Closing this reference might be done by the GC: @@ -270,7 +283,6 @@ public class RealmImpl private constructor( current = initialRealmReference.value?.uncheckedVersion(), active = versionTracker.versions() ) - return VersionInfo( main = mainVersions, notifier = notifier.versions(), diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index 6224e89df0..fba9f61311 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -565,6 +565,7 @@ public interface SyncConfiguration : Configuration { schemaVersion, SchemaMode.RLM_SCHEMA_MODE_ADDITIVE_DISCOVERED, encryptionKey, + encryptionKeyAsCallback, compactOnLaunchCallback, null, // migration is not relevant for sync, false, // automatic backlink handling is not relevant for sync diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index cfc7d1f33f..17322ed3e2 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -56,6 +56,17 @@ actual object PlatformUtils { } SystemClock.sleep(5000) // 5 seconds to give the GC some time to process } + + actual fun allocateEncryptionKeyOnNativeMemory(aesKey: ByteArray): Long { + // Note: the ByteBuffer is not guaranteed to be in native memory (it could use a backing array) + // use allocateDirect.hasArray() to find out. Ideally we want to use JNI for Android to + // create such native array. + TODO() + } + + actual fun freeEncryptionKeyFromNativeMemory(aesKeyPointer: Long) { + TODO() + } } // Allocs as much garbage as we can. Pass maxSize = 0 to use all available memory in the process. diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index ee6a77661d..93db00afa7 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -10,4 +10,17 @@ expect object PlatformUtils { fun sleep(duration: Duration) fun threadId(): ULong fun triggerGC() + + /** + * Allocate a 64 byte array in native memory that contains the encryption key to be used. + * + * @param aesKey the value of the byte array to be copied. + * @return the address pointer to the memory region allocated. + */ + fun allocateEncryptionKeyOnNativeMemory(aesKey: ByteArray): Long + + /** + * Zero-out and release a previously written encryption key from native memory. + */ + fun freeEncryptionKeyFromNativeMemory(aesKeyPointer: Long) } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt index 0377f0a106..661e97d26a 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt @@ -16,14 +16,19 @@ */ package io.realm.kotlin.test.common +import io.realm.kotlin.EncryptionKeyCallback import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.use +import kotlinx.atomicfu.atomic +import kotlinx.coroutines.runBlocking import kotlin.random.Random import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertFailsWith /** @@ -122,4 +127,58 @@ class EncryptionTests { } } } + + @Test + fun openEncryptedRealmWithEncryptionKeyCallback() = runBlocking { + val key: ByteArray = Random.nextBytes(64) + val keyPointer: Long = PlatformUtils.allocateEncryptionKeyOnNativeMemory(key) + + val keyPointerCallbackInvocation = atomic(0) + val keyPointerReleaseCallbackInvocation = atomic(0) + + val encryptedConf = RealmConfiguration + .Builder( + schema = setOf(Sample::class) + ) + .directory(tmpDir) + .encryptionKey(object : EncryptionKeyCallback { + override fun keyPointer(): Long { + keyPointerCallbackInvocation.incrementAndGet() + return keyPointer + } + + override fun releaseKey() { + keyPointerReleaseCallbackInvocation.incrementAndGet() + PlatformUtils.freeEncryptionKeyFromNativeMemory(keyPointer) + } + }) + .build() + + // Initializes an encrypted Realm + Realm.open(encryptedConf).use { + it.writeBlocking { + copyToRealm(Sample().apply { stringField = "Foo Bar" }) + } + } + + assertEquals(3, keyPointerCallbackInvocation.value, "Encryption key pointer should have been invoked 3 times (Frozen Realm, Notifier and Writer Realms)") + assertEquals(1, keyPointerReleaseCallbackInvocation.value, "Releasing the key should only be invoked once all the 3 Realms have been opened") + + val keyPointer2 = PlatformUtils.allocateEncryptionKeyOnNativeMemory(key) + val encryptedConf2 = RealmConfiguration + .Builder( + schema = setOf(Sample::class) + ) + .directory(tmpDir) + .encryptionKey(object : EncryptionKeyCallback { + override fun keyPointer() = keyPointer2 + override fun releaseKey() = PlatformUtils.freeEncryptionKeyFromNativeMemory(keyPointer2) + }) + .build() + + Realm.open(encryptedConf2).use { + val sample: Sample = it.query(Sample::class).find().first() + assertEquals("Foo Bar", sample.stringField) + } + } } diff --git a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index a20938ed2c..11efffc86d 100644 --- a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test.platform +import sun.misc.Unsafe import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -65,6 +66,29 @@ actual object PlatformUtils { actual fun threadId(): ULong = Thread.currentThread().id.toULong() + actual fun allocateEncryptionKeyOnNativeMemory(aesKey: ByteArray): Long { + @Suppress("DiscouragedPrivateApi") + val field = Unsafe::class.java.getDeclaredField("theUnsafe") + field.isAccessible = true + val unsafe: Unsafe = field.get(null) as Unsafe + + val keyPointer: Long = unsafe.allocateMemory(aesKey.size.toLong()) + for (i in aesKey.indices) { + unsafe.putByte(keyPointer + i, aesKey[i]) + } + + return keyPointer + } + + actual fun freeEncryptionKeyFromNativeMemory(aesKeyPointer: Long) { + @Suppress("DiscouragedPrivateApi") + val field = Unsafe::class.java.getDeclaredField("theUnsafe") + field.isAccessible = true + val unsafe: Unsafe = field.get(null) as Unsafe + + unsafe.freeMemory(aesKeyPointer) + } + @Suppress("ExplicitGarbageCollectionCall") actual fun triggerGC() { for (i in 1..30) { diff --git a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 87cd82b932..e20ce5bf1b 100644 --- a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -17,11 +17,17 @@ package io.realm.kotlin.test.platform import io.realm.kotlin.test.util.Utils +import kotlinx.cinterop.ByteVarOf +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ULongVar import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArray import kotlinx.cinterop.cValue import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr +import kotlinx.cinterop.set +import kotlinx.cinterop.toCPointer import kotlinx.cinterop.value import platform.posix.S_IRGRP import platform.posix.S_IROTH @@ -67,6 +73,24 @@ actual object PlatformUtils { } } + @ExperimentalForeignApi + actual fun allocateEncryptionKeyOnNativeMemory(aesKey: ByteArray): Long { + val byteArrayPointer: CPointer> = kotlinx.cinterop.nativeHeap.allocArray(64) + + for (i in 0 until 64) { + byteArrayPointer[i] = aesKey[i] + } + + return byteArrayPointer.rawValue.toLong() + } + + @ExperimentalForeignApi + actual fun freeEncryptionKeyFromNativeMemory(aesKeyPointer: Long) { + aesKeyPointer.toCPointer>()?.let { + kotlinx.cinterop.nativeHeap.free(it.rawValue) + } + } + actual fun triggerGC() { GC.collect() } From c52776ef12a223eeed7dde93c7c2015fb9cfee3c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 18 Jan 2024 21:34:32 +0000 Subject: [PATCH 02/11] - PR feedback - Adding jni lib for Android test --- .../io/realm/kotlin/internal/RealmImpl.kt | 19 +++++++++------ packages/test-base/build.gradle.kts | 7 ++++++ .../src/androidMain/cpp/CMakeLists.txt | 1 + .../androidMain/cpp/android_jni_helper.cpp | 23 +++++++++++++++++++ .../kotlin/test/platform/PlatformUtils.kt | 11 +++++++-- 5 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 packages/test-base/src/androidMain/cpp/CMakeLists.txt create mode 100644 packages/test-base/src/androidMain/cpp/android_jni_helper.cpp diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index ef26560c6f..60aca34f4e 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -42,6 +42,8 @@ import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow @@ -142,13 +144,16 @@ public class RealmImpl private constructor( // with the given configuration because the key might be deleted from memory after the Realm is open. // These touches the notifier and writer lazy initialised Realms to open them with the provided configuration. - launch(notificationScheduler.dispatcher) { - notifier.realm.version().version - } - launch(writeScheduler.dispatcher) { - writer.realm.version().version - it.releaseKey() - } + awaitAll( + async(notificationScheduler.dispatcher) { + notifier.realm.version().version + }, + async(writeScheduler.dispatcher) { + writer.realm.version().version + } + ) + + it.releaseKey() } notifier.realmChanged().collect { diff --git a/packages/test-base/build.gradle.kts b/packages/test-base/build.gradle.kts index e56161f7d7..4ec6e2a1d1 100644 --- a/packages/test-base/build.gradle.kts +++ b/packages/test-base/build.gradle.kts @@ -126,6 +126,13 @@ android { } } + externalNativeBuild { + cmake { + version = Versions.cmake + path = project.file("src/androidMain/cpp/CMakeLists.txt") + } + } + buildTypes { // LibraryBuildType is not minifiable, but the current dependency from test-sync doesn't // allow test-base to be configured as a library. To test test-base with minification diff --git a/packages/test-base/src/androidMain/cpp/CMakeLists.txt b/packages/test-base/src/androidMain/cpp/CMakeLists.txt new file mode 100644 index 0000000000..5fb4091f21 --- /dev/null +++ b/packages/test-base/src/androidMain/cpp/CMakeLists.txt @@ -0,0 +1 @@ +add_library(android_jni_test_helper SHARED android_jni_helper.cpp) \ No newline at end of file diff --git a/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp b/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp new file mode 100644 index 0000000000..79933ad282 --- /dev/null +++ b/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp @@ -0,0 +1,23 @@ +#include + +extern "C" { + +JNIEXPORT jlong JNICALL +Java_io_realm_kotlin_test_platform_PlatformUtils_nativeAllocateEncryptionKeyOnNativeMemory( + JNIEnv *env, jclass, jbyteArray byteArray) { + jsize arrayLength = env->GetArrayLength(byteArray); + jbyte *nativeArray = new jbyte[arrayLength]; + // Copy the contents of the Kotlin ByteArray to the native array + env->GetByteArrayRegion(byteArray, 0, arrayLength, nativeArray); + + // Return the address of the native array + return reinterpret_cast(nativeArray); +} + +JNIEXPORT void JNICALL +Java_io_realm_kotlin_test_platform_PlatformUtils_nativeFreeEncryptionKeyFromNativeMemory( + JNIEnv *env, jclass, jlong keyPtr) { + delete[] reinterpret_cast(keyPtr); +} + +} \ No newline at end of file diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 17322ed3e2..652876174a 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -25,6 +25,10 @@ import kotlin.io.path.absolutePathString import kotlin.time.Duration actual object PlatformUtils { + init { + System.loadLibrary("android_jni_test_helper") + } + @SuppressLint("NewApi") actual fun createTempDir(prefix: String, readOnly: Boolean): String { val dir: Path = Files.createTempDirectory("$prefix-android_tests") @@ -61,12 +65,15 @@ actual object PlatformUtils { // Note: the ByteBuffer is not guaranteed to be in native memory (it could use a backing array) // use allocateDirect.hasArray() to find out. Ideally we want to use JNI for Android to // create such native array. - TODO() + return nativeAllocateEncryptionKeyOnNativeMemory(aesKey) } actual fun freeEncryptionKeyFromNativeMemory(aesKeyPointer: Long) { - TODO() + nativeFreeEncryptionKeyFromNativeMemory(aesKeyPointer) } + + private external fun nativeAllocateEncryptionKeyOnNativeMemory(byteArray: ByteArray): Long + private external fun nativeFreeEncryptionKeyFromNativeMemory(pointer: Long) } // Allocs as much garbage as we can. Pass maxSize = 0 to use all available memory in the process. From 44549febcf41fa76b0681e8ac4fca0f854f2f68c Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 18 Jan 2024 22:27:42 +0000 Subject: [PATCH 03/11] Added experimental API --- CHANGELOG.md | 1 + .../kotlin/io/realm/kotlin/Configuration.kt | 5 +++ .../io/realm/kotlin/RealmConfiguration.kt | 2 ++ .../ExperimentalEncryptionCallbackApi.kt | 35 +++++++++++++++++++ .../kotlin/internal/ConfigurationImpl.kt | 3 ++ .../kotlin/internal/RealmConfigurationImpl.kt | 3 ++ .../io/realm/kotlin/internal/RealmImpl.kt | 2 ++ .../kotlin/mongodb/sync/SyncConfiguration.kt | 2 ++ .../kotlin/test/common/EncryptionTests.kt | 2 ++ 9 files changed, 55 insertions(+) create mode 100644 packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/ExperimentalEncryptionCallbackApi.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ef8c00d2..fdd46fc2cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ ### Enhancements * [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). * `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580)) +* Added an experimental configuration API which will allow to pass the encryption key using a callback https://github.com/realm/realm-kotlin/pull/1636. ### Fixed * Cache notification callback JNI references at startup to ensure that symbols can be resolved in core callbacks. (Issue [#1577](https://github.com/realm/realm-kotlin/issues/1577)) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index fdefddc2cf..0e9351fbf4 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -17,6 +17,7 @@ package io.realm.kotlin import io.realm.kotlin.Configuration.SharedBuilder +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.internal.MISSING_PLUGIN_MESSAGE import io.realm.kotlin.internal.REALM_FILE_EXTENSION import io.realm.kotlin.internal.platform.PATH_SEPARATOR @@ -106,6 +107,7 @@ public data class InitialRealmFileConfiguration( val checksum: String? ) +@ExperimentalEncryptionCallbackApi public interface EncryptionKeyCallback { /** * Provides the native memory address of the 64 byte array containing the key used to encrypt and decrypt the Realm file. @@ -170,6 +172,7 @@ public interface Configuration { * * @return null on unencrypted Realms. */ + @OptIn(ExperimentalEncryptionCallbackApi::class) public val encryptionKeyAsCallback: EncryptionKeyCallback? /** @@ -253,6 +256,7 @@ public interface Configuration { protected var writeDispatcher: CoroutineDispatcher? = null protected var schemaVersion: Long = 0 protected var encryptionKey: ByteArray? = null + @OptIn(ExperimentalEncryptionCallbackApi::class) protected var encryptionKeyAsCallback: EncryptionKeyCallback? = null protected var compactOnLaunchCallback: CompactOnLaunchCallback? = null protected var initialDataCallback: InitialDataCallback? = null @@ -416,6 +420,7 @@ public interface Configuration { * val realm = Realm.open(encryptedConf) *``` */ + @OptIn(ExperimentalEncryptionCallbackApi::class) public fun encryptionKey(encryptionKeyAsCallback: EncryptionKeyCallback): S = apply { this.encryptionKeyAsCallback = encryptionKeyAsCallback } as S diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt index 873efc8cb0..a6ec2bc9ca 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/RealmConfiguration.kt @@ -16,6 +16,7 @@ package io.realm.kotlin +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.internal.ContextLogger import io.realm.kotlin.internal.RealmConfigurationImpl import io.realm.kotlin.internal.platform.appFilesDirectory @@ -185,6 +186,7 @@ public interface RealmConfiguration : Configuration { writerDispatcherFactory, schemaVersion, encryptionKey, + @OptIn(ExperimentalEncryptionCallbackApi::class) encryptionKeyAsCallback, deleteRealmIfMigrationNeeded, compactOnLaunchCallback, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/ExperimentalEncryptionCallbackApi.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/ExperimentalEncryptionCallbackApi.kt new file mode 100644 index 0000000000..9fd2f28325 --- /dev/null +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/annotations/ExperimentalEncryptionCallbackApi.kt @@ -0,0 +1,35 @@ +/* + * 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.kotlin.annotations + +/** + * This annotation mark Realm API for encryption callback **experimental**, i.e. + * there are no guarantees given that this API cannot change without warning between minor and + * major versions. They will not change between patch versions. + * + * For all other purposes these APIs are considered stable, i.e. they undergo the same testing + * as other parts of the API and should behave as documented with no bugs. It is primarily + * marked as experimental because we are unsure if this API provide value and solve the use + * cases that people have. If not, they will be changed or removed altogether. + */ +@MustBeDocumented +@Target( + AnnotationTarget.CLASS, + AnnotationTarget.PROPERTY, + AnnotationTarget.FUNCTION, + AnnotationTarget.TYPEALIAS +) +@RequiresOptIn(level = RequiresOptIn.Level.ERROR) +public annotation class ExperimentalEncryptionCallbackApi diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt index ee9f285a37..39553fd280 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/ConfigurationImpl.kt @@ -21,6 +21,7 @@ import io.realm.kotlin.EncryptionKeyCallback import io.realm.kotlin.InitialDataCallback import io.realm.kotlin.InitialRealmFileConfiguration import io.realm.kotlin.LogConfiguration +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.dynamic.DynamicMutableRealm import io.realm.kotlin.dynamic.DynamicMutableRealmObject import io.realm.kotlin.dynamic.DynamicRealm @@ -61,6 +62,7 @@ public open class ConfigurationImpl( schemaVersion: Long, schemaMode: SchemaMode, private val userEncryptionKey: ByteArray?, + @OptIn(ExperimentalEncryptionCallbackApi::class) override val encryptionKeyAsCallback: EncryptionKeyCallback?, compactOnLaunchCallback: CompactOnLaunchCallback?, userMigration: RealmMigration?, @@ -232,6 +234,7 @@ public open class ConfigurationImpl( RealmInterop.realm_config_set_encryption_key(nativeConfig, key) } + @OptIn(ExperimentalEncryptionCallbackApi::class) encryptionKeyAsCallback?.let { RealmInterop.realm_config_set_encryption_key_from_pointer(nativeConfig, it.keyPointer()) } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt index 3468f6b2ee..079ad4b0bd 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmConfigurationImpl.kt @@ -22,6 +22,7 @@ import io.realm.kotlin.InitialDataCallback import io.realm.kotlin.InitialRealmFileConfiguration import io.realm.kotlin.LogConfiguration import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.internal.interop.SchemaMode import io.realm.kotlin.internal.util.CoroutineDispatcherFactory import io.realm.kotlin.migration.RealmMigration @@ -41,6 +42,7 @@ internal class RealmConfigurationImpl( writeDispatcherFactory: CoroutineDispatcherFactory, schemaVersion: Long, encryptionKey: ByteArray?, + @OptIn(ExperimentalEncryptionCallbackApi::class) encryptionKeyAsCallback: EncryptionKeyCallback?, override val deleteRealmIfMigrationNeeded: Boolean, compactOnLaunchCallback: CompactOnLaunchCallback?, @@ -64,6 +66,7 @@ internal class RealmConfigurationImpl( false -> SchemaMode.RLM_SCHEMA_MODE_AUTOMATIC }, encryptionKey, + @OptIn(ExperimentalEncryptionCallbackApi::class) encryptionKeyAsCallback, compactOnLaunchCallback, migration, diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 60aca34f4e..30f5338e47 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -19,6 +19,7 @@ package io.realm.kotlin.internal import io.realm.kotlin.Configuration import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.dynamic.DynamicRealm import io.realm.kotlin.internal.dynamic.DynamicRealmImpl import io.realm.kotlin.internal.interop.ClassKey @@ -139,6 +140,7 @@ public class RealmImpl private constructor( } realmScope.launch { + @OptIn(ExperimentalEncryptionCallbackApi::class) configuration.encryptionKeyAsCallback?.let { // if we're using an encryption key as a callback, we preemptively open the notifier and writer Realm // with the given configuration because the key might be deleted from memory after the Realm is open. diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt index fba9f61311..bd2984c53d 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncConfiguration.kt @@ -20,6 +20,7 @@ import io.realm.kotlin.LogConfiguration import io.realm.kotlin.MutableRealm import io.realm.kotlin.Realm import io.realm.kotlin.TypedRealm +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.internal.ConfigurationImpl import io.realm.kotlin.internal.ContextLogger import io.realm.kotlin.internal.ObjectIdImpl @@ -565,6 +566,7 @@ public interface SyncConfiguration : Configuration { schemaVersion, SchemaMode.RLM_SCHEMA_MODE_ADDITIVE_DISCOVERED, encryptionKey, + @OptIn(ExperimentalEncryptionCallbackApi::class) encryptionKeyAsCallback, compactOnLaunchCallback, null, // migration is not relevant for sync, diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt index 661e97d26a..b7d03ea522 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt @@ -19,6 +19,7 @@ package io.realm.kotlin.test.common import io.realm.kotlin.EncryptionKeyCallback import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration +import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.entities.Sample import io.realm.kotlin.test.platform.PlatformUtils import io.realm.kotlin.test.util.use @@ -128,6 +129,7 @@ class EncryptionTests { } } + @OptIn(ExperimentalEncryptionCallbackApi::class) @Test fun openEncryptedRealmWithEncryptionKeyCallback() = runBlocking { val key: ByteArray = Random.nextBytes(64) From d69884c4549cc513f5ced191882142ca6cf1a3bc Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Fri, 19 Jan 2024 10:29:24 +0000 Subject: [PATCH 04/11] Fixing test on JVM --- .../io/realm/kotlin/test/common/EncryptionTests.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt index b7d03ea522..78831e7892 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/EncryptionTests.kt @@ -22,6 +22,8 @@ import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.annotations.ExperimentalEncryptionCallbackApi import io.realm.kotlin.entities.Sample import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.TestChannel +import io.realm.kotlin.test.util.receiveOrFail import io.realm.kotlin.test.util.use import kotlinx.atomicfu.atomic import kotlinx.coroutines.runBlocking @@ -31,6 +33,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertTrue /** * This class contains all the Realm encryption integration tests that validate opening a Realm with an encryption key. @@ -136,7 +139,7 @@ class EncryptionTests { val keyPointer: Long = PlatformUtils.allocateEncryptionKeyOnNativeMemory(key) val keyPointerCallbackInvocation = atomic(0) - val keyPointerReleaseCallbackInvocation = atomic(0) + val releaseKeyCallbackInvoked = TestChannel() val encryptedConf = RealmConfiguration .Builder( @@ -150,8 +153,8 @@ class EncryptionTests { } override fun releaseKey() { - keyPointerReleaseCallbackInvocation.incrementAndGet() PlatformUtils.freeEncryptionKeyFromNativeMemory(keyPointer) + releaseKeyCallbackInvoked.trySend(true) } }) .build() @@ -163,8 +166,8 @@ class EncryptionTests { } } + assertTrue(releaseKeyCallbackInvoked.receiveOrFail(), "Releasing the key should only be invoked once all the 3 Realms have been opened") assertEquals(3, keyPointerCallbackInvocation.value, "Encryption key pointer should have been invoked 3 times (Frozen Realm, Notifier and Writer Realms)") - assertEquals(1, keyPointerReleaseCallbackInvocation.value, "Releasing the key should only be invoked once all the 3 Realms have been opened") val keyPointer2 = PlatformUtils.allocateEncryptionKeyOnNativeMemory(key) val encryptedConf2 = RealmConfiguration From cc38c24aeae1dd42fe744077e7b3f47edbc24ca5 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 22 Jan 2024 12:49:14 +0000 Subject: [PATCH 05/11] Update packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt Co-authored-by: Christian Melchior --- .../src/commonMain/kotlin/io/realm/kotlin/Configuration.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index 0e9351fbf4..e2207f351a 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -387,7 +387,6 @@ public interface Configuration { * There's also extra protection for JVM Windows target, where the underlying storage engine uses the Windows Kernel * to encrypt/decrypt the Realm's encryption key before each usage. * - * * Note: The RealmConfiguration doesn't take ownership of this native memory, the caller is responsible of disposing it * appropriately after the Realm is open using the [EncryptionKeyCallback.releaseKey]. * From 2946b8d0dec5a033aff000686e97c9091abc312d Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 22 Jan 2024 12:50:24 +0000 Subject: [PATCH 06/11] Update packages/test-base/src/androidMain/cpp/CMakeLists.txt Co-authored-by: Christian Melchior --- packages/test-base/src/androidMain/cpp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-base/src/androidMain/cpp/CMakeLists.txt b/packages/test-base/src/androidMain/cpp/CMakeLists.txt index 5fb4091f21..f3c11643d8 100644 --- a/packages/test-base/src/androidMain/cpp/CMakeLists.txt +++ b/packages/test-base/src/androidMain/cpp/CMakeLists.txt @@ -1 +1 @@ -add_library(android_jni_test_helper SHARED android_jni_helper.cpp) \ No newline at end of file +add_library(android_jni_test_helper SHARED android_jni_helper.cpp) From 3accfe96fe44be3cb59cecc79603a645d917bde9 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 22 Jan 2024 12:50:33 +0000 Subject: [PATCH 07/11] Update packages/test-base/src/androidMain/cpp/android_jni_helper.cpp Co-authored-by: Christian Melchior --- packages/test-base/src/androidMain/cpp/android_jni_helper.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp b/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp index 79933ad282..94eeb48cdb 100644 --- a/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp +++ b/packages/test-base/src/androidMain/cpp/android_jni_helper.cpp @@ -20,4 +20,4 @@ Java_io_realm_kotlin_test_platform_PlatformUtils_nativeFreeEncryptionKeyFromNati delete[] reinterpret_cast(keyPtr); } -} \ No newline at end of file +} From 59aac573676e7e89074b840dc9153408e3ed47b4 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 22 Jan 2024 13:06:49 +0000 Subject: [PATCH 08/11] PR feedback --- .../src/commonMain/kotlin/io/realm/kotlin/Configuration.kt | 5 +++++ .../kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index e2207f351a..17721fe6b8 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -111,11 +111,16 @@ public data class InitialRealmFileConfiguration( public interface EncryptionKeyCallback { /** * Provides the native memory address of the 64 byte array containing the key used to encrypt and decrypt the Realm file. + * This can be called multiple times internally, so the key needs to be the same for between calls. + * + * Note: The Realm SDK is not responsible of checking that the pointer is a valid 64 byte array, providing an invalid address will probably + * causes a segmentation fault and will crash the app. */ public fun keyPointer(): Long /** * This callback will be invoked by Realm after it's open. This hint to the user that the key provided in [keyPointer] can now be released. + * This will be called once the Realm is open and it's safe to dispose of the encryption key. */ public fun releaseKey() } diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 652876174a..f1d9540f21 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -63,8 +63,8 @@ actual object PlatformUtils { actual fun allocateEncryptionKeyOnNativeMemory(aesKey: ByteArray): Long { // Note: the ByteBuffer is not guaranteed to be in native memory (it could use a backing array) - // use allocateDirect.hasArray() to find out. Ideally we want to use JNI for Android to - // create such native array. + // use allocateDirect.hasArray() to find out. + // We use JNI for Android to create such native array. return nativeAllocateEncryptionKeyOnNativeMemory(aesKey) } From 5e61217262b4afc88bedda0a49b15f68dee15ffa Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 22 Jan 2024 14:51:02 +0000 Subject: [PATCH 09/11] Update packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt Co-authored-by: Christian Melchior --- .../src/commonMain/kotlin/io/realm/kotlin/Configuration.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt index 17721fe6b8..59fd093752 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/Configuration.kt @@ -111,7 +111,7 @@ public data class InitialRealmFileConfiguration( public interface EncryptionKeyCallback { /** * Provides the native memory address of the 64 byte array containing the key used to encrypt and decrypt the Realm file. - * This can be called multiple times internally, so the key needs to be the same for between calls. + * This can be called multiple times internally, so the key needs to be the same between calls. * * Note: The Realm SDK is not responsible of checking that the pointer is a valid 64 byte array, providing an invalid address will probably * causes a segmentation fault and will crash the app. From 4ca311a31b1e75131c770977ae3406e1d1f77968 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 24 Jan 2024 18:09:40 +0000 Subject: [PATCH 10/11] Updating the Core branch (to the one using Windows kernel encryption on Windows) --- buildSrc/src/main/kotlin/Config.kt | 2 +- packages/external/core | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index bcb85872dd..6b7b413b22 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null || System.getenv("CI") != null) - const val version = "1.14.0-SNAPSHOT" + const val version = "1.14.0-ENCRYPTION-POC-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" diff --git a/packages/external/core b/packages/external/core index 71f94d75e2..59d49ce853 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 71f94d75e25bfc8913fcd93ae8de550b57577a4a +Subproject commit 59d49ce8535ea64f0fa32baac7f2fe9168eb6bb8 From 09209b751ec9c18c473785185d745251ed22f460 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Thu, 29 Feb 2024 14:39:44 +0000 Subject: [PATCH 11/11] bump core --- packages/external/core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/external/core b/packages/external/core index 59d49ce853..f1559304c5 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 59d49ce8535ea64f0fa32baac7f2fe9168eb6bb8 +Subproject commit f1559304c52815f7adaa70b4a31bf862acf03061