mirror of
https://github.com/ente-io/ente.git
synced 2025-08-13 09:47:17 +00:00
fix: use random path, add date based fields, use collection id to encrypt file key
This commit is contained in:
@@ -3,10 +3,8 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import "package:photos/models/encryption_result.dart";
|
|
||||||
import "package:photos/module/upload/model/multipart.dart";
|
import "package:photos/module/upload/model/multipart.dart";
|
||||||
import "package:photos/module/upload/service/multipart.dart";
|
import "package:photos/module/upload/service/multipart.dart";
|
||||||
import "package:photos/utils/crypto_util.dart";
|
|
||||||
import 'package:sqflite/sqflite.dart';
|
import 'package:sqflite/sqflite.dart';
|
||||||
import "package:sqflite_migration/sqflite_migration.dart";
|
import "package:sqflite_migration/sqflite_migration.dart";
|
||||||
|
|
||||||
@@ -25,14 +23,18 @@ class UploadLocksDB {
|
|||||||
columnID: "id",
|
columnID: "id",
|
||||||
columnLocalID: "local_id",
|
columnLocalID: "local_id",
|
||||||
columnFileHash: "file_hash",
|
columnFileHash: "file_hash",
|
||||||
|
columnCollectionID: "collection_id",
|
||||||
columnEncryptedFilePath: "encrypted_file_path",
|
columnEncryptedFilePath: "encrypted_file_path",
|
||||||
columnEncryptedFileSize: "encrypted_file_size",
|
columnEncryptedFileSize: "encrypted_file_size",
|
||||||
columnFileKey: "file_key",
|
columnEncryptedFileKey: "encrypted_file_key",
|
||||||
columnFileNonce: "file_nonce",
|
columnFileEncryptionNonce: "file_encryption_nonce",
|
||||||
|
columnKeyEncryptionNonce: "key_encryption_nonce",
|
||||||
columnObjectKey: "object_key",
|
columnObjectKey: "object_key",
|
||||||
columnCompleteUrl: "complete_url",
|
columnCompleteUrl: "complete_url",
|
||||||
columnStatus: "status",
|
columnStatus: "status",
|
||||||
columnPartSize: "part_size",
|
columnPartSize: "part_size",
|
||||||
|
columnLastAttemptedAt: "last_attempted_at",
|
||||||
|
columnCreatedAt: "created_at",
|
||||||
);
|
);
|
||||||
|
|
||||||
static const _partsTable = (
|
static const _partsTable = (
|
||||||
@@ -93,14 +95,18 @@ class UploadLocksDB {
|
|||||||
${_trackUploadTable.columnID} INTEGER PRIMARY KEY,
|
${_trackUploadTable.columnID} INTEGER PRIMARY KEY,
|
||||||
${_trackUploadTable.columnLocalID} TEXT NOT NULL,
|
${_trackUploadTable.columnLocalID} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnFileHash} TEXT NOT NULL,
|
${_trackUploadTable.columnFileHash} TEXT NOT NULL,
|
||||||
|
${_trackUploadTable.columnCollectionID} INTEGER NOT NULL,
|
||||||
${_trackUploadTable.columnEncryptedFilePath} TEXT NOT NULL,
|
${_trackUploadTable.columnEncryptedFilePath} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnEncryptedFileSize} INTEGER NOT NULL,
|
${_trackUploadTable.columnEncryptedFileSize} INTEGER NOT NULL,
|
||||||
${_trackUploadTable.columnFileKey} TEXT NOT NULL,
|
${_trackUploadTable.columnEncryptedFileKey} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnFileNonce} TEXT NOT NULL,
|
${_trackUploadTable.columnFileEncryptionNonce} TEXT NOT NULL,
|
||||||
|
${_trackUploadTable.columnKeyEncryptionNonce} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnObjectKey} TEXT NOT NULL,
|
${_trackUploadTable.columnObjectKey} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnCompleteUrl} TEXT NOT NULL,
|
${_trackUploadTable.columnCompleteUrl} TEXT NOT NULL,
|
||||||
${_trackUploadTable.columnStatus} TEXT DEFAULT '${MultipartStatus.pending.name}' NOT NULL,
|
${_trackUploadTable.columnStatus} TEXT DEFAULT '${MultipartStatus.pending.name}' NOT NULL,
|
||||||
${_trackUploadTable.columnPartSize} INTEGER NOT NULL
|
${_trackUploadTable.columnPartSize} INTEGER NOT NULL,
|
||||||
|
${_trackUploadTable.columnLastAttemptedAt} INTEGER,
|
||||||
|
${_trackUploadTable.columnCreatedAt} INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
)
|
)
|
||||||
''',
|
''',
|
||||||
'''
|
'''
|
||||||
@@ -177,29 +183,33 @@ class UploadLocksDB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For multipart download tracking
|
// For multipart download tracking
|
||||||
Future<bool> doesExists(String localId, String hash) async {
|
Future<bool> doesExists(String localId, String hash, int collectionID) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
final rows = await db.query(
|
final rows = await db.query(
|
||||||
_trackUploadTable.table,
|
_trackUploadTable.table,
|
||||||
where:
|
where: '${_trackUploadTable.columnLocalID} = ?'
|
||||||
'${_trackUploadTable.columnLocalID} = ? AND ${_trackUploadTable.columnFileHash} = ?',
|
' AND ${_trackUploadTable.columnFileHash} = ?'
|
||||||
whereArgs: [localId, hash],
|
' AND ${_trackUploadTable.columnCollectionID} = ?',
|
||||||
|
whereArgs: [localId, hash, collectionID],
|
||||||
);
|
);
|
||||||
|
|
||||||
return rows.isNotEmpty;
|
return rows.isNotEmpty;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<EncryptionResult> getFileEncryptionData(
|
Future<({String encryptedFileKey, String fileNonce, String keyNonce})>
|
||||||
|
getFileEncryptionData(
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
|
int collectionID,
|
||||||
) async {
|
) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
|
|
||||||
final rows = await db.query(
|
final rows = await db.query(
|
||||||
_trackUploadTable.table,
|
_trackUploadTable.table,
|
||||||
where:
|
where: '${_trackUploadTable.columnLocalID} = ?'
|
||||||
'${_trackUploadTable.columnLocalID} = ? AND ${_trackUploadTable.columnFileHash} = ?',
|
' AND ${_trackUploadTable.columnFileHash} = ?'
|
||||||
whereArgs: [localId, fileHash],
|
' AND ${_trackUploadTable.columnCollectionID} = ?',
|
||||||
|
whereArgs: [localId, fileHash, collectionID],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rows.isEmpty) {
|
if (rows.isEmpty) {
|
||||||
@@ -207,25 +217,25 @@ class UploadLocksDB {
|
|||||||
}
|
}
|
||||||
final row = rows.first;
|
final row = rows.first;
|
||||||
|
|
||||||
return EncryptionResult(
|
return (
|
||||||
key:
|
encryptedFileKey: row[_trackUploadTable.columnEncryptedFileKey] as String,
|
||||||
CryptoUtil.base642bin(row[_trackUploadTable.columnFileKey] as String),
|
fileNonce: row[_trackUploadTable.columnFileEncryptionNonce] as String,
|
||||||
header: CryptoUtil.base642bin(
|
keyNonce: row[_trackUploadTable.columnKeyEncryptionNonce] as String,
|
||||||
row[_trackUploadTable.columnFileNonce] as String,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<MultipartInfo> getCachedLinks(
|
Future<MultipartInfo> getCachedLinks(
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
|
int collectionID,
|
||||||
) async {
|
) async {
|
||||||
final db = await instance.database;
|
final db = await instance.database;
|
||||||
final rows = await db.query(
|
final rows = await db.query(
|
||||||
_trackUploadTable.table,
|
_trackUploadTable.table,
|
||||||
where:
|
where: '${_trackUploadTable.columnLocalID} = ?'
|
||||||
'${_trackUploadTable.columnLocalID} = ? AND ${_trackUploadTable.columnFileHash} = ?',
|
' AND ${_trackUploadTable.columnFileHash} = ?'
|
||||||
whereArgs: [localId, fileHash],
|
' AND ${_trackUploadTable.columnCollectionID} = ?',
|
||||||
|
whereArgs: [localId, fileHash, collectionID],
|
||||||
);
|
);
|
||||||
if (rows.isEmpty) {
|
if (rows.isEmpty) {
|
||||||
throw Exception("No cached links found for $localId and $fileHash");
|
throw Exception("No cached links found for $localId and $fileHash");
|
||||||
@@ -274,11 +284,13 @@ class UploadLocksDB {
|
|||||||
Future<void> createTrackUploadsEntry(
|
Future<void> createTrackUploadsEntry(
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
|
int collectionID,
|
||||||
MultipartUploadURLs urls,
|
MultipartUploadURLs urls,
|
||||||
String encryptedFilePath,
|
String encryptedFilePath,
|
||||||
int fileSize,
|
int fileSize,
|
||||||
String fileKey,
|
String fileKey,
|
||||||
String fileNonce,
|
String fileNonce,
|
||||||
|
String keyNonce,
|
||||||
) async {
|
) async {
|
||||||
final db = await UploadLocksDB.instance.database;
|
final db = await UploadLocksDB.instance.database;
|
||||||
final objectKey = urls.objectKey;
|
final objectKey = urls.objectKey;
|
||||||
@@ -288,13 +300,16 @@ class UploadLocksDB {
|
|||||||
{
|
{
|
||||||
_trackUploadTable.columnLocalID: localId,
|
_trackUploadTable.columnLocalID: localId,
|
||||||
_trackUploadTable.columnFileHash: fileHash,
|
_trackUploadTable.columnFileHash: fileHash,
|
||||||
|
_trackUploadTable.columnCollectionID: collectionID,
|
||||||
_trackUploadTable.columnObjectKey: objectKey,
|
_trackUploadTable.columnObjectKey: objectKey,
|
||||||
_trackUploadTable.columnCompleteUrl: urls.completeURL,
|
_trackUploadTable.columnCompleteUrl: urls.completeURL,
|
||||||
_trackUploadTable.columnEncryptedFilePath: encryptedFilePath,
|
_trackUploadTable.columnEncryptedFilePath: encryptedFilePath,
|
||||||
_trackUploadTable.columnEncryptedFileSize: fileSize,
|
_trackUploadTable.columnEncryptedFileSize: fileSize,
|
||||||
_trackUploadTable.columnFileKey: fileKey,
|
_trackUploadTable.columnEncryptedFileKey: fileKey,
|
||||||
_trackUploadTable.columnFileNonce: fileNonce,
|
_trackUploadTable.columnFileEncryptionNonce: fileNonce,
|
||||||
_trackUploadTable.columnPartSize: MultiPartUploader.multipartPartSizeForUpload,
|
_trackUploadTable.columnKeyEncryptionNonce: keyNonce,
|
||||||
|
_trackUploadTable.columnPartSize:
|
||||||
|
MultiPartUploader.multipartPartSizeForUpload,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -357,4 +372,27 @@ class UploadLocksDB {
|
|||||||
whereArgs: [localId],
|
whereArgs: [localId],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> isEncryptedPathSafeToDelete(String encryptedPath) {
|
||||||
|
// If lastAttemptedAt exceeds 3 days or createdAt exceeds 7 days
|
||||||
|
final db = instance.database;
|
||||||
|
return db.then((db) async {
|
||||||
|
final rows = await db.query(
|
||||||
|
_trackUploadTable.table,
|
||||||
|
where: '${_trackUploadTable.columnEncryptedFilePath} = ?',
|
||||||
|
whereArgs: [encryptedPath],
|
||||||
|
);
|
||||||
|
if (rows.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final row = rows.first;
|
||||||
|
final lastAttemptedAt =
|
||||||
|
row[_trackUploadTable.columnLastAttemptedAt] as int?;
|
||||||
|
final createdAt = row[_trackUploadTable.columnCreatedAt] as int;
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
return (lastAttemptedAt == null ||
|
||||||
|
now - lastAttemptedAt > 3 * 24 * 60 * 60 * 1000) &&
|
||||||
|
now - createdAt > 7 * 24 * 60 * 60 * 1000;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import "package:photos/db/upload_locks_db.dart";
|
|||||||
import "package:photos/models/encryption_result.dart";
|
import "package:photos/models/encryption_result.dart";
|
||||||
import "package:photos/module/upload/model/multipart.dart";
|
import "package:photos/module/upload/model/multipart.dart";
|
||||||
import "package:photos/module/upload/model/xml.dart";
|
import "package:photos/module/upload/model/xml.dart";
|
||||||
|
import "package:photos/services/collections_service.dart";
|
||||||
import "package:photos/services/feature_flag_service.dart";
|
import "package:photos/services/feature_flag_service.dart";
|
||||||
import "package:photos/utils/crypto_util.dart";
|
import "package:photos/utils/crypto_util.dart";
|
||||||
|
|
||||||
@@ -28,8 +29,25 @@ class MultiPartUploader {
|
|||||||
Future<EncryptionResult> getEncryptionResult(
|
Future<EncryptionResult> getEncryptionResult(
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
) {
|
int collectionID,
|
||||||
return _db.getFileEncryptionData(localId, fileHash);
|
) async {
|
||||||
|
final collection =
|
||||||
|
CollectionsService.instance.getCollectionByID(collectionID);
|
||||||
|
if (collection == null) {
|
||||||
|
throw Exception("Collection not found");
|
||||||
|
}
|
||||||
|
final result =
|
||||||
|
await _db.getFileEncryptionData(localId, fileHash, collectionID);
|
||||||
|
final encryptedFileKey = CryptoUtil.base642bin(result.encryptedFileKey);
|
||||||
|
final fileNonce = CryptoUtil.base642bin(result.fileNonce);
|
||||||
|
|
||||||
|
final key = CryptoUtil.base642bin(collection.encryptedKey);
|
||||||
|
final encryptKeyNonce = CryptoUtil.base642bin(result.keyNonce);
|
||||||
|
|
||||||
|
return EncryptionResult(
|
||||||
|
key: CryptoUtil.decryptSync(encryptedFileKey, key, encryptKeyNonce),
|
||||||
|
nonce: fileNonce,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int get multipartPartSizeForUpload {
|
static int get multipartPartSizeForUpload {
|
||||||
@@ -40,6 +58,10 @@ class MultiPartUploader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int> calculatePartCount(int fileSize) async {
|
Future<int> calculatePartCount(int fileSize) async {
|
||||||
|
// Multipart upload is only enabled for internal users
|
||||||
|
// and debug builds till it's battle tested.
|
||||||
|
if (!FeatureFlagService.instance.isInternalUserOrDebugBuild()) return 1;
|
||||||
|
|
||||||
final partCount = (fileSize / multipartPartSizeForUpload).ceil();
|
final partCount = (fileSize / multipartPartSizeForUpload).ceil();
|
||||||
return partCount;
|
return partCount;
|
||||||
}
|
}
|
||||||
@@ -67,20 +89,34 @@ class MultiPartUploader {
|
|||||||
Future<void> createTableEntry(
|
Future<void> createTableEntry(
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
|
int collectionID,
|
||||||
MultipartUploadURLs urls,
|
MultipartUploadURLs urls,
|
||||||
String encryptedFilePath,
|
String encryptedFilePath,
|
||||||
int fileSize,
|
int fileSize,
|
||||||
Uint8List fileKey,
|
Uint8List fileKey,
|
||||||
Uint8List fileNonce,
|
Uint8List fileNonce,
|
||||||
) async {
|
) async {
|
||||||
|
final collection =
|
||||||
|
CollectionsService.instance.getCollectionByID(collectionID);
|
||||||
|
if (collection == null) {
|
||||||
|
throw Exception("Collection not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
final encryptedResult = CryptoUtil.encryptSync(
|
||||||
|
fileKey,
|
||||||
|
CryptoUtil.base642bin(collection.encryptedKey),
|
||||||
|
);
|
||||||
|
|
||||||
await _db.createTrackUploadsEntry(
|
await _db.createTrackUploadsEntry(
|
||||||
localId,
|
localId,
|
||||||
fileHash,
|
fileHash,
|
||||||
|
collectionID,
|
||||||
urls,
|
urls,
|
||||||
encryptedFilePath,
|
encryptedFilePath,
|
||||||
fileSize,
|
fileSize,
|
||||||
CryptoUtil.bin2base64(fileKey),
|
CryptoUtil.bin2base64(encryptedResult.key!),
|
||||||
CryptoUtil.bin2base64(fileNonce),
|
CryptoUtil.bin2base64(fileNonce),
|
||||||
|
CryptoUtil.bin2base64(encryptedResult.nonce!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +124,10 @@ class MultiPartUploader {
|
|||||||
File encryptedFile,
|
File encryptedFile,
|
||||||
String localId,
|
String localId,
|
||||||
String fileHash,
|
String fileHash,
|
||||||
|
int collectionID,
|
||||||
) async {
|
) async {
|
||||||
final multipartInfo = await _db.getCachedLinks(localId, fileHash);
|
final multipartInfo =
|
||||||
|
await _db.getCachedLinks(localId, fileHash, collectionID);
|
||||||
|
|
||||||
Map<int, String> etags = multipartInfo.partETags ?? {};
|
Map<int, String> etags = multipartInfo.partETags ?? {};
|
||||||
|
|
||||||
|
@@ -41,6 +41,7 @@ import 'package:photos/utils/file_uploader_util.dart';
|
|||||||
import "package:photos/utils/file_util.dart";
|
import "package:photos/utils/file_util.dart";
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
import "package:uuid/uuid.dart";
|
||||||
|
|
||||||
class FileUploader {
|
class FileUploader {
|
||||||
static const kMaximumConcurrentUploads = 4;
|
static const kMaximumConcurrentUploads = 4;
|
||||||
@@ -426,11 +427,7 @@ class FileUploader {
|
|||||||
MediaUploadData? mediaUploadData;
|
MediaUploadData? mediaUploadData;
|
||||||
mediaUploadData = await getUploadDataFromEnteFile(file);
|
mediaUploadData = await getUploadDataFromEnteFile(file);
|
||||||
|
|
||||||
final String uniqueID = lockKey +
|
final String uniqueID = const Uuid().v4().toString();
|
||||||
"_" +
|
|
||||||
mediaUploadData.hashData!.fileHash!
|
|
||||||
.replaceAll('+', '')
|
|
||||||
.replaceAll('/', '');
|
|
||||||
|
|
||||||
final encryptedFilePath =
|
final encryptedFilePath =
|
||||||
'$tempDirectory$kUploadTempPrefix${uniqueID}_file.encrypted';
|
'$tempDirectory$kUploadTempPrefix${uniqueID}_file.encrypted';
|
||||||
@@ -453,6 +450,7 @@ class FileUploader {
|
|||||||
await _uploadLocks.doesExists(
|
await _uploadLocks.doesExists(
|
||||||
lockKey,
|
lockKey,
|
||||||
mediaUploadData.hashData!.fileHash!,
|
mediaUploadData.hashData!.fileHash!,
|
||||||
|
collectionID,
|
||||||
);
|
);
|
||||||
|
|
||||||
Uint8List? key;
|
Uint8List? key;
|
||||||
@@ -464,6 +462,7 @@ class FileUploader {
|
|||||||
? await _multiPartUploader.getEncryptionResult(
|
? await _multiPartUploader.getEncryptionResult(
|
||||||
lockKey,
|
lockKey,
|
||||||
mediaUploadData.hashData!.fileHash!,
|
mediaUploadData.hashData!.fileHash!,
|
||||||
|
collectionID,
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
key = multipartEncryptionResult?.key;
|
key = multipartEncryptionResult?.key;
|
||||||
@@ -534,13 +533,10 @@ class FileUploader {
|
|||||||
final String thumbnailObjectKey =
|
final String thumbnailObjectKey =
|
||||||
await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
|
await _putFile(thumbnailUploadURL, encryptedThumbnailFile);
|
||||||
|
|
||||||
// Calculate the number of parts for the file. Multiple part upload
|
// Calculate the number of parts for the file.
|
||||||
// is only enabled for internal users and debug builds till it's battle tested.
|
final count = await _multiPartUploader.calculatePartCount(
|
||||||
final count = FeatureFlagService.instance.isInternalUserOrDebugBuild()
|
|
||||||
? await _multiPartUploader.calculatePartCount(
|
|
||||||
await encryptedFile.length(),
|
await encryptedFile.length(),
|
||||||
)
|
);
|
||||||
: 1;
|
|
||||||
|
|
||||||
late String fileObjectKey;
|
late String fileObjectKey;
|
||||||
|
|
||||||
@@ -553,6 +549,7 @@ class FileUploader {
|
|||||||
encryptedFile,
|
encryptedFile,
|
||||||
lockKey,
|
lockKey,
|
||||||
mediaUploadData.hashData!.fileHash!,
|
mediaUploadData.hashData!.fileHash!,
|
||||||
|
collectionID,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final fileUploadURLs =
|
final fileUploadURLs =
|
||||||
@@ -560,6 +557,7 @@ class FileUploader {
|
|||||||
await _multiPartUploader.createTableEntry(
|
await _multiPartUploader.createTableEntry(
|
||||||
lockKey,
|
lockKey,
|
||||||
mediaUploadData.hashData!.fileHash!,
|
mediaUploadData.hashData!.fileHash!,
|
||||||
|
collectionID,
|
||||||
fileUploadURLs,
|
fileUploadURLs,
|
||||||
encryptedFilePath,
|
encryptedFilePath,
|
||||||
await encryptedFile.length(),
|
await encryptedFile.length(),
|
||||||
|
Reference in New Issue
Block a user