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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy