From b3f2685c21c4db62e4f8d22ab5e36ea71859b5b3 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Mon, 27 Jul 2026 11:22:19 +0300 Subject: [PATCH] fix(marketplace): add sideload security notice and optional SHA256 field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #402 — extension sideload dialog warns about unverified installs and accepts optional checksum before LocalExtensionInstaller runs. --- docs/security.md | 6 +- .../pages/extension_manager_dialog.dart | 17 ++- .../widgets/extension_sideload_dialog.dart | 128 ++++++++++++++++++ 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 lib/features/extensions/presentation/widgets/extension_sideload_dialog.dart diff --git a/docs/security.md b/docs/security.md index 32f46518..8480bf26 100644 --- a/docs/security.md +++ b/docs/security.md @@ -49,4 +49,8 @@ Process-sandbox database drivers launch inside OS-level isolation when available Querya refuses **silent** unsandboxed launch. `SandboxProcessRunner` throws `SandboxOsIsolationUnavailableException` until the user approves via the consent dialog registered from the main window. -**Linux:** install `bubblewrap` and ensure unprivileged user namespaces are enabled if you want OS sandbox without manual confirmation. \ No newline at end of file +**Linux:** install `bubblewrap` and ensure unprivileged user namespaces are enabled if you want OS sandbox without manual confirmation. + +## Local extension sideload (`.zip` / `.qext`) + +Installing from a local file does **not** verify integrity unless you paste an optional **SHA-256 checksum** in the install dialog. Marketplace installs always require a manifest checksum (#396). Sideload is intended for trusted local packages and development builds. diff --git a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart index 89bcc724..2fce9aca 100644 --- a/lib/features/extensions/presentation/pages/extension_manager_dialog.dart +++ b/lib/features/extensions/presentation/pages/extension_manager_dialog.dart @@ -7,6 +7,7 @@ import 'package:querya_desktop/core/extensions/models/extension_manifest.dart'; import 'package:querya_desktop/core/layout/window_layout.dart'; import 'package:querya_desktop/core/market/marketplace_repository.dart'; import 'package:querya_desktop/features/extensions/presentation/widgets/extension_card.dart'; +import 'package:querya_desktop/features/extensions/presentation/widgets/extension_sideload_dialog.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; void showExtensionManagerDialog(material.BuildContext context) { @@ -124,7 +125,18 @@ class _ExtensionManagerContentState ], ); if (file == null) return; - await LocalExtensionInstaller().installFromPath(file.path); + + if (!mounted) return; + final request = await showExtensionSideloadDialog( + context, + archivePath: file.path, + ); + if (request == null) return; + + await LocalExtensionInstaller().installFromPath( + request.archivePath, + expectedSha256: request.sha256Checksum, + ); await LocalExtensionRegistry.instance.reload(); if (!mounted) return; setState(() { @@ -269,7 +281,8 @@ class _ExtensionManagerContentState const material.SizedBox(width: 12), material.Expanded( child: const Text( - 'Install a local .zip or .qext package without the Marketplace.', + 'Install a local .zip or .qext package. Integrity verification ' + 'is optional — paste SHA-256 if the publisher provides one.', ).muted().small(), ), ], diff --git a/lib/features/extensions/presentation/widgets/extension_sideload_dialog.dart b/lib/features/extensions/presentation/widgets/extension_sideload_dialog.dart new file mode 100644 index 00000000..dfb84ff7 --- /dev/null +++ b/lib/features/extensions/presentation/widgets/extension_sideload_dialog.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart' as material; +import 'package:path/path.dart' as p; +import 'package:querya_desktop/shared/widgets/widgets.dart'; + +class ExtensionSideloadRequest { + const ExtensionSideloadRequest({ + required this.archivePath, + this.sha256Checksum, + }); + + final String archivePath; + final String? sha256Checksum; +} + +Future showExtensionSideloadDialog( + material.BuildContext context, { + required String archivePath, +}) async { + return showAppDialog( + context: context, + builder: (dialogContext) => _ExtensionSideloadDialog( + archivePath: archivePath, + ), + ); +} + +class _ExtensionSideloadDialog extends material.StatefulWidget { + const _ExtensionSideloadDialog({required this.archivePath}); + + final String archivePath; + + @override + material.State<_ExtensionSideloadDialog> createState() => + _ExtensionSideloadDialogState(); +} + +class _ExtensionSideloadDialogState + extends material.State<_ExtensionSideloadDialog> { + final _checksumController = material.TextEditingController(); + + @override + void dispose() { + _checksumController.dispose(); + super.dispose(); + } + + void _submit() { + final checksum = _checksumController.text.trim(); + material.Navigator.pop( + context, + ExtensionSideloadRequest( + archivePath: widget.archivePath, + sha256Checksum: checksum.isEmpty ? null : checksum, + ), + ); + } + + @override + material.Widget build(material.BuildContext context) { + final cs = Theme.of(context).colorScheme; + final fileName = p.basename(widget.archivePath); + + return material.AlertDialog( + title: const material.Text('Install local extension'), + content: material.SizedBox( + width: 440, + child: material.Column( + mainAxisSize: material.MainAxisSize.min, + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Text('File: $fileName'), + const material.SizedBox(height: 12), + material.Container( + width: double.infinity, + padding: const material.EdgeInsets.all(12), + decoration: material.BoxDecoration( + color: cs.muted.withValues(alpha: 0.35), + borderRadius: material.BorderRadius.circular(8), + border: material.Border.all(color: cs.border), + ), + child: material.Row( + crossAxisAlignment: material.CrossAxisAlignment.start, + children: [ + material.Icon( + material.Icons.warning_amber_rounded, + size: 18, + color: cs.mutedForeground, + ), + const material.SizedBox(width: 10), + material.Expanded( + child: material.Text( + 'Local packages are not verified unless you provide a ' + 'SHA-256 checksum. Only install archives from sources you ' + 'trust.', + style: material.TextStyle( + fontSize: 13, + color: cs.mutedForeground, + ), + ), + ), + ], + ), + ), + const material.SizedBox(height: 12), + material.TextField( + controller: _checksumController, + decoration: const material.InputDecoration( + labelText: 'SHA-256 checksum (optional)', + hintText: '64-character hex digest from publisher', + ), + autocorrect: false, + ), + ], + ), + ), + actions: [ + OutlineButton( + onPressed: () => material.Navigator.pop(context), + child: const material.Text('Cancel'), + ), + PrimaryButton( + onPressed: _submit, + child: const material.Text('Install'), + ), + ], + ); + } +}