Merge conflict: Resolved-2

This commit is contained in:
Muhammed Ayimen 2023-11-15 08:12:09 +09:00
commit a4df578665
4 changed files with 194 additions and 87 deletions

View file

@ -340,6 +340,13 @@
"showQRAuthMessage": "Authenticate to show QR code",
"confirmAccountDeleteTitle": "Confirm account deletion",
"confirmAccountDeleteMessage": "This account is linked to other ente apps, if you use any.\n\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted.",
"reminderText": "Reminder",
"reminderPopupBody": "Please delete the screenshot before resuming any photo cloud sync",
"invalidQrCodeText": "Invalid QR code",
"googleAuthImagePopupBody": "Please turn off all photo cloud sync from all apps, including iCloud, Google Photo, OneDrive, etc. \nAlso if you have a second smartphone, it is safer to import by scanning QR code.",
"importGoogleAuthImageButtonText": "Import from image",
"unableToRecognizeQrCodeText": "Unable to recognize a valid code from the uploaded image",
"qrCodeImageNotSelectedText": "Qr code image not selected",
"androidBiometricHint": "Verify identity",
"@androidBiometricHint": {
"description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters."

View file

@ -1,9 +1,11 @@
import 'dart:io';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/settings/data/import/google_auth_image_import.dart';
import 'package:ente_auth/ui/settings/data/import/qr_scanner_overlay.dart';
import 'package:ente_auth/utils/toast_util.dart';
import 'package:flutter/material.dart';
@ -21,6 +23,7 @@ class QrScanner extends StatefulWidget {
class _QrScannerState extends State<QrScanner> {
bool isNavigationPerformed = false;
bool isScannedByImage = false;
//Scanner Initialization
MobileScannerController scannerController = MobileScannerController(
@ -40,85 +43,40 @@ class _QrScannerState extends State<QrScanner> {
children: [
MobileScanner(
controller: scannerController,
onDetect: (capture) {
onDetect: (capture) async {
if (!isNavigationPerformed) {
isNavigationPerformed = true;
HapticFeedback.vibrate();
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
buttonPadding: const EdgeInsets.all(0),
actionsAlignment: MainAxisAlignment.center,
alignment: Alignment.center,
insetPadding: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 24,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: const Text(
'Scan result',
style: TextStyle(
letterSpacing: 0.5,
fontWeight: FontWeight.w600,
fontSize: 18,
color: Colors.black,
),
textAlign: TextAlign.center,
),
content: Text(
' ${capture.barcodes[0].rawValue!}',
style: const TextStyle(
letterSpacing: 0.5,
fontWeight: FontWeight.w600,
fontSize: 15,
color: Colors.black,
),
textAlign: TextAlign.center,
),
actions: [
Column(
children: [
GestureDetector(
onTap: () {
Navigator.pop(context);
isNavigationPerformed = false;
},
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(24),
),
child: const Padding(
padding: EdgeInsets.symmetric(
horizontal: 20,
vertical: 8,
),
child: Text(
'OK',
style: TextStyle(
letterSpacing: 0.5,
fontWeight: FontWeight.w500,
fontSize: 16,
color: Colors.white,
),
),
),
),
),
const SizedBox(
height: 30,
),
],
if (capture.barcodes[0].rawValue!
.startsWith(kGoogleAuthExportPrefix)) {
if (isScannedByImage) {
final result = await showDialogWidget(
context: context,
title: l10n.reminderText,
body: l10n.reminderPopupBody,
buttons: [
ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.ok,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
),
],
);
},
);
if (result?.action != null &&
result!.action == ButtonAction.first) {
isScannedByImage = false;
}
}
HapticFeedback.vibrate();
List<Code> codes =
parseGoogleAuth(capture.barcodes[0].rawValue!);
scannerController.dispose();
Navigator.of(context).pop(codes);
} else {
showToast(context, l10n.invalidQrCodeText);
isNavigationPerformed = false;
}
}
},
),
@ -163,13 +121,14 @@ class _QrScannerState extends State<QrScanner> {
onPressed: () async {
final result = await showDialogWidget(
context: context,
title: l10n.importFromApp("Google Authenticator (saved image)"),
body:
'Please turn off all photo cloud sync from all apps, including iCloud, Google Photo, OneDrive, etc. \nAlso if you have a second smartphone, it is safer to import by scanning QR code.',
title: l10n.importFromApp(
"Google Authenticator (saved image)",
),
body: l10n.googleAuthImagePopupBody,
buttons: [
const ButtonWidget(
ButtonWidget(
buttonType: ButtonType.primary,
labelText: 'Import from image',
labelText: l10n.importGoogleAuthImageButtonText,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
@ -191,6 +150,7 @@ class _QrScannerState extends State<QrScanner> {
context,
pickerConfig: const AssetPickerConfig(
maxAssets: 1,
requestType: RequestType.image,
),
);
@ -201,14 +161,22 @@ class _QrScannerState extends State<QrScanner> {
if (await scannerController
.analyzeImage(path)) {
isScannedByImage = true;
if (!mounted) return;
} else {
if (!mounted) return;
showToast(context, "Failed to scan image");
isScannedByImage = false;
showToast(
context,
l10n.unableToRecognizeQrCodeText,
);
}
} else {
if (!mounted) return;
showToast(context, "Image not selected");
showToast(
context,
l10n.qrCodeImageNotSelectedText,
);
}
}
}
@ -250,13 +218,10 @@ class _QrScannerState extends State<QrScanner> {
const SizedBox(
height: 25,
),
const Text(
'Scan QR code',
Text(
l10n.scanACode,
textAlign: TextAlign.center,
style: TextStyle(
letterSpacing: 0.5,
fontWeight: FontWeight.w600,
fontSize: 14,
style: const TextStyle(
color: Colors.black,
),
),

View file

@ -0,0 +1,133 @@
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:base32/base32.dart';
import 'package:ente_auth/l10n/l10n.dart';
import 'package:ente_auth/models/code.dart';
import 'package:ente_auth/models/protos/googleauth.pb.dart';
import 'package:ente_auth/services/authenticator_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/ui/components/buttons/button_widget.dart';
import 'package:ente_auth/ui/components/dialog_widget.dart';
import 'package:ente_auth/ui/components/models/button_type.dart';
import 'package:ente_auth/ui/settings/data/import/analyze_qr_code.dart';
import 'package:ente_auth/ui/settings/data/import/import_success.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
const kGoogleAuthExportPrefix = 'otpauth-migration://offline?data=';
Future<void> showGoogleAuthImageInstruction(BuildContext context) async {
final l10n = context.l10n;
final result = await showDialogWidget(
context: context,
title: l10n.importFromApp("Google Authenticator"),
body: l10n.importGoogleAuthGuide,
buttons: [
ButtonWidget(
buttonType: ButtonType.primary,
labelText: l10n.scanAQrCode,
isInAlert: true,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
),
ButtonWidget(
buttonType: ButtonType.secondary,
labelText: context.l10n.cancel,
buttonSize: ButtonSize.large,
isInAlert: true,
buttonAction: ButtonAction.second,
),
],
);
if (result?.action != null && result!.action != ButtonAction.cancel) {
if (result.action == ButtonAction.first) {
final List<Code>? codes = await Navigator.of(context).push(
MaterialPageRoute(
builder: (BuildContext context) {
return const QrScanner();
},
),
);
if (codes == null || codes.isEmpty) {
return;
}
for (final code in codes) {
await CodeStore.instance.addCode(code, shouldSync: false);
}
unawaited(AuthenticatorService.instance.onlineSync());
importSuccessDialog(context, codes.length);
}
}
}
List<Code> parseGoogleAuth(String qrCodeData) {
try {
List<Code> codes = <Code>[];
final String payload = qrCodeData.substring(kGoogleAuthExportPrefix.length);
final Uint8List base64Decoded = base64Decode(Uri.decodeComponent(payload));
final MigrationPayload mPayload =
MigrationPayload.fromBuffer(base64Decoded);
for (var otpParameter in mPayload.otpParameters) {
// Build the OTP URL
String otpUrl;
String issuer = otpParameter.issuer;
String account = otpParameter.name;
var counter = otpParameter.counter;
// Create a list of bytes from the list of integers.
Uint8List bytes = Uint8List.fromList(otpParameter.secret);
// Encode the bytes to base 32.
String base32String = base32.encode(bytes);
String secret = base32String;
// identify digit count
int digits = 6;
int timer = 30; // default timer, no field in Google Auth
Algorithm algorithm = Algorithm.sha1;
switch (otpParameter.algorithm) {
case MigrationPayload_Algorithm.ALGORITHM_MD5:
throw Exception('GoogleAuthImport: MD5 is not supported');
case MigrationPayload_Algorithm.ALGORITHM_SHA1:
algorithm = Algorithm.sha1;
break;
case MigrationPayload_Algorithm.ALGORITHM_SHA256:
algorithm = Algorithm.sha256;
break;
case MigrationPayload_Algorithm.ALGORITHM_SHA512:
algorithm = Algorithm.sha512;
break;
case MigrationPayload_Algorithm.ALGORITHM_UNSPECIFIED:
algorithm = Algorithm.sha1;
break;
}
switch (otpParameter.digits) {
case MigrationPayload_DigitCount.DIGIT_COUNT_EIGHT:
digits = 8;
break;
case MigrationPayload_DigitCount.DIGIT_COUNT_SIX:
digits = 6;
break;
case MigrationPayload_DigitCount.DIGIT_COUNT_UNSPECIFIED:
digits = 6;
}
if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_TOTP ||
otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_UNSPECIFIED) {
otpUrl =
'otpauth://totp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer';
} else if (otpParameter.type == MigrationPayload_OtpType.OTP_TYPE_HOTP) {
otpUrl =
'otpauth://hotp/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter';
} else {
throw Exception('Invalid OTP type');
}
codes.add(Code.fromRawData(otpUrl));
}
return codes;
} catch (e, s) {
Logger("GoogleAuthImport")
.severe("Error while parsing Google Auth QR code", e, s);
throw Exception('Failed to parse Google Auth QR code \n ${e.toString()}');
}
}

View file

@ -2,6 +2,7 @@ import 'package:ente_auth/ui/settings/data/import/aegis_import.dart';
import 'package:ente_auth/ui/settings/data/import/analyze_qr_code.dart';
import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart';
import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart';
import 'package:ente_auth/ui/settings/data/import/google_auth_image_import.dart';
import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart';
import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart';
import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart';
@ -43,6 +44,7 @@ class ImportService {
);
case ImportType.bitwarden:
showBitwardenImportInstruction(context);
showGoogleAuthImageInstruction(context);
break;
}
}