diff --git a/helper/helper/qr.py b/helper/helper/qr.py index fd2670c03..ce22f72cf 100644 --- a/helper/helper/qr.py +++ b/helper/helper/qr.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .base import RpcException import mss import zxingcpp import base64 @@ -21,7 +22,7 @@ import subprocess # nosec import tempfile from mss.exception import ScreenShotError -from PIL import Image +from PIL import Image, UnidentifiedImageError def _capture_screen(): @@ -64,11 +65,22 @@ def _capture_screen(): raise ValueError("Unable to capture screenshot") +class InvalidImageException(RpcException): + def __init__(self): + super().__init__( + "invalid-image", + "The provided file is not a valid image", + ) + + def scan_qr(image_data=None): if image_data: - msg = base64.b64decode(image_data) - buf = io.BytesIO(msg) - img = Image.open(buf) + try: + msg = base64.b64decode(image_data) + buf = io.BytesIO(msg) + img = Image.open(buf) + except UnidentifiedImageError: + raise InvalidImageException() else: img = _capture_screen() diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index dd9bebe5b..70610e7a8 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -611,6 +611,13 @@ "l_qr_scanned": "QR-Code aufgenommen", "l_invalid_qr": "Ungültiger QR-Code", "l_qr_not_found": "Kein QR-Code gefunden", + "l_qr_file_too_large": null, + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": null, "l_qr_select_file": null, "l_qr_not_read": "Fehler beim Lesen des QR-Codes: {message}", "@l_qr_not_read": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 5063ab92c..6689e6929 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -611,6 +611,13 @@ "l_qr_scanned": "Scanned QR code", "l_invalid_qr": "Invalid QR code", "l_qr_not_found": "No QR code found", + "l_qr_file_too_large": "File too large (max {max})", + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": "Invalid image file", "l_qr_select_file": "Select file with QR code", "l_qr_not_read": "Failed reading QR code: {message}", "@l_qr_not_read": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index e8b6845d5..0661eaa03 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -611,6 +611,13 @@ "l_qr_scanned": "QR code scanné", "l_invalid_qr": "QR code invalide", "l_qr_not_found": "Aucun QR code trouvé", + "l_qr_file_too_large": null, + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": null, "l_qr_select_file": null, "l_qr_not_read": "Erreur de lecture du QR code: {message}", "@l_qr_not_read": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index c240c55de..d6bcb02fb 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -611,6 +611,13 @@ "l_qr_scanned": "スキャンしたQRコード", "l_invalid_qr": "無効なQRコード", "l_qr_not_found": "QRコードが見つかりませんでした", + "l_qr_file_too_large": null, + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": null, "l_qr_select_file": null, "l_qr_not_read": "QRコードの読み取りに失敗しました:{message}", "@l_qr_not_read": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index d15a417dd..a1451626d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -611,6 +611,13 @@ "l_qr_scanned": "Zeskanowany kod QR", "l_invalid_qr": "Nieprawidłowy kod QR", "l_qr_not_found": "Nie znaleziono kodu QR", + "l_qr_file_too_large": null, + "@l_qr_file_too_large": { + "placeholders": { + "max": {} + } + }, + "l_qr_invalid_image_file": null, "l_qr_select_file": "Wybierz plik z kodem QR", "l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}", "@l_qr_not_read": { diff --git a/lib/oath/views/add_account_dialog.dart b/lib/oath/views/add_account_dialog.dart index 0646b796c..4d58c1513 100644 --- a/lib/oath/views/add_account_dialog.dart +++ b/lib/oath/views/add_account_dialog.dart @@ -14,8 +14,6 @@ * limitations under the License. */ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -55,19 +53,14 @@ class _AddAccountDialogState extends ConsumerState { onFileDropped: (file) async { Navigator.of(context).pop(); if (qrScanner != null) { - final fileData = await file.readAsBytes(); - final b64Image = base64Encode(fileData); - final qrData = await qrScanner.scanQr(b64Image); - await withContext( - (context) async { - if (qrData != null) { - await handleUri(context, credentials, qrData, widget.devicePath, - widget.state, l10n); - } else { - showMessage(context, l10n.l_qr_not_found); - } - }, - ); + final qrData = + await handleQrFile(file, context, withContext, qrScanner); + if (qrData != null) { + await withContext((context) async { + await handleUri(context, credentials, qrData, widget.devicePath, + widget.state, l10n); + }); + } } }, overlay: FileDropOverlay( diff --git a/lib/oath/views/add_account_page.dart b/lib/oath/views/add_account_page.dart index 2644596ab..5de01c4a8 100755 --- a/lib/oath/views/add_account_page.dart +++ b/lib/oath/views/add_account_page.dart @@ -15,7 +15,6 @@ */ import 'dart:async'; -import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -314,11 +313,10 @@ class _OathAddAccountPageState extends ConsumerState { final qrScanner = ref.read(qrScannerProvider); final withContext = ref.read(withContextProvider); if (qrScanner != null) { - final fileData = await file.readAsBytes(); - final b64Image = base64Encode(fileData); - final qrData = await qrScanner.scanQr(b64Image); - await withContext((context) async { - if (qrData != null) { + final qrData = + await handleQrFile(file, context, withContext, qrScanner); + if (qrData != null) { + await withContext((context) async { List creds; try { creds = CredentialData.fromUri(Uri.parse(qrData)); @@ -333,10 +331,8 @@ class _OathAddAccountPageState extends ConsumerState { await handleUri(context, widget.credentials, qrData, widget.devicePath, widget.state, l10n); } - } else { - showMessage(context, l10n.l_qr_not_found); - } - }); + }); + } } }, overlay: FileDropOverlay( diff --git a/lib/oath/views/oath_screen.dart b/lib/oath/views/oath_screen.dart index a00dc1a03..f3f8722eb 100755 --- a/lib/oath/views/oath_screen.dart +++ b/lib/oath/views/oath_screen.dart @@ -15,7 +15,6 @@ */ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -144,21 +143,16 @@ class _UnlockedViewState extends ConsumerState<_UnlockedView> { Future onFileDropped(File file) async { final qrScanner = ref.read(qrScannerProvider); if (qrScanner != null) { - final fileData = await file.readAsBytes(); - final b64Image = base64Encode(fileData); - final qrData = await qrScanner.scanQr(b64Image); final withContext = ref.read(withContextProvider); - await withContext( - (context) async { - if (qrData != null) { - final credentials = ref.read(credentialsProvider); - await handleUri(context, credentials, qrData, widget.devicePath, - widget.oathState, l10n); - } else { - showMessage(context, l10n.l_qr_not_found); - } - }, - ); + final qrData = + await handleQrFile(file, context, withContext, qrScanner); + if (qrData != null) { + await withContext((context) async { + final credentials = ref.read(credentialsProvider); + await handleUri(context, credentials, qrData, widget.devicePath, + widget.oathState, l10n); + }); + } } } diff --git a/lib/oath/views/utils.dart b/lib/oath/views/utils.dart index ee8ebb2ff..7a4f7df6b 100755 --- a/lib/oath/views/utils.dart +++ b/lib/oath/views/utils.dart @@ -14,6 +14,8 @@ * limitations under the License. */ +import 'dart:convert'; +import 'dart:io'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -21,6 +23,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../app/message.dart'; import '../../app/models.dart'; +import '../../app/state.dart'; +import '../../desktop/models.dart'; import '../../widgets/utf8_utils.dart'; import '../keys.dart'; import '../models.dart'; @@ -97,3 +101,48 @@ Future handleUri( ); } } + +const maxQrFileSize = 5 * 1024 * 1024; + +Future handleQrFile(File file, BuildContext context, + WithContext withContext, QrScanner qrScanner) async { + final l10n = AppLocalizations.of(context)!; + if (await file.length() > maxQrFileSize) { + await withContext((context) async { + showMessage( + context, + l10n.l_qr_not_read( + l10n.l_qr_file_too_large('${maxQrFileSize / (1024 * 1024)} MB'))); + }); + return null; + } + + final fileData = await file.readAsBytes(); + final b64Image = base64Encode(fileData); + + try { + final qrData = await qrScanner.scanQr(b64Image); + if (qrData == null) { + await withContext((context) async { + showMessage(context, l10n.l_qr_not_found); + }); + return null; + } + return qrData; + } catch (e) { + final String errorMessage; + if (e is RpcError) { + if (e.status == 'invalid-image') { + errorMessage = l10n.l_qr_invalid_image_file; + } else { + errorMessage = e.message; + } + } else { + errorMessage = e.toString(); + } + await withContext((context) async { + showMessage(context, l10n.l_qr_not_read(errorMessage)); + }); + return null; + } +}