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-07-12 20:20:16 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             :           '/usr/lib/libsqlcipher_flutter_libs_plugin.dylib');
      85             :     }
      86           0 :     if (Platform.isWindows) {
      87           0 :       return DynamicLibrary.open('libsqlcipher.dll');
      88             :     }
      89             : 
      90           0 :     throw UnsupportedError('Unsupported platform: ${Platform.operatingSystem}');
      91             :   }
      92             : 
      93             :   /// checks whether the database exists and is encrypted
      94             :   ///
      95             :   /// In case it is not encrypted, the file is being migrated
      96             :   /// to SQLCipher and encrypted using the given cipher and checks
      97             :   /// whether that operation was successful
      98           0 :   Future<void> ensureDatabaseFileEncrypted() async {
      99           0 :     final file = File(path);
     100             : 
     101             :     // in case the file does not exist there is no need to migrate
     102           0 :     if (!await file.exists()) {
     103             :       return;
     104             :     }
     105             : 
     106             :     // no work to do in case the DB is already encrypted
     107           0 :     if (!await _isPlainText(file)) {
     108             :       return;
     109             :     }
     110             : 
     111           0 :     Logs().d(
     112             :         'Warning: Found unencrypted sqlite database. Encrypting using SQLCipher.');
     113             : 
     114             :     // hell, it's unencrypted. This should not happen. Time to encrypt it.
     115           0 :     final plainDb = await factory.openDatabase(path);
     116             : 
     117           0 :     final encryptedPath = '$path.encrypted';
     118             : 
     119           0 :     await plainDb.execute(
     120           0 :         "ATTACH DATABASE '$encryptedPath' AS encrypted KEY '$cipher';");
     121           0 :     await plainDb.execute("SELECT sqlcipher_export('encrypted');");
     122             :     // ignore: prefer_single_quotes
     123           0 :     await plainDb.execute("DETACH DATABASE encrypted;");
     124           0 :     await plainDb.close();
     125             : 
     126           0 :     Logs().d('Migrated data to temporary database. Checking integrity.');
     127             : 
     128           0 :     final encryptedFile = File(encryptedPath);
     129             :     // we should now have a second file - which is encrypted
     130           0 :     assert(await encryptedFile.exists());
     131           0 :     assert(!await _isPlainText(encryptedFile));
     132             : 
     133           0 :     Logs().d('New file encrypted. Deleting plain text database.');
     134             : 
     135             :     // deleting the plain file and replacing it with the new one
     136           0 :     await file.delete();
     137           0 :     await encryptedFile.copy(path);
     138             :     // delete the temporary encrypted file
     139           0 :     await encryptedFile.delete();
     140             : 
     141           0 :     Logs().d('Migration done.');
     142             :   }
     143             : 
     144             :   /// safely applies the PRAGMA key to a [Database]
     145             :   ///
     146             :   /// To be directly used as [OpenDatabaseOptions.onConfigure].
     147             :   ///
     148             :   /// * ensures PRAGMA is supported by the given [database]
     149             :   /// * applies [cipher] as PRAGMA key
     150             :   /// * checks whether this operation was successful
     151           0 :   Future<void> applyPragmaKey(Database database) async {
     152           0 :     final cipherVersion = await database.rawQuery('PRAGMA cipher_version;');
     153           0 :     if (cipherVersion.isEmpty) {
     154             :       // Make sure that we're actually using SQLCipher, since the pragma
     155             :       // used to encrypt databases just fails silently with regular
     156             :       // sqlite3
     157             :       // (meaning that we'd accidentally use plaintext databases).
     158           0 :       throw StateError(
     159             :         'SQLCipher library is not available, '
     160             :         'please check your dependencies!',
     161             :       );
     162             :     } else {
     163           0 :       final version = cipherVersion.singleOrNull?['cipher_version'];
     164           0 :       Logs().d(
     165           0 :           'PRAGMA supported by bundled SQLite. Encryption supported. SQLCipher version: $version.');
     166             :     }
     167             : 
     168           0 :     final result = await database.rawQuery("PRAGMA KEY='$cipher';");
     169           0 :     assert(result.single['ok'] == 'ok');
     170             :   }
     171             : 
     172             :   /// checks whether a File has a plain text SQLite header
     173           0 :   Future<bool> _isPlainText(File file) async {
     174           0 :     final raf = await file.open();
     175           0 :     final bytes = await raf.read(15);
     176           0 :     await raf.close();
     177             : 
     178             :     const header = [
     179             :       83,
     180             :       81,
     181             :       76,
     182             :       105,
     183             :       116,
     184             :       101,
     185             :       32,
     186             :       102,
     187             :       111,
     188             :       114,
     189             :       109,
     190             :       97,
     191             :       116,
     192             :       32,
     193             :       51,
     194             :     ];
     195             : 
     196           0 :     return _listEquals(bytes, header);
     197             :   }
     198             : 
     199             :   /// Taken from `package:flutter/foundation.dart`;
     200             :   ///
     201             :   /// Compares two lists for element-by-element equality.
     202           0 :   bool _listEquals<T>(List<T>? a, List<T>? b) {
     203             :     if (a == null) {
     204             :       return b == null;
     205             :     }
     206           0 :     if (b == null || a.length != b.length) {
     207             :       return false;
     208             :     }
     209             :     if (identical(a, b)) {
     210             :       return true;
     211             :     }
     212           0 :     for (int index = 0; index < a.length; index += 1) {
     213           0 :       if (a[index] != b[index]) {
     214             :         return false;
     215             :       }
     216             :     }
     217             :     return true;
     218             :   }
     219             : }

Generated by: LCOV version 1.14