[auth] Add hook to share code

This commit is contained in:
Neeraj Gupta 2024-09-12 15:37:49 +05:30
parent af9e865745
commit 9ccb597e6e
4 changed files with 228 additions and 3 deletions

View File

@ -118,6 +118,7 @@
"emailVerificationToggle": "Email verification",
"emailVerificationEnableWarning": "To avoid getting locked out of your account, be sure to store a copy of your email 2FA outside of Ente Auth before enabling email verification.",
"authToChangeEmailVerificationSetting": "Please authenticate to change email verification",
"authenticateGeneric": "Please authenticate",
"authToViewYourRecoveryKey": "Please authenticate to view your recovery key",
"authToChangeYourEmail": "Please authenticate to change your email",
"authToChangeYourPassword": "Please authenticate to change your password",
@ -211,6 +212,7 @@
"scanAQrCode": "Scan a QR code",
"enterDetailsManually": "Enter details manually",
"edit": "Edit",
"share": "Share",
"restore": "Restore",
"copiedToClipboard": "Copied to clipboard",
"copiedNextToClipboard": "Copied next code to clipboard",

View File

@ -1,5 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:auto_size_text/auto_size_text.dart';
@ -15,6 +17,7 @@ import 'package:ente_auth/services/preference_service.dart';
import 'package:ente_auth/store/code_store.dart';
import 'package:ente_auth/theme/ente_theme.dart';
import 'package:ente_auth/ui/code_timer_progress.dart';
import 'package:ente_auth/ui/share/code_share.dart';
import 'package:ente_auth/ui/utils/icon_utils.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/platform_util.dart';
@ -291,13 +294,13 @@ class _CodeWidgetState extends State<CodeWidget> {
if (!widget.code.isTrashed) SizedBox(width: slideSpace),
if (!widget.code.isTrashed)
SlidableAction(
onPressed: _onShowQrPressed,
onPressed: _onSharePressed,
backgroundColor: Colors.grey.withOpacity(0.1),
borderRadius: const BorderRadius.all(Radius.circular(8)),
foregroundColor:
Theme.of(context).colorScheme.inverseBackgroundColor,
icon: Icons.qr_code_2_outlined,
label: "QR",
icon: Icons.adaptive.share_outlined,
label: l10n.share,
padding: const EdgeInsets.only(left: 4, right: 0),
spacing: 8,
),
@ -605,6 +608,16 @@ class _CodeWidgetState extends State<CodeWidget> {
);
}
Future<void> _onSharePressed(_) async {
bool isAuthSuccessful = await LocalAuthenticationService.instance
.requestLocalAuthentication(context, context.l10n.authenticateGeneric);
await PlatformUtil.refocusWindows();
if (!isAuthSuccessful) {
return;
}
showShareDialog(context, widget.code);
}
Future<void> _onPinPressed(_) async {
bool currentlyPinned = widget.code.isPinned;
final display = widget.code.display;

View File

@ -0,0 +1,159 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:dropdown_button2/dropdown_button2.dart';
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/models/button_type.dart';
import 'package:ente_auth/utils/dialog_util.dart';
import 'package:ente_auth/utils/share_utils.dart';
import 'package:ente_auth/utils/totp_util.dart';
import 'package:ente_crypto_dart/ente_crypto_dart.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
class ShareCodeDialog extends StatefulWidget {
final Code code;
const ShareCodeDialog({super.key, required this.code});
@override
State<ShareCodeDialog> createState() => _ShareCodeDialogState();
}
class _ShareCodeDialogState extends State<ShareCodeDialog> {
final Logger logger = Logger('_ShareCodeDialogState');
List<int> items = [5, 15, 30, 60];
String getItemLabel(int min) {
if (min == 60) return '1 hour';
if (min > 60) {
var hour = '${min ~/ 60}';
if (min % 60 == 0) return '$hour hour';
var minx = '${min % 60}';
return '$hour hr $minx min';
}
return '$min min';
}
int selectedValue = 15;
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Share codes'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Select the duration for which you want to share codes.',
),
const SizedBox(height: 10),
DropdownButtonHideUnderline(
child: DropdownButton2(
hint: const Text('Select an option'),
items: items
.map(
(item) => DropdownMenuItem<int>(
value: item,
child: Align(
alignment: Alignment.centerLeft,
child: Text(getItemLabel(item)),
),
),
)
.toList(),
value: selectedValue,
onChanged: (value) {
setState(() {
selectedValue = value ?? 15;
});
},
),
),
],
),
actions: [
ButtonWidget(
buttonType: ButtonType.primary,
buttonSize: ButtonSize.large,
labelText: context.l10n.share,
onTap: () async {
try {
await shareCode();
Navigator.of(context).pop();
} catch (e) {
logger.warning('Failed to share code: ${e.toString()}');
showGenericErrorDialog(context: context, error: e).ignore();
}
},
),
const SizedBox(height: 8),
ButtonWidget(
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
labelText: context.l10n.cancel,
onTap: () async {
Navigator.of(context).pop();
},
),
],
);
}
Future<void> shareCode() async {
final result = generateFutureTotpCodes(widget.code, 30);
// show toast with total time taken
Map<String, dynamic> data = {
'startTime': result.$1,
'step': widget.code.period,
'codes': result.$2.join(","),
};
try {
// generated 128 bit crypto secure random key
final Uint8List key = generate256BitKey();
Uint8List input = utf8.encode(jsonEncode(data));
final encResult = CryptoUtil.encryptSync(input, key);
String url =
'https://auth.io/share?data=${uint8ListToUrlSafeBase64(encResult.encryptedData!)}&nonce=${uint8ListToUrlSafeBase64(encResult.nonce!)}#key=${uint8ListToUrlSafeBase64(key)}';
logger.info('url: $url');
shareText(url, context: context).ignore();
} catch (e) {
logger.warning('Failed to encrypt data: ${e.toString()}');
}
}
String uint8ListToUrlSafeBase64(Uint8List data) {
String base64Str = base64UrlEncode(data);
return base64Str.replaceAll('=', '');
}
Uint8List generate256BitKey() {
final random = Random.secure();
final bytes = Uint8List(32); // 32 bytes = 32 * 8 bits = 256 bits
for (int i = 0; i < bytes.length; i++) {
bytes[i] = random
.nextInt(256); // Generates a random number between 0 and 255 (1 byte)
}
return bytes;
}
}
void showShareDialog(BuildContext context, Code code) {
showDialog(
context: context,
builder: (BuildContext context) {
return ShareCodeDialog(
code: code,
);
},
);
}

View File

@ -5,6 +5,8 @@ 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:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
Future<void> shareDialog(
BuildContext context,
@ -49,3 +51,52 @@ Future<void> shareDialog(
],
);
}
Rect _sharePosOrigin(BuildContext? context, GlobalKey? key) {
late final Rect rect;
if (context != null) {
rect = shareButtonRect(context, key);
} else {
rect = const Offset(20.0, 20.0) & const Size(10, 10);
}
return rect;
}
/// Returns the rect of button if context and key are not null
/// If key is null, returned rect will be at the center of the screen
Rect shareButtonRect(BuildContext context, GlobalKey? shareButtonKey) {
Size size = MediaQuery.sizeOf(context);
final RenderObject? renderObject =
shareButtonKey?.currentContext?.findRenderObject();
RenderBox? renderBox;
if (renderObject != null && renderObject is RenderBox) {
renderBox = renderObject;
}
if (renderBox == null) {
return Rect.fromLTWH(0, 0, size.width, size.height / 2);
}
size = renderBox.size;
final Offset position = renderBox.localToGlobal(Offset.zero);
return Rect.fromCenter(
center: position + Offset(size.width / 2, size.height / 2),
width: size.width,
height: size.height,
);
}
Future<ShareResult> shareText(
String text, {
BuildContext? context,
GlobalKey? key,
}) async {
try {
final sharePosOrigin = _sharePosOrigin(context, key);
return Share.share(
text,
sharePositionOrigin: sharePosOrigin,
);
} catch (e, s) {
Logger("ShareUtil").severe("failed to share text", e, s);
return ShareResult.unavailable;
}
}