Switch to libsodium for file encryption and decryption

This commit is contained in:
Vishnu Mohandas 2020-09-26 00:37:32 +05:30
parent 44866f7ffe
commit 727a1684ce
9 changed files with 198 additions and 117 deletions

View file

@ -1,6 +1,7 @@
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';
@ -31,8 +32,9 @@ class FilesDB {
static final columnCreationTime = 'creation_time';
static final columnModificationTime = 'modification_time';
static final columnUpdationTime = 'updation_time';
static final columnEncryptedPassword = 'encrypted_password';
static final columnEncryptedPasswordIV = 'encrypted_password_iv';
static final columnFileDecryptionParams = 'file_decryption_params';
static final columnThumbnailDecryptionParams = 'thumbnail_decryption_params';
static final columnMetadataDecryptionParams = 'metadata_decryption_params';
// make this a singleton class
FilesDB._privateConstructor();
@ -74,8 +76,9 @@ class FilesDB {
$columnCreationTime TEXT NOT NULL,
$columnModificationTime TEXT NOT NULL,
$columnUpdationTime TEXT,
$columnEncryptedPassword TEXT,
$columnEncryptedPasswordIV TEXT
$columnFileDecryptionParams TEXT,
$columnThumbnailDecryptionParams TEXT,
$columnMetadataDecryptionParams TEXT
)
''');
}
@ -224,15 +227,18 @@ class FilesDB {
int generatedID,
int uploadedID,
int updationTime,
String encryptedKey,
String iv,
DecryptionParams fileDecryptionParams,
DecryptionParams thumbnailDecryptionParams,
DecryptionParams metadataDecryptionParams,
) async {
final db = await instance.database;
final values = new Map<String, dynamic>();
values[columnUploadedFileID] = uploadedID;
values[columnUpdationTime] = updationTime;
values[columnEncryptedPassword] = encryptedKey;
values[columnEncryptedPasswordIV] = iv;
values[columnFileDecryptionParams] = fileDecryptionParams.toJson();
values[columnThumbnailDecryptionParams] =
thumbnailDecryptionParams.toJson();
values[columnMetadataDecryptionParams] = metadataDecryptionParams.toJson();
return await db.update(
table,
values,
@ -384,8 +390,16 @@ class FilesDB {
row[columnCreationTime] = file.creationTime;
row[columnModificationTime] = file.modificationTime;
row[columnUpdationTime] = file.updationTime;
row[columnEncryptedPassword] = file.encryptedPassword;
row[columnEncryptedPasswordIV] = file.encryptedPasswordIV;
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();
return row;
}
@ -408,8 +422,12 @@ class FilesDB {
file.updationTime = row[columnUpdationTime] == null
? -1
: int.parse(row[columnUpdationTime]);
file.encryptedPassword = row[columnEncryptedPassword];
file.encryptedPasswordIV = row[columnEncryptedPasswordIV];
file.fileDecryptionParams =
DecryptionParams.fromJson(row[columnFileDecryptionParams]);
file.thumbnailDecryptionParams =
DecryptionParams.fromJson(row[columnThumbnailDecryptionParams]);
file.metadataDecryptionParams =
DecryptionParams.fromJson(row[columnMetadataDecryptionParams]);
return file;
}
}

View file

@ -5,6 +5,7 @@ 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';
@ -28,18 +29,23 @@ class DiffFetcher {
if (response != null) {
Bus.instance.fire(RemoteSyncEvent(true));
final diff = response.data["diff"] as List;
for (final fileItem in diff) {
for (final item in diff) {
final file = File();
file.uploadedFileID = fileItem["id"];
file.ownerID = fileItem["ownerID"];
file.updationTime = fileItem["updationTime"];
file.uploadedFileID = item["id"];
file.ownerID = item["ownerID"];
file.updationTime = item["updationTime"];
file.isEncrypted = true;
file.encryptedPassword = fileItem["encryptedPassword"];
file.encryptedPasswordIV = fileItem["encryptedPasswordIV"];
file.fileDecryptionParams =
DecryptionParams.fromMap(item["file"]["decryptionParams"]);
file.thumbnailDecryptionParams = DecryptionParams.fromMap(
item["thumbnail"]["decryptionParams"]);
file.metadataDecryptionParams = DecryptionParams.fromMap(
item["metadata"]["decryptionParams"]);
Map<String, dynamic> metadata = jsonDecode(utf8.decode(
await CryptoUtil.decryptDataToData(
base64.decode(fileItem["encryptedMetadata"]),
file.getPassword())));
await CryptoUtil.decryptWithDecryptionParams(
base64.decode(item["metadata"]["encryptedData"]),
file.metadataDecryptionParams,
Configuration.instance.getBase64EncodedKey())));
file.applyMetadata(metadata);
files.add(file);
}

View file

@ -4,8 +4,8 @@ import 'package:dio/dio.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/file_type.dart';
import 'package:photos/models/upload_url.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_name_util.dart';
@ -41,46 +41,30 @@ class FileUploader {
});
}
// TODO: Remove encryption and decryption time logging
Future<File> encryptAndUploadFile(File file) async {
_logger.info("Uploading " + file.toString());
final password = CryptoUtil.getSecureRandomString(length: 32);
final iv = CryptoUtil.getSecureRandomBytes(length: 16);
final base64EncodedIV = base64.encode(iv);
final encryptedKey = CryptoUtil.aesEncrypt(
utf8.encode(password), Configuration.instance.getKey(), iv);
final base64EncodedEncryptedKey = base64.encode(encryptedKey);
final encryptedFileName = file.generatedID.toString() + ".aes";
final encryptedFileName = file.generatedID.toString() + ".encrypted";
final tempDirectory = Configuration.instance.getTempDirectory();
final encryptedFilePath = tempDirectory + encryptedFileName;
_logger.info("File size " +
(await (await file.getAsset()).file).lengthSync().toString());
final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
if (file.fileType == FileType.image) {
await CryptoUtil.encryptDataToFile(
await getBytesFromDisk(file), encryptedFilePath, password);
} else {
await CryptoUtil.encryptFileToFile(
(await (await file.getAsset()).originFile).path,
encryptedFilePath,
password);
}
final encryptionStopTime = DateTime.now().millisecondsSinceEpoch;
_logger.info("Encryption time: " +
(encryptionStopTime - encryptionStartTime).toString());
final decryptionStartTime = DateTime.now().millisecondsSinceEpoch;
await CryptoUtil.decryptFileToData(encryptedFilePath, password);
final decryptionStopTime = DateTime.now().millisecondsSinceEpoch;
_logger.info("Decryption time: " +
(decryptionStopTime - decryptionStartTime).toString());
final sourceFile = (await (await file.getAsset()).originFile);
final encryptedFile = io.File(encryptedFilePath);
final fileAttributes =
await CryptoUtil.chachaEncrypt(sourceFile, encryptedFile);
final fileUploadURL = await getUploadURL();
String fileObjectKey =
await putFile(fileUploadURL, io.File(encryptedFilePath));
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,
@ -88,24 +72,50 @@ class FileUploader {
quality: 50,
));
final encryptedThumbnailName =
file.generatedID.toString() + "_thumbnail.aes";
file.generatedID.toString() + "_thumbnail.encrypted";
final encryptedThumbnailPath = tempDirectory + encryptedThumbnailName;
await CryptoUtil.encryptDataToFile(
thumbnailData, encryptedThumbnailPath, password);
final encryptedThumbnail = await CryptoUtil.encrypt(thumbnailData);
io.File(encryptedThumbnailPath)
.writeAsBytesSync(encryptedThumbnail.encryptedData.bytes);
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 metadata = jsonEncode(file.getMetadata());
final encryptedMetadata =
await CryptoUtil.encryptDataToData(utf8.encode(metadata), password);
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 = {
"fileObjectKey": fileObjectKey,
"thumbnailObjectKey": thumbnailObjectKey,
"encryptedMetadata": base64.encode(encryptedMetadata),
"encryptedPassword": base64EncodedEncryptedKey,
"encryptedPasswordIV": base64EncodedIV,
"file": {
"objectKey": fileObjectKey,
"decryptionParams": fileDecryptionParams.toMap(),
},
"thumbnail": {
"objectKey": thumbnailObjectKey,
"decryptionParams": thumbnailDecryptionParams,
},
"metadata": {
"encryptedData": encryptedMetadata.encryptedData.base64,
"decryptionParams": metadataDecryptionParams,
}
};
return _dio
.post(
@ -115,14 +125,15 @@ class FileUploader {
data: data,
)
.then((response) {
io.File(encryptedFilePath).deleteSync();
encryptedFile.deleteSync();
io.File(encryptedThumbnailPath).deleteSync();
final data = response.data;
file.uploadedFileID = data["id"];
file.updationTime = data["updationTime"];
file.ownerID = data["ownerID"];
file.encryptedPassword = base64EncodedEncryptedKey;
file.encryptedPasswordIV = base64EncodedIV;
file.fileDecryptionParams = fileDecryptionParams;
file.thumbnailDecryptionParams = thumbnailDecryptionParams;
file.metadataDecryptionParams = metadataDecryptionParams;
return file;
});
}

View file

@ -71,11 +71,13 @@ class FolderSharingService {
var existingPhoto =
await FilesDB.instance.getMatchingRemoteFile(file.uploadedFileID);
await FilesDB.instance.update(
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.encryptedPassword,
file.encryptedPasswordIV);
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
);
} catch (e) {
await FilesDB.instance.insert(file);
}

View file

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

View file

@ -1,11 +1,9 @@
import 'dart:convert';
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';
import 'package:photos/utils/crypto_util.dart';
class File {
int generatedID;
@ -21,8 +19,9 @@ class File {
int updationTime;
Location location;
FileType fileType;
String encryptedPassword;
String encryptedPasswordIV;
DecryptionParams fileDecryptionParams;
DecryptionParams thumbnailDecryptionParams;
DecryptionParams metadataDecryptionParams;
File();
@ -36,8 +35,6 @@ class File {
creationTime = json["creationTime"];
modificationTime = json["modificationTime"];
updationTime = json["updationTime"];
encryptedPassword = json["encryptedPassword"];
encryptedPasswordIV = json["encryptedPasswordIV"];
}
static Future<File> fromAsset(
@ -137,14 +134,6 @@ class File {
Configuration.instance.getToken();
}
String getPassword() {
if (encryptedPassword == null) {
return null;
}
return utf8.decode(CryptoUtil.aesDecrypt(base64.decode(encryptedPassword),
Configuration.instance.getKey(), base64.decode(encryptedPasswordIV)));
}
@override
String toString() {
return '''File(generatedId: $generatedID, uploadedFileId: $uploadedFileID,

View file

@ -223,11 +223,13 @@ class PhotoSyncManager {
uploadedFile = await _uploader.uploadFile(file);
}
await _db.update(
file.generatedID,
uploadedFile.uploadedFileID,
uploadedFile.updationTime,
file.encryptedPassword,
file.encryptedPasswordIV);
file.generatedID,
uploadedFile.uploadedFileID,
uploadedFile.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
);
Bus.instance.fire(PhotoUploadEvent(
completed: i + 1, total: filesToBeUploaded.length));
} catch (e) {
@ -248,11 +250,13 @@ class PhotoSyncManager {
file.modificationTime,
alternateTitle: getHEICFileNameForJPG(file));
await _db.update(
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.encryptedPassword,
file.encryptedPasswordIV);
existingPhoto.generatedID,
file.uploadedFileID,
file.updationTime,
file.fileDecryptionParams,
file.thumbnailDecryptionParams,
file.metadataDecryptionParams,
);
} catch (e) {
file.localID = null; // File uploaded from a different device
await _db.insert(file);

View file

@ -8,7 +8,10 @@ import 'package:flutter_sodium/flutter_sodium.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/decryption_params.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:steel_crypt/steel_crypt.dart' as steel;
import 'package:uuid/uuid.dart';
@ -19,7 +22,36 @@ class CryptoUtil {
static int decryptionBlockSize =
encryptionBlockSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes;
static Future<EncryptedFileAttributes> chachaEncrypt(
static Future<EncryptedData> encrypt(Uint8List source,
{Uint8List key}) async {
if (key == null) {
key = Sodium.cryptoSecretboxKeygen();
}
final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
final encryptedData = Sodium.cryptoSecretboxEasy(source, nonce, key);
return EncryptedData(
EncryptionAttribute(bytes: key),
EncryptionAttribute(bytes: nonce),
EncryptionAttribute(bytes: encryptedData));
}
static Future<Uint8List> decrypt(
String base64Cipher, String base64Key, String base64Nonce) async {
return Sodium.cryptoSecretboxOpenEasy(Sodium.base642bin(base64Cipher),
Sodium.base642bin(base64Nonce), Sodium.base642bin(base64Key));
}
static Future<Uint8List> decryptWithDecryptionParams(
Uint8List source, DecryptionParams params, String base64Kek) async {
final key = Sodium.cryptoSecretboxOpenEasy(
Sodium.base642bin(params.encryptedKey),
Sodium.base642bin(params.keyDecryptionNonce),
Sodium.base642bin(base64Kek));
return Sodium.cryptoSecretboxOpenEasy(
source, Sodium.base642bin(params.nonce), key);
}
static Future<ChaChaAttributes> chachaEncrypt(
io.File sourceFile,
io.File destinationFile,
) async {
@ -62,13 +94,14 @@ class CryptoUtil {
(DateTime.now().millisecondsSinceEpoch - encryptionStartTime)
.toString());
return EncryptedFileAttributes(key, initPushResult.header);
return ChaChaAttributes(EncryptionAttribute(bytes: key),
EncryptionAttribute(bytes: initPushResult.header));
}
static Future<void> chachaDecrypt(
io.File sourceFile,
io.File destinationFile,
EncryptedFileAttributes attributes,
ChaChaAttributes attributes,
) async {
var decryptionStartTime = DateTime.now().millisecondsSinceEpoch;
@ -79,7 +112,7 @@ class CryptoUtil {
final outputFile =
await (destinationFile.open(mode: io.FileMode.writeOnlyAppend));
final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
attributes.header, attributes.key);
attributes.header.bytes, attributes.key.bytes);
var bytesRead = 0;
var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;

View file

@ -13,6 +13,8 @@ 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';
@ -139,23 +141,36 @@ Future<io.File> getThumbnailFromServer(File file) async {
Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
{ProgressCallback progressCallback}) async {
final temporaryPath = Configuration.instance.getTempDirectory() +
final encryptedFilePath = Configuration.instance.getTempDirectory() +
file.generatedID.toString() +
".aes";
".encrypted";
final decryptedFilePath = Configuration.instance.getTempDirectory() +
file.generatedID.toString() +
".decrypted";
final encryptedFile = io.File(encryptedFilePath);
final decryptedFile = io.File(decryptedFilePath);
return Dio()
.download(
file.getDownloadUrl(),
temporaryPath,
encryptedFilePath,
onReceiveProgress: progressCallback,
)
.then((_) async {
final data =
await CryptoUtil.decryptFileToData(temporaryPath, file.getPassword());
io.File(temporaryPath).deleteSync();
var attributes = ChaChaAttributes(
EncryptionAttribute(base64: file.fileDecryptionParams.header),
EncryptionAttribute(
bytes: await CryptoUtil.decrypt(
file.fileDecryptionParams.encryptedKey,
Configuration.instance.getBase64EncodedKey(),
file.fileDecryptionParams.nonce,
)));
await CryptoUtil.chachaDecrypt(encryptedFile, decryptedFile, attributes);
encryptedFile.deleteSync();
decryptedFile.deleteSync();
final fileExtension = extension(file.title).substring(1).toLowerCase();
return cacheManager.putFile(
file.getDownloadUrl(),
data,
decryptedFile.readAsBytesSync(),
eTag: file.getDownloadUrl(),
maxAge: Duration(days: 365),
fileExtension: fileExtension,
@ -166,11 +181,14 @@ Future<io.File> _downloadAndDecrypt(File file, BaseCacheManager cacheManager,
Future<io.File> _downloadAndDecryptThumbnail(File file) async {
final temporaryPath = Configuration.instance.getTempDirectory() +
file.generatedID.toString() +
"_thumbnail.aes";
"_thumbnail.decrypted";
return Dio().download(file.getThumbnailUrl(), temporaryPath).then((_) async {
final data =
await CryptoUtil.decryptFileToData(temporaryPath, file.getPassword());
io.File(temporaryPath).deleteSync();
final encryptedFile = io.File(temporaryPath);
final data = await CryptoUtil.decryptWithDecryptionParams(
encryptedFile.readAsBytesSync(),
file.thumbnailDecryptionParams,
Configuration.instance.getBase64EncodedKey());
encryptedFile.deleteSync();
return ThumbnailCacheManager().putFile(
file.getThumbnailUrl(),
data,