Bladeren bron

Merge branch 'encrypt-collection-name' into ui_refresh

Vishnu Mohandas 4 jaren geleden
bovenliggende
commit
013c1576be

+ 68 - 27
lib/db/collections_db.dart

@@ -5,24 +5,35 @@ import 'package:path/path.dart';
 import 'package:path_provider/path_provider.dart';
 import 'package:photos/models/collection.dart';
 import 'package:sqflite/sqflite.dart';
+import 'package:sqflite_migration/sqflite_migration.dart';
 
 class CollectionsDB {
   static final _databaseName = "ente.collections.db";
-  static final _databaseVersion = 1;
-
-  static final collectionsTable = 'collections';
+  static final table = 'collections';
+  static final tempTable = 'temp_collections';
 
   static final columnID = 'collection_id';
   static final columnOwner = 'owner';
   static final columnEncryptedKey = 'encrypted_key';
   static final columnKeyDecryptionNonce = 'key_decryption_nonce';
   static final columnName = 'name';
+  static final columnEncryptedName = 'encrypted_name';
+  static final columnNameDecryptionNonce = 'name_decryption_nonce';
   static final columnType = 'type';
   static final columnEncryptedPath = 'encrypted_path';
   static final columnPathDecryptionNonce = 'path_decryption_nonce';
   static final columnSharees = 'sharees';
   static final columnUpdationTime = 'updation_time';
 
+  static final intitialScript = [...createTable(table)];
+  static final migrationScripts = [
+    ...alterNameToAllowNULL(),
+    ...addEncryptedName(),
+  ];
+
+  final dbConfig = MigrationConfig(
+      initializationScript: intitialScript, migrationScripts: migrationScripts);
+
   CollectionsDB._privateConstructor();
   static final CollectionsDB instance = CollectionsDB._privateConstructor();
 
@@ -36,35 +47,61 @@ class CollectionsDB {
   _initDatabase() async {
     Directory documentsDirectory = await getApplicationDocumentsDirectory();
     String path = join(documentsDirectory.path, _databaseName);
-    return await openDatabase(
-      path,
-      version: _databaseVersion,
-      onCreate: _onCreate,
-    );
+    return await openDatabaseWithMigration(path, dbConfig);
+  }
+
+  static List<String> createTable(String tableName) {
+    return [
+      '''
+        CREATE TABLE $tableName (
+          $columnID INTEGER PRIMARY KEY NOT NULL,
+          $columnOwner TEXT NOT NULL,
+          $columnEncryptedKey TEXT NOT NULL,
+          $columnKeyDecryptionNonce TEXT,
+          $columnName TEXT,
+          $columnType TEXT NOT NULL,
+          $columnEncryptedPath TEXT,
+          $columnPathDecryptionNonce TEXT,
+          $columnSharees TEXT,
+          $columnUpdationTime TEXT NOT NULL
+        );
+    '''
+    ];
+  }
+
+  static List<String> alterNameToAllowNULL() {
+    return [
+      ...createTable(tempTable),
+      '''
+        INSERT INTO $tempTable
+        SELECT *
+        FROM $table;
+
+        DROP TABLE $table;
+        
+        ALTER TABLE $tempTable 
+        RENAME TO $table;
+    '''
+    ];
   }
 
-  Future _onCreate(Database db, int version) async {
-    await db.execute('''
-                CREATE TABLE $collectionsTable (
-                  $columnID INTEGER PRIMARY KEY NOT NULL,
-                  $columnOwner TEXT NOT NULL,
-                  $columnEncryptedKey TEXT NOT NULL,
-                  $columnKeyDecryptionNonce TEXT,
-                  $columnName TEXT NOT NULL,
-                  $columnType TEXT NOT NULL,
-                  $columnEncryptedPath TEXT,
-                  $columnPathDecryptionNonce TEXT,
-                  $columnSharees TEXT,
-                  $columnUpdationTime TEXT NOT NULL
-                )
-                ''');
+  static List<String> addEncryptedName() {
+    return [
+      '''
+        ALTER TABLE $table
+        ADD COLUMN $columnEncryptedName TEXT;
+      ''',
+      '''ALTER TABLE $table
+        ADD COLUMN $columnNameDecryptionNonce TEXT;
+      '''
+    ];
   }
 
   Future<List<dynamic>> insert(List<Collection> collections) async {
     final db = await instance.database;
     var batch = db.batch();
     for (final collection in collections) {
-      batch.insert(collectionsTable, _getRowForCollection(collection),
+      batch.insert(table, _getRowForCollection(collection),
           conflictAlgorithm: ConflictAlgorithm.replace);
     }
     return await batch.commit();
@@ -72,7 +109,7 @@ class CollectionsDB {
 
   Future<List<Collection>> getAllCollections() async {
     final db = await instance.database;
-    final rows = await db.query(collectionsTable);
+    final rows = await db.query(table);
     final collections = List<Collection>();
     for (final row in rows) {
       collections.add(_convertToCollection(row));
@@ -83,7 +120,7 @@ class CollectionsDB {
   Future<int> getLastCollectionUpdationTime() async {
     final db = await instance.database;
     final rows = await db.query(
-      collectionsTable,
+      table,
       orderBy: '$columnUpdationTime DESC',
       limit: 1,
     );
@@ -97,7 +134,7 @@ class CollectionsDB {
   Future<int> deleteCollection(int collectionID) async {
     final db = await instance.database;
     return db.delete(
-      collectionsTable,
+      table,
       where: '$columnID = ?',
       whereArgs: [collectionID],
     );
@@ -110,6 +147,8 @@ class CollectionsDB {
     row[columnEncryptedKey] = collection.encryptedKey;
     row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
     row[columnName] = collection.name;
+    row[columnEncryptedName] = collection.encryptedName;
+    row[columnNameDecryptionNonce] = collection.nameDecryptionNonce;
     row[columnType] = Collection.typeToString(collection.type);
     row[columnEncryptedPath] = collection.attributes.encryptedPath;
     row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce;
@@ -126,6 +165,8 @@ class CollectionsDB {
       row[columnEncryptedKey],
       row[columnKeyDecryptionNonce],
       row[columnName],
+      row[columnEncryptedName],
+      row[columnNameDecryptionNonce],
       Collection.typeFromString(row[columnType]),
       CollectionAttributes(
           encryptedPath: row[columnEncryptedPath],

+ 86 - 43
lib/db/files_db.dart

@@ -7,14 +7,15 @@ import 'package:photos/models/file.dart';
 import 'package:path/path.dart';
 import 'package:sqflite/sqflite.dart';
 import 'package:path_provider/path_provider.dart';
+import 'package:sqflite_migration/sqflite_migration.dart';
 
 class FilesDB {
   static final _databaseName = "ente.files.db";
-  static final _databaseVersion = 1;
 
   static final Logger _logger = Logger("FilesDB");
 
   static final table = 'files';
+  static final tempTable = 'temp_files';
 
   static final columnGeneratedID = '_id';
   static final columnUploadedFileID = 'uploaded_file_id';
@@ -37,6 +38,11 @@ class FilesDB {
   static final columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
   static final columnMetadataDecryptionHeader = 'metadata_decryption_header';
 
+  static final intitialScript = [...createTable(table), ...addIndex()];
+  static final migrationScripts = [...alterDeviceFolderToAllowNULL()];
+
+  final dbConfig = MigrationConfig(
+      initializationScript: intitialScript, migrationScripts: migrationScripts);
   // make this a singleton class
   FilesDB._privateConstructor();
   static final FilesDB instance = FilesDB._privateConstructor();
@@ -54,42 +60,65 @@ class FilesDB {
   _initDatabase() async {
     Directory documentsDirectory = await getApplicationDocumentsDirectory();
     String path = join(documentsDirectory.path, _databaseName);
-    return await openDatabase(path,
-        version: _databaseVersion, onCreate: _onCreate);
+    return await openDatabaseWithMigration(path, dbConfig);
   }
 
   // SQL code to create the database table
-  Future _onCreate(Database db, int version) async {
-    await db.execute('''
-          CREATE TABLE $table (
-            $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
-            $columnLocalID TEXT,
-            $columnUploadedFileID INTEGER,
-            $columnOwnerID INTEGER,
-            $columnCollectionID INTEGER,
-            $columnTitle TEXT NOT NULL,
-            $columnDeviceFolder TEXT NOT NULL,
-            $columnLatitude REAL,
-            $columnLongitude REAL,
-            $columnFileType INTEGER,
-            $columnIsEncrypted INTEGER DEFAULT 1,
-            $columnModificationTime TEXT NOT NULL,
-            $columnEncryptedKey TEXT,
-            $columnKeyDecryptionNonce TEXT,
-            $columnFileDecryptionHeader TEXT,
-            $columnThumbnailDecryptionHeader TEXT,
-            $columnMetadataDecryptionHeader TEXT,
-            $columnIsDeleted INTEGER DEFAULT 0,
-            $columnCreationTime TEXT NOT NULL,
-            $columnUpdationTime TEXT,
-            UNIQUE($columnUploadedFileID, $columnCollectionID)
-          );
-
-          CREATE INDEX collection_id_index ON $table($columnCollectionID);
-          CREATE INDEX device_folder_index ON $table($columnDeviceFolder);
-          CREATE INDEX creation_time_index ON $table($columnCreationTime);
-          CREATE INDEX updation_time_index ON $table($columnUpdationTime);
-          ''');
+  static List<String> createTable(String tableName) {
+    return [
+      '''
+        CREATE TABLE $tableName (
+          $columnGeneratedID INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+          $columnLocalID TEXT,
+          $columnUploadedFileID INTEGER,
+          $columnOwnerID INTEGER,
+          $columnCollectionID INTEGER,
+          $columnTitle TEXT NOT NULL,
+          $columnDeviceFolder TEXT,
+          $columnLatitude REAL,
+          $columnLongitude REAL,
+          $columnFileType INTEGER,
+          $columnIsEncrypted INTEGER DEFAULT 1,
+          $columnModificationTime TEXT NOT NULL,
+          $columnEncryptedKey TEXT,
+          $columnKeyDecryptionNonce TEXT,
+          $columnFileDecryptionHeader TEXT,
+          $columnThumbnailDecryptionHeader TEXT,
+          $columnMetadataDecryptionHeader TEXT,
+          $columnIsDeleted INTEGER DEFAULT 0,
+          $columnCreationTime TEXT NOT NULL,
+          $columnUpdationTime TEXT,
+          UNIQUE($columnUploadedFileID, $columnCollectionID)
+        );
+      ''',
+    ];
+  }
+
+  static List<String> addIndex() {
+    return [
+      '''
+        CREATE INDEX collection_id_index ON $table($columnCollectionID);
+        CREATE INDEX device_folder_index ON $table($columnDeviceFolder);
+        CREATE INDEX creation_time_index ON $table($columnCreationTime);
+        CREATE INDEX updation_time_index ON $table($columnUpdationTime);
+      '''
+    ];
+  }
+
+  static List<String> alterDeviceFolderToAllowNULL() {
+    return [
+      ...createTable(tempTable),
+      '''
+        INSERT INTO $tempTable
+        SELECT *
+        FROM $table;
+
+        DROP TABLE $table;
+        
+        ALTER TABLE $tempTable 
+        RENAME TO $table;
+    '''
+    ];
   }
 
   Future<int> insert(File file) async {
@@ -427,16 +456,30 @@ class FilesDB {
     int creationTime,
   ) async {
     final db = await instance.database;
-    final rows = await db.query(
-      table,
-      where: '''$columnTitle=? AND $columnDeviceFolder=? AND 
+    var query;
+    if (deviceFolder != null) {
+      query = db.query(
+        table,
+        where: '''$columnTitle=? AND $columnDeviceFolder=? AND 
           $columnCreationTime=?''',
-      whereArgs: [
-        title,
-        deviceFolder,
-        creationTime,
-      ],
-    );
+        whereArgs: [
+          title,
+          deviceFolder,
+          creationTime,
+        ],
+      );
+    } else {
+      query = db.query(
+        table,
+        where: '''$columnTitle=? AND 
+          $columnCreationTime=?''',
+        whereArgs: [
+          title,
+          creationTime,
+        ],
+      );
+    }
+    final rows = await query;
     if (rows.isNotEmpty) {
       return _convertToFiles(rows);
     } else {

+ 28 - 4
lib/models/collection.dart

@@ -8,6 +8,8 @@ class Collection {
   final String encryptedKey;
   final String keyDecryptionNonce;
   final String name;
+  final String encryptedName;
+  final String nameDecryptionNonce;
   final CollectionType type;
   final CollectionAttributes attributes;
   final List<User> sharees;
@@ -20,6 +22,8 @@ class Collection {
     this.encryptedKey,
     this.keyDecryptionNonce,
     this.name,
+    this.encryptedName,
+    this.nameDecryptionNonce,
     this.type,
     this.attributes,
     this.sharees,
@@ -54,6 +58,8 @@ class Collection {
     String encryptedKey,
     String keyDecryptionNonce,
     String name,
+    String encryptedName,
+    String nameDecryptionNonce,
     CollectionType type,
     CollectionAttributes attributes,
     List<User> sharees,
@@ -66,6 +72,8 @@ class Collection {
       encryptedKey ?? this.encryptedKey,
       keyDecryptionNonce ?? this.keyDecryptionNonce,
       name ?? this.name,
+      encryptedName ?? this.encryptedName,
+      nameDecryptionNonce ?? this.nameDecryptionNonce,
       type ?? this.type,
       attributes ?? this.attributes,
       sharees ?? this.sharees,
@@ -81,6 +89,8 @@ class Collection {
       'encryptedKey': encryptedKey,
       'keyDecryptionNonce': keyDecryptionNonce,
       'name': name,
+      'encryptedName': encryptedName,
+      'nameDecryptionNonce': nameDecryptionNonce,
       'type': typeToString(type),
       'attributes': attributes?.toMap(),
       'sharees': sharees?.map((x) => x?.toMap())?.toList(),
@@ -100,6 +110,8 @@ class Collection {
       map['encryptedKey'],
       map['keyDecryptionNonce'],
       map['name'],
+      map['encryptedName'],
+      map['nameDecryptionNonce'],
       typeFromString(map['type']),
       CollectionAttributes.fromMap(map['attributes']),
       sharees,
@@ -115,7 +127,7 @@ class Collection {
 
   @override
   String toString() {
-    return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, attributes: $attributes, sharees: $sharees, updationTime: $updationTime, isDeleted: $isDeleted)';
+    return 'Collection(id: $id, owner: $owner, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, encryptedName: $encryptedName, nameDecryptionNonce: $nameDecryptionNonce, type: $type, attributes: $attributes, sharees: $sharees, updationTime: $updationTime, isDeleted: $isDeleted)';
   }
 
   @override
@@ -128,6 +140,8 @@ class Collection {
         o.encryptedKey == encryptedKey &&
         o.keyDecryptionNonce == keyDecryptionNonce &&
         o.name == name &&
+        o.encryptedName == encryptedName &&
+        o.nameDecryptionNonce == nameDecryptionNonce &&
         o.type == type &&
         o.attributes == attributes &&
         listEquals(o.sharees, sharees) &&
@@ -142,6 +156,8 @@ class Collection {
         encryptedKey.hashCode ^
         keyDecryptionNonce.hashCode ^
         name.hashCode ^
+        encryptedName.hashCode ^
+        nameDecryptionNonce.hashCode ^
         type.hashCode ^
         attributes.hashCode ^
         sharees.hashCode ^
@@ -159,19 +175,23 @@ enum CollectionType {
 class CollectionAttributes {
   final String encryptedPath;
   final String pathDecryptionNonce;
+  final int version;
 
   CollectionAttributes({
     this.encryptedPath,
     this.pathDecryptionNonce,
+    this.version,
   });
 
   CollectionAttributes copyWith({
     String encryptedPath,
     String pathDecryptionNonce,
+    int version,
   }) {
     return CollectionAttributes(
       encryptedPath: encryptedPath ?? this.encryptedPath,
       pathDecryptionNonce: pathDecryptionNonce ?? this.pathDecryptionNonce,
+      version: version ?? this.version,
     );
   }
 
@@ -183,6 +203,7 @@ class CollectionAttributes {
     if (pathDecryptionNonce != null) {
       map['pathDecryptionNonce'] = pathDecryptionNonce;
     }
+    if (version != null) map['version'] = version;
     return map;
   }
 
@@ -192,6 +213,7 @@ class CollectionAttributes {
     return CollectionAttributes(
       encryptedPath: map['encryptedPath'],
       pathDecryptionNonce: map['pathDecryptionNonce'],
+      version: map['version'] ?? 0,
     );
   }
 
@@ -202,7 +224,7 @@ class CollectionAttributes {
 
   @override
   String toString() =>
-      'CollectionAttributes(encryptedPath: $encryptedPath, pathDecryptionNonce: $pathDecryptionNonce)';
+      'CollectionAttributes(encryptedPath: $encryptedPath, pathDecryptionNonce: $pathDecryptionNonce, version: $version)';
 
   @override
   bool operator ==(Object o) {
@@ -210,11 +232,13 @@ class CollectionAttributes {
 
     return o is CollectionAttributes &&
         o.encryptedPath == encryptedPath &&
-        o.pathDecryptionNonce == pathDecryptionNonce;
+        o.pathDecryptionNonce == pathDecryptionNonce &&
+        o.version == version;
   }
 
   @override
-  int get hashCode => encryptedPath.hashCode ^ pathDecryptionNonce.hashCode;
+  int get hashCode =>
+      encryptedPath.hashCode ^ pathDecryptionNonce.hashCode ^ version.hashCode;
 }
 
 class User {

+ 58 - 18
lib/services/collections_service.dart

@@ -160,20 +160,24 @@ class CollectionsService {
   Uint8List getCollectionKey(int collectionID) {
     if (!_cachedKeys.containsKey(collectionID)) {
       final collection = _collectionIDToCollections[collectionID];
-      final encryptedKey = Sodium.base642bin(collection.encryptedKey);
-      if (collection.owner.id == _config.getUserID()) {
-        _cachedKeys[collectionID] = CryptoUtil.decryptSync(encryptedKey,
-            _config.getKey(), Sodium.base642bin(collection.keyDecryptionNonce));
-      } else {
-        _cachedKeys[collectionID] = CryptoUtil.openSealSync(
-            encryptedKey,
-            Sodium.base642bin(_config.getKeyAttributes().publicKey),
-            _config.getSecretKey());
-      }
+      _cachedKeys[collectionID] = _getDecryptedKey(collection);
     }
     return _cachedKeys[collectionID];
   }
 
+  Uint8List _getDecryptedKey(Collection collection) {
+    final encryptedKey = Sodium.base642bin(collection.encryptedKey);
+    if (collection.owner.id == _config.getUserID()) {
+      return CryptoUtil.decryptSync(encryptedKey, _config.getKey(),
+          Sodium.base642bin(collection.keyDecryptionNonce));
+    } else {
+      return CryptoUtil.openSealSync(
+          encryptedKey,
+          Sodium.base642bin(_config.getKeyAttributes().publicKey),
+          _config.getSecretKey());
+    }
+  }
+
   Future<List<Collection>> _fetchCollections(int sinceTime) {
     return _dio
         .get(
@@ -203,12 +207,15 @@ class CollectionsService {
   Future<Collection> createAlbum(String albumName) async {
     final key = CryptoUtil.generateKey();
     final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
+    final encryptedName = CryptoUtil.encryptSync(utf8.encode(albumName), key);
     final collection = await createAndCacheCollection(Collection(
       null,
       null,
       Sodium.bin2base64(encryptedKeyData.encryptedData),
       Sodium.bin2base64(encryptedKeyData.nonce),
-      albumName,
+      null,
+      Sodium.bin2base64(encryptedName.encryptedData),
+      Sodium.bin2base64(encryptedName.nonce),
       CollectionType.album,
       CollectionAttributes(),
       null,
@@ -223,18 +230,20 @@ class CollectionsService {
     }
     final key = CryptoUtil.generateKey();
     final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
-    final encryptedPath =
-        CryptoUtil.encryptSync(utf8.encode(path), _config.getKey());
+    final encryptedPath = CryptoUtil.encryptSync(utf8.encode(path), key);
     final collection = await createAndCacheCollection(Collection(
       null,
       null,
       Sodium.bin2base64(encryptedKeyData.encryptedData),
       Sodium.bin2base64(encryptedKeyData.nonce),
-      path,
+      null,
+      Sodium.bin2base64(encryptedPath.encryptedData),
+      Sodium.bin2base64(encryptedPath.nonce),
       CollectionType.folder,
       CollectionAttributes(
           encryptedPath: Sodium.bin2base64(encryptedPath.encryptedData),
-          pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce)),
+          pathDecryptionNonce: Sodium.bin2base64(encryptedPath.nonce),
+          version: 1),
       null,
       null,
     ));
@@ -308,18 +317,49 @@ class CollectionsService {
   }
 
   void _cacheCollectionAttributes(Collection collection) {
+    final collectionWithDecryptedName =
+        _getCollectionWithDecryptedName(collection);
     if (collection.attributes.encryptedPath != null) {
-      _localCollections[decryptCollectionPath(collection)] = collection;
+      _localCollections[decryptCollectionPath(collection)] =
+          collectionWithDecryptedName;
     }
-    _collectionIDToCollections[collection.id] = collection;
+    _collectionIDToCollections[collection.id] = collectionWithDecryptedName;
   }
 
   String decryptCollectionPath(Collection collection) {
+    final key = collection.attributes.version == 1
+        ? getCollectionKey(collection.id)
+        : _config.getKey();
     return utf8.decode(CryptoUtil.decryptSync(
         Sodium.base642bin(collection.attributes.encryptedPath),
-        _config.getKey(),
+        key,
         Sodium.base642bin(collection.attributes.pathDecryptionNonce)));
   }
+
+  Collection _getCollectionWithDecryptedName(Collection collection) {
+    var name;
+    if (collection.encryptedName != null &&
+        collection.encryptedName.isNotEmpty) {
+      name = utf8.decode(CryptoUtil.decryptSync(
+          Sodium.base642bin(collection.encryptedName),
+          _getDecryptedKey(collection),
+          Sodium.base642bin(collection.nameDecryptionNonce)));
+      return Collection(
+        collection.id,
+        collection.owner,
+        collection.encryptedKey,
+        collection.keyDecryptionNonce,
+        name,
+        collection.encryptedName,
+        collection.nameDecryptionNonce,
+        collection.type,
+        collection.attributes,
+        collection.sharees,
+        collection.updationTime,
+      );
+    } else
+      return collection;
+  }
 }
 
 class AddFilesRequest {

+ 6 - 1
lib/services/favorites_service.dart

@@ -1,3 +1,5 @@
+import 'dart:convert';
+
 import 'package:flutter_sodium/flutter_sodium.dart';
 import 'package:photos/core/configuration.dart';
 import 'package:photos/core/event_bus.dart';
@@ -74,13 +76,16 @@ class FavoritesService {
     }
     final key = CryptoUtil.generateKey();
     final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
+    final encryptedName = CryptoUtil.encryptSync(utf8.encode("Favorites"), key);
     final collection =
         await _collectionsService.createAndCacheCollection(Collection(
       null,
       null,
       Sodium.bin2base64(encryptedKeyData.encryptedData),
       Sodium.bin2base64(encryptedKeyData.nonce),
-      "Favorites",
+      null,
+      Sodium.bin2base64(encryptedName.encryptedData),
+      Sodium.bin2base64(encryptedName.nonce),
       CollectionType.favorites,
       CollectionAttributes(),
       null,

+ 7 - 0
pubspec.lock

@@ -728,6 +728,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.3"
+  sqflite_migration:
+    dependency: "direct main"
+    description:
+      name: sqflite_migration
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0"
   stack_trace:
     dependency: transitive
     description:

+ 2 - 1
pubspec.yaml

@@ -14,7 +14,7 @@ description: ente photos application
 version: 0.0.20+20
 
 environment:
-  sdk: ">=2.1.0 <3.0.0"
+  sdk: ">=2.2.2 <3.0.0"
 
 dependencies:
   flutter:
@@ -27,6 +27,7 @@ dependencies:
     path: thirdparty/flutter_photo_manager
   provider: ^3.1.0
   sqflite: ^1.3.0
+  sqflite_migration: ^0.2.0
   path_provider: ^1.6.5
   shared_preferences: ^0.5.6
   dio: ^3.0.9