From d3e7a2c6f9ecea705525d9fa62ace93bd53737bd Mon Sep 17 00:00:00 2001 From: jt <32107801+Jtplouffe@users.noreply.github.com> Date: Sat, 24 Feb 2024 13:34:54 -0500 Subject: [PATCH] feat: change encryption key --- packages/isar/lib/src/impl/isar_impl.dart | 6 ++ packages/isar/lib/src/isar.dart | 5 ++ packages/isar/lib/src/native/bindings.dart | 18 +++++ packages/isar/lib/src/web/bindings.dart | 8 ++ packages/isar_core/src/core/instance.rs | 2 + .../isar_core/src/native/native_instance.rs | 4 + packages/isar_core/src/sqlite/sqlite3.rs | 7 ++ .../isar_core/src/sqlite/sqlite_instance.rs | 11 +++ packages/isar_core_ffi/src/instance.rs | 21 +++++ packages/isar_test/test/encryption_test.dart | 80 ++++++++++++++++++- tool/build_linux.sh | 2 +- tool/generate_bindings.sh | 2 +- 12 files changed, 162 insertions(+), 4 deletions(-) mode change 100644 => 100755 tool/build_linux.sh mode change 100644 => 100755 tool/generate_bindings.sh diff --git a/packages/isar/lib/src/impl/isar_impl.dart b/packages/isar/lib/src/impl/isar_impl.dart index 6ddd8c43c..9d6e08f89 100644 --- a/packages/isar/lib/src/impl/isar_impl.dart +++ b/packages/isar/lib/src/impl/isar_impl.dart @@ -413,6 +413,12 @@ class _IsarImpl extends Isar { } } + @override + void changeEncryptionKey(String encryptionKey) { + final string = IsarCore._toNativeString(encryptionKey); + IsarCore.b.isar_change_encryption_key(getPtr(), string); + } + @override bool close({bool deleteFromDisk = false}) { final closed = IsarCore.b.isar_close(getPtr(), deleteFromDisk); diff --git a/packages/isar/lib/src/isar.dart b/packages/isar/lib/src/isar.dart index 9467d6a8d..21f8807f8 100644 --- a/packages/isar/lib/src/isar.dart +++ b/packages/isar/lib/src/isar.dart @@ -287,6 +287,11 @@ abstract class Isar { @visibleForTesting void verify(); + /// Changes the encryption key for an encrypted database. + /// Only supported on engines with encryption encryption support, + /// and for databases that are already encrypted. + void changeEncryptionKey(String encryptionKey); + /// FNV-1a 64bit hash algorithm optimized for Dart Strings static int fastHash(String string) { return platformFastHash(string); diff --git a/packages/isar/lib/src/native/bindings.dart b/packages/isar/lib/src/native/bindings.dart index 28f949a76..8304576ec 100644 --- a/packages/isar/lib/src/native/bindings.dart +++ b/packages/isar/lib/src/native/bindings.dart @@ -644,6 +644,24 @@ class IsarCoreBindings { int Function( ffi.Pointer, ffi.Pointer>)>(); + int isar_change_encryption_key( + ffi.Pointer isar, + ffi.Pointer encryption_key, + ) { + return _isar_change_encryption_key( + isar, + encryption_key, + ); + } + + late final _isar_change_encryption_keyPtr = _lookup< + ffi.NativeFunction< + ffi.Uint8 Function(ffi.Pointer, + ffi.Pointer)>>('isar_change_encryption_key'); + late final _isar_change_encryption_key = + _isar_change_encryption_keyPtr.asFunction< + int Function(ffi.Pointer, ffi.Pointer)>(); + int isar_txn_begin( ffi.Pointer isar, ffi.Pointer> txn, diff --git a/packages/isar/lib/src/web/bindings.dart b/packages/isar/lib/src/web/bindings.dart index 2e3369aa9..e32ac23fb 100644 --- a/packages/isar/lib/src/web/bindings.dart +++ b/packages/isar/lib/src/web/bindings.dart @@ -283,6 +283,14 @@ extension IsarBindingsX on JSIsar { ffi.Pointer> dir, ); + @ffi.Native< + ffi.Uint8 Function(ffi.Pointer, ffi.Pointer)>( + symbol: 'isar_change_encryption_key') + external int isar_change_encryption_key( + ffi.Pointer isar, + ffi.Pointer encryption_key, + ); + @ffi.Native< ffi.Uint8 Function( ffi.Pointer, diff --git a/packages/isar_core/src/core/instance.rs b/packages/isar_core/src/core/instance.rs index dbe26364f..eb3bede52 100644 --- a/packages/isar_core/src/core/instance.rs +++ b/packages/isar_core/src/core/instance.rs @@ -60,6 +60,8 @@ pub trait IsarInstance: Sized { compact_condition: Option, ) -> Result; + fn change_encryption_key(&self, encryption_key: Option<&str>) -> Result<()>; + fn begin_txn(&self, write: bool) -> Result; fn commit_txn(&self, txn: Self::Txn) -> Result<()>; diff --git a/packages/isar_core/src/native/native_instance.rs b/packages/isar_core/src/native/native_instance.rs index 082b982f6..35118a74b 100644 --- a/packages/isar_core/src/native/native_instance.rs +++ b/packages/isar_core/src/native/native_instance.rs @@ -141,6 +141,10 @@ impl IsarInstance for NativeInstance { } } + fn change_encryption_key(&self, encryption_key: Option<&str>) -> Result<()> { + Err(IsarError::UnsupportedOperation {}) + } + fn begin_txn(&self, write: bool) -> Result { NativeTxn::new(self.instance_id, &self.env, write) } diff --git a/packages/isar_core/src/sqlite/sqlite3.rs b/packages/isar_core/src/sqlite/sqlite3.rs index 82f784ee3..6cb64ced4 100644 --- a/packages/isar_core/src/sqlite/sqlite3.rs +++ b/packages/isar_core/src/sqlite/sqlite3.rs @@ -52,6 +52,13 @@ impl SQLite3 { Ok(()) } + pub fn rekey(&self, encryption_key: &str) -> Result<()> { + let sql = format!("PRAGMA rekey = \"{encryption_key}\""); + self.prepare(&sql)?.step()?; + self.prepare("SELECT count(*) FROM sqlite_master")?.step()?; // check if key is correct + Ok(()) + } + fn initialize(&self) -> Result<()> { unsafe { sqlite3_busy_timeout(self.db, 5000); diff --git a/packages/isar_core/src/sqlite/sqlite_instance.rs b/packages/isar_core/src/sqlite/sqlite_instance.rs index c3888801d..882d4eb8f 100644 --- a/packages/isar_core/src/sqlite/sqlite_instance.rs +++ b/packages/isar_core/src/sqlite/sqlite_instance.rs @@ -142,6 +142,17 @@ impl IsarInstance for SQLiteInstance { }) } + fn change_encryption_key(&self, encryption_key: Option<&str>) -> Result<()> { + if !cfg!(feature = "sqlcipher") { + return Err(IsarError::UnsupportedOperation {}); + } + + match encryption_key { + Some(encryption_key) => self.sqlite.rekey(encryption_key), + None => unimplemented!("database decryption"), + } + } + fn begin_txn(&self, write: bool) -> Result { if write { self.info.write_mutex.lock(); diff --git a/packages/isar_core_ffi/src/instance.rs b/packages/isar_core_ffi/src/instance.rs index b0c599d55..4bc6084ee 100644 --- a/packages/isar_core_ffi/src/instance.rs +++ b/packages/isar_core_ffi/src/instance.rs @@ -137,6 +137,27 @@ pub unsafe extern "C" fn isar_get_dir(isar: &'static CIsarInstance, dir: *mut *c value.len() as u32 } +#[no_mangle] +pub unsafe extern "C" fn isar_change_encryption_key( + isar: &'static CIsarInstance, + encryption_key: *mut String, +) -> u8 { + let encryption_key = if encryption_key.is_null() { + None + } else { + Some(*Box::from_raw(encryption_key)) + }; + + isar_try! { + match isar { + #[cfg(feature = "native")] + CIsarInstance::Native(isar) => isar.change_encryption_key(encryption_key.as_deref())?, + #[cfg(feature = "sqlite")] + CIsarInstance::SQLite(isar) => isar.change_encryption_key(encryption_key.as_deref())?, + } + } +} + unsafe fn _isar_txn_begin( isar: &'static CIsarInstance, txn: *mut *const CIsarTxn, diff --git a/packages/isar_test/test/encryption_test.dart b/packages/isar_test/test/encryption_test.dart index 56c6fdf9c..f25c028d1 100644 --- a/packages/isar_test/test/encryption_test.dart +++ b/packages/isar_test/test/encryption_test.dart @@ -51,10 +51,86 @@ void main() { expect(isar.close(), true); await expectLater( - () => - openTempIsar([ModelSchema], name: isarName, encryptionKey: 'test2'), + () => openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'test2', + ), throwsA(isA()), ); }); + + isarTest('Change key', isar: false, web: false, () async { + final isarName = getRandomName(); + final isar1 = await openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'key1', + ); + isar1.write((isar) => isar.models.put(Model('test1'))); + expect(isar1.close(), true); + + final isar2 = await openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'key1', + ); + expect(isar2.models.where().findAll(), [Model('test1')]); + + isar2.changeEncryptionKey('key2'); + expect(isar2.models.where().findAll(), [Model('test1')]); + isar2.write((isar) => isar.models.put(Model('test2'))); + expect(isar2.models.where().findAll(), [Model('test1'), Model('test2')]); + expect(isar2.close(), true); + + // Using the old key (should throw) + await expectLater( + () => openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'key1', + ), + throwsA(isA()), + ); + + final isar3 = await openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'key2', + ); + expect(isar3.models.where().findAll(), [Model('test1'), Model('test2')]); + + isar3.write((isar) => isar.models.put(Model('test3'))); + isar3.changeEncryptionKey('key3'); + isar3.write((isar) => isar.models.put(Model('test4'))); + isar3.changeEncryptionKey('key4'); + isar3.write((isar) => isar.models.clear()); + isar3.write((isar) => isar.models.put(Model('test5'))); + isar3.changeEncryptionKey('key1'); + isar3.write((isar) => isar.models.put(Model('test6'))); + + expect(isar3.models.where().findAll(), [Model('test5'), Model('test6')]); + expect(isar3.close(), true); + + for (final oldKey in ['key2', 'key3', 'key4']) { + // Using the old key (should throw) + await expectLater( + () => openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: oldKey, + ), + throwsA(isA()), + ); + } + + final isar4 = await openTempIsar( + [ModelSchema], + name: isarName, + encryptionKey: 'key1', + ); + expect(isar4.models.where().findAll(), [Model('test5'), Model('test6')]); + expect(isar4.close(), true); + }); }); } diff --git a/tool/build_linux.sh b/tool/build_linux.sh old mode 100644 new mode 100755 index 0a71a233b..91902b6b0 --- a/tool/build_linux.sh +++ b/tool/build_linux.sh @@ -6,4 +6,4 @@ else rustup target add aarch64-unknown-linux-gnu cargo build --target aarch64-unknown-linux-gnu --features sqlcipher-vendored --release mv "target/aarch64-unknown-linux-gnu/release/libisar.so" "libisar_linux_arm64.so" -fi \ No newline at end of file +fi diff --git a/tool/generate_bindings.sh b/tool/generate_bindings.sh old mode 100644 new mode 100755 index 9a95a6835..e18e12b00 --- a/tool/generate_bindings.sh +++ b/tool/generate_bindings.sh @@ -14,4 +14,4 @@ rm isar-dart.h dart tool/fix_web_bindings.dart dart format --fix lib/src/impl/bindings.dart -dart format --fix lib/src/web/bindings.dart +dart format --fix lib/src/web/bindings.dart