Skip to content

Commit

Permalink
Merge pull request #20 from isar/feat/change-encryption-key
Browse files Browse the repository at this point in the history
feat: change encryption key
  • Loading branch information
mrclauss authored Mar 7, 2024
2 parents 92b5a67 + d3e7a2c commit d5ae3f1
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 3 deletions.
6 changes: 6 additions & 0 deletions packages/isar/lib/src/impl/isar_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions packages/isar/lib/src/isar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
18 changes: 18 additions & 0 deletions packages/isar/lib/src/native/bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,24 @@ class IsarCoreBindings {
int Function(
ffi.Pointer<CIsarInstance>, ffi.Pointer<ffi.Pointer<ffi.Uint8>>)>();

int isar_change_encryption_key(
ffi.Pointer<CIsarInstance> isar,
ffi.Pointer<CString> encryption_key,
) {
return _isar_change_encryption_key(
isar,
encryption_key,
);
}

late final _isar_change_encryption_keyPtr = _lookup<
ffi.NativeFunction<
ffi.Uint8 Function(ffi.Pointer<CIsarInstance>,
ffi.Pointer<CString>)>>('isar_change_encryption_key');
late final _isar_change_encryption_key =
_isar_change_encryption_keyPtr.asFunction<
int Function(ffi.Pointer<CIsarInstance>, ffi.Pointer<CString>)>();

int isar_txn_begin(
ffi.Pointer<CIsarInstance> isar,
ffi.Pointer<ffi.Pointer<CIsarTxn>> txn,
Expand Down
8 changes: 8 additions & 0 deletions packages/isar/lib/src/web/bindings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,14 @@ extension IsarBindingsX on JSIsar {
ffi.Pointer<ffi.Pointer<ffi.Uint8>> dir,
);

@ffi.Native<
ffi.Uint8 Function(ffi.Pointer<CIsarInstance>, ffi.Pointer<CString>)>(
symbol: 'isar_change_encryption_key')
external int isar_change_encryption_key(
ffi.Pointer<CIsarInstance> isar,
ffi.Pointer<CString> encryption_key,
);

@ffi.Native<
ffi.Uint8 Function(
ffi.Pointer<CIsarInstance>,
Expand Down
2 changes: 2 additions & 0 deletions packages/isar_core/src/core/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ pub trait IsarInstance: Sized {
compact_condition: Option<CompactCondition>,
) -> Result<Self::Instance>;

fn change_encryption_key(&self, encryption_key: Option<&str>) -> Result<()>;

fn begin_txn(&self, write: bool) -> Result<Self::Txn>;

fn commit_txn(&self, txn: Self::Txn) -> Result<()>;
Expand Down
4 changes: 4 additions & 0 deletions packages/isar_core/src/native/native_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self::Txn> {
NativeTxn::new(self.instance_id, &self.env, write)
}
Expand Down
7 changes: 7 additions & 0 deletions packages/isar_core/src/sqlite/sqlite3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
11 changes: 11 additions & 0 deletions packages/isar_core/src/sqlite/sqlite_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SQLiteTxn> {
if write {
self.info.write_mutex.lock();
Expand Down
21 changes: 21 additions & 0 deletions packages/isar_core_ffi/src/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
80 changes: 78 additions & 2 deletions packages/isar_test/test/encryption_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<EncryptionError>()),
);
});

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<EncryptionError>()),
);

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<EncryptionError>()),
);
}

final isar4 = await openTempIsar(
[ModelSchema],
name: isarName,
encryptionKey: 'key1',
);
expect(isar4.models.where().findAll(), [Model('test5'), Model('test6')]);
expect(isar4.close(), true);
});
});
}
Empty file modified tool/build_linux.sh
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion tool/generate_bindings.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit d5ae3f1

Please sign in to comment.