Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: change encryption key #20

Merged
merged 1 commit into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
}
2 changes: 1 addition & 1 deletion tool/build_linux.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
fi
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
Loading