diff --git a/lib/db/collections_db.dart b/lib/db/collections_db.dart index 6229a4ae7..e08bd71ec 100644 --- a/lib/db/collections_db.dart +++ b/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); } - 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 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 alterNameToAllowNULL() { + return [ + ...createTable(tempTable), + ''' + INSERT INTO $tempTable + SELECT * + FROM $table; + + DROP TABLE $table; + + ALTER TABLE $tempTable + RENAME TO $table; + ''' + ]; + } + + static List addEncryptedName() { + return [ + ''' + ALTER TABLE $table + ADD COLUMN $columnEncryptedName TEXT; + ''', + '''ALTER TABLE $table + ADD COLUMN $columnNameDecryptionNonce TEXT; + ''' + ]; } Future> insert(List 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> getAllCollections() async { final db = await instance.database; - final rows = await db.query(collectionsTable); + final rows = await db.query(table); final collections = List(); for (final row in rows) { collections.add(_convertToCollection(row)); @@ -83,7 +120,7 @@ class CollectionsDB { Future 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 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], diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 8ae69c2cc..1e61f67b5 100644 --- a/lib/db/files_db.dart +++ b/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) - ); + static List 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) + ); + ''', + ]; + } - 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 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 alterDeviceFolderToAllowNULL() { + return [ + ...createTable(tempTable), + ''' + INSERT INTO $tempTable + SELECT * + FROM $table; + + DROP TABLE $table; + + ALTER TABLE $tempTable + RENAME TO $table; + ''' + ]; } Future 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 { diff --git a/lib/models/collection.dart b/lib/models/collection.dart index 011ab8990..8595d711f 100644 --- a/lib/models/collection.dart +++ b/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 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 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 { diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index a3dc2eaa6..cf0ec2ae5 100644 --- a/lib/services/collections_service.dart +++ b/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> _fetchCollections(int sinceTime) { return _dio .get( @@ -203,12 +207,15 @@ class CollectionsService { Future 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 { diff --git a/lib/services/favorites_service.dart b/lib/services/favorites_service.dart index 32b0d2aed..59312b619 100644 --- a/lib/services/favorites_service.dart +++ b/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, diff --git a/pubspec.lock b/pubspec.lock index abf81ca1d..161452eed 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index f8155a232..9d58295ea 100644 --- a/pubspec.yaml +++ b/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