From b79b42d5d37ca82015ab94ef35ffa7b3b533cb18 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Fri, 14 Jun 2024 00:11:50 -0300 Subject: [PATCH] Hash verification changes, closes #89, closes #103 --- lib/models/app.dart | 21 ++++++-- lib/screens/app_detail_screen.dart | 81 +++++++++++++++++++++++++----- 2 files changed, 86 insertions(+), 16 deletions(-) diff --git a/lib/models/app.dart b/lib/models/app.dart index 1fb4383..6f1dd80 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -97,7 +97,7 @@ class App extends BaseApp with DataModelMixin { } final hash = latestMetadata!.hash!; - final size = latestMetadata!.size ?? ''; + final size = int.tryParse(latestMetadata!.size ?? ''); final dir = await getApplicationSupportDirectory(); final file = File(path.join(dir.path, hash)); @@ -106,11 +106,22 @@ class App extends BaseApp with DataModelMixin { notifier.state = DeviceInstallProgress(); if (await _isHashMismatch(file.path, hash)) { + var e = 'Hash mismatch, '; + if (size != null) { + if (size == await file.length()) { + e += 'likely a malicious file.'; + } else { + e += 'bad data ($size is not ${await file.length()}).'; + } + } else { + e += 'possibly broken download.'; + } + e += ' Please try again.'; await file.delete(); - notifier.state = ErrorInstallProgress( - Exception('Hash mismatch, aborted installation')); + notifier.state = ErrorInstallProgress(Exception(e)); return; } + notifier.state = HashVerifiedInstallProgress(); final result = await InstallPlugin.install(file.path); if (result['isSuccess']) { @@ -139,7 +150,7 @@ class App extends BaseApp with DataModelMixin { uri = Uri.parse(backupUrl); response = await client.send(http.Request('GET', uri)); } - final totalBytes = response.contentLength ?? int.tryParse(size) ?? 1; + final totalBytes = response.contentLength ?? size ?? 1; sub = response.stream.listen((chunk) { final data = Uint8List.fromList(chunk); @@ -333,6 +344,8 @@ class DownloadingInstallProgress extends AppInstallProgress { class DeviceInstallProgress extends AppInstallProgress {} +class HashVerifiedInstallProgress extends AppInstallProgress {} + class ErrorInstallProgress extends AppInstallProgress { final Exception e; ErrorInstallProgress(this.e); diff --git a/lib/screens/app_detail_screen.dart b/lib/screens/app_detail_screen.dart index 739de6a..0a3875c 100644 --- a/lib/screens/app_detail_screen.dart +++ b/lib/screens/app_detail_screen.dart @@ -184,15 +184,15 @@ class AppDetailScreen extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text('SHA-256 hash (tap to copy)'), + Text('APK package SHA-256'), Flexible( child: GestureDetector( onTap: () { Clipboard.setData(ClipboardData( text: app.latestMetadata!.hash!)); showToast( - 'Copied SHA-256 to the clipboard', - duration: Duration(seconds: 2), + 'Copied APK package SHA-256 to the clipboard', + duration: Duration(seconds: 3), animDuration: Duration(milliseconds: 300), animation: @@ -204,9 +204,60 @@ class AppDetailScreen extends HookConsumerWidget { context: context, ); }, - child: Text( - '${app.latestMetadata!.hash!.substring(0, 6)}...${app.latestMetadata!.hash!.substring(58, 64)}', - maxLines: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${app.latestMetadata!.hash!.substring(0, 6)}...${app.latestMetadata!.hash!.substring(58, 64)}', + maxLines: 1, + ), + Gap(6), + Icon(Icons.copy_rounded, size: 18) + ], + ), + ), + ), + ], + ), + ), + if (app.latestMetadata?.apkSignatureHash != null) + Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('APK certificate SHA-256'), + Flexible( + child: GestureDetector( + onTap: () { + Clipboard.setData(ClipboardData( + text: app.latestMetadata! + .apkSignatureHash!)); + showToast( + 'Copied APK certificate SHA-256 to the clipboard', + duration: Duration(seconds: 3), + animDuration: + Duration(milliseconds: 300), + animation: + StyledToastAnimation.fade, + reverseAnimation: + StyledToastAnimation.fade, + position: + StyledToastPosition.bottom, + context: context, + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '${app.latestMetadata!.apkSignatureHash!.substring(0, 6)}...${app.latestMetadata!.apkSignatureHash!.substring(58, 64)}', + maxLines: 1, + ), + Gap(6), + Icon(Icons.copy_rounded, size: 18) + ], ), ), ), @@ -461,7 +512,7 @@ class InstallButton extends ConsumerWidget { }, ErrorInstallProgress(:final e) => () { // show error and reset state to idle - context.showError(e.toString()); + context.showError((e as dynamic).message); ref .read(installationProgressProvider(app.id!.toString()) .notifier) @@ -514,7 +565,12 @@ class InstallButton extends ConsumerWidget { ? SizedBox( width: 14, height: 14, child: CircularProgressIndicator()) : Text( - '${app.canUpdate ? 'Updating' : 'Installing'} on device'), + 'Requesting ${app.canUpdate ? 'update' : 'installation'} on device'), + HashVerifiedInstallProgress() => compact + ? SizedBox( + width: 14, height: 14, child: CircularProgressIndicator()) + : Text( + 'Hash verified, ${app.canUpdate ? 'updating' : 'installing'}'), ErrorInstallProgress() => compact ? SizedBox(width: 14, height: 14, child: Icon(Icons.error)) : Text('Error, tap to see message'), @@ -647,7 +703,7 @@ class WebOfTrustContainer extends HookConsumerWidget { text: TextSpan( children: [ if (hasUser && npub != franzapsNpub) TextSpan(text: 'You, '), - for (final tu in trustedUsersWithoutUser) + for (final trustedUser in trustedUsersWithoutUser) TextSpan( style: TextStyle(height: 1.6), children: [ @@ -656,15 +712,16 @@ class WebOfTrustContainer extends HookConsumerWidget { child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ - RoundedImage(url: tu.avatarUrl, size: 20), + RoundedImage( + url: trustedUser.avatarUrl, size: 20), Text( - ' ${tu.nameOrNpub}${trustedUsersWithoutUser.indexOf(tu) == trustedUsersWithoutUser.length - 1 ? '' : ', '}', + ' ${trustedUser.nameOrNpub}${trustedUsersWithoutUser.indexOf(trustedUser) == trustedUsersWithoutUser.length - 1 ? '' : ', '}', style: TextStyle(fontWeight: FontWeight.bold), ), ], ), ), - if (trustedUsersWithoutUser.indexOf(tu) == + if (trustedUsersWithoutUser.indexOf(trustedUser) == trustedUsersWithoutUser.length - 1) TextSpan( text: ' and others follow this signer on nostr.',