Merge pull request #3 from ente-io/collections

Replace `folders` with `collections`.
This commit is contained in:
Vishnu Mohandas 2020-10-16 00:37:47 +05:30 committed by GitHub
commit 9f60842402
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1367 additions and 734 deletions

View file

@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:io' as io;
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:path_provider/path_provider.dart';
@ -21,11 +22,13 @@ class Configuration {
static const hasOptedForE2EKey = "has_opted_for_e2e_encryption";
static const foldersToBackUpKey = "folders_to_back_up";
static const keyKey = "key";
static const secretKeyKey = "secret_key";
static const keyAttributesKey = "key_attributes";
SharedPreferences _preferences;
FlutterSecureStorage _secureStorage;
String _key;
String _secretKey;
String _documentsDirectory;
String _tempDirectory;
String _thumbnailsDirectory;
@ -39,11 +42,12 @@ class Configuration {
new io.Directory(_tempDirectory).createSync(recursive: true);
new io.Directory(_thumbnailsDirectory).createSync(recursive: true);
_key = await _secureStorage.read(key: keyKey);
_secretKey = await _secureStorage.read(key: secretKeyKey);
}
Future<KeyAttributes> generateAndSaveKey(String passphrase) async {
// Create a master key
final key = CryptoUtil.generateMasterKey();
final key = CryptoUtil.generateKey();
// Derive a key from the passphrase that will be used to encrypt and
// decrypt the master key
@ -51,26 +55,26 @@ class Configuration {
final kek = CryptoUtil.deriveKey(utf8.encode(passphrase), kekSalt);
// Encrypt the key with this derived key
final encryptedKeyData = await CryptoUtil.encrypt(key, key: kek);
final encryptedKeyData = CryptoUtil.encryptSync(key, kek);
// Hash the passphrase so that its correctness can be compared later
final kekHash = await CryptoUtil.hash(kek);
// Generate a public-private keypair and encrypt the latter
final keyPair = await CryptoUtil.generateKeyPair();
final encryptedSecretKeyData =
await CryptoUtil.encrypt(keyPair.sk, key: kek);
final encryptedSecretKeyData = CryptoUtil.encryptSync(keyPair.sk, kek);
final attributes = KeyAttributes(
Sodium.bin2base64(kekSalt),
kekHash,
encryptedKeyData.encryptedData.base64,
encryptedKeyData.nonce.base64,
Sodium.bin2base64(encryptedKeyData.encryptedData),
Sodium.bin2base64(encryptedKeyData.nonce),
Sodium.bin2base64(keyPair.pk),
encryptedSecretKeyData.encryptedData.base64,
encryptedSecretKeyData.nonce.base64,
Sodium.bin2base64(encryptedSecretKeyData.encryptedData),
Sodium.bin2base64(encryptedSecretKeyData.nonce),
);
await setKey(Sodium.bin2base64(key));
await setSecretKey(Sodium.bin2base64(keyPair.sk));
await setKeyAttributes(attributes);
return attributes;
}
@ -84,14 +88,22 @@ class Configuration {
if (!correctPassphrase) {
throw Exception("Incorrect passphrase");
}
final key = await CryptoUtil.decrypt(
final key = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedKey),
kek,
Sodium.base642bin(attributes.keyDecryptionNonce));
final secretKey = CryptoUtil.decryptSync(
Sodium.base642bin(attributes.encryptedSecretKey),
kek,
Sodium.base642bin(attributes.secretKeyDecryptionNonce));
await setKey(Sodium.bin2base64(key));
await setSecretKey(Sodium.bin2base64(secretKey));
}
String getHttpEndpoint() {
if (kDebugMode) {
return "http://192.168.1.3:80";
}
return "https://api.staging.ente.io";
}
@ -157,7 +169,7 @@ class Configuration {
KeyAttributes getKeyAttributes() {
final jsonValue = _preferences.getString(keyAttributesKey);
if (keyAttributesKey == null) {
if (jsonValue == null) {
return null;
} else {
return KeyAttributes.fromJson(jsonValue);
@ -165,12 +177,29 @@ class Configuration {
}
Future<void> setKey(String key) async {
await _secureStorage.write(key: keyKey, value: key);
_key = key;
if (key == null) {
await _secureStorage.delete(key: keyKey);
} else {
await _secureStorage.write(key: keyKey, value: key);
}
}
Future<void> setSecretKey(String secretKey) async {
_secretKey = secretKey;
if (secretKey == null) {
await _secureStorage.delete(key: secretKeyKey);
} else {
await _secureStorage.write(key: secretKeyKey, value: secretKey);
}
}
Uint8List getKey() {
return Sodium.base642bin(_key);
return _key == null ? null : Sodium.base642bin(_key);
}
Uint8List getSecretKey() {
return _secretKey == null ? null : Sodium.base642bin(_secretKey);
}
String getDocumentsDirectory() {

192
lib/db/collections_db.dart Normal file
View file

@ -0,0 +1,192 @@
import 'dart:io';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/shared_collection.dart';
import 'package:sqflite/sqflite.dart';
class CollectionsDB {
static final _databaseName = "ente.collections.db";
static final _databaseVersion = 1;
static final collectionsTable = 'collections';
static final sharedCollectionsTable = 'shared_collections';
static final columnID = 'collection_id';
static final columnOwnerID = 'owner_id';
static final columnEncryptedKey = 'encrypted_key';
static final columnKeyDecryptionNonce = 'key_decryption_nonce';
static final columnName = 'name';
static final columnType = 'type';
static final columnEncryptedPath = 'encrypted_path';
static final columnPathDecryptionNonce = 'path_decryption_nonce';
static final columnCreationTime = 'creation_time';
CollectionsDB._privateConstructor();
static final CollectionsDB instance = CollectionsDB._privateConstructor();
static Database _database;
Future<Database> get database async {
if (_database != null) return _database;
_database = await _initDatabase();
return _database;
}
_initDatabase() async {
Directory documentsDirectory = await getApplicationDocumentsDirectory();
String path = join(documentsDirectory.path, _databaseName);
return await openDatabase(
path,
version: _databaseVersion,
onCreate: _onCreate,
);
}
Future _onCreate(Database db, int version) async {
await db.execute('''
CREATE TABLE $collectionsTable (
$columnID INTEGER PRIMARY KEY NOT NULL,
$columnOwnerID INTEGER NOT NULL,
$columnEncryptedKey TEXT NOT NULL,
$columnKeyDecryptionNonce TEXT NOT NULL,
$columnName TEXT NOT NULL,
$columnType TEXT NOT NULL,
$columnEncryptedPath TEXT,
$columnPathDecryptionNonce TEXT,
$columnCreationTime TEXT NOT NULL
)
''');
await db.execute('''
CREATE TABLE $sharedCollectionsTable (
$columnID INTEGER PRIMARY KEY NOT NULL,
$columnOwnerID INTEGER NOT NULL,
$columnEncryptedKey TEXT NOT NULL,
$columnName TEXT NOT NULL,
$columnType TEXT NOT NULL,
$columnCreationTime TEXT NOT NULL
)
''');
}
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),
conflictAlgorithm: ConflictAlgorithm.replace);
}
return await batch.commit();
}
Future<List<dynamic>> insertSharedCollections(
List<SharedCollection> collections) async {
final db = await instance.database;
var batch = db.batch();
for (final collection in collections) {
batch.insert(
sharedCollectionsTable, _getRowForSharedCollection(collection),
conflictAlgorithm: ConflictAlgorithm.replace);
}
return await batch.commit();
}
Future<List<Collection>> getAllCollections() async {
final db = await instance.database;
final rows = await db.query(collectionsTable);
final collections = List<Collection>();
for (final row in rows) {
collections.add(_convertToCollection(row));
}
return collections;
}
Future<List<SharedCollection>> getAllSharedCollections() async {
final db = await instance.database;
final rows = await db.query(sharedCollectionsTable);
final collections = List<SharedCollection>();
for (final row in rows) {
collections.add(_convertToSharedCollection(row));
}
return collections;
}
Future<int> getLastCollectionCreationTime() async {
final db = await instance.database;
final rows = await db.query(
collectionsTable,
orderBy: '$columnCreationTime DESC',
limit: 1,
);
if (rows.isNotEmpty) {
return int.parse(rows[0][columnCreationTime]);
} else {
return null;
}
}
Future<int> getLastSharedCollectionCreationTime() async {
final db = await instance.database;
final rows = await db.query(
sharedCollectionsTable,
orderBy: '$columnCreationTime DESC',
limit: 1,
);
if (rows.isNotEmpty) {
return int.parse(rows[0][columnCreationTime]);
} else {
return null;
}
}
Map<String, dynamic> _getRowForCollection(Collection collection) {
var row = new Map<String, dynamic>();
row[columnID] = collection.id;
row[columnOwnerID] = collection.ownerID;
row[columnEncryptedKey] = collection.encryptedKey;
row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce;
row[columnName] = collection.name;
row[columnType] = Collection.typeToString(collection.type);
row[columnEncryptedPath] = collection.encryptedPath;
row[columnPathDecryptionNonce] = collection.pathDecryptionNonce;
row[columnCreationTime] = collection.creationTime;
return row;
}
Collection _convertToCollection(Map<String, dynamic> row) {
return Collection(
row[columnID],
row[columnOwnerID],
row[columnEncryptedKey],
row[columnKeyDecryptionNonce],
row[columnName],
Collection.typeFromString(row[columnType]),
row[columnEncryptedPath],
row[columnPathDecryptionNonce],
int.parse(row[columnCreationTime]),
);
}
Map<String, dynamic> _getRowForSharedCollection(SharedCollection collection) {
var row = new Map<String, dynamic>();
row[columnID] = collection.id;
row[columnOwnerID] = collection.ownerID;
row[columnEncryptedKey] = collection.encryptedKey;
row[columnName] = collection.name;
row[columnType] = Collection.typeToString(collection.type);
row[columnCreationTime] = collection.creationTime;
return row;
}
SharedCollection _convertToSharedCollection(Map<String, dynamic> row) {
return SharedCollection(
row[columnID],
row[columnOwnerID],
row[columnEncryptedKey],
row[columnName],
Collection.typeFromString(row[columnType]),
int.parse(row[columnCreationTime]),
);
}
}

View file

@ -1,7 +1,6 @@
import 'dart:io';
import 'package:logging/logging.dart';
import 'package:photos/models/decryption_params.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
import 'package:photos/models/file.dart';
@ -20,6 +19,7 @@ class FilesDB {
static final columnGeneratedID = '_id';
static final columnUploadedFileID = 'uploaded_file_id';
static final columnOwnerID = 'owner_id';
static final columnCollectionID = 'collection_id';
static final columnLocalID = 'local_id';
static final columnTitle = 'title';
static final columnDeviceFolder = 'device_folder';
@ -32,9 +32,11 @@ class FilesDB {
static final columnCreationTime = 'creation_time';
static final columnModificationTime = 'modification_time';
static final columnUpdationTime = 'updation_time';
static final columnFileDecryptionParams = 'file_decryption_params';
static final columnThumbnailDecryptionParams = 'thumbnail_decryption_params';
static final columnMetadataDecryptionParams = 'metadata_decryption_params';
static final columnEncryptedKey = 'encrypted_key';
static final columnKeyDecryptionNonce = 'key_decryption_nonce';
static final columnFileDecryptionHeader = 'file_decryption_header';
static final columnThumbnailDecryptionHeader = 'thumbnail_decryption_header';
static final columnMetadataDecryptionHeader = 'metadata_decryption_header';
// make this a singleton class
FilesDB._privateConstructor();
@ -65,20 +67,23 @@ class FilesDB {
$columnLocalID TEXT,
$columnUploadedFileID INTEGER,
$columnOwnerID INTEGER,
$columnCollectionID INTEGER,
$columnTitle TEXT NOT NULL,
$columnDeviceFolder TEXT NOT NULL,
$columnLatitude REAL,
$columnLongitude REAL,
$columnFileType INTEGER,
$columnRemoteFolderID INTEGER,
$columnIsEncrypted INTEGER DEFAULT 0,
$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,
$columnModificationTime TEXT NOT NULL,
$columnUpdationTime TEXT,
$columnFileDecryptionParams TEXT,
$columnThumbnailDecryptionParams TEXT,
$columnMetadataDecryptionParams TEXT
$columnUpdationTime TEXT
)
''');
}
@ -144,6 +149,20 @@ class FilesDB {
return _convertToFiles(results);
}
Future<List<File>> getAllInCollection(
int collectionID, int beforeCreationTime, int limit) async {
final db = await instance.database;
final results = await db.query(
table,
where:
'$columnCollectionID = ? AND $columnIsDeleted = 0 AND $columnCreationTime < ?',
whereArgs: [collectionID, beforeCreationTime],
orderBy: '$columnCreationTime DESC',
limit: limit,
);
return _convertToFiles(results);
}
Future<List<File>> getFilesCreatedWithinDuration(
int startCreationTime, int endCreationTime) async {
final db = await instance.database;
@ -226,19 +245,26 @@ class FilesDB {
Future<int> update(
int generatedID,
int uploadedID,
int ownerID,
int collectionID,
int updationTime,
DecryptionParams fileDecryptionParams,
DecryptionParams thumbnailDecryptionParams,
DecryptionParams metadataDecryptionParams,
String encryptedKey,
String keyDecryptionNonce,
String fileDecryptionHeader,
String thumbnailDecryptionHeader,
String metadataDecryptionHeader,
) async {
final db = await instance.database;
final values = new Map<String, dynamic>();
values[columnUploadedFileID] = uploadedID;
values[columnOwnerID] = ownerID;
values[columnCollectionID] = collectionID;
values[columnUpdationTime] = updationTime;
values[columnFileDecryptionParams] = fileDecryptionParams.toJson();
values[columnThumbnailDecryptionParams] =
thumbnailDecryptionParams.toJson();
values[columnMetadataDecryptionParams] = metadataDecryptionParams.toJson();
values[columnEncryptedKey] = encryptedKey;
values[columnKeyDecryptionNonce] = keyDecryptionNonce;
values[columnFileDecryptionHeader] = fileDecryptionHeader;
values[columnThumbnailDecryptionHeader] = thumbnailDecryptionHeader;
values[columnMetadataDecryptionHeader] = metadataDecryptionHeader;
return await db.update(
table,
values,
@ -325,6 +351,22 @@ class FilesDB {
}
}
Future<File> getLatestFileInCollection(int collectionID) async {
final db = await instance.database;
final rows = await db.query(
table,
where: '$columnCollectionID =?',
whereArgs: [collectionID],
orderBy: '$columnCreationTime DESC',
limit: 1,
);
if (rows.isNotEmpty) {
return _getFileFromRow(rows[0]);
} else {
throw ("No file found in collection " + collectionID.toString());
}
}
Future<File> getLastSyncedFileInRemoteFolder(int folderID) async {
final db = await instance.database;
final rows = await db.query(
@ -369,6 +411,7 @@ class FilesDB {
row[columnLocalID] = file.localID;
row[columnUploadedFileID] = file.uploadedFileID;
row[columnOwnerID] = file.ownerID;
row[columnCollectionID] = file.collectionID;
row[columnTitle] = file.title;
row[columnDeviceFolder] = file.deviceFolder;
if (file.location != null) {
@ -390,16 +433,11 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnFileDecryptionParams] = file.fileDecryptionParams == null
? null
: file.fileDecryptionParams.toJson();
row[columnThumbnailDecryptionParams] =
file.thumbnailDecryptionParams == null
? null
: file.thumbnailDecryptionParams.toJson();
row[columnMetadataDecryptionParams] = file.metadataDecryptionParams == null
? null
: file.metadataDecryptionParams.toJson();
row[columnEncryptedKey] = file.encryptedKey;
row[columnKeyDecryptionNonce] = file.keyDecryptionNonce;
row[columnFileDecryptionHeader] = file.fileDecryptionHeader;
row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader;
row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader;
return row;
}
@ -408,7 +446,8 @@ class FilesDB {
file.generatedID = row[columnGeneratedID];
file.localID = row[columnLocalID];
file.uploadedFileID = row[columnUploadedFileID];
file.ownerID = row[columnUploadedFileID];
file.ownerID = row[columnOwnerID];
file.collectionID = row[columnCollectionID];
file.title = row[columnTitle];
file.deviceFolder = row[columnDeviceFolder];
if (row[columnLatitude] != null && row[columnLongitude] != null) {
@ -422,12 +461,11 @@ class FilesDB {
file.updationTime = row[columnUpdationTime] == null
? -1
: int.parse(row[columnUpdationTime]);
file.fileDecryptionParams =
DecryptionParams.fromJson(row[columnFileDecryptionParams]);
file.thumbnailDecryptionParams =
DecryptionParams.fromJson(row[columnThumbnailDecryptionParams]);
file.metadataDecryptionParams =
DecryptionParams.fromJson(row[columnMetadataDecryptionParams]);
file.encryptedKey = row[columnEncryptedKey];
file.keyDecryptionNonce = row[columnKeyDecryptionNonce];
file.fileDecryptionHeader = row[columnFileDecryptionHeader];
file.thumbnailDecryptionHeader = row[columnThumbnailDecryptionHeader];
file.metadataDecryptionHeader = row[columnMetadataDecryptionHeader];
return file;
}
}

View file

@ -6,7 +6,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/services/favorites_service.dart';
import 'package:photos/services/folder_service.dart';
import 'package:photos/services/memories_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/ui/home_widget.dart';
@ -51,11 +50,8 @@ void _main() async {
}
void _sync() async {
FolderSharingService.instance.sync().catchError((e) {
_logger.warning(e);
});
SyncService.instance.sync().catchError((e) {
_logger.warning(e);
SyncService.instance.sync().catchError((e, s) {
_logger.severe("Sync error", e, s);
});
}
@ -85,6 +81,9 @@ class MyApp extends StatelessWidget with WidgetsBindingObserver {
hintColor: Colors.grey,
accentColor: Colors.pink[400],
buttonColor: Colors.pink,
buttonTheme: ButtonThemeData().copyWith(
buttonColor: Colors.pink,
),
toggleableActiveColor: Colors.pink[400],
),
home: HomeWidget(_title),

146
lib/models/collection.dart Normal file
View file

@ -0,0 +1,146 @@
import 'dart:convert';
class Collection {
final int id;
final int ownerID;
final String encryptedKey;
final String keyDecryptionNonce;
final String name;
final CollectionType type;
final String encryptedPath;
final String pathDecryptionNonce;
final int creationTime;
Collection(
this.id,
this.ownerID,
this.encryptedKey,
this.keyDecryptionNonce,
this.name,
this.type,
this.encryptedPath,
this.pathDecryptionNonce,
this.creationTime,
);
Collection copyWith({
int id,
int ownerID,
String encryptedKey,
String keyDecryptionNonce,
String name,
CollectionType type,
String encryptedPath,
String pathDecryptionNonce,
int creationTime,
List<String> sharees,
}) {
return Collection(
id ?? this.id,
ownerID ?? this.ownerID,
encryptedKey ?? this.encryptedKey,
keyDecryptionNonce ?? this.keyDecryptionNonce,
name ?? this.name,
type ?? this.type,
encryptedPath ?? this.encryptedPath,
encryptedPath ?? this.pathDecryptionNonce,
creationTime ?? this.creationTime,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'ownerID': ownerID,
'encryptedKey': encryptedKey,
'keyDecryptionNonce': keyDecryptionNonce,
'name': name,
'type': typeToString(type),
'creationTime': creationTime,
'encryptedPath': encryptedPath,
'pathDecryptionNonce': pathDecryptionNonce,
};
}
factory Collection.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return Collection(
map['id'],
map['ownerID'],
map['encryptedKey'],
map['keyDecryptionNonce'],
map['name'],
typeFromString(map['type']),
map['encryptedPath'],
map['pathDecryptionNonce'],
map['creationTime'],
);
}
String toJson() => json.encode(toMap());
factory Collection.fromJson(String source) =>
Collection.fromMap(json.decode(source));
@override
String toString() {
return 'Collection(id: $id, ownerID: $ownerID, encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, name: $name, type: $type, encryptedPath: $encryptedPath, pathDecryptionNonce: $pathDecryptionNonce, creationTime: $creationTime)';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is Collection &&
o.id == id &&
o.ownerID == ownerID &&
o.encryptedKey == encryptedKey &&
o.keyDecryptionNonce == keyDecryptionNonce &&
o.name == name &&
o.type == type &&
o.encryptedPath == encryptedPath &&
o.pathDecryptionNonce == pathDecryptionNonce &&
o.creationTime == creationTime;
}
@override
int get hashCode {
return id.hashCode ^
ownerID.hashCode ^
encryptedKey.hashCode ^
keyDecryptionNonce.hashCode ^
name.hashCode ^
type.hashCode ^
encryptedPath.hashCode ^
pathDecryptionNonce.hashCode ^
creationTime.hashCode;
}
static CollectionType typeFromString(String type) {
switch (type) {
case "folder":
return CollectionType.folder;
case "favorites":
return CollectionType.favorites;
}
return CollectionType.album;
}
static String typeToString(CollectionType type) {
switch (type) {
case CollectionType.folder:
return "folder";
case CollectionType.favorites:
return "favorites";
default:
return "album";
}
}
}
enum CollectionType {
folder,
favorites,
album,
}

View file

@ -1,82 +0,0 @@
import 'dart:convert';
class DecryptionParams {
final String encryptedKey;
final String keyDecryptionNonce;
String header;
String nonce;
DecryptionParams({
this.encryptedKey,
this.keyDecryptionNonce,
this.header,
this.nonce,
});
DecryptionParams copyWith({
String encryptedKey,
String keyDecryptionNonce,
String header,
String nonce,
}) {
return DecryptionParams(
encryptedKey: encryptedKey ?? this.encryptedKey,
keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
header: header ?? this.header,
nonce: nonce ?? this.nonce,
);
}
Map<String, dynamic> toMap() {
return {
'encryptedKey': encryptedKey,
'keyDecryptionNonce': keyDecryptionNonce,
'header': header,
'nonce': nonce,
};
}
factory DecryptionParams.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return DecryptionParams(
encryptedKey: map['encryptedKey'],
keyDecryptionNonce: map['keyDecryptionNonce'],
header: map['header'],
nonce: map['nonce'],
);
}
String toJson() => json.encode(toMap());
factory DecryptionParams.fromJson(String source) {
if (source == null) {
return null;
}
return DecryptionParams.fromMap(json.decode(source));
}
@override
String toString() {
return 'DecryptionParams(encryptedKey: $encryptedKey, keyDecryptionNonce: $keyDecryptionNonce, header: $header, nonce: $nonce)';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is DecryptionParams &&
o.encryptedKey == encryptedKey &&
o.keyDecryptionNonce == keyDecryptionNonce &&
o.header == header &&
o.nonce == nonce;
}
@override
int get hashCode {
return encryptedKey.hashCode ^
keyDecryptionNonce.hashCode ^
header.hashCode ^
nonce.hashCode;
}
}

View file

@ -1,9 +0,0 @@
import 'package:photos/models/encryption_attribute.dart';
class EncryptedData {
final EncryptionAttribute key;
final EncryptionAttribute nonce;
final EncryptionAttribute encryptedData;
EncryptedData(this.key, this.nonce, this.encryptedData);
}

View file

@ -1,8 +0,0 @@
import 'package:photos/models/encryption_attribute.dart';
class ChaChaAttributes {
final EncryptionAttribute key;
final EncryptionAttribute header;
ChaChaAttributes(this.key, this.header);
}

View file

@ -1,16 +0,0 @@
import 'dart:typed_data';
import 'package:flutter_sodium/flutter_sodium.dart';
class EncryptionAttribute {
String base64;
Uint8List bytes;
EncryptionAttribute({this.base64, this.bytes}) {
if (base64 != null) {
this.bytes = Sodium.base642bin(base64);
} else {
this.base64 = Sodium.bin2base64(bytes);
}
}
}

View file

@ -0,0 +1,10 @@
import 'dart:typed_data';
class EncryptionResult {
final Uint8List encryptedData;
final Uint8List key;
final Uint8List header;
final Uint8List nonce;
EncryptionResult({this.encryptedData, this.key, this.header, this.nonce});
}

View file

@ -1,7 +1,6 @@
import 'package:photo_manager/photo_manager.dart';
import 'package:path/path.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/decryption_params.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/models/location.dart';
@ -9,6 +8,7 @@ class File {
int generatedID;
int uploadedFileID;
int ownerID;
int collectionID;
String localID;
String title;
String deviceFolder;
@ -19,15 +19,18 @@ class File {
int updationTime;
Location location;
FileType fileType;
DecryptionParams fileDecryptionParams;
DecryptionParams thumbnailDecryptionParams;
DecryptionParams metadataDecryptionParams;
String encryptedKey;
String keyDecryptionNonce;
String fileDecryptionHeader;
String thumbnailDecryptionHeader;
String metadataDecryptionHeader;
File();
File.fromJson(Map<String, dynamic> json) {
uploadedFileID = json["id"];
ownerID = json["ownerID"];
collectionID = json["collectionID"];
localID = json["deviceFileID"];
deviceFolder = json["deviceFolder"];
title = json["title"];
@ -103,11 +106,8 @@ class File {
}
String getDownloadUrl() {
final api = isEncrypted ? "encrypted-files" : "files";
return Configuration.instance.getHttpEndpoint() +
"/" +
api +
"/download/" +
"/files/download/" +
uploadedFileID.toString() +
"?token=" +
Configuration.instance.getToken();
@ -124,11 +124,8 @@ class File {
}
String getThumbnailUrl() {
final api = isEncrypted ? "encrypted-files" : "files";
return Configuration.instance.getHttpEndpoint() +
"/" +
api +
"/preview/" +
"/files/preview/" +
uploadedFileID.toString() +
"?token=" +
Configuration.instance.getToken();
@ -137,10 +134,8 @@ class File {
@override
String toString() {
return '''File(generatedId: $generatedID, uploadedFileId: $uploadedFileID,
localId: $localID, title: $title, deviceFolder: $deviceFolder,
fileDecryptionParams: $fileDecryptionParams,
thumbnailDecryptionParams: $thumbnailDecryptionParams,
metadataDecryptionParams: $metadataDecryptionParams,
ownerID: $ownerID, collectionID: $collectionID,
localId: $localID, title: $title, deviceFolder: $deviceFolder,
location: $location, fileType: $fileType, creationTime: $creationTime,
modificationTime: $modificationTime, updationTime: $updationTime)''';
}

View file

@ -0,0 +1,96 @@
import 'dart:convert';
import 'package:photos/models/collection.dart';
class SharedCollection {
final int id;
final int ownerID;
final String encryptedKey;
final String name;
final CollectionType type;
final int creationTime;
SharedCollection(
this.id,
this.ownerID,
this.encryptedKey,
this.name,
this.type,
this.creationTime,
);
SharedCollection copyWith({
int id,
int ownerID,
String encryptedKey,
String name,
CollectionType type,
int creationTime,
}) {
return SharedCollection(
id ?? this.id,
ownerID ?? this.ownerID,
encryptedKey ?? this.encryptedKey,
name ?? this.name,
type ?? this.type,
creationTime ?? this.creationTime,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'ownerID': ownerID,
'encryptedKey': encryptedKey,
'name': name,
'type': Collection.typeToString(type),
'creationTime': creationTime,
};
}
factory SharedCollection.fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return SharedCollection(
map['id'],
map['ownerID'],
map['encryptedKey'],
map['name'],
Collection.typeFromString(map['type']),
map['creationTime'],
);
}
String toJson() => json.encode(toMap());
factory SharedCollection.fromJson(String source) =>
SharedCollection.fromMap(json.decode(source));
@override
String toString() {
return 'SharedCollection(id: $id, ownerID: $ownerID, encryptedKey: $encryptedKey, name: $name, type: $type, creationTime: $creationTime)';
}
@override
bool operator ==(Object o) {
if (identical(this, o)) return true;
return o is SharedCollection &&
o.id == id &&
o.ownerID == ownerID &&
o.encryptedKey == encryptedKey &&
o.name == name &&
o.type == type &&
o.creationTime == creationTime;
}
@override
int get hashCode {
return id.hashCode ^
ownerID.hashCode ^
encryptedKey.hashCode ^
name.hashCode ^
type.hashCode ^
creationTime.hashCode;
}
}

View file

@ -0,0 +1,204 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/collections_db.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/shared_collection.dart';
import 'package:photos/utils/crypto_util.dart';
class CollectionsService {
final _logger = Logger("CollectionsService");
CollectionsDB _db;
Configuration _config;
final _localCollections = Map<String, Collection>();
final _collectionIDToOwnedCollections = Map<int, Collection>();
final _collectionIDToSharedCollections = Map<int, SharedCollection>();
final _cachedKeys = Map<int, Uint8List>();
CollectionsService._privateConstructor() {
_db = CollectionsDB.instance;
_config = Configuration.instance;
}
static final CollectionsService instance =
CollectionsService._privateConstructor();
Future<void> sync() async {
final lastCollectionCreationTime =
await _db.getLastCollectionCreationTime();
var collections =
await getOwnedCollections(lastCollectionCreationTime ?? 0);
await _db.insert(collections);
collections = await _db.getAllCollections();
for (final collection in collections) {
_cacheCollectionAttributes(collection);
}
final lastSharedCollectionCreationTime =
await _db.getLastCollectionCreationTime();
var sharedCollections =
await getSharedCollections(lastSharedCollectionCreationTime ?? 0);
await _db.insertSharedCollections(sharedCollections);
sharedCollections = await _db.getAllSharedCollections();
for (final collection in sharedCollections) {
_collectionIDToSharedCollections[collection.id] = collection;
}
}
Collection getCollectionForPath(String path) {
return _localCollections[path];
}
Future<List<String>> getSharees(int collectionID) {
return Dio()
.get(
Configuration.instance.getHttpEndpoint() + "/collections/sharees",
queryParameters: {
"collectionID": collectionID,
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
_logger.info(response.toString());
final emails = List<String>();
for (final email in response.data["emails"]) {
emails.add(email);
}
return emails;
});
}
Future<void> share(int collectionID, String email, String publicKey) {
final encryptedKey = CryptoUtil.sealSync(
getCollectionKey(collectionID), Sodium.base642bin(publicKey));
return Dio().post(
Configuration.instance.getHttpEndpoint() + "/collections/share",
data: {
"collectionID": collectionID,
"email": email,
"encryptedKey": Sodium.bin2base64(encryptedKey),
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
);
}
Uint8List getCollectionKey(int collectionID) {
if (!_cachedKeys.containsKey(collectionID)) {
var key;
if (_collectionIDToOwnedCollections.containsKey(collectionID)) {
final collection = _collectionIDToOwnedCollections[collectionID];
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
key = CryptoUtil.decryptSync(encryptedKey, _config.getKey(),
Sodium.base642bin(collection.keyDecryptionNonce));
} else {
final collection = _collectionIDToSharedCollections[collectionID];
final encryptedKey = Sodium.base642bin(collection.encryptedKey);
key = CryptoUtil.openSealSync(
encryptedKey,
Sodium.base642bin(_config.getKeyAttributes().publicKey),
_config.getSecretKey());
}
_cachedKeys[collectionID] = key;
}
return _cachedKeys[collectionID];
}
Future<List<Collection>> getOwnedCollections(int sinceTime) {
return Dio()
.get(
Configuration.instance.getHttpEndpoint() + "/collections/owned",
queryParameters: {
"sinceTime": sinceTime,
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
final collections = List<Collection>();
if (response != null) {
final c = response.data["collections"];
for (final collection in c) {
collections.add(Collection.fromMap(collection));
}
}
return collections;
});
}
Future<List<SharedCollection>> getSharedCollections(int sinceTime) {
return Dio()
.get(
Configuration.instance.getHttpEndpoint() + "/collections/shared",
queryParameters: {
"sinceTime": sinceTime,
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
final collections = List<SharedCollection>();
if (response != null) {
final c = response.data["collections"];
for (final collection in c) {
collections.add(SharedCollection.fromMap(collection));
}
}
return collections;
});
}
Future<Collection> getOrCreateForPath(String path) async {
if (_localCollections.containsKey(path)) {
return _localCollections[path];
}
final key = CryptoUtil.generateKey();
final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey());
final encryptedPath =
CryptoUtil.encryptSync(utf8.encode(path), _config.getKey());
final collection = await createCollection(Collection(
null,
null,
Sodium.bin2base64(encryptedKeyData.encryptedData),
Sodium.bin2base64(encryptedKeyData.nonce),
path,
CollectionType.folder,
Sodium.bin2base64(encryptedPath.encryptedData),
Sodium.bin2base64(encryptedPath.nonce),
null,
));
_cacheCollectionAttributes(collection);
return collection;
}
Future<Collection> createCollection(Collection collection) async {
return Dio()
.post(
Configuration.instance.getHttpEndpoint() + "/collections/",
data: collection.toMap(),
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
return Collection.fromMap(response.data["collection"]);
});
}
void _cacheCollectionAttributes(Collection collection) {
if (collection.ownerID == _config.getUserID()) {
var path = utf8.decode(CryptoUtil.decryptSync(
Sodium.base642bin(collection.encryptedPath),
_config.getKey(),
Sodium.base642bin(collection.pathDecryptionNonce)));
_localCollections[path] = collection;
}
_collectionIDToOwnedCollections[collection.id] = collection;
getCollectionKey(collection.id);
}
}

View file

@ -1,185 +0,0 @@
import 'dart:developer';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/folders_db.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/remote_sync_event.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/models/folder.dart';
import 'package:photos/models/file.dart';
import '../core/event_bus.dart';
class FolderSharingService {
final _logger = Logger("FolderSharingService");
final _dio = Dio();
static final _diffLimit = 100;
bool _isSyncInProgress = false;
FolderSharingService._privateConstructor() {
Bus.instance.on<UserAuthenticatedEvent>().listen((event) {
sync();
});
}
static final FolderSharingService instance =
FolderSharingService._privateConstructor();
Future<void> sync() {
_logger.info("Syncing...");
if (_isSyncInProgress || !Configuration.instance.hasConfiguredAccount()) {
return Future.value();
}
_isSyncInProgress = true;
return getFolders().then((f) async {
var folders = f.toSet();
var currentFolders = await FoldersDB.instance.getFolders();
for (final currentFolder in currentFolders) {
if (!folders.contains(currentFolder)) {
_logger.info("Folder deleted: " + currentFolder.toString());
await FilesDB.instance.deleteFilesInRemoteFolder(currentFolder.id);
await FoldersDB.instance.deleteFolder(currentFolder);
}
}
for (final folder in folders) {
if (folder.ownerID != Configuration.instance.getUserID()) {
await syncDiff(folder);
await FoldersDB.instance.putFolder(folder);
}
}
Bus.instance.fire(RemoteSyncEvent(true));
_isSyncInProgress = false;
return Future.value();
});
}
Future<void> syncDiff(Folder folder) async {
int lastSyncTimestamp = 0;
try {
File file =
await FilesDB.instance.getLastSyncedFileInRemoteFolder(folder.id);
lastSyncTimestamp = file.updationTime;
} catch (e) {
// Folder has never been synced
}
var diff = await getDiff(folder.id, lastSyncTimestamp, _diffLimit);
for (File file in diff) {
try {
var existingPhoto =
await FilesDB.instance.getMatchingRemoteFile(file.uploadedFileID);
await FilesDB.instance.update(
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
);
} catch (e) {
await FilesDB.instance.insert(file);
}
}
if (diff.length == _diffLimit) {
await syncDiff(folder);
}
}
Future<List<File>> getDiff(int folderId, int sinceTime, int limit) async {
Response response = await _dio.get(
Configuration.instance.getHttpEndpoint() +
"/folders/diff/" +
folderId.toString(),
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
queryParameters: {
"sinceTime": sinceTime,
"limit": limit,
},
).catchError((e) => _logger.severe(e));
if (response != null) {
return (response.data["diff"] as List).map((p) {
File file = new File.fromJson(p);
file.localID = null;
file.remoteFolderID = folderId;
return file;
}).toList();
} else {
return List<File>();
}
}
Future<List<Folder>> getFolders() async {
return _dio
.get(
Configuration.instance.getHttpEndpoint() + "/folders/",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((foldersResponse) {
return (foldersResponse.data as List)
.map((f) => Folder.fromMap(f))
.toList();
});
}
Future<Folder> getFolder(String deviceFolder) async {
return _dio
.get(
Configuration.instance.getHttpEndpoint() + "/folders/folder/",
queryParameters: {
"deviceFolder": deviceFolder,
},
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
return Folder.fromMap(response.data);
}).catchError((e) {
try {
return Folder(
null,
Configuration.instance.getEmail() + "s " + deviceFolder,
Configuration.instance.getUserID(),
deviceFolder,
Set<int>(),
null,
);
} catch (e) {
_logger.severe(e);
return null;
}
});
}
Future<Map<int, bool>> getSharingStatus(Folder folder) async {
return _dio
.get(
Configuration.instance.getHttpEndpoint() + "/users",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
.then((response) {
final users = (response.data["users"] as List).toList();
final result = Map<int, bool>();
for (final user in users) {
if (user["id"] != Configuration.instance.getUserID()) {
result[user["id"]] = folder.sharedWith.contains(user["id"]);
}
}
return result;
});
}
Future<void> updateFolder(Folder folder) {
return _dio
.put(Configuration.instance.getHttpEndpoint() + "/folders/",
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
data: folder.toMap())
.then((response) => log(response.toString()))
.catchError((error) => log(error.toString()));
}
}

View file

@ -6,6 +6,7 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/photo_upload_event.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/utils/file_downloader.dart';
import 'package:photos/repositories/file_repository.dart';
import 'package:photo_manager/photo_manager.dart';
@ -29,7 +30,6 @@ class SyncService {
Future<void> _existingSync;
SharedPreferences _prefs;
static final _syncTimeKey = "sync_time";
static final _encryptedFilesSyncTimeKey = "encrypted_files_sync_time";
static final _dbUpdationTimeKey = "db_updation_time";
static final _diffLimit = 100;
@ -154,36 +154,15 @@ class SyncService {
}
Future<void> _syncWithRemote() async {
// TODO: Fix race conditions triggered due to concurrent syncs.
// Add device_id/last_sync_timestamp to the upload request?
if (!Configuration.instance.hasConfiguredAccount()) {
return Future.error("Account not configured yet");
}
await _persistFilesDiff();
await CollectionsService.instance.sync();
await _persistEncryptedFilesDiff();
await _uploadDiff();
await _deletePhotosOnServer();
}
Future<void> _persistFilesDiff() async {
final diff = await _downloader.getFilesDiff(_getSyncTime(), _diffLimit);
if (diff != null && diff.isNotEmpty) {
await _storeDiff(diff, _syncTimeKey);
FileRepository.instance.reloadFiles();
if (diff.length == _diffLimit) {
return await _persistFilesDiff();
}
}
}
int _getSyncTime() {
var syncTime = _prefs.getInt(_syncTimeKey);
if (syncTime == null) {
syncTime = 0;
}
return syncTime;
}
Future<void> _persistEncryptedFilesDiff() async {
final diff = await _downloader.getEncryptedFilesDiff(
_getEncryptedFilesSyncTime(), _diffLimit);
@ -228,10 +207,14 @@ class SyncService {
await _db.update(
file.generatedID,
uploadedFile.uploadedFileID,
uploadedFile.ownerID,
uploadedFile.collectionID,
uploadedFile.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
file.encryptedKey,
file.keyDecryptionNonce,
file.fileDecryptionHeader,
file.thumbnailDecryptionHeader,
file.metadataDecryptionHeader,
);
Bus.instance.fire(PhotoUploadEvent(
completed: i + 1, total: filesToBeUploaded.length));
@ -245,20 +228,20 @@ class SyncService {
Future _storeDiff(List<File> diff, String prefKey) async {
for (File file in diff) {
try {
final existingPhoto = await _db.getMatchingFile(
file.localID,
file.title,
file.deviceFolder,
file.creationTime,
file.modificationTime,
final existingFile = await _db.getMatchingFile(file.localID, file.title,
file.deviceFolder, file.creationTime, file.modificationTime,
alternateTitle: getHEICFileNameForJPG(file));
await _db.update(
existingPhoto.generatedID,
existingFile.generatedID,
file.uploadedFileID,
file.ownerID,
file.collectionID,
file.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
file.encryptedKey,
file.keyDecryptionNonce,
file.fileDecryptionHeader,
file.thumbnailDecryptionHeader,
file.metadataDecryptionHeader,
);
} catch (e) {
file.localID = null; // File uploaded from a different device

View file

@ -47,6 +47,30 @@ class UserService {
});
}
Future<String> getPublicKey({String email, int userID}) async {
final queryParams = Map<String, dynamic>();
if (userID != null) {
queryParams["userID"] = userID;
} else {
queryParams["email"] = email;
}
try {
final response = await _dio.get(
Configuration.instance.getHttpEndpoint() + "/users/public-key",
queryParameters: queryParams,
options: Options(
headers: {
"X-Auth-Token": Configuration.instance.getToken(),
},
),
);
return response.data["publicKey"];
} on DioError catch (e) {
_logger.info(e);
return null;
}
}
Future<void> getCredentials(BuildContext context, String ott) async {
final dialog = createProgressDialog(context, "Please wait...");
await dialog.show();

View file

@ -1,3 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
final nothingToSeeHere = Center(child: Text("Nothing to see here! 👀"));
RaisedButton button(String text, {VoidCallback onPressed}) {
return RaisedButton(
child: Text(text),
onPressed: onPressed,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6.0),
),
);
}

View file

@ -6,6 +6,7 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/events/user_authenticated_event.dart';
import 'package:photos/repositories/file_repository.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/ui/email_entry_page.dart';
import 'package:photos/ui/passphrase_entry_page.dart';
import 'package:photos/ui/passphrase_reentry_page.dart';
@ -18,7 +19,7 @@ import 'package:photos/utils/share_util.dart';
enum GalleryAppBarType {
homepage,
local_folder,
remote_folder,
shared_collection,
search_results,
}
@ -87,18 +88,27 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
List<Widget> _getDefaultActions(BuildContext context) {
List<Widget> actions = List<Widget>();
if (Configuration.instance.hasConfiguredAccount()) {
actions.add(IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SettingsPage();
},
),
);
},
));
if (widget.type == GalleryAppBarType.homepage) {
actions.add(IconButton(
icon: Icon(Icons.settings),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return SettingsPage();
},
),
);
},
));
} else if (widget.type == GalleryAppBarType.local_folder) {
actions.add(IconButton(
icon: Icon(Icons.share),
onPressed: () {
_showShareCollectionDialog();
},
));
}
} else {
actions.add(IconButton(
icon: Icon(Icons.sync_disabled),
@ -133,7 +143,12 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
return showDialog<void>(
context: context,
builder: (BuildContext context) {
return ShareFolderWidget(widget.title, widget.path);
return ShareFolderWidget(
widget.title,
widget.path,
collection:
CollectionsService.instance.getCollectionForPath(widget.path),
);
},
);
}
@ -141,7 +156,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
List<Widget> _getActions(BuildContext context) {
List<Widget> actions = List<Widget>();
if (widget.selectedFiles.files.isNotEmpty) {
if (widget.type != GalleryAppBarType.remote_folder &&
if (widget.type != GalleryAppBarType.shared_collection &&
widget.type != GalleryAppBarType.search_results) {
actions.add(IconButton(
icon: Icon(Icons.delete),

View file

@ -21,6 +21,7 @@ import 'package:photos/ui/memories_widget.dart';
import 'package:photos/ui/remote_folder_gallery_widget.dart';
import 'package:photos/ui/search_page.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/shared_collections_gallery.dart';
import 'package:photos/utils/logging_util.dart';
import 'package:shake/shake.dart';
import 'package:logging/logging.dart';
@ -38,7 +39,7 @@ class HomeWidget extends StatefulWidget {
class _HomeWidgetState extends State<HomeWidget> {
static final importantItemsFilter = ImportantItemsFilter();
final _logger = Logger("HomeWidgetState");
final _remoteFolderGalleryWidget = RemoteFolderGalleryWidget();
final _sharedCollectionGallery = SharedCollectionGallery();
final _deviceFolderGalleryWidget = DeviceFolderGalleryWidget();
final _selectedFiles = SelectedFiles();
final _memoriesWidget = MemoriesWidget();
@ -80,7 +81,7 @@ class _HomeWidgetState extends State<HomeWidget> {
? _getMainGalleryWidget()
: LoadingPhotosWidget(),
_deviceFolderGalleryWidget,
_remoteFolderGalleryWidget,
_sharedCollectionGallery,
],
index: _selectedNavBarItem,
),

View file

@ -91,7 +91,6 @@ class _OTTVerificationPageState extends State<OTTVerificationPage> {
child: Text(
"Verify",
),
color: Colors.pink,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
),

View file

@ -1,6 +1,5 @@
import 'package:flutter/material.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/services/folder_service.dart';
import 'package:photos/models/folder.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/ui/gallery.dart';
@ -27,13 +26,13 @@ class _RemoteFolderPageState extends State<RemoteFolderPage> {
? DateTime.now().microsecondsSinceEpoch
: lastFile.creationTime,
limit),
onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
// onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
tagPrefix: "remote_folder",
selectedFiles: _selectedFiles,
);
return Scaffold(
appBar: GalleryAppBarWidget(
GalleryAppBarType.remote_folder,
GalleryAppBarType.shared_collection,
widget.folder.name,
_selectedFiles,
widget.folder.deviceFolder,

View file

@ -1,19 +1,27 @@
import 'dart:developer';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/services/folder_service.dart';
import 'package:photos/models/folder.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/user_service.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/email_util.dart';
import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/toast_util.dart';
class ShareFolderWidget extends StatefulWidget {
final String title;
final String path;
final Collection collection;
const ShareFolderWidget(
this.title,
this.path, {
this.collection,
Key key,
}) : super(key: key);
@ -22,19 +30,15 @@ class ShareFolderWidget extends StatefulWidget {
}
class _ShareFolderWidgetState extends State<ShareFolderWidget> {
Folder _folder;
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<int, bool>>(
future:
FolderSharingService.instance.getFolder(widget.path).then((folder) {
_folder = folder;
return FolderSharingService.instance.getSharingStatus(folder);
}),
return FutureBuilder<List<String>>(
future: widget.collection == null
? Future.value(List<String>())
: CollectionsService.instance.getSharees(widget.collection.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
return _getSharingDialog(snapshot.data);
return SharingDialog(widget.collection, snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else {
@ -43,102 +47,146 @@ class _ShareFolderWidgetState extends State<ShareFolderWidget> {
},
);
}
Widget _getSharingDialog(Map<int, bool> sharingStatus) {
return AlertDialog(
title: Text('Sharing'),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
// SharingCheckboxWidget(sharingStatus),
SharingWidget(["vishnu@ente.io", "shanthy@ente.io"]),
],
),
),
// actions: <Widget>[
// FlatButton(
// child: Text("Save"),
// onPressed: () async {
// var sharedWith = Set<int>();
// for (var user in sharingStatus.keys) {
// if (sharingStatus[user]) {
// sharedWith.add(user);
// }
// }
// _folder.sharedWith.clear();
// _folder.sharedWith.addAll(sharedWith);
// await FolderSharingService.instance.updateFolder(_folder);
// showToast("Sharing configuration updated successfully.");
// Navigator.of(context).pop();
// },
// ),
// ],
);
}
}
class SharingWidget extends StatefulWidget {
final List<String> emails;
SharingWidget(this.emails, {Key key}) : super(key: key);
class SharingDialog extends StatefulWidget {
final Collection collection;
final List<String> sharees;
SharingDialog(this.collection, this.sharees, {Key key}) : super(key: key);
@override
_SharingWidgetState createState() => _SharingWidgetState();
_SharingDialogState createState() => _SharingDialogState();
}
class _SharingWidgetState extends State<SharingWidget> {
class _SharingDialogState extends State<SharingDialog> {
bool _showEntryField = false;
List<String> _emails;
@override
void initState() {
_emails = widget.emails;
super.initState();
}
List<String> _sharees;
String _email;
@override
Widget build(BuildContext context) {
_sharees = widget.sharees;
final children = List<Widget>();
for (final email in _emails) {
children.add(EmailItemWidget(email));
if (!_showEntryField &&
(widget.collection == null || _sharees.length == 0)) {
children.add(Text("Click the + button to share this folder."));
} else {
for (final email in _sharees) {
children.add(EmailItemWidget(email));
}
}
if (_showEntryField) {
children.add(TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
border: InputBorder.none,
hintText: "email@your-friend.com",
),
autofocus: true,
onSubmitted: (s) {
final progressDialog = createProgressDialog(context, "Sharing...");
progressDialog.show();
Future.delayed(Duration(milliseconds: 1000), () {
progressDialog.hide();
showToast("Shared with " + s + ".");
setState(() {
_emails.add(s);
_showEntryField = false;
});
onChanged: (s) {
setState(() {
_email = s;
});
},
onSubmitted: (s) {
_addEmailToCollection(context);
},
));
}
children.add(Padding(
padding: EdgeInsets.all(8),
));
children.add(Container(
width: 220,
child: OutlineButton(
child: Icon(
Icons.add,
if (!_showEntryField) {
children.add(Container(
width: 220,
child: OutlineButton(
child: Icon(
Icons.add,
),
onPressed: () {
setState(() {
_showEntryField = true;
});
},
),
));
} else {
children.add(Container(
width: 220,
child: button(
"Add",
onPressed: () async {
await _addEmailToCollection(context);
},
),
));
}
return AlertDialog(
title: Text("Sharing"),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
children: children,
)),
],
),
onPressed: () {
setState(() {
_showEntryField = true;
});
},
),
));
var column = Column(
children: children,
);
return column;
}
Future<void> _addEmailToCollection(BuildContext context) async {
if (!isValidEmail(_email)) {
showErrorDialog(context, "Invalid email address",
"Please enter a valid email address");
return;
}
final dialog = createProgressDialog(context, "Searching for user...");
await dialog.show();
final publicKey = await UserService.instance.getPublicKey(email: _email);
await dialog.hide();
if (publicKey == null) {
Navigator.of(context).pop();
final dialog = AlertDialog(
title: Text("Invite to ente?"),
content: Text("Looks like " +
_email +
" hasn't signed up for ente yet. Would you like to invite them?"),
actions: [
FlatButton(
child: Text("Invite"),
onPressed: () {
shareText(
"Hey, I've got some really nice photos to share. Please install ente.io so that I can share them privately.");
},
),
],
);
showDialog(
context: context,
builder: (BuildContext context) {
return dialog;
},
);
} else {
if (widget.collection == null) {
log("Collection is null");
// TODO: Create collection
// TODO: Add files to collection
}
CollectionsService.instance
.share(widget.collection.id, _email, publicKey)
.then((value) {
setState(() {
_sharees.add(_email);
_showEntryField = false;
});
});
}
}
}
@ -152,63 +200,21 @@ class EmailItemWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
email,
style: TextStyle(fontSize: 14),
),
Icon(
Icons.remove_circle_outline,
color: Colors.redAccent,
),
],
),
);
}
}
class SharingCheckboxWidget extends StatefulWidget {
final Map<int, bool> sharingStatus;
const SharingCheckboxWidget(
this.sharingStatus, {
Key key,
}) : super(key: key);
@override
_SharingCheckboxWidgetState createState() => _SharingCheckboxWidgetState();
}
class _SharingCheckboxWidgetState extends State<SharingCheckboxWidget> {
Map<int, bool> _sharingStatus;
@override
void initState() {
_sharingStatus = widget.sharingStatus;
super.initState();
}
@override
Widget build(BuildContext context) {
final checkboxes = List<Widget>();
for (final user in _sharingStatus.keys) {
checkboxes.add(Row(
children: <Widget>[
Checkbox(
materialTapTargetSize: MaterialTapTargetSize.padded,
value: _sharingStatus[user],
onChanged: (value) {
setState(() {
_sharingStatus[user] = value;
});
}),
Text(user.toString()),
],
));
}
return Column(children: checkboxes);
padding: const EdgeInsets.fromLTRB(0, 4, 0, 4),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Text(
email,
style: TextStyle(fontSize: 16),
),
),
Icon(
Icons.delete_forever,
color: Colors.redAccent,
),
],
));
}
}

View file

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/models/shared_collection.dart';
import 'package:photos/ui/gallery.dart';
import 'package:photos/ui/gallery_app_bar_widget.dart';
class SharedCollectionPage extends StatefulWidget {
final SharedCollection collection;
const SharedCollectionPage(this.collection, {Key key}) : super(key: key);
@override
_SharedCollectionPageState createState() => _SharedCollectionPageState();
}
class _SharedCollectionPageState extends State<SharedCollectionPage> {
final _selectedFiles = SelectedFiles();
@override
Widget build(Object context) {
var gallery = Gallery(
asyncLoader: (lastFile, limit) => FilesDB.instance.getAllInCollection(
widget.collection.id,
lastFile == null
? DateTime.now().microsecondsSinceEpoch
: lastFile.creationTime,
limit),
// onRefresh: () => FolderSharingService.instance.syncDiff(widget.folder),
tagPrefix: "shared_collection",
selectedFiles: _selectedFiles,
);
return Scaffold(
appBar: GalleryAppBarWidget(
GalleryAppBarType.shared_collection,
widget.collection.name,
_selectedFiles,
),
body: gallery,
);
}
}

View file

@ -0,0 +1,146 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/collections_db.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/remote_sync_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/shared_collection.dart';
import 'package:photos/ui/common_elements.dart';
import 'package:photos/ui/loading_widget.dart';
import 'package:photos/ui/shared_collection_page.dart';
import 'package:photos/ui/thumbnail_widget.dart';
class SharedCollectionGallery extends StatefulWidget {
const SharedCollectionGallery({Key key}) : super(key: key);
@override
_SharedCollectionGalleryState createState() =>
_SharedCollectionGalleryState();
}
class _SharedCollectionGalleryState extends State<SharedCollectionGallery> {
Logger _logger = Logger("SharedCollectionGallery");
StreamSubscription<RemoteSyncEvent> _subscription;
@override
void initState() {
_subscription = Bus.instance.on<RemoteSyncEvent>().listen((event) {
if (event.success) {
setState(() {});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<SharedCollectionWithThumbnail>>(
future: CollectionsDB.instance
.getAllSharedCollections()
.then((collections) async {
final c = List<SharedCollectionWithThumbnail>();
for (final collection in collections) {
var thumbnail;
try {
thumbnail =
await FilesDB.instance.getLatestFileInCollection(collection.id);
} catch (e) {
_logger.warning(e.toString());
}
c.add(SharedCollectionWithThumbnail(collection, thumbnail));
}
return c;
}),
builder: (context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data.isEmpty) {
return nothingToSeeHere;
} else {
return _getSharedCollectionsGallery(snapshot.data);
}
} else if (snapshot.hasError) {
_logger.shout(snapshot.error);
return Center(child: Text(snapshot.error.toString()));
} else {
return loadWidget;
}
},
);
}
Widget _getSharedCollectionsGallery(
List<SharedCollectionWithThumbnail> collections) {
return Container(
margin: EdgeInsets.only(top: 24),
child: GridView.builder(
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 12),
physics: ScrollPhysics(), // to disable GridView's scrolling
itemBuilder: (context, index) {
return _buildCollection(context, collections[index]);
},
itemCount: collections.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
),
);
}
Widget _buildCollection(
BuildContext context, SharedCollectionWithThumbnail c) {
_logger.info("Building collection " + c.collection.toString());
return GestureDetector(
child: Column(
children: <Widget>[
Container(
child: c.thumbnail ==
null // When the user has shared a folder without photos
? Icon(Icons.error)
: Hero(
tag: "shared_collection" + c.thumbnail.tag(),
child: ThumbnailWidget(c.thumbnail)),
height: 150,
width: 150,
),
Padding(padding: EdgeInsets.all(2)),
Expanded(
child: Text(
c.collection.name,
style: TextStyle(
fontSize: 16,
),
),
),
],
),
onTap: () {
final page = SharedCollectionPage(c.collection);
Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return page;
},
),
);
},
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
}
class SharedCollectionWithThumbnail {
final SharedCollection collection;
final File thumbnail;
SharedCollectionWithThumbnail(this.collection, this.thumbnail);
}

View file

@ -5,10 +5,7 @@ import 'dart:io' as io;
import 'package:computer/computer.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/models/encrypted_data_attributes.dart';
import 'package:photos/models/encrypted_file_attributes.dart';
import 'package:photos/models/encryption_attribute.dart';
import 'package:photos/models/encryption_result.dart';
final int encryptionChunkSize = 4 * 1024 * 1024;
final int decryptionChunkSize =
@ -32,7 +29,7 @@ bool cryptoPwhashStrVerify(Map<String, dynamic> args) {
return Sodium.cryptoPwhashStrVerify(args["hash"], args["input"]) == 0;
}
ChaChaAttributes chachaEncrypt(Map<String, dynamic> args) {
EncryptionResult chachaEncryptFile(Map<String, dynamic> args) {
final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
final logger = Logger("ChaChaEncrypt");
final sourceFile = io.File(args["sourceFilePath"]);
@ -63,8 +60,7 @@ ChaChaAttributes chachaEncrypt(Map<String, dynamic> args) {
logger.info("Encryption time: " +
(DateTime.now().millisecondsSinceEpoch - encryptionStartTime).toString());
return ChaChaAttributes(EncryptionAttribute(bytes: key),
EncryptionAttribute(bytes: initPushResult.header));
return EncryptionResult(key: key, header: initPushResult.header);
}
void chachaDecrypt(Map<String, dynamic> args) {
@ -100,65 +96,89 @@ void chachaDecrypt(Map<String, dynamic> args) {
}
class CryptoUtil {
static Future<EncryptedData> encrypt(Uint8List source,
{Uint8List key}) async {
if (key == null) {
key = Sodium.cryptoSecretboxKeygen();
}
static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
final args = Map<String, dynamic>();
args["source"] = source;
args["nonce"] = nonce;
args["key"] = key;
final encryptedData =
await Computer().compute(cryptoSecretboxEasy, param: args);
return EncryptedData(
EncryptionAttribute(bytes: key),
EncryptionAttribute(bytes: nonce),
EncryptionAttribute(bytes: encryptedData));
final encryptedData = cryptoSecretboxEasy(args);
return EncryptionResult(
key: key, nonce: nonce, encryptedData: encryptedData);
}
static Future<Uint8List> decrypt(
Uint8List cipher, Uint8List key, Uint8List nonce,
{bool background = false}) async {
Uint8List cipher,
Uint8List key,
Uint8List nonce,
) async {
final args = Map<String, dynamic>();
args["cipher"] = cipher;
args["nonce"] = nonce;
args["key"] = key;
if (background) {
return Computer().compute(cryptoSecretboxOpenEasy, param: args);
} else {
return cryptoSecretboxOpenEasy(args);
}
return Computer().compute(cryptoSecretboxOpenEasy, param: args);
}
static Future<ChaChaAttributes> encryptFile(
static Uint8List decryptSync(
Uint8List cipher,
Uint8List key,
Uint8List nonce,
) {
final args = Map<String, dynamic>();
args["cipher"] = cipher;
args["nonce"] = nonce;
args["key"] = key;
return cryptoSecretboxOpenEasy(args);
}
static EncryptionResult encryptChaCha(Uint8List source, Uint8List key) {
final initPushResult =
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
initPushResult.state,
source,
null,
Sodium.cryptoSecretstreamXchacha20poly1305TagFinal);
return EncryptionResult(
encryptedData: encryptedData, header: initPushResult.header);
}
static Uint8List decryptChaCha(
Uint8List source, Uint8List key, Uint8List header) {
final pullState =
Sodium.cryptoSecretstreamXchacha20poly1305InitPull(header, key);
final pullResult =
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, source, null);
return pullResult.m;
}
static Future<EncryptionResult> encryptFile(
String sourceFilePath,
String destinationFilePath,
) {
final args = Map<String, dynamic>();
args["sourceFilePath"] = sourceFilePath;
args["destinationFilePath"] = destinationFilePath;
return Computer().compute(chachaEncrypt, param: args);
return Computer().compute(chachaEncryptFile, param: args);
}
static Future<void> decryptFile(
String sourceFilePath,
String destinationFilePath,
ChaChaAttributes attributes,
Uint8List header,
Uint8List key,
) {
final args = Map<String, dynamic>();
args["sourceFilePath"] = sourceFilePath;
args["destinationFilePath"] = destinationFilePath;
args["header"] = attributes.header.bytes;
args["key"] = attributes.key.bytes;
args["header"] = header;
args["key"] = key;
return Computer().compute(chachaDecrypt, param: args);
}
static Uint8List generateMasterKey() {
return Sodium.cryptoKdfKeygen();
static Uint8List generateKey() {
return Sodium.cryptoSecretboxKeygen();
}
static Uint8List getSaltToDeriveKey() {
@ -194,4 +214,13 @@ class CryptoUtil {
static Future<KeyPair> generateKeyPair() async {
return Sodium.cryptoBoxKeypair();
}
static Uint8List openSealSync(
Uint8List input, Uint8List publicKey, Uint8List secretKey) {
return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
}
static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
return Sodium.cryptoBoxSeal(input, publicKey);
}
}

View file

@ -0,0 +1,5 @@
bool isValidEmail(String email) {
return RegExp(
r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+")
.hasMatch(email);
}

View file

@ -6,9 +6,9 @@ import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/remote_sync_event.dart';
import 'package:photos/models/decryption_params.dart';
import 'package:photos/models/file.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_util.dart';
class DiffFetcher {
final _logger = Logger("FileDownloader");
@ -17,9 +17,10 @@ class DiffFetcher {
Future<List<File>> getEncryptedFilesDiff(int lastSyncTime, int limit) async {
return _dio
.get(
Configuration.instance.getHttpEndpoint() + "/encrypted-files/diff",
Configuration.instance.getHttpEndpoint() + "/files/diff",
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
queryParameters: {
"token": Configuration.instance.getToken(),
"sinceTime": lastSyncTime,
"limit": limit,
},
@ -34,23 +35,23 @@ class DiffFetcher {
final file = File();
file.uploadedFileID = item["id"];
file.ownerID = item["ownerID"];
file.collectionID = item["collectionID"];
if (file.collectionID == 4) {
_logger.info("Found");
}
file.updationTime = item["updationTime"];
file.isEncrypted = true;
file.fileDecryptionParams =
DecryptionParams.fromMap(item["file"]["decryptionParams"]);
file.thumbnailDecryptionParams = DecryptionParams.fromMap(
item["thumbnail"]["decryptionParams"]);
file.metadataDecryptionParams = DecryptionParams.fromMap(
item["metadata"]["decryptionParams"]);
final metadataDecryptionKey = await CryptoUtil.decrypt(
Sodium.base642bin(file.metadataDecryptionParams.encryptedKey),
Configuration.instance.getKey(),
Sodium.base642bin(
file.metadataDecryptionParams.keyDecryptionNonce));
final encodedMetadata = await CryptoUtil.decrypt(
file.encryptedKey = item["encryptedKey"];
file.keyDecryptionNonce = item["keyDecryptionNonce"];
file.fileDecryptionHeader = item["file"]["decryptionHeader"];
file.thumbnailDecryptionHeader =
item["thumbnail"]["decryptionHeader"];
file.metadataDecryptionHeader =
item["metadata"]["decryptionHeader"];
final encodedMetadata = CryptoUtil.decryptChaCha(
Sodium.base642bin(item["metadata"]["encryptedData"]),
metadataDecryptionKey,
Sodium.base642bin(file.metadataDecryptionParams.nonce),
decryptFileKey(file),
Sodium.base642bin(file.metadataDecryptionHeader),
);
Map<String, dynamic> metadata =
jsonDecode(utf8.decode(encodedMetadata));
@ -63,25 +64,4 @@ class DiffFetcher {
return files;
});
}
Future<List<File>> getFilesDiff(int lastSyncTime, int limit) async {
Response response = await _dio.get(
Configuration.instance.getHttpEndpoint() + "/files/diff",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
queryParameters: {
"sinceTime": lastSyncTime,
"limit": limit,
},
).catchError((e) => _logger.severe(e));
if (response != null) {
Bus.instance.fire(RemoteSyncEvent(true));
return (response.data["diff"] as List)
.map((file) => new File.fromJson(file))
.toList();
} else {
Bus.instance.fire(RemoteSyncEvent(false));
return null;
}
}
}

View file

@ -1,12 +1,13 @@
import 'dart:convert';
import 'dart:io' as io;
import 'package:dio/dio.dart';
import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/models/decryption_params.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/upload_url.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_name_util.dart';
import 'package:photos/utils/file_util.dart';
@ -18,8 +19,7 @@ class FileUploader {
Future<UploadURL> getUploadURL() {
return Dio()
.get(
Configuration.instance.getHttpEndpoint() +
"/encrypted-files/upload-url",
Configuration.instance.getHttpEndpoint() + "/files/upload-url",
options: Options(
headers: {"X-Auth-Token": Configuration.instance.getToken()}),
)
@ -44,6 +44,10 @@ class FileUploader {
Future<File> encryptAndUploadFile(File file) async {
_logger.info("Uploading " + file.toString());
file.collectionID = (await CollectionsService.instance
.getOrCreateForPath(file.deviceFolder))
.id;
final encryptedFileName = file.generatedID.toString() + ".encrypted";
final tempDirectory = Configuration.instance.getTempDirectory();
final encryptedFilePath = tempDirectory + encryptedFileName;
@ -56,16 +60,6 @@ class FileUploader {
final fileUploadURL = await getUploadURL();
String fileObjectKey = await putFile(fileUploadURL, encryptedFile);
final encryptedFileKey = await CryptoUtil.encrypt(
fileAttributes.key.bytes,
key: Configuration.instance.getKey(),
);
final fileDecryptionParams = DecryptionParams(
encryptedKey: encryptedFileKey.encryptedData.base64,
keyDecryptionNonce: encryptedFileKey.nonce.base64,
header: fileAttributes.header.base64,
);
final thumbnailData = (await (await file.getAsset()).thumbDataWithSize(
THUMBNAIL_LARGE_SIZE,
THUMBNAIL_LARGE_SIZE,
@ -74,52 +68,53 @@ class FileUploader {
final encryptedThumbnailName =
file.generatedID.toString() + "_thumbnail.encrypted";
final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
final encryptedThumbnail = await CryptoUtil.encrypt(thumbnailData);
final encryptedThumbnail =
CryptoUtil.encryptChaCha(thumbnailData, fileAttributes.key);
io.File(encryptedThumbnailPath)
.writeAsBytesSync(encryptedThumbnail.encryptedData.bytes);
.writeAsBytesSync(encryptedThumbnail.encryptedData);
final thumbnailUploadURL = await getUploadURL();
String thumbnailObjectKey =
await putFile(thumbnailUploadURL, io.File(encryptedThumbnailPath));
final encryptedThumbnailKey = await CryptoUtil.encrypt(
encryptedThumbnail.key.bytes,
key: Configuration.instance.getKey(),
);
final thumbnailDecryptionParams = DecryptionParams(
encryptedKey: encryptedThumbnailKey.encryptedData.base64,
keyDecryptionNonce: encryptedThumbnailKey.nonce.base64,
nonce: encryptedThumbnail.nonce.base64,
final encryptedMetadataData = CryptoUtil.encryptChaCha(
utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key);
final encryptedFileKeyData = CryptoUtil.encryptSync(
fileAttributes.key,
CollectionsService.instance.getCollectionKey(file.collectionID),
);
final metadata = jsonEncode(file.getMetadata());
final encryptedMetadata = await CryptoUtil.encrypt(utf8.encode(metadata));
final encryptedMetadataKey = await CryptoUtil.encrypt(
encryptedMetadata.key.bytes,
key: Configuration.instance.getKey(),
);
final metadataDecryptionParams = DecryptionParams(
encryptedKey: encryptedMetadataKey.encryptedData.base64,
keyDecryptionNonce: encryptedMetadataKey.nonce.base64,
nonce: encryptedMetadata.nonce.base64,
);
final encryptedKey = Sodium.bin2base64(encryptedFileKeyData.encryptedData);
final keyDecryptionNonce = Sodium.bin2base64(encryptedFileKeyData.nonce);
final fileDecryptionHeader = Sodium.bin2base64(fileAttributes.header);
final thumbnailDecryptionHeader =
Sodium.bin2base64(encryptedThumbnail.header);
final encryptedMetadata =
Sodium.bin2base64(encryptedMetadataData.encryptedData);
final metadataDecryptionHeader =
Sodium.bin2base64(encryptedMetadataData.header);
final data = {
"collectionID": file.collectionID,
"encryptedKey": encryptedKey,
"keyDecryptionNonce": keyDecryptionNonce,
"file": {
"objectKey": fileObjectKey,
"decryptionParams": fileDecryptionParams.toMap(),
"decryptionHeader": fileDecryptionHeader,
},
"thumbnail": {
"objectKey": thumbnailObjectKey,
"decryptionParams": thumbnailDecryptionParams.toMap(),
"decryptionHeader": thumbnailDecryptionHeader,
},
"metadata": {
"encryptedData": encryptedMetadata.encryptedData.base64,
"decryptionParams": metadataDecryptionParams.toMap(),
"encryptedData": encryptedMetadata,
"decryptionHeader": metadataDecryptionHeader,
}
};
return _dio
.post(
Configuration.instance.getHttpEndpoint() + "/encrypted-files",
Configuration.instance.getHttpEndpoint() + "/files",
options:
Options(headers: {"X-Auth-Token": Configuration.instance.getToken()}),
data: data,
@ -131,9 +126,11 @@ class FileUploader {
file.uploadedFileID = data["id"];
file.updationTime = data["updationTime"];
file.ownerID = data["ownerID"];
file.fileDecryptionParams = fileDecryptionParams;
file.thumbnailDecryptionParams = thumbnailDecryptionParams;
file.metadataDecryptionParams = metadataDecryptionParams;
file.encryptedKey = encryptedKey;
file.keyDecryptionNonce = keyDecryptionNonce;
file.fileDecryptionHeader = fileDecryptionHeader;
file.thumbnailDecryptionHeader = thumbnailDecryptionHeader;
file.metadataDecryptionHeader = metadataDecryptionHeader;
return file;
});
}

View file

@ -15,10 +15,9 @@ import 'package:photos/core/cache/video_cache_manager.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/constants.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/encrypted_file_attributes.dart';
import 'package:photos/models/encryption_attribute.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
import 'package:photos/services/collections_service.dart';
import 'crypto_util.dart';
@ -176,17 +175,8 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
return null;
}
logger.info("File downloaded: " + file.uploadedFileID.toString());
var attributes = ChaChaAttributes(
EncryptionAttribute(
bytes: await CryptoUtil.decrypt(
Sodium.base642bin(file.fileDecryptionParams.encryptedKey),
Configuration.instance.getKey(),
Sodium.base642bin(file.fileDecryptionParams.keyDecryptionNonce),
)),
EncryptionAttribute(base64: file.fileDecryptionParams.header),
);
await CryptoUtil.decryptFile(
encryptedFilePath, decryptedFilePath, attributes);
await CryptoUtil.decryptFile(encryptedFilePath, decryptedFilePath,
Sodium.base642bin(file.fileDecryptionHeader), decryptFileKey(file));
logger.info("File decrypted: " + file.uploadedFileID.toString());
io.File(encryptedFilePath).deleteSync();
final fileExtension = extension(file.title).substring(1).toLowerCase();
@ -200,6 +190,8 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
decryptedFile.deleteSync();
downloadsInProgress.remove(file.uploadedFileID);
return cachedFile;
}).catchError((e) {
downloadsInProgress.remove(file.uploadedFileID);
});
}
@ -209,14 +201,11 @@ Future<io.File> _downloadAndDecryptThumbnail(File file) async {
"_thumbnail.decrypted";
return Dio().download(file.getThumbnailUrl(), temporaryPath).then((_) async {
final encryptedFile = io.File(temporaryPath);
final thumbnailDecryptionKey = await CryptoUtil.decrypt(
Sodium.base642bin(file.thumbnailDecryptionParams.encryptedKey),
Configuration.instance.getKey(),
Sodium.base642bin(file.thumbnailDecryptionParams.keyDecryptionNonce));
final data = await CryptoUtil.decrypt(
final thumbnailDecryptionKey = decryptFileKey(file);
final data = CryptoUtil.decryptChaCha(
encryptedFile.readAsBytesSync(),
thumbnailDecryptionKey,
Sodium.base642bin(file.thumbnailDecryptionParams.nonce),
Sodium.base642bin(file.thumbnailDecryptionHeader),
);
encryptedFile.deleteSync();
return ThumbnailCacheManager().putFile(
@ -227,3 +216,11 @@ Future<io.File> _downloadAndDecryptThumbnail(File file) async {
);
});
}
Uint8List decryptFileKey(File file) {
final encryptedKey = Sodium.base642bin(file.encryptedKey);
final nonce = Sodium.base642bin(file.keyDecryptionNonce);
final collectionKey =
CollectionsService.instance.getCollectionKey(file.collectionID);
return CryptoUtil.decryptSync(encryptedKey, collectionKey, nonce);
}

View file

@ -38,6 +38,10 @@ Future<void> _shareVideo(ProgressDialog dialog, File file) async {
return ShareExtend.share(path, "image");
}
Future<void> shareText(String text) async {
return ShareExtend.share(text, "text");
}
Future<void> _shareImage(ProgressDialog dialog, File file) async {
await dialog.show();
final bytes = await getBytes(file);

View file

@ -28,14 +28,14 @@ packages:
name: async
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.2"
version: "2.5.0-nullsafety.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0-nullsafety.1"
cached_network_image:
dependency: "direct main"
description:
@ -49,14 +49,14 @@ packages:
name: characters
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
version: "1.1.0-nullsafety.3"
charcode:
dependency: transitive
description:
name: charcode
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.3"
version: "1.2.0-nullsafety.1"
chewie:
dependency: "direct main"
description:
@ -70,14 +70,14 @@ packages:
name: clock
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
version: "1.1.0-nullsafety.1"
collection:
dependency: transitive
description:
name: collection
url: "https://pub.dartlang.org"
source: hosted
version: "1.14.13"
version: "1.15.0-nullsafety.3"
computer:
dependency: "direct main"
description:
@ -196,7 +196,7 @@ packages:
name: fake_async
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0-nullsafety.1"
ffi:
dependency: transitive
description:
@ -345,13 +345,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.16.1"
json_annotation:
dependency: transitive
description:
name: json_annotation
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
like_button:
dependency: "direct main"
description:
@ -359,13 +352,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0"
local_image_provider:
dependency: "direct main"
description:
name: local_image_provider
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
logging:
dependency: "direct main"
description:
@ -379,14 +365,14 @@ packages:
name: matcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.8"
version: "0.12.10-nullsafety.1"
meta:
dependency: transitive
description:
name: meta
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
version: "1.3.0-nullsafety.3"
octo_image:
dependency: transitive
description:
@ -414,7 +400,7 @@ packages:
name: path
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0-nullsafety.1"
path_provider:
dependency: "direct main"
description:
@ -444,12 +430,12 @@ packages:
source: hosted
version: "1.0.2"
pedantic:
dependency: transitive
dependency: "direct main"
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.0"
version: "1.9.2"
petitparser:
dependency: transitive
description:
@ -526,7 +512,7 @@ packages:
name: pull_to_refresh
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.1"
version: "1.6.2"
rxdart:
dependency: transitive
description:
@ -608,7 +594,7 @@ packages:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
version: "1.8.0-nullsafety.2"
sqflite:
dependency: "direct main"
description:
@ -629,21 +615,21 @@ packages:
name: stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.9.5"
version: "1.10.0-nullsafety.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.0"
version: "2.1.0-nullsafety.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
version: "1.1.0-nullsafety.1"
super_logging:
dependency: "direct main"
description:
@ -664,21 +650,21 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
version: "1.2.0-nullsafety.1"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.17"
version: "0.2.19-nullsafety.2"
typed_data:
dependency: transitive
description:
name: typed_data
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0"
version: "1.3.0-nullsafety.3"
uni_links:
dependency: "direct main"
description:
@ -741,7 +727,7 @@ packages:
name: vector_math
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
version: "2.1.0-nullsafety.3"
video_player:
dependency: "direct main"
description:
@ -799,5 +785,5 @@ packages:
source: hosted
version: "2.2.1"
sdks:
dart: ">=2.9.0-14.0.dev <3.0.0"
dart: ">=2.10.0-110 <2.11.0"
flutter: ">=1.19.0-2.0.pre <2.0.0"

View file

@ -29,7 +29,6 @@ dependencies:
path_provider: ^1.6.5
shared_preferences: ^0.5.6
dio: ^3.0.9
local_image_provider: ^1.0.0
image: ^2.1.4
esys_flutter_share: ^1.0.2
share_extend: ^1.1.9
@ -47,7 +46,7 @@ dependencies:
logging: ^0.11.4
flutter_image_compress: ^0.6.5+1
flutter_typeahead: ^1.8.1
pull_to_refresh: ^1.5.7
pull_to_refresh: ^1.6.2
fluttertoast: ^4.0.1
extended_image: ^0.9.0
video_player: ^0.10.11+1
@ -61,6 +60,7 @@ dependencies:
uni_links: ^0.4.0
crisp: ^0.1.3
flutter_sodium: ^0.1.8
pedantic: ^1.9.2
dev_dependencies:
flutter_test: