Add a small curated catalog of Iberian horticultural species and let a variety be linked to it from the edit sheet. - assets/catalog/species.json: 14 species with botanical family and ES/EN common names (wikidata_qid/gbif_key deferred to the varilla enrichment). - SpeciesRepository: idempotent seedBundled (is_bundled rows, keyed by scientific name) + search by scientific/common name with a locale-best label. Seeded on startup from DI. - VarietyRepository.linkSpecies: sets species_id and prefills category from the species' family when empty (never overwrites an existing category). VarietyDetail now carries the scientific name. - Edit sheet gains a live species-search field; the detail view shows the scientific name (italic). i18n strings added (ES/EN). Tests: catalog parse, idempotent seeding, search by scientific/common name, linkSpecies prefill semantics, and a widget test for the autocomplete → link → scientific-name-shown flow. Full suite: 32 passing, 0 skipped.
70 lines
2.4 KiB
Dart
70 lines
2.4 KiB
Dart
import 'dart:ffi';
|
|
import 'dart:io';
|
|
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:sqlcipher_flutter_libs/sqlcipher_flutter_libs.dart';
|
|
import 'package:sqlite3/open.dart';
|
|
import 'package:sqlite3/sqlite3.dart';
|
|
|
|
/// Routes package:sqlite3 to the **SQLCipher** build instead of plain SQLite.
|
|
///
|
|
/// - Android: the bundled SQLCipher `.so` (from sqlcipher_flutter_libs).
|
|
/// - Linux: the system `libsqlcipher.so` (dev machines / CI install it).
|
|
/// - iOS & macOS: SQLCipher is linked into the app binary — no override needed.
|
|
void useSqlCipher() {
|
|
open
|
|
..overrideFor(OperatingSystem.android, openCipherOnAndroid)
|
|
..overrideFor(OperatingSystem.linux, _openLinuxCipher);
|
|
}
|
|
|
|
DynamicLibrary _openLinuxCipher() {
|
|
// The dev package ships the `libsqlcipher.so` symlink; runtime packages ship
|
|
// only a versioned name (`.so.0`, `.so.1`, …). Try them in turn.
|
|
const candidates = [
|
|
'libsqlcipher.so',
|
|
'libsqlcipher.so.1',
|
|
'libsqlcipher.so.0',
|
|
];
|
|
Object? lastError;
|
|
for (final name in candidates) {
|
|
try {
|
|
return DynamicLibrary.open(name);
|
|
} on ArgumentError catch (e) {
|
|
lastError = e;
|
|
}
|
|
}
|
|
throw StateError('Could not load SQLCipher (tried $candidates): $lastError');
|
|
}
|
|
|
|
/// Opens [file] as an encrypted database using the raw 256-bit [keyHex].
|
|
///
|
|
/// Verifies SQLCipher is actually linked (`PRAGMA cipher_version`) and refuses
|
|
/// to fall back to plaintext — enforcing "no plaintext at rest, ever".
|
|
QueryExecutor openEncryptedExecutor(File file, String keyHex) {
|
|
return LazyDatabase(() async {
|
|
if (Platform.isAndroid) {
|
|
await applyWorkaroundToOpenSqlCipherOnOldAndroidVersions();
|
|
}
|
|
return NativeDatabase.createInBackground(
|
|
file,
|
|
isolateSetup: useSqlCipher,
|
|
setup: (db) => applyKeyAndVerify(db, keyHex),
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Applies the SQLCipher key to [db] and asserts encryption is active.
|
|
///
|
|
/// `x'…'` passes the key as raw bytes, skipping the KDF (our key is already a
|
|
/// random 256-bit value from the OS keystore). Exposed for the security test.
|
|
void applyKeyAndVerify(Database db, String keyHex) {
|
|
db.execute('PRAGMA key = "x\'$keyHex\'";');
|
|
final cipher = db.select('PRAGMA cipher_version;');
|
|
if (cipher.isEmpty) {
|
|
throw StateError(
|
|
'SQLCipher is not linked: PRAGMA cipher_version is empty. Encryption at '
|
|
'rest is mandatory — refusing to open a plaintext database.',
|
|
);
|
|
}
|
|
}
|