feat: verify embedded backup media checksums
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -200,6 +200,7 @@ final class EmbeddedBackupMediaFile {
|
||||
required this.fileName,
|
||||
required this.mimeType,
|
||||
required this.sizeBytes,
|
||||
required this.sha256,
|
||||
required this.base64,
|
||||
});
|
||||
|
||||
@ -208,6 +209,7 @@ final class EmbeddedBackupMediaFile {
|
||||
final String fileName;
|
||||
final String? mimeType;
|
||||
final int sizeBytes;
|
||||
final String sha256;
|
||||
final String base64;
|
||||
}
|
||||
|
||||
|
||||
@ -4900,6 +4900,7 @@ Map<String, Object?> _mediaFileToJson(EmbeddedBackupMediaFile file) => {
|
||||
'fileName': file.fileName,
|
||||
'mimeType': file.mimeType,
|
||||
'sizeBytes': file.sizeBytes,
|
||||
'sha256': file.sha256,
|
||||
'base64': file.base64,
|
||||
};
|
||||
|
||||
@ -4949,6 +4950,7 @@ List<EmbeddedBackupMediaFile> _mediaFilesFromBackup(Object? value) {
|
||||
fileName: _stringFromBackup(payload, 'fileName'),
|
||||
mimeType: payload['mimeType'] as String?,
|
||||
sizeBytes: payload['sizeBytes'] as int? ?? 0,
|
||||
sha256: _stringFromBackup(payload, 'sha256'),
|
||||
base64: _stringFromBackup(payload, 'base64'),
|
||||
);
|
||||
})
|
||||
|
||||
@ -2,6 +2,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
@ -101,6 +102,7 @@ final class PathProviderLocalMediaStorage
|
||||
fileName: p.basename(file.path),
|
||||
mimeType: resource.payload['mimeType'] as String?,
|
||||
sizeBytes: bytes.length,
|
||||
sha256: crypto.sha256.convert(bytes).toString(),
|
||||
base64: base64Encode(bytes),
|
||||
),
|
||||
);
|
||||
@ -123,6 +125,9 @@ final class PathProviderLocalMediaStorage
|
||||
if (bytes == null) {
|
||||
continue;
|
||||
}
|
||||
if (crypto.sha256.convert(bytes).toString() != file.sha256) {
|
||||
continue;
|
||||
}
|
||||
final extension = p.extension(file.fileName).toLowerCase();
|
||||
final target = File(
|
||||
p.join(directory.path, '${file.mediaAssetId}$extension'),
|
||||
|
||||
@ -226,7 +226,7 @@ packages:
|
||||
source: hosted
|
||||
version: "0.3.5+4"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
|
||||
@ -11,6 +11,7 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
cupertino_icons: ^1.0.8
|
||||
crypto: ^3.0.6
|
||||
drift: ^2.34.2
|
||||
drift_flutter: ^0.3.1
|
||||
flutter_secure_storage: ^9.2.4
|
||||
|
||||
@ -2359,6 +2359,8 @@ void main() {
|
||||
fileName: 'media-1.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 4,
|
||||
sha256:
|
||||
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
||||
base64: 'dGVzdA==',
|
||||
),
|
||||
],
|
||||
@ -2444,6 +2446,8 @@ void main() {
|
||||
fileName: 'media-1.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 4,
|
||||
sha256:
|
||||
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
||||
base64: 'dGVzdA==',
|
||||
),
|
||||
],
|
||||
@ -2485,9 +2489,7 @@ void main() {
|
||||
expect(repository.appliedModes, isEmpty);
|
||||
});
|
||||
|
||||
test(
|
||||
'Data import use case restores media and applies selected mode',
|
||||
() async {
|
||||
test('Data import use case restores media and applies selected mode', () async {
|
||||
final repository = _FakeLocalDataBackupRepository();
|
||||
final mediaStore = _FakeLocalBackupMediaStore(
|
||||
restored: const {
|
||||
@ -2526,6 +2528,8 @@ void main() {
|
||||
fileName: 'media-1.png',
|
||||
mimeType: 'image/png',
|
||||
sizeBytes: 4,
|
||||
sha256:
|
||||
'9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08',
|
||||
base64: 'dGVzdA==',
|
||||
),
|
||||
],
|
||||
@ -2540,16 +2544,10 @@ void main() {
|
||||
expect(result.insertedCount, 0);
|
||||
expect(repository.appliedModes, [LocalBackupImportMode.replaceAll]);
|
||||
expect(
|
||||
repository
|
||||
.appliedSnapshots
|
||||
.single
|
||||
.mediaAssets
|
||||
.single
|
||||
.payload['localUri'],
|
||||
repository.appliedSnapshots.single.mediaAssets.single.payload['localUri'],
|
||||
'file:///restored/media-1.png',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('Program use case duplicates deeply with copy name conflicts', () async {
|
||||
final programRepository = _FakeProgramRepository();
|
||||
|
||||
Reference in New Issue
Block a user