From d3af2b1f69af99957fca6ce7f29b84d5eb1046d7 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Fri, 5 Jan 2024 16:49:43 +0000 Subject: [PATCH] (mobile): ios - calculate hash using CryptoKit (#5976) * ios: calculate hash using CryptoKit * chore: remove unused crypto dep --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex Tran --- .../BackgroundServicePlugin.swift | 51 +++++++++++++++++++ .../background.service.dart | 1 + mobile/lib/shared/services/hash.service.dart | 37 ++------------ mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 1 - 5 files changed, 58 insertions(+), 34 deletions(-) diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift index 622f13967a..8e5db08b1e 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift @@ -8,6 +8,7 @@ import Flutter import BackgroundTasks import path_provider_foundation +import CryptoKit class BackgroundServicePlugin: NSObject, FlutterPlugin { @@ -102,12 +103,62 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { case "backgroundAppRefreshEnabled": handleBackgroundRefreshStatus(call: call, result: result) break + case "digestFiles": + handleDigestFiles(call: call, result: result) + break default: result(FlutterMethodNotImplemented) break } } + // Calculates the SHA-1 hash of each file from the list of paths provided + func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) { + + let bufsize = 2 * 1024 * 1024 + // Private error to throw if file cannot be read + enum DigestError: String, LocalizedError { + case NoFileHandle = "Cannot Open File Handle" + + public var errorDescription: String? { self.rawValue } + } + + // Parse the arguments or else fail + guard let args = call.arguments as? Array else { + print("Cannot parse args as array: \(String(describing: call.arguments))") + result(FlutterError(code: "Malformed", + message: "Received args is not an Array", + details: nil)) + return + } + + // Compute hash in background thread + DispatchQueue.global(qos: .background).async { + var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count) + for i in (0 ..< args.count) { + do { + guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle } + var hasher = Insecure.SHA1.init(); + while autoreleasepool(invoking: { + let chunk = file.readData(ofLength: bufsize) + guard !chunk.isEmpty else { return false } // EOF + hasher.update(data: chunk) + return true // continue + }) { } + let digest = hasher.finalize() + hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator()))) + } catch { + print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)") + } + } + + // Return result in main thread + DispatchQueue.main.async { + result(Array(hashes)) + } + } + } + // Called by the flutter code when enabled so that we can turn on the backround services // and save the callback information to communicate on this method channel public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 45d921a142..cbee121105 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -132,6 +132,7 @@ class BackgroundService { } } + // Yet to be implemented Future digestFile(String path) { return _foregroundChannel.invokeMethod("digestFile", [path]); } diff --git a/mobile/lib/shared/services/hash.service.dart b/mobile/lib/shared/services/hash.service.dart index 914c8bc287..2a0d4f2f84 100644 --- a/mobile/lib/shared/services/hash.service.dart +++ b/mobile/lib/shared/services/hash.service.dart @@ -1,7 +1,5 @@ -import 'dart:convert'; import 'dart:io'; -import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; @@ -119,37 +117,12 @@ class HashService { /// Hashes the given files and returns a list of the same length /// files that could not be hashed have a `null` value Future> _hashFiles(List paths) async { - if (Platform.isAndroid) { - final List? hashes = - await _backgroundService.digestFiles(paths); - if (hashes == null) { - throw Exception("Hashing ${paths.length} files failed"); - } - return hashes; - } else if (Platform.isIOS) { - final List result = List.filled(paths.length, null); - for (int i = 0; i < paths.length; i++) { - result[i] = await _hashAssetDart(File(paths[i])); - } - return result; - } else { - throw Exception("_hashFiles implementation missing"); + final List? hashes = + await _backgroundService.digestFiles(paths); + if (hashes == null) { + throw Exception("Hashing ${paths.length} files failed"); } - } - - /// Hashes a single file using Dart's crypto package - Future _hashAssetDart(File f) async { - late Digest output; - final sink = sha1.startChunkedConversion( - ChunkedConversionSink.withCallback((accumulated) { - output = accumulated.first; - }), - ); - await for (final chunk in f.openRead()) { - sink.add(chunk); - } - sink.close(); - return Uint8List.fromList(output.bytes); + return hashes; } /// Converts [AssetEntity]s that were successfully hashed to [Asset]s diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index dbbba41195..be8a29a707 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -282,7 +282,7 @@ packages: source: hosted version: "0.3.3+4" crypto: - dependency: "direct main" + dependency: transitive description: name: crypto sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 036c310bab..fe08258dcb 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -54,7 +54,6 @@ dependencies: permission_handler: ^10.2.0 device_info_plus: ^8.1.0 connectivity_plus: ^4.0.1 - crypto: ^3.0.3 # TODO remove once native crypto is used on iOS wakelock_plus: ^1.1.1 flutter_local_notifications: ^15.1.0+1 timezone: ^0.9.2