Merge pull request #7 from ente-io/encrypt-collection-name

Encrypt collection name
This commit is contained in:
Abhinav-grd 2021-01-25 17:08:01 +00:00 committed by GitHub
commit 4532ae0b7a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 254 additions and 93 deletions

View file

@ -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<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;
'''
];
}
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],

View file

@ -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<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)
);
''',
];
}
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> 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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -667,6 +667,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2+1"
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:

View file

@ -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