LCOV - code coverage report
Current view: top level - lib/src/database/sqflite_encryption_helper - io.dart (source / functions) Coverage Total Hit
Test: merged.info Lines: 0.0 % 58 0
Test Date: 2025-01-14 13:39:53 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:ffi';
       2              : import 'dart:io';
       3              : import 'dart:math' show max;
       4              : 
       5              : import 'package:sqflite_common/sqlite_api.dart';
       6              : import 'package:sqlite3/open.dart';
       7              : 
       8              : import 'package:matrix/matrix.dart';
       9              : 
      10              : /// A helper utility for SQfLite related encryption operations
      11              : ///
      12              : /// * helps loading the required dynamic libraries - even on cursed systems
      13              : /// * migrates unencrypted SQLite databases to SQLCipher
      14              : /// * applies the PRAGMA key to a database and ensure it is properly loading
      15              : class SQfLiteEncryptionHelper {
      16              :   /// the factory to use for all SQfLite operations
      17              :   final DatabaseFactory factory;
      18              : 
      19              :   /// the path of the database
      20              :   final String path;
      21              : 
      22              :   /// the (supposed) PRAGMA key of the database
      23              :   final String cipher;
      24              : 
      25            0 :   const SQfLiteEncryptionHelper({
      26              :     required this.factory,
      27              :     required this.path,
      28              :     required this.cipher,
      29              :   });
      30              : 
      31              :   /// Loads the correct [DynamicLibrary] required for SQLCipher
      32              :   ///
      33              :   /// To be used with `package:sqlite3/open.dart`:
      34              :   /// ```dart
      35              :   /// void main() {
      36              :   ///   final factory = createDatabaseFactoryFfi(
      37              :   ///     ffiInit: SQfLiteEncryptionHelper.ffiInit,
      38              :   ///   );
      39              :   /// }
      40              :   /// ```
      41            0 :   static void ffiInit() => open.overrideForAll(_loadSQLCipherDynamicLibrary);
      42              : 
      43            0 :   static DynamicLibrary _loadSQLCipherDynamicLibrary() {
      44              :     // Taken from https://github.com/simolus3/sqlite3.dart/blob/e66702c5bec7faec2bf71d374c008d5273ef2b3b/sqlite3/lib/src/load_library.dart#L24
      45            0 :     if (Platform.isAndroid) {
      46              :       try {
      47            0 :         return DynamicLibrary.open('libsqlcipher.so');
      48              :       } catch (_) {
      49              :         // On some (especially old) Android devices, we somehow can't dlopen
      50              :         // libraries shipped with the apk. We need to find the full path of the
      51              :         // library (/data/data/<id>/lib/libsqlcipher.so) and open that one.
      52              :         // For details, see https://github.com/simolus3/moor/issues/420
      53            0 :         final appIdAsBytes = File('/proc/self/cmdline').readAsBytesSync();
      54              : 
      55              :         // app id ends with the first \0 character in here.
      56            0 :         final endOfAppId = max(appIdAsBytes.indexOf(0), 0);
      57            0 :         final appId = String.fromCharCodes(appIdAsBytes.sublist(0, endOfAppId));
      58              : 
      59            0 :         return DynamicLibrary.open('/data/data/$appId/lib/libsqlcipher.so');
      60              :       }
      61              :     }
      62            0 :     if (Platform.isLinux) {
      63              :       // *not my fault grumble*
      64              :       //
      65              :       // On many Linux systems, I encountered issues opening the system provided
      66              :       // libsqlcipher.so. I hence decided to ship an own one - statically linked
      67              :       // against a patched version of OpenSSL compiled with the correct options.
      68              :       //
      69              :       // This was the only way I reached to run on particular Fedora and Arch
      70              :       // systems.
      71              :       //
      72              :       // Hours wasted : 12
      73              :       try {
      74            0 :         return DynamicLibrary.open('libsqlcipher_flutter_libs_plugin.so');
      75              :       } catch (_) {
      76            0 :         return DynamicLibrary.open('libsqlcipher.so');
      77              :       }
      78              :     }
      79            0 :     if (Platform.isIOS) {
      80            0 :       return DynamicLibrary.process();
      81              :     }
      82            0 :     if (Platform.isMacOS) {
      83            0 :       return DynamicLibrary.open(
      84              :         'sqlcipher_flutter_libs.framework/Versions/Current/'
      85              :         'sqlcipher_flutter_libs',
      86              :       );
      87              :     }
      88            0 :     if (Platform.isWindows) {
      89            0 :       return DynamicLibrary.open('libsqlcipher.dll');
      90              :     }
      91              : 
      92            0 :     throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
      93              :   }
      94              : 
      95              :   /// checks whether the database exists and is encrypted
      96              :   ///
      97              :   /// In case it is not encrypted, the file is being migrated
      98              :   /// to SQLCipher and encrypted using the given cipher and checks
      99              :   /// whether that operation was successful
     100            0 :   Future<void> ensureDatabaseFileEncrypted() async {
     101            0 :     final file = File(path);
     102              : 
     103              :     // in case the file does not exist there is no need to migrate
     104            0 :     if (!await file.exists()) {
     105              :       return;
     106              :     }
     107              : 
     108              :     // no work to do in case the DB is already encrypted
     109            0 :     if (!await _isPlainText(file)) {
     110              :       return;
     111              :     }
     112              : 
     113            0 :     Logs().d(
     114              :       'Warning: Found unencrypted sqlite database. Encrypting using SQLCipher.',
     115              :     );
     116              : 
     117              :     // hell, it's unencrypted. This should not happen. Time to encrypt it.
     118            0 :     final plainDb = await factory.openDatabase(path);
     119              : 
     120            0 :     final encryptedPath = '$path.encrypted';
     121              : 
     122            0 :     await plainDb.execute(
     123            0 :       "ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';",
     124              :     );
     125            0 :     await plainDb.execute("SELECT sqlcipher_export('encrypted');");
     126              :     // ignore: prefer_single_quotes
     127            0 :     await plainDb.execute("DETACH DATABASE encrypted;");
     128            0 :     await plainDb.close();
     129              : 
     130            0 :     Logs().d('Migrated data to temporary database. Checking integrity.');
     131              : 
     132            0 :     final encryptedFile = File(encryptedPath);
     133              :     // we should now have a second file - which is encrypted
     134            0 :     assert(await encryptedFile.exists());
     135            0 :     assert(!await _isPlainText(encryptedFile));
     136              : 
     137            0 :     Logs().d('New file encrypted. Deleting plain text database.');
     138              : 
     139              :     // deleting the plain file and replacing it with the new one
     140            0 :     await file.delete();
     141            0 :     await encryptedFile.copy(path);
     142              :     // delete the temporary encrypted file
     143            0 :     await encryptedFile.delete();
     144              : 
     145            0 :     Logs().d('Migration done.');
     146              :   }
     147              : 
     148              :   /// safely applies the PRAGMA key to a [Database]
     149              :   ///
     150              :   /// To be directly used as [OpenDatabaseOptions.onConfigure].
     151              :   ///
     152              :   /// * ensures PRAGMA is supported by the given [database]
     153              :   /// * applies [cipher] as PRAGMA key
     154              :   /// * checks whether this operation was successful
     155            0 :   Future<void> applyPragmaKey(Database database) async {
     156            0 :     final cipherVersion = await database.rawQuery('PRAGMA cipher_version;');
     157            0 :     if (cipherVersion.isEmpty) {
     158              :       // Make sure that we're actually using SQLCipher, since the pragma
     159              :       // used to encrypt databases just fails silently with regular
     160              :       // sqlite3
     161              :       // (meaning that we'd accidentally use plaintext databases).
     162            0 :       throw StateError(
     163              :         'SQLCipher library is not available, '
     164              :         'please check your dependencies!',
     165              :       );
     166              :     } else {
     167            0 :       final version = cipherVersion.singleOrNull?['cipher_version'];
     168            0 :       Logs().d(
     169            0 :         'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.',
     170              :       );
     171              :     }
     172              : 
     173            0 :     final result = await database.rawQuery("PRAGMA KEY='$cipher';");
     174            0 :     assert(result.single['ok'] == 'ok');
     175              :   }
     176              : 
     177              :   /// checks whether a File has a plain text SQLite header
     178            0 :   Future<bool> _isPlainText(File file) async {
     179            0 :     final raf = await file.open();
     180            0 :     final bytes = await raf.read(15);
     181            0 :     await raf.close();
     182              : 
     183              :     const header = [
     184              :       83,
     185              :       81,
     186              :       76,
     187              :       105,
     188              :       116,
     189              :       101,
     190              :       32,
     191              :       102,
     192              :       111,
     193              :       114,
     194              :       109,
     195              :       97,
     196              :       116,
     197              :       32,
     198              :       51,
     199              :     ];
     200              : 
     201            0 :     return _listEquals(bytes, header);
     202              :   }
     203              : 
     204              :   /// Taken from `package:flutter/foundation.dart`;
     205              :   ///
     206              :   /// Compares two lists for element-by-element equality.
     207            0 :   bool _listEquals<T>(List<T>? a, List<T>? b) {
     208              :     if (a == null) {
     209              :       return b == null;
     210              :     }
     211            0 :     if (b == null || a.length != b.length) {
     212              :       return false;
     213              :     }
     214              :     if (identical(a, b)) {
     215              :       return true;
     216              :     }
     217            0 :     for (int index = 0; index < a.length; index += 1) {
     218            0 :       if (a[index] != b[index]) {
     219              :         return false;
     220              :       }
     221              :     }
     222              :     return true;
     223              :   }
     224              : }
        

Generated by: LCOV version 2.0-1