LCOV - code coverage report
Current view: top level - lib/src/database/sqflite_encryption_helper - io.dart (source / functions) Hit Total Coverage
Test: merged.info Lines: 0 58 0.0 %
Date: 2024-11-12 07:37:08 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 1.14