Merge conflict: Resolved-2
This commit is contained in:
commit
a4df578665
4 changed files with 194 additions and 87 deletions
|
@ -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."
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
133
lib/ui/settings/data/import/google_auth_image_import.dart
Normal file
133
lib/ui/settings/data/import/google_auth_image_import.dart
Normal 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()}');
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue