Move to XChaCha for thumbnail and metadata encryption

This commit is contained in:
Vishnu Mohandas 2020-10-07 04:25:59 +05:30
parent bdd2e35a66
commit 27f019af57
11 changed files with 137 additions and 206 deletions

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';
@ -32,9 +31,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();
@ -76,9 +77,12 @@ class FilesDB {
$columnCreationTime TEXT NOT NULL,
$columnModificationTime TEXT NOT NULL,
$columnUpdationTime TEXT,
$columnFileDecryptionParams TEXT,
$columnThumbnailDecryptionParams TEXT,
$columnMetadataDecryptionParams TEXT
$columnEncryptedKey TEXT,
$columnKeyDecryptionNonce TEXT,
$columnUpdationTime TEXT,
$columnFileDecryptionHeader TEXT,
$columnThumbnailDecryptionHeader TEXT,
$columnMetadataDecryptionHeader TEXT
)
''');
}
@ -227,18 +231,21 @@ class FilesDB {
int generatedID,
int uploadedID,
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[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,
@ -390,16 +397,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;
}
@ -422,12 +424,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

@ -0,0 +1,8 @@
import 'dart:typed_data';
class ChaChaEncryptionResult {
final Uint8List encryptedData;
final Uint8List header;
ChaChaEncryptionResult(this.encryptedData, this.header);
}

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,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';
@ -19,9 +18,11 @@ 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();
@ -137,10 +138,7 @@ class File {
@override
String toString() {
return '''File(generatedId: $generatedID, uploadedFileId: $uploadedFileID,
localId: $localID, title: $title, deviceFolder: $deviceFolder,
fileDecryptionParams: $fileDecryptionParams,
thumbnailDecryptionParams: $thumbnailDecryptionParams,
metadataDecryptionParams: $metadataDecryptionParams,
localId: $localID, title: $title, deviceFolder: $deviceFolder,
location: $location, fileType: $fileType, creationTime: $creationTime,
modificationTime: $modificationTime, updationTime: $updationTime)''';
}

View file

@ -0,0 +1,5 @@
import 'package:logging/logging.dart';
class CollectionsService {
final _logger = Logger("CollectionsService");
}

View file

@ -74,9 +74,11 @@ class FolderSharingService {
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
file.encryptedKey,
file.keyDecryptionNonce,
file.fileDecryptionHeader,
file.thumbnailDecryptionHeader,
file.metadataDecryptionHeader,
);
} catch (e) {
await FilesDB.instance.insert(file);

View file

@ -229,9 +229,11 @@ class SyncService {
file.generatedID,
uploadedFile.uploadedFileID,
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 +247,18 @@ 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.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

@ -5,6 +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/chacha_encryption_result.dart';
import 'package:photos/models/encrypted_data_attributes.dart';
import 'package:photos/models/encrypted_file_attributes.dart';
@ -32,7 +33,7 @@ bool cryptoPwhashStrVerify(Map<String, dynamic> args) {
return Sodium.cryptoPwhashStrVerify(args["hash"], args["input"]) == 0;
}
ChaChaAttributes chachaEncrypt(Map<String, dynamic> args) {
ChaChaAttributes chachaEncryptFile(Map<String, dynamic> args) {
final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
final logger = Logger("ChaChaEncrypt");
final sourceFile = io.File(args["sourceFilePath"]);
@ -111,15 +112,33 @@ class CryptoUtil {
args["source"] = source;
args["nonce"] = nonce;
args["key"] = key;
final encryptedData =
await Computer().compute(cryptoSecretboxEasy, param: args);
final encryptedData = cryptoSecretboxEasy(args);
return EncryptedData(
EncryptionAttribute(bytes: key),
EncryptionAttribute(bytes: nonce),
EncryptionAttribute(bytes: encryptedData));
}
static ChaChaEncryptionResult encryptStream(Uint8List source, Uint8List key) {
final initPushResult =
Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
initPushResult.state,
source,
null,
Sodium.cryptoSecretstreamXchacha20poly1305TagFinal);
return ChaChaEncryptionResult(encryptedData, initPushResult.header);
}
static Uint8List decryptStream(
Uint8List source, Uint8List key, Uint8List header) {
final pullState =
Sodium.cryptoSecretstreamXchacha20poly1305InitPull(header, key);
final pullResult =
Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, source, null);
return pullResult.m;
}
static Future<Uint8List> decrypt(
Uint8List cipher, Uint8List key, Uint8List nonce,
{bool background = false}) async {
@ -141,7 +160,7 @@ class CryptoUtil {
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(

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");
@ -36,21 +36,16 @@ class DiffFetcher {
file.ownerID = item["ownerID"];
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["fileDecryptionHeader"];
file.thumbnailDecryptionHeader =
item["thumbnailDecryptionHeader"];
file.metadataDecryptionHeader = item["metadataDecryptionHeader"];
final encodedMetadata = CryptoUtil.decryptStream(
Sodium.base642bin(item["metadata"]["encryptedData"]),
metadataDecryptionKey,
Sodium.base642bin(file.metadataDecryptionParams.nonce),
await decryptFileKey(file),
Sodium.base642bin(file.metadataDecryptionHeader),
);
Map<String, dynamic> metadata =
jsonDecode(utf8.decode(encodedMetadata));

View file

@ -1,10 +1,10 @@
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/utils/crypto_util.dart';
@ -56,16 +56,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,47 +64,37 @@ class FileUploader {
final encryptedThumbnailName =
file.generatedID.toString() + "_thumbnail.encrypted";
final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
final encryptedThumbnail = await CryptoUtil.encrypt(thumbnailData);
final encryptedThumbnail =
CryptoUtil.encryptStream(thumbnailData, fileAttributes.key.bytes);
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,
final encryptedMetadata = CryptoUtil.encryptStream(
utf8.encode(jsonEncode(file.getMetadata())), fileAttributes.key.bytes);
final encryptedFileKey = await CryptoUtil.encrypt(
fileAttributes.key.bytes,
key: Configuration.instance.getKey(),
);
final thumbnailDecryptionParams = DecryptionParams(
encryptedKey: encryptedThumbnailKey.encryptedData.base64,
keyDecryptionNonce: encryptedThumbnailKey.nonce.base64,
nonce: encryptedThumbnail.nonce.base64,
);
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 data = {
"encryptedKey": encryptedFileKey.encryptedData.base64,
"keyDecryptionNonce": encryptedFileKey.nonce.base64,
"file": {
"objectKey": fileObjectKey,
"decryptionParams": fileDecryptionParams.toMap(),
"header": fileAttributes.header.base64,
},
"thumbnail": {
"objectKey": thumbnailObjectKey,
"decryptionParams": thumbnailDecryptionParams.toMap(),
"header": Sodium.bin2base64(encryptedThumbnail.header),
},
"metadata": {
"encryptedData": encryptedMetadata.encryptedData.base64,
"decryptionParams": metadataDecryptionParams.toMap(),
"encryptedData": Sodium.bin2base64(encryptedMetadata.encryptedData),
"header": Sodium.bin2base64(encryptedMetadata.header),
}
};
return _dio
@ -131,9 +111,13 @@ 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 = encryptedFileKey.encryptedData.base64;
file.keyDecryptionNonce = encryptedFileKey.nonce.base64;
file.fileDecryptionHeader = fileAttributes.header.base64;
file.thumbnailDecryptionHeader =
Sodium.bin2base64(encryptedThumbnail.header);
file.metadataDecryptionHeader =
Sodium.bin2base64(encryptedMetadata.header);
return file;
});
}

View file

@ -178,12 +178,9 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
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),
bytes: await decryptFileKey(file),
),
EncryptionAttribute(base64: file.fileDecryptionHeader),
);
await CryptoUtil.decryptFile(
encryptedFilePath, decryptedFilePath, attributes);
@ -209,14 +206,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 = await decryptFileKey(file);
final data = CryptoUtil.decryptStream(
encryptedFile.readAsBytesSync(),
thumbnailDecryptionKey,
Sodium.base642bin(file.thumbnailDecryptionParams.nonce),
Sodium.base642bin(file.thumbnailDecryptionHeader),
);
encryptedFile.deleteSync();
return ThumbnailCacheManager().putFile(
@ -227,3 +221,10 @@ Future<io.File> _downloadAndDecryptThumbnail(File file) async {
);
});
}
Future<Uint8List> decryptFileKey(File file) {
return CryptoUtil.decrypt(
Sodium.base642bin(file.encryptedKey),
Configuration.instance.getKey(),
Sodium.base642bin(file.keyDecryptionNonce));
}