From f52094c1050a9d7341265a1d330b48a2f5748bfb Mon Sep 17 00:00:00 2001 From: Christien Rioux Date: Wed, 2 Aug 2023 21:09:28 -0400 Subject: [PATCH] contact invitation algorithm --- assets/i18n/en.json | 29 ++- doc/invitations.md | 9 +- ios/Podfile.lock | 12 ++ .../bottom_sheet_action_button.dart | 67 +++++++ lib/components/chat_component.dart | 96 ++++++---- .../contact_invitation_display.dart | 75 ++++++-- lib/components/enter_pin.dart | 162 ++++++++++++++++ lib/components/send_invite_dialog.dart | 162 ++++++++++++++++ lib/entities/identity.dart | 6 +- lib/entities/local_account.dart | 21 +++ lib/entities/proto/veilidchat.pb.dart | 68 +++---- lib/entities/proto/veilidchat.pbenum.dart | 24 +-- lib/entities/proto/veilidchat.pbjson.dart | 60 +++--- lib/entities/veilidchat.proto | 29 ++- lib/pages/home.dart | 2 + lib/pages/main_pager/main_pager.dart | 95 ++++++++-- lib/providers/account.dart | 60 ++++++ lib/providers/account.g.dart | 21 +++ lib/providers/contact.dart | 103 ++++++++++ lib/providers/local_accounts.dart | 31 +-- lib/providers/local_accounts.g.dart | 2 +- lib/providers/logins.dart | 12 +- lib/providers/logins.g.dart | 2 +- lib/tools/radix_generator.dart | 82 ++++---- lib/tools/secret_crypto.dart | 25 +++ lib/tools/theme_service.dart | 177 ++++++++++++------ lib/tools/tools.dart | 1 + lib/tools/widget_helpers.dart | 40 ++-- .../dht_support/dht_record.dart | 2 +- .../dht_support/dht_record_crypto.dart | 24 +-- .../dht_support/dht_record_pool.dart | 61 +++--- .../dht_support/dht_record_pool.freezed.dart | 51 ++--- .../dht_support/dht_record_pool.g.dart | 21 +-- lib/veilid_support/identity_master.dart | 74 ++++---- lib/veilid_support/table_db.dart | 3 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + macos/Podfile.lock | 12 ++ pubspec.lock | 36 +++- pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 43 files changed, 1319 insertions(+), 451 deletions(-) create mode 100644 lib/components/bottom_sheet_action_button.dart create mode 100644 lib/components/enter_pin.dart create mode 100644 lib/components/send_invite_dialog.dart create mode 100644 lib/providers/contact.dart create mode 100644 lib/tools/secret_crypto.dart diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 87bf1aa..fc85b53 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -43,12 +43,37 @@ "account_page": { "missing_account_title": "Missing Account", "missing_account_text": "Account is missing, removing from list", - "invalid_account_title": "Missing Account", - "invalid_account_text": "Account is missing, removing from list" + "invalid_account_title": "Invalid Account", + "invalid_account_text": "Account is invalid, removing from list" }, "empty_contact_list": { "invite_people": "Invite people to VeilidChat" }, + "accounts_menu": { + "invite_contact": "Invite Contact", + "send_invite": "Send Invite", + "receive_invite": "Receive Invite" + }, + "send_invite_dialog": { + "connect_with_me": "Connect with me on VeilidChat!", + "enter_message_hint": "enter message for contact (optional)", + "message_to_contact": "Message to send with invitation (not encrypted)", + "generate": "Generate Invite", + "message": "Message", + "unlocked": "Unlocked", + "numeric_pin": "Numeric PIN", + "password": "Password", + "protect_this_invitation": "Protect this invitation:", + "note": "Note:", + "note_text": "Contact invitations can be used by anyone. Make sure you send the invitation to your contact over a secure medium, and preferably use a password or pin to ensure that they are the only ones who can unlock the invitation and accept it.", + "pin_description": "Choose a PIN to protect the contact invite.\n\nThis level of security is appropriate only for casual connections in public environments for 'shoulder surfing' protection.", + "password_description": "Choose a strong password to protect the contact invite.\n\nThis level of security is appropriate when you must be sure the contact invitation is only accepted by its intended recipient. Share this password over a different medium than the invite itself.", + "pin_does_not_match": "PIN does not match" + }, + "enter_pin_dialog": { + "enter_pin": "Enter PIN", + "reenter_pin": "Re-Enter PIN To Confirm" + }, "themes": { "vapor": "Vapor" } diff --git a/doc/invitations.md b/doc/invitations.md index b714bb1..cb5699a 100644 --- a/doc/invitations.md +++ b/doc/invitations.md @@ -3,12 +3,13 @@ 2. Encrypt secret with requested encryption type 3. Create Local Chat DHT record (no content yet, will be encrypted with DH of contact identity key) 4. Create ContactRequestPrivate and encrypt with the writer secret -5. Create ContactRequest and embed possibly encrypted ContactRequestPrivate +5. Create ContactRequest and embed encrypted ContactRequestPrivate 6. Create DHT unicast inbox for ContactRequest and store ContactRequest in owner subkey -7. Create ContactInvitation and add invitation record to local table +7. Create ContactInvitation 8. Create SignedContactInvitation embedding ContactInvitation -9. Render SignedContactInvitation to shareable encoding (qr code, text blob, etc) -10. Share SignedContactInvitation out of band to desired contact, along with password somehow if used +9. Create ContactInvitationRecord and add to local table in Account +10. Render SignedContactInvitation to shareable encoding (qr code, text blob, etc) +11. Share SignedContactInvitation out of band to desired contact, along with password somehow if used ## Receiving an invitation 1. Receive SignedContactInvitation from out of band, and the password somehow if used diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 0e55b08..3bbcdd4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -6,6 +6,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - share_plus (0.0.1): + - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -14,15 +16,19 @@ PODS: - FMDB (>= 2.7.5) - system_info_plus (0.0.1): - Flutter + - url_launcher_ios (0.0.1): + - Flutter - veilid (0.0.1): - Flutter DEPENDENCIES: - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - system_info_plus (from `.symlinks/plugins/system_info_plus/ios`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - veilid (from `.symlinks/plugins/veilid/ios`) SPEC REPOS: @@ -34,12 +40,16 @@ EXTERNAL SOURCES: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" veilid: :path: ".symlinks/plugins/veilid/ios" @@ -47,9 +57,11 @@ SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 veilid: f5c2e662f91907b30cf95762619526ac3e4512fd PODFILE CHECKSUM: fcab1959fbc0528061dce4ed4f921740dc46f1e5 diff --git a/lib/components/bottom_sheet_action_button.dart b/lib/components/bottom_sheet_action_button.dart new file mode 100644 index 0000000..3d1cfb2 --- /dev/null +++ b/lib/components/bottom_sheet_action_button.dart @@ -0,0 +1,67 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class BottomSheetActionButton extends ConsumerStatefulWidget { + const BottomSheetActionButton( + {required this.bottomSheetBuilder, + required this.builder, + this.foregroundColor, + this.backgroundColor, + this.shape, + super.key}); + final Color? foregroundColor; + final Color? backgroundColor; + final ShapeBorder? shape; + final Widget Function(BuildContext) builder; + final Widget Function(BuildContext) bottomSheetBuilder; + + @override + BottomSheetActionButtonState createState() => BottomSheetActionButtonState(); + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(ObjectFlagProperty.has( + 'bottomSheetBuilder', bottomSheetBuilder)) + ..add(ColorProperty('foregroundColor', foregroundColor)) + ..add(ColorProperty('backgroundColor', backgroundColor)) + ..add(DiagnosticsProperty('shape', shape)) + ..add(ObjectFlagProperty.has( + 'builder', builder)); + } +} + +class BottomSheetActionButtonState + extends ConsumerState { + bool _showFab = true; + + @override + void initState() { + super.initState(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + // + return _showFab + ? FloatingActionButton( + shape: widget.shape, + foregroundColor: widget.foregroundColor, + backgroundColor: widget.backgroundColor, + child: widget.builder(context), + onPressed: () async { + await showModalBottomSheet( + context: context, builder: widget.bottomSheetBuilder); + }, + ) + : Container(); + } + + void showFloatingActionButton(bool value) { + setState(() { + _showFab = value; + }); + } +} diff --git a/lib/components/chat_component.dart b/lib/components/chat_component.dart index 0395f07..13774c8 100644 --- a/lib/components/chat_component.dart +++ b/lib/components/chat_component.dart @@ -5,6 +5,8 @@ import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:intl/date_symbol_data_local.dart'; import 'package:uuid/uuid.dart'; +import '../tools/theme_service.dart'; + class ChatComponent extends ConsumerStatefulWidget { const ChatComponent({super.key}); @@ -63,56 +65,68 @@ class ChatComponentState extends ConsumerState { _addMessage(textMessage); } + void _handleAttachmentPressed() { + // + } + @override // ignore: prefer_expression_function_bodies Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final chatTheme = scale.toChatTheme(); + final textTheme = Theme.of(context).textTheme; + // - return Align( - alignment: AlignmentDirectional.centerEnd, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - ), - child: Stack( - children: [ - Column( + return DefaultTextStyle( + style: textTheme.bodySmall!, + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Container( + decoration: BoxDecoration( + color: scale.primaryScale.appBackground, + ), + child: Stack( children: [ - Container( - height: 48, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - ), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: - const EdgeInsetsDirectional.fromSTEB(16, 0, 16, 0), - child: Text("current contact", - textAlign: TextAlign.start, - style: Theme.of(context).textTheme.titleMedium), + Column( + children: [ + Container( + height: 48, + decoration: BoxDecoration( + color: scale.primaryScale.subtleBackground, + ), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.fromSTEB( + 16, 0, 16, 0), + child: Text("current contact", + textAlign: TextAlign.start, + style: textTheme.titleMedium), + ), + ), ), - ), - ), - Expanded( - child: Container( - decoration: const BoxDecoration(), - child: Chat( - //theme: _chatTheme, - messages: _messages, - //onAttachmentPressed: _handleAttachmentPressed, - //onMessageTap: _handleMessageTap, - //onPreviewDataFetched: _handlePreviewDataFetched, - onSendPressed: _handleSendPressed, - showUserAvatars: true, - showUserNames: true, - user: _user, + Expanded( + child: Container( + decoration: const BoxDecoration(), + child: Chat( + theme: chatTheme, + messages: _messages, + //onAttachmentPressed: _handleAttachmentPressed, + //onMessageTap: _handleMessageTap, + //onPreviewDataFetched: _handlePreviewDataFetched, + + onSendPressed: _handleSendPressed, + showUserAvatars: true, + showUserNames: true, + user: _user, + ), + ), ), - ), + ], ), ], ), - ], - ), - )); + ))); } } diff --git a/lib/components/contact_invitation_display.dart b/lib/components/contact_invitation_display.dart index eef2d1b..61ef4d0 100644 --- a/lib/components/contact_invitation_display.dart +++ b/lib/components/contact_invitation_display.dart @@ -1,25 +1,74 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; -class ContactInvitationDisplay extends ConsumerWidget { - const ContactInvitationDisplay({super.key}); - //final LocalAccount account; +import '../tools/tools.dart'; + +class ContactInvitationDisplayDialog extends ConsumerStatefulWidget { + const ContactInvitationDisplayDialog({ + super.key, + }); + + // EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; + // _encryptionKey = ''; @override - Widget build(BuildContext context, WidgetRef ref) { - //final logins = ref.watch(loginsProvider); + ContactInvitationDisplayDialogState createState() => + ContactInvitationDisplayDialogState(); +} - return ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 300), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [const Expanded(child: Text('Contact Invitation'))])); +class ContactInvitationDisplayDialogState + extends ConsumerState { + final focusNode = FocusNode(); + final formKey = GlobalKey(); + Future? _generateFuture; + + @override + void initState() { + super.initState(); + if (_generateFuture == null) { + _generateFuture = _generate(); + } + } + + Future _generate() async { + // Generate invitation + + setState(() { + _generateFuture = null; + }); } @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - //properties.add(DiagnosticsProperty('account', account)); + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + final cardsize = MediaQuery.of(context).size.shortestSide - 24; + // + + return Dialog( + backgroundColor: Colors.white, + child: SizedBox( + width: cardsize, + height: cardsize, + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [Text("Contact Invitation")])) + .withModalHUD(context, _generateFuture != null))); } } diff --git a/lib/components/enter_pin.dart b/lib/components/enter_pin.dart new file mode 100644 index 0000000..f3145db --- /dev/null +++ b/lib/components/enter_pin.dart @@ -0,0 +1,162 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:pinput/pinput.dart'; + +import '../tools/tools.dart'; + +class EnterPinDialog extends ConsumerStatefulWidget { + const EnterPinDialog({ + this.matchPin, + this.description, + super.key, + }); + + final String? matchPin; + final String? description; + + @override + EnterPinDialogState createState() => EnterPinDialogState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(StringProperty('matchPin', matchPin)) + ..add(StringProperty('description', description)); + } +} + +class EnterPinDialogState extends ConsumerState { + final pinController = TextEditingController(); + final focusNode = FocusNode(); + final formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + pinController.dispose(); + focusNode.dispose(); + super.dispose(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + final focusedBorderColor = scale.primaryScale.hoverBorder; + final fillColor = scale.primaryScale.elementBackground; + final borderColor = scale.primaryScale.border; + + final defaultPinTheme = PinTheme( + width: 56, + height: 60, + textStyle: TextStyle(fontSize: 22, color: scale.primaryScale.text), + decoration: BoxDecoration( + color: fillColor, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: borderColor), + ), + ); + + /// Optionally you can use form to validate the Pinput + return Dialog( + backgroundColor: scale.grayScale.subtleBackground, + child: Form( + key: formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.matchPin == null + ? translate('enter_pin_dialog.enter_pin') + : translate('enter_pin_dialog.reenter_pin'), + style: theme.textTheme.titleLarge, + ).paddingAll(16), + Directionality( + // Specify direction if desired + textDirection: TextDirection.ltr, + child: Pinput( + controller: pinController, + focusNode: focusNode, + autofocus: true, + defaultPinTheme: defaultPinTheme, + enableSuggestions: false, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly + ], + // validator: (widget.matchPin != null) + // ? (value) => value == widget.matchPin + // ? null + // : translate('enter_pin_dialog.pin_does_not_match') + // : null, + // onClipboardFound: (value) { + // debugPrint('onClipboardFound: $value'); + // pinController.setText(value); + // }, + hapticFeedbackType: HapticFeedbackType.lightImpact, + onCompleted: (pin) { + debugPrint('onCompleted: $pin'); + Navigator.pop(context, pin); + }, + onChanged: (value) { + debugPrint('onChanged: $value'); + }, + cursor: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(bottom: 9), + width: 22, + height: 1, + color: focusedBorderColor, + ), + ], + ), + focusedPinTheme: defaultPinTheme.copyWith( + height: 68, + width: 64, + decoration: defaultPinTheme.decoration!.copyWith( + border: Border.all(color: borderColor), + ), + ), + errorText: '', + errorPinTheme: defaultPinTheme.copyWith( + decoration: BoxDecoration( + color: scale.errorScale.border, + borderRadius: BorderRadius.circular(8), + ), + ), + ).paddingAll(16), + ), + if (widget.description != null) + SizedBox( + width: 400, + child: Text( + widget.description!, + textAlign: TextAlign.center, + ).paddingAll(16)) + ], + ), + )); + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty( + 'pinController', pinController)) + ..add(DiagnosticsProperty('focusNode', focusNode)) + ..add(DiagnosticsProperty>('formKey', formKey)); + } +} diff --git a/lib/components/send_invite_dialog.dart b/lib/components/send_invite_dialog.dart new file mode 100644 index 0000000..441961d --- /dev/null +++ b/lib/components/send_invite_dialog.dart @@ -0,0 +1,162 @@ +import 'package:awesome_extensions/awesome_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_translate/flutter_translate.dart'; +import 'package:quickalert/quickalert.dart'; + +import '../entities/local_account.dart'; +import '../tools/tools.dart'; +import 'contact_invitation_display.dart'; +import 'enter_pin.dart'; + +class SendInviteDialog extends ConsumerStatefulWidget { + const SendInviteDialog({super.key}); + + @override + SendInviteDialogState createState() => SendInviteDialogState(); +} + +class SendInviteDialogState extends ConsumerState { + final messageTextController = TextEditingController( + text: translate('send_invite_dialog.connect_with_me')); + + EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; + String _encryptionKey = ''; + + @override + void initState() { + super.initState(); + } + + Future _onNoneEncryptionSelected(bool selected) async { + setState(() { + if (selected) { + _encryptionKeyType = EncryptionKeyType.none; + } + }); + } + + Future _onPinEncryptionSelected(bool selected) async { + final description = translate('send_invite_dialog.pin_description'); + final pin = await showDialog( + context: context, + builder: (context) => EnterPinDialog(description: description)); + if (pin == null) { + return; + } + // ignore: use_build_context_synchronously + if (!context.mounted) { + return; + } + final matchpin = await showDialog( + context: context, + builder: (context) => EnterPinDialog( + matchPin: pin, + description: description, + )); + if (matchpin == null) { + return; + } else if (pin == matchpin) { + setState(() { + _encryptionKeyType = EncryptionKeyType.pin; + _encryptionKey = pin; + }); + } else { + // ignore: use_build_context_synchronously + if (!context.mounted) { + return; + } + showErrorToast( + context, translate('send_invite_dialog.pin_does_not_match')); + setState(() { + _encryptionKeyType = EncryptionKeyType.none; + _encryptionKey = ''; + }); + } + } + + Future _onPasswordEncryptionSelected(bool selected) async { + setState(() { + if (selected) { + _encryptionKeyType = EncryptionKeyType.password; + } + }); + } + + Future _onGenerateButtonPressed() async { + await showDialog( + context: context, + builder: (context) => ContactInvitationDisplayDialog()); + // if (ret == null) { + // return; + // } + Navigator.of(context).pop(); + } + + @override + // ignore: prefer_expression_function_bodies + Widget build(BuildContext context) { + final theme = Theme.of(context); + //final scale = theme.extension()!; + final textTheme = theme.textTheme; + return SizedBox( + height: 400, + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + translate('send_invite_dialog.message_to_contact'), + ).paddingAll(8), + TextField( + controller: messageTextController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + hintText: translate('send_invite_dialog.enter_message_hint'), + labelText: translate('send_invite_dialog.message')), + ).paddingAll(8), + const SizedBox(height: 10), + Text(translate('send_invite_dialog.protect_this_invitation'), + style: textTheme.labelLarge) + .paddingAll(8), + Wrap(spacing: 5, children: [ + ChoiceChip( + label: Text(translate('send_invite_dialog.unlocked')), + selected: _encryptionKeyType == EncryptionKeyType.none, + onSelected: _onNoneEncryptionSelected, + ), + ChoiceChip( + label: Text(translate('send_invite_dialog.numeric_pin')), + selected: _encryptionKeyType == EncryptionKeyType.pin, + onSelected: _onPinEncryptionSelected, + ), + ChoiceChip( + label: Text(translate('send_invite_dialog.password')), + selected: _encryptionKeyType == EncryptionKeyType.password, + onSelected: _onPasswordEncryptionSelected, + ) + ]).paddingAll(8), + Container( + width: double.infinity, + height: 60, + padding: const EdgeInsets.all(8), + child: ElevatedButton( + onPressed: _onGenerateButtonPressed, + child: Text( + translate('send_invite_dialog.generate'), + ), + ), + ), + Text(translate('send_invite_dialog.note')).paddingAll(8), + Text( + translate('send_invite_dialog.note_text'), + style: Theme.of(context).textTheme.bodySmall, + ).paddingAll(8), + ], + ), + ), + ); + } +} diff --git a/lib/entities/identity.dart b/lib/entities/identity.dart index 2e1dcbc..456764f 100644 --- a/lib/entities/identity.dart +++ b/lib/entities/identity.dart @@ -131,8 +131,8 @@ extension IdentityMasterExtension on IdentityMaster { // Create new account to insert into identity await (await pool.create(parent: identityRec.key)) .deleteScope((accountRec) async { - // Make empty contact request list - final contactRequests = await (await DHTShortArray.create()) + // Make empty contact invitation record list + final contactInvitationRecords = await (await DHTShortArray.create()) .scope((r) => r.record.ownedDHTRecordPointer); // Make account object @@ -140,7 +140,7 @@ extension IdentityMasterExtension on IdentityMaster { ..profile = (proto.Profile() ..name = name ..title = title) - ..contactRequests = contactRequests.toProto(); + ..contactInvitationRecords = contactInvitationRecords.toProto(); // Write account key await accountRec.eventualWriteProtobuf(account); diff --git a/lib/entities/local_account.dart b/lib/entities/local_account.dart index 5c52157..a62351a 100644 --- a/lib/entities/local_account.dart +++ b/lib/entities/local_account.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'package:change_case/change_case.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import '../entities/proto.dart' as proto; import '../veilid_support/veilid_support.dart'; import 'identity.dart'; @@ -22,7 +23,27 @@ enum EncryptionKeyType { factory EncryptionKeyType.fromJson(dynamic j) => EncryptionKeyType.values.byName((j as String).toCamelCase()); + factory EncryptionKeyType.fromProto(proto.EncryptionKeyType p) { + // ignore: exhaustive_cases + switch (p) { + case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE: + return EncryptionKeyType.none; + case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN: + return EncryptionKeyType.pin; + case proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD: + return EncryptionKeyType.password; + } + throw StateError('unknown EncryptionKeyType enum value'); + } String toJson() => name.toPascalCase(); + proto.EncryptionKeyType toProto() => switch (this) { + EncryptionKeyType.none => + proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_NONE, + EncryptionKeyType.pin => + proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PIN, + EncryptionKeyType.password => + proto.EncryptionKeyType.ENCRYPTION_KEY_TYPE_PASSWORD, + }; } // Local Accounts are stored in a table locally and not backed by a DHT key diff --git a/lib/entities/proto/veilidchat.pb.dart b/lib/entities/proto/veilidchat.pb.dart index 6c6b9fe..03c5330 100644 --- a/lib/entities/proto/veilidchat.pb.dart +++ b/lib/entities/proto/veilidchat.pb.dart @@ -1240,7 +1240,7 @@ class Account extends $pb.GeneratedMessage { ..aOB(2, _omitFieldNames ? '' : 'invisible') ..a<$core.int>(3, _omitFieldNames ? '' : 'autoAwayTimeoutSec', $pb.PbFieldType.OU3) ..aOM(4, _omitFieldNames ? '' : 'contactList', subBuilder: OwnedDHTRecordPointer.create) - ..aOM(5, _omitFieldNames ? '' : 'contactRequests', subBuilder: OwnedDHTRecordPointer.create) + ..aOM(5, _omitFieldNames ? '' : 'contactInvitationRecords', subBuilder: OwnedDHTRecordPointer.create) ..hasRequiredFields = false ; @@ -1306,15 +1306,15 @@ class Account extends $pb.GeneratedMessage { OwnedDHTRecordPointer ensureContactList() => $_ensure(3); @$pb.TagNumber(5) - OwnedDHTRecordPointer get contactRequests => $_getN(4); + OwnedDHTRecordPointer get contactInvitationRecords => $_getN(4); @$pb.TagNumber(5) - set contactRequests(OwnedDHTRecordPointer v) { setField(5, v); } + set contactInvitationRecords(OwnedDHTRecordPointer v) { setField(5, v); } @$pb.TagNumber(5) - $core.bool hasContactRequests() => $_has(4); + $core.bool hasContactInvitationRecords() => $_has(4); @$pb.TagNumber(5) - void clearContactRequests() => clearField(5); + void clearContactInvitationRecords() => clearField(5); @$pb.TagNumber(5) - OwnedDHTRecordPointer ensureContactRequests() => $_ensure(4); + OwnedDHTRecordPointer ensureContactInvitationRecords() => $_ensure(4); } class ContactInvitation extends $pb.GeneratedMessage { @@ -1432,9 +1432,8 @@ class ContactRequest extends $pb.GeneratedMessage { factory ContactRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequest', createEmptyInstance: create) - ..a<$core.List<$core.int>>(1, _omitFieldNames ? '' : 'writerSalt', $pb.PbFieldType.OY) - ..e(2, _omitFieldNames ? '' : 'encryptionKeyType', $pb.PbFieldType.OE, defaultOrMaker: EncryptionKind.ENCRYPTION_KIND_UNSPECIFIED, valueOf: EncryptionKind.valueOf, enumValues: EncryptionKind.values) - ..a<$core.List<$core.int>>(3, _omitFieldNames ? '' : 'private', $pb.PbFieldType.OY) + ..e(1, _omitFieldNames ? '' : 'encryptionKeyType', $pb.PbFieldType.OE, defaultOrMaker: EncryptionKeyType.ENCRYPTION_KEY_TYPE_UNSPECIFIED, valueOf: EncryptionKeyType.valueOf, enumValues: EncryptionKeyType.values) + ..a<$core.List<$core.int>>(2, _omitFieldNames ? '' : 'private', $pb.PbFieldType.OY) ..hasRequiredFields = false ; @@ -1460,31 +1459,22 @@ class ContactRequest extends $pb.GeneratedMessage { static ContactRequest? _defaultInstance; @$pb.TagNumber(1) - $core.List<$core.int> get writerSalt => $_getN(0); + EncryptionKeyType get encryptionKeyType => $_getN(0); @$pb.TagNumber(1) - set writerSalt($core.List<$core.int> v) { $_setBytes(0, v); } + set encryptionKeyType(EncryptionKeyType v) { setField(1, v); } @$pb.TagNumber(1) - $core.bool hasWriterSalt() => $_has(0); + $core.bool hasEncryptionKeyType() => $_has(0); @$pb.TagNumber(1) - void clearWriterSalt() => clearField(1); + void clearEncryptionKeyType() => clearField(1); @$pb.TagNumber(2) - EncryptionKind get encryptionKeyType => $_getN(1); + $core.List<$core.int> get private => $_getN(1); @$pb.TagNumber(2) - set encryptionKeyType(EncryptionKind v) { setField(2, v); } + set private($core.List<$core.int> v) { $_setBytes(1, v); } @$pb.TagNumber(2) - $core.bool hasEncryptionKeyType() => $_has(1); + $core.bool hasPrivate() => $_has(1); @$pb.TagNumber(2) - void clearEncryptionKeyType() => clearField(2); - - @$pb.TagNumber(3) - $core.List<$core.int> get private => $_getN(2); - @$pb.TagNumber(3) - set private($core.List<$core.int> v) { $_setBytes(2, v); } - @$pb.TagNumber(3) - $core.bool hasPrivate() => $_has(2); - @$pb.TagNumber(3) - void clearPrivate() => clearField(3); + void clearPrivate() => clearField(2); } class ContactRequestPrivate extends $pb.GeneratedMessage { @@ -1697,13 +1687,13 @@ class SignedContactResponse extends $pb.GeneratedMessage { Signature ensureIdentitySignature() => $_ensure(1); } -class ContactRequestRecord extends $pb.GeneratedMessage { - factory ContactRequestRecord() => create(); - ContactRequestRecord._() : super(); - factory ContactRequestRecord.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory ContactRequestRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); +class ContactInvitationRecord extends $pb.GeneratedMessage { + factory ContactInvitationRecord() => create(); + ContactInvitationRecord._() : super(); + factory ContactInvitationRecord.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory ContactInvitationRecord.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestRecord', createEmptyInstance: create) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactInvitationRecord', createEmptyInstance: create) ..aOM(1, _omitFieldNames ? '' : 'contactRequestRecordKey', subBuilder: TypedKey.create) ..aOM(2, _omitFieldNames ? '' : 'writerKey', subBuilder: CryptoKey.create) ..aOM(3, _omitFieldNames ? '' : 'writerSecret', subBuilder: CryptoKey.create) @@ -1717,22 +1707,22 @@ class ContactRequestRecord extends $pb.GeneratedMessage { 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - ContactRequestRecord clone() => ContactRequestRecord()..mergeFromMessage(this); + ContactInvitationRecord clone() => ContactInvitationRecord()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - ContactRequestRecord copyWith(void Function(ContactRequestRecord) updates) => super.copyWith((message) => updates(message as ContactRequestRecord)) as ContactRequestRecord; + ContactInvitationRecord copyWith(void Function(ContactInvitationRecord) updates) => super.copyWith((message) => updates(message as ContactInvitationRecord)) as ContactInvitationRecord; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static ContactRequestRecord create() => ContactRequestRecord._(); - ContactRequestRecord createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static ContactInvitationRecord create() => ContactInvitationRecord._(); + ContactInvitationRecord createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static ContactRequestRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static ContactRequestRecord? _defaultInstance; + static ContactInvitationRecord getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static ContactInvitationRecord? _defaultInstance; @$pb.TagNumber(1) TypedKey get contactRequestRecordKey => $_getN(0); diff --git a/lib/entities/proto/veilidchat.pbenum.dart b/lib/entities/proto/veilidchat.pbenum.dart index 41e681f..55a0323 100644 --- a/lib/entities/proto/veilidchat.pbenum.dart +++ b/lib/entities/proto/veilidchat.pbenum.dart @@ -51,21 +51,23 @@ class Availability extends $pb.ProtobufEnum { const Availability._($core.int v, $core.String n) : super(v, n); } -class EncryptionKind extends $pb.ProtobufEnum { - static const EncryptionKind ENCRYPTION_KIND_UNSPECIFIED = EncryptionKind._(0, _omitEnumNames ? '' : 'ENCRYPTION_KIND_UNSPECIFIED'); - static const EncryptionKind ENCRYPTION_KIND_PIN = EncryptionKind._(1, _omitEnumNames ? '' : 'ENCRYPTION_KIND_PIN'); - static const EncryptionKind ENCRYPTION_KIND_PASSWORD = EncryptionKind._(2, _omitEnumNames ? '' : 'ENCRYPTION_KIND_PASSWORD'); +class EncryptionKeyType extends $pb.ProtobufEnum { + static const EncryptionKeyType ENCRYPTION_KEY_TYPE_UNSPECIFIED = EncryptionKeyType._(0, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_UNSPECIFIED'); + static const EncryptionKeyType ENCRYPTION_KEY_TYPE_NONE = EncryptionKeyType._(1, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_NONE'); + static const EncryptionKeyType ENCRYPTION_KEY_TYPE_PIN = EncryptionKeyType._(2, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_PIN'); + static const EncryptionKeyType ENCRYPTION_KEY_TYPE_PASSWORD = EncryptionKeyType._(3, _omitEnumNames ? '' : 'ENCRYPTION_KEY_TYPE_PASSWORD'); - static const $core.List values = [ - ENCRYPTION_KIND_UNSPECIFIED, - ENCRYPTION_KIND_PIN, - ENCRYPTION_KIND_PASSWORD, + static const $core.List values = [ + ENCRYPTION_KEY_TYPE_UNSPECIFIED, + ENCRYPTION_KEY_TYPE_NONE, + ENCRYPTION_KEY_TYPE_PIN, + ENCRYPTION_KEY_TYPE_PASSWORD, ]; - static final $core.Map<$core.int, EncryptionKind> _byValue = $pb.ProtobufEnum.initByValue(values); - static EncryptionKind? valueOf($core.int value) => _byValue[value]; + static final $core.Map<$core.int, EncryptionKeyType> _byValue = $pb.ProtobufEnum.initByValue(values); + static EncryptionKeyType? valueOf($core.int value) => _byValue[value]; - const EncryptionKind._($core.int v, $core.String n) : super(v, n); + const EncryptionKeyType._($core.int v, $core.String n) : super(v, n); } diff --git a/lib/entities/proto/veilidchat.pbjson.dart b/lib/entities/proto/veilidchat.pbjson.dart index 512010a..67ee4ea 100644 --- a/lib/entities/proto/veilidchat.pbjson.dart +++ b/lib/entities/proto/veilidchat.pbjson.dart @@ -46,20 +46,22 @@ final $typed_data.Uint8List availabilityDescriptor = $convert.base64Decode( 'lMSVRZX09GRkxJTkUQARIVChFBVkFJTEFCSUxJVFlfRlJFRRACEhUKEUFWQUlMQUJJTElUWV9C' 'VVNZEAMSFQoRQVZBSUxBQklMSVRZX0FXQVkQBA=='); -@$core.Deprecated('Use encryptionKindDescriptor instead') -const EncryptionKind$json = { - '1': 'EncryptionKind', +@$core.Deprecated('Use encryptionKeyTypeDescriptor instead') +const EncryptionKeyType$json = { + '1': 'EncryptionKeyType', '2': [ - {'1': 'ENCRYPTION_KIND_UNSPECIFIED', '2': 0}, - {'1': 'ENCRYPTION_KIND_PIN', '2': 1}, - {'1': 'ENCRYPTION_KIND_PASSWORD', '2': 2}, + {'1': 'ENCRYPTION_KEY_TYPE_UNSPECIFIED', '2': 0}, + {'1': 'ENCRYPTION_KEY_TYPE_NONE', '2': 1}, + {'1': 'ENCRYPTION_KEY_TYPE_PIN', '2': 2}, + {'1': 'ENCRYPTION_KEY_TYPE_PASSWORD', '2': 3}, ], }; -/// Descriptor for `EncryptionKind`. Decode as a `google.protobuf.EnumDescriptorProto`. -final $typed_data.Uint8List encryptionKindDescriptor = $convert.base64Decode( - 'Cg5FbmNyeXB0aW9uS2luZBIfChtFTkNSWVBUSU9OX0tJTkRfVU5TUEVDSUZJRUQQABIXChNFTk' - 'NSWVBUSU9OX0tJTkRfUElOEAESHAoYRU5DUllQVElPTl9LSU5EX1BBU1NXT1JEEAI='); +/// Descriptor for `EncryptionKeyType`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List encryptionKeyTypeDescriptor = $convert.base64Decode( + 'ChFFbmNyeXB0aW9uS2V5VHlwZRIjCh9FTkNSWVBUSU9OX0tFWV9UWVBFX1VOU1BFQ0lGSUVEEA' + 'ASHAoYRU5DUllQVElPTl9LRVlfVFlQRV9OT05FEAESGwoXRU5DUllQVElPTl9LRVlfVFlQRV9Q' + 'SU4QAhIgChxFTkNSWVBUSU9OX0tFWV9UWVBFX1BBU1NXT1JEEAM='); @$core.Deprecated('Use cryptoKeyDescriptor instead') const CryptoKey$json = { @@ -344,7 +346,7 @@ const Account$json = { {'1': 'invisible', '3': 2, '4': 1, '5': 8, '10': 'invisible'}, {'1': 'auto_away_timeout_sec', '3': 3, '4': 1, '5': 13, '10': 'autoAwayTimeoutSec'}, {'1': 'contact_list', '3': 4, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactList'}, - {'1': 'contact_requests', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactRequests'}, + {'1': 'contact_invitation_records', '3': 5, '4': 1, '5': 11, '6': '.OwnedDHTRecordPointer', '10': 'contactInvitationRecords'}, ], }; @@ -353,8 +355,8 @@ final $typed_data.Uint8List accountDescriptor = $convert.base64Decode( 'CgdBY2NvdW50EiIKB3Byb2ZpbGUYASABKAsyCC5Qcm9maWxlUgdwcm9maWxlEhwKCWludmlzaW' 'JsZRgCIAEoCFIJaW52aXNpYmxlEjEKFWF1dG9fYXdheV90aW1lb3V0X3NlYxgDIAEoDVISYXV0' 'b0F3YXlUaW1lb3V0U2VjEjkKDGNvbnRhY3RfbGlzdBgEIAEoCzIWLk93bmVkREhUUmVjb3JkUG' - '9pbnRlclILY29udGFjdExpc3QSQQoQY29udGFjdF9yZXF1ZXN0cxgFIAEoCzIWLk93bmVkREhU' - 'UmVjb3JkUG9pbnRlclIPY29udGFjdFJlcXVlc3Rz'); + '9pbnRlclILY29udGFjdExpc3QSVAoaY29udGFjdF9pbnZpdGF0aW9uX3JlY29yZHMYBSABKAsy' + 'Fi5Pd25lZERIVFJlY29yZFBvaW50ZXJSGGNvbnRhY3RJbnZpdGF0aW9uUmVjb3Jkcw=='); @$core.Deprecated('Use contactInvitationDescriptor instead') const ContactInvitation$json = { @@ -390,17 +392,15 @@ final $typed_data.Uint8List signedContactInvitationDescriptor = $convert.base64D const ContactRequest$json = { '1': 'ContactRequest', '2': [ - {'1': 'writer_salt', '3': 1, '4': 1, '5': 12, '10': 'writerSalt'}, - {'1': 'encryption_key_type', '3': 2, '4': 1, '5': 14, '6': '.EncryptionKind', '10': 'encryptionKeyType'}, - {'1': 'private', '3': 3, '4': 1, '5': 12, '10': 'private'}, + {'1': 'encryption_key_type', '3': 1, '4': 1, '5': 14, '6': '.EncryptionKeyType', '10': 'encryptionKeyType'}, + {'1': 'private', '3': 2, '4': 1, '5': 12, '10': 'private'}, ], }; /// Descriptor for `ContactRequest`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List contactRequestDescriptor = $convert.base64Decode( - 'Cg5Db250YWN0UmVxdWVzdBIfCgt3cml0ZXJfc2FsdBgBIAEoDFIKd3JpdGVyU2FsdBI/ChNlbm' - 'NyeXB0aW9uX2tleV90eXBlGAIgASgOMg8uRW5jcnlwdGlvbktpbmRSEWVuY3J5cHRpb25LZXlU' - 'eXBlEhgKB3ByaXZhdGUYAyABKAxSB3ByaXZhdGU='); + 'Cg5Db250YWN0UmVxdWVzdBJCChNlbmNyeXB0aW9uX2tleV90eXBlGAEgASgOMhIuRW5jcnlwdG' + 'lvbktleVR5cGVSEWVuY3J5cHRpb25LZXlUeXBlEhgKB3ByaXZhdGUYAiABKAxSB3ByaXZhdGU='); @$core.Deprecated('Use contactRequestPrivateDescriptor instead') const ContactRequestPrivate$json = { @@ -453,9 +453,9 @@ final $typed_data.Uint8List signedContactResponseDescriptor = $convert.base64Dec 'FjdFJlc3BvbnNlEjkKEmlkZW50aXR5X3NpZ25hdHVyZRgCIAEoCzIKLlNpZ25hdHVyZVIRaWRl' 'bnRpdHlTaWduYXR1cmU='); -@$core.Deprecated('Use contactRequestRecordDescriptor instead') -const ContactRequestRecord$json = { - '1': 'ContactRequestRecord', +@$core.Deprecated('Use contactInvitationRecordDescriptor instead') +const ContactInvitationRecord$json = { + '1': 'ContactInvitationRecord', '2': [ {'1': 'contact_request_record_key', '3': 1, '4': 1, '5': 11, '6': '.TypedKey', '10': 'contactRequestRecordKey'}, {'1': 'writer_key', '3': 2, '4': 1, '5': 11, '6': '.CryptoKey', '10': 'writerKey'}, @@ -466,12 +466,12 @@ const ContactRequestRecord$json = { ], }; -/// Descriptor for `ContactRequestRecord`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List contactRequestRecordDescriptor = $convert.base64Decode( - 'ChRDb250YWN0UmVxdWVzdFJlY29yZBJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleRgBIA' - 'EoCzIJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIpCgp3cml0ZXJfa2V5GAIg' - 'ASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLkNyeX' - 'B0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVkS2V5' - 'Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW52aX' - 'RhdGlvbhgGIAEoDFIKaW52aXRhdGlvbg=='); +/// Descriptor for `ContactInvitationRecord`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List contactInvitationRecordDescriptor = $convert.base64Decode( + 'ChdDb250YWN0SW52aXRhdGlvblJlY29yZBJGChpjb250YWN0X3JlcXVlc3RfcmVjb3JkX2tleR' + 'gBIAEoCzIJLlR5cGVkS2V5Uhdjb250YWN0UmVxdWVzdFJlY29yZEtleRIpCgp3cml0ZXJfa2V5' + 'GAIgASgLMgouQ3J5cHRvS2V5Ugl3cml0ZXJLZXkSLwoNd3JpdGVyX3NlY3JldBgDIAEoCzIKLk' + 'NyeXB0b0tleVIMd3JpdGVyU2VjcmV0EjEKD2NoYXRfcmVjb3JkX2tleRgEIAEoCzIJLlR5cGVk' + 'S2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCmV4cGlyYXRpb24SHgoKaW' + '52aXRhdGlvbhgGIAEoDFIKaW52aXRhdGlvbg=='); diff --git a/lib/entities/veilidchat.proto b/lib/entities/veilidchat.proto index 89bc8fa..a114e3f 100644 --- a/lib/entities/veilidchat.proto +++ b/lib/entities/veilidchat.proto @@ -258,17 +258,18 @@ message Account { // The contacts DHTList for this account // DHT Private OwnedDHTRecordPointer contact_list = 4; - // The contact requests DHTShortArray for this account + // The ContactInvitationRecord DHTShortArray for this account // DHT Private - OwnedDHTRecordPointer contact_requests = 5; + OwnedDHTRecordPointer contact_invitation_records = 5; } -// EncryptionKind +// EncryptionKeyType // Encryption of secret -enum EncryptionKind { - ENCRYPTION_KIND_UNSPECIFIED = 0; - ENCRYPTION_KIND_PIN = 1; - ENCRYPTION_KIND_PASSWORD =2; +enum EncryptionKeyType { + ENCRYPTION_KEY_TYPE_UNSPECIFIED = 0; + ENCRYPTION_KEY_TYPE_NONE = 1; + ENCRYPTION_KEY_TYPE_PIN = 2; + ENCRYPTION_KEY_TYPE_PASSWORD = 3; } // Invitation that is shared for VeilidChat contact connections @@ -277,7 +278,7 @@ enum EncryptionKind { message ContactInvitation { // Contact request DHT record key TypedKey contact_request_record_key = 1; - // Writer secret key bytes possibly encrypted + // Writer secret key bytes possibly encrypted with nonce appended bytes writer_secret = 2; } @@ -290,14 +291,12 @@ message SignedContactInvitation { } // Contact request unicastinbox on the DHT -// DHTSchema: SMPL 2 owner key, 1 writer key symmetrically encrypted with writer secret +// DHTSchema: SMPL 1 owner key, 1 writer key symmetrically encrypted with writer secret message ContactRequest { - // The salt for the encryption used on the unicastinbox writer secret - bytes writer_salt = 1; // The kind of encryption used on the unicastinbox writer key - EncryptionKind encryption_key_type = 2; + EncryptionKeyType encryption_key_type = 1; // The private part encoded and symmetrically encrypted with the unicastinbox writer secret - bytes private = 3; + bytes private = 2; } // The private part of a possibly encrypted contact request @@ -335,7 +334,7 @@ message SignedContactResponse { } // Contact request record kept in Account DHTList to keep track of extant contact invitations -message ContactRequestRecord { +message ContactInvitationRecord { // Contact request unicastinbox DHT record key TypedKey contact_request_record_key = 1; // Unencrypted writer key for this request @@ -345,6 +344,6 @@ message ContactRequestRecord { TypedKey chat_record_key = 4; // Expiration timestamp uint64 expiration = 5; - // A copy of the raw invitation bytes post-encryption + // A copy of the raw SignedContactResponse invitation bytes post-encryption and signing bytes invitation = 6; } \ No newline at end of file diff --git a/lib/pages/home.dart b/lib/pages/home.dart index f6b1357..94f711a 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -5,6 +5,8 @@ import 'package:split_view/split_view.dart'; import 'package:signal_strength_indicator/signal_strength_indicator.dart'; import '../components/chat_component.dart'; +import '../providers/local_accounts.dart'; +import '../providers/logins.dart'; import '../providers/window_control.dart'; import '../tools/tools.dart'; import 'main_pager/main_pager.dart'; diff --git a/lib/pages/main_pager/main_pager.dart b/lib/pages/main_pager/main_pager.dart index 686e9a5..6c29af8 100644 --- a/lib/pages/main_pager/main_pager.dart +++ b/lib/pages/main_pager/main_pager.dart @@ -1,12 +1,19 @@ +import 'dart:async'; + +import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_translate/flutter_translate.dart'; import 'package:stylish_bottom_bar/model/bar_items.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; +import '../../components/bottom_sheet_action_button.dart'; import '../../components/contact_invitation_display.dart'; +import '../../components/send_invite_dialog.dart'; +import '../../tools/tools.dart'; import 'account_page.dart'; import 'chats_page.dart'; @@ -102,22 +109,81 @@ class MainPagerState extends ConsumerState return bottomBarItems; } - Future _onNewContactInvitation(BuildContext context) async { - Scaffold.of(context).showBottomSheet((context) => SizedBox( - height: 200, child: Center(child: ContactInvitationDisplay()))); + Future sendContactInvitationDialog(BuildContext context) async { + await showDialog( + context: context, + // ignore: prefer_expression_function_bodies + builder: (context) { + return const AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + contentPadding: EdgeInsets.only( + top: 10, + ), + title: Text( + 'Send Contact Invite', + style: TextStyle(fontSize: 24), + ), + content: SendInviteDialog()); + }); } - Future _onNewChat(BuildContext context) async { - // + Widget _newContactInvitationBottomSheetBuilder( + // ignore: prefer_expression_function_bodies + BuildContext context) { + return KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (ke) { + if (ke.logicalKey == LogicalKeyboardKey.escape) { + Navigator.pop(context); + } + }, + child: SizedBox( + height: 200, + child: Column(children: [ + Text(translate('accounts_menu.invite_contact'), + style: Theme.of(context).textTheme.titleMedium) + .paddingAll(8), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ + Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + IconButton( + onPressed: () async { + Navigator.pop(context); + await sendContactInvitationDialog(context); + }, + iconSize: 64, + icon: const Icon(Icons.output)), + Text(translate('accounts_menu.send_invite')) + ]), + Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + IconButton( + onPressed: () { + Navigator.pop(context); + }, + iconSize: 64, + icon: const Icon(Icons.input)), + Text(translate('accounts_menu.receive_invite')) + ]) + ]).expanded() + ]))); } - Future _onFloatingActionButtonPressed(BuildContext context) async { + // ignore: prefer_expression_function_bodies + Widget _onNewChatBottomSheetBuilder(BuildContext context) { + return const SizedBox(height: 200, child: Center(child: Text("test"))); + } + + Widget _bottomSheetBuilder(BuildContext context) { if (_currentPage == 0) { // New contact invitation - return _onNewContactInvitation(context); + return _newContactInvitationBottomSheetBuilder(context); } else if (_currentPage == 1) { // New chat - return _onNewChat(context); + return _onNewChatBottomSheetBuilder(context); + } else { + // Unknown error + return waitingPage(context); } } @@ -151,6 +217,7 @@ class MainPagerState extends ConsumerState // theme.colorScheme.primary, // theme.colorScheme.primaryContainer, // ]), + //borderRadius: BorderRadius.all(Radius.circular(16)), option: AnimatedBarOptions( // iconSize: 32, //barAnimation: BarAnimation.fade, @@ -172,16 +239,16 @@ class MainPagerState extends ConsumerState }, ), - floatingActionButton: FloatingActionButton( + floatingActionButton: BottomSheetActionButton( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(14))), //foregroundColor: theme.colorScheme.secondary, backgroundColor: theme.colorScheme.secondaryContainer, - child: Icon( - _fabIconList[_currentPage], - color: theme.colorScheme.onSecondaryContainer, - ), - onPressed: () async => _onFloatingActionButtonPressed(context)), + builder: (context) => Icon( + _fabIconList[_currentPage], + color: theme.colorScheme.onSecondaryContainer, + ), + bottomSheetBuilder: _bottomSheetBuilder), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, ); } diff --git a/lib/providers/account.dart b/lib/providers/account.dart index 5dfd8ad..836fb52 100644 --- a/lib/providers/account.dart +++ b/lib/providers/account.dart @@ -1,6 +1,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../entities/local_account.dart'; import '../entities/proto.dart' as proto; +import '../entities/user_login.dart'; import '../veilid_support/veilid_support.dart'; import 'local_accounts.dart'; @@ -70,3 +72,61 @@ Future fetchAccount(FetchAccountRef ref, return AccountInfo( status: AccountInfoStatus.accountReady, active: active, account: account); } + +class ActiveAccountInfo { + ActiveAccountInfo({ + required this.localAccount, + required this.userLogin, + required this.account, + }); + + LocalAccount localAccount; + UserLogin userLogin; + proto.Account account; +} + +/// Get the active account info +@riverpod +Future fetchActiveAccount(FetchAccountRef ref) async { + // See if we've logged into this account or if it is locked + final activeUserLogin = await ref.watch(loginsProvider.future + .select((value) async => (await value).activeUserLogin)); + if (activeUserLogin == null) { + return null; + } + + // Get the user login + final userLogin = await ref.watch( + fetchLoginProvider(accountMasterRecordKey: activeUserLogin).future); + if (userLogin == null) { + // Account was locked + return null; + } + + // Get which local account we want to fetch the profile for + final localAccount = await ref.watch( + fetchLocalAccountProvider(accountMasterRecordKey: activeUserLogin) + .future); + if (localAccount == null) { + // Local account does not exist + return null; + } + + // Pull the account DHT key, decode it and return it + final pool = await DHTRecordPool.instance(); + final account = await (await pool.openOwned( + userLogin.accountRecordInfo.accountRecord, + parent: localAccount.identityMaster.identityRecordKey)) + .scope((accountRec) => accountRec.getProtobuf(proto.Account.fromBuffer)); + if (account == null) { + // Account could not be read or decrypted from DHT + return null; + } + + // Got account, decrypted and decoded + return ActiveAccountInfo( + localAccount: localAccount, + userLogin: userLogin, + account: account, + ); +} diff --git a/lib/providers/account.g.dart b/lib/providers/account.g.dart index 1c9df41..1c92f78 100644 --- a/lib/providers/account.g.dart +++ b/lib/providers/account.g.dart @@ -128,4 +128,25 @@ class FetchAccountProvider extends AutoDisposeFutureProvider { return _SystemHash.finish(hash); } } + +String _$fetchActiveAccountHash() => + r'8c7e571c135deeb5cacf56c61459d71f7447baaf'; + +/// Get the active account info +/// +/// Copied from [fetchActiveAccount]. +@ProviderFor(fetchActiveAccount) +final fetchActiveAccountProvider = + AutoDisposeFutureProvider.internal( + fetchActiveAccount, + name: r'fetchActiveAccountProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchActiveAccountHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef FetchActiveAccountRef + = AutoDisposeFutureProviderRef; // ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart new file mode 100644 index 0000000..cf75edd --- /dev/null +++ b/lib/providers/contact.dart @@ -0,0 +1,103 @@ +import 'dart:typed_data'; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../entities/local_account.dart'; +import '../entities/proto.dart' as proto; +import '../entities/user_login.dart'; +import '../tools/tools.dart'; +import '../veilid_support/veilid_support.dart'; +import 'account.dart'; + +Future createContactInvitation( + ActiveAccountInfo activeAccountInfo, + EncryptionKeyType encryptionKeyType, + String encryptionKey, + Timestamp expiration) async { + final pool = await DHTRecordPool.instance(); + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + final identityKey = + activeAccountInfo.localAccount.identityMaster.identityPublicKey; + final identitySecret = activeAccountInfo.userLogin.identitySecret.value; + + // Generate writer keypair to share with new contact + final cs = await pool.veilid.bestCryptoSystem(); + final writer = await cs.generateKeyPair(); + + // Encrypt the writer secret with the encryption key + final encryptedSecret = await encryptSecretToBytes( + secret: writer.secret, + cryptoKind: cs.kind(), + encryptionKey: encryptionKey, + encryptionKeyType: encryptionKeyType); + + // Create local chat DHT record with the account record key as its parent + // Do not set the encryption of this key yet as it will not yet be written + // to and it will be eventually encrypted with the DH of the contact's + // identity key + late final Uint8List signedContactInvitationBytes; + await (await pool.create(parent: accountRecordKey)) + .deleteScope((localChatRecord) async { + // Make ContactRequestPrivate and encrypt with the writer secret + final crpriv = proto.ContactRequestPrivate() + ..writerKey = writer.key.toProto() + ..profile = activeAccountInfo.account.profile + ..accountMasterRecordKey = + activeAccountInfo.userLogin.accountMasterRecordKey.toProto() + ..chatRecordKey = localChatRecord.key.toProto() + ..expiration = expiration.toInt64(); + final crprivbytes = crpriv.writeToBuffer(); + final encryptedContactRequestPrivate = + await cs.encryptNoAuthWithNonce(crprivbytes, writer.secret); + + // Create ContactRequest and embed contactrequestprivate + final creq = proto.ContactRequest() + ..encryptionKeyType = encryptionKeyType.toProto() + ..private = encryptedContactRequestPrivate; + + // Create DHT unicast inbox for ContactRequest + await (await pool.create( + parent: accountRecordKey, + schema: DHTSchema.smpl( + oCnt: 1, members: [DHTSchemaMember(mCnt: 1, mKey: writer.key)]), + crypto: const DHTRecordCryptoPublic())) + .deleteScope((inboxRecord) async { + // Store ContactRequest in owner subkey + await inboxRecord.eventualWriteProtobuf(creq); + + // Create ContactInvitation and SignedContactInvitation + final cinv = proto.ContactInvitation() + ..contactRequestRecordKey = inboxRecord.key.toProto() + ..writerSecret = encryptedSecret; + final cinvbytes = cinv.writeToBuffer(); + final scinv = proto.SignedContactInvitation() + ..contactInvitation = cinvbytes + ..identitySignature = + (await cs.sign(identityKey, identitySecret, cinvbytes)).toProto(); + signedContactInvitationBytes = scinv.writeToBuffer(); + + // Create ContactInvitationRecord + final cinvrec = proto.ContactInvitationRecord() + ..contactRequestRecordKey = inboxRecord.key.toProto() + ..writerKey = writer.key.toProto() + ..writerSecret = writer.secret.toProto() + ..chatRecordKey = localChatRecord.key.toProto() + ..expiration = expiration.toInt64() + ..invitation = signedContactInvitationBytes; + + // Add ContactInvitationRecord to local table + await (await DHTShortArray.openOwned( + proto.OwnedDHTRecordPointerProto.fromProto( + activeAccountInfo.account.contactInvitationRecords), + parent: accountRecordKey)) + .scope((cirList) async { + if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) { + throw StateError('Failed to add contact invitation record'); + } + }); + }); + }); + + return signedContactInvitationBytes; +} diff --git a/lib/providers/local_accounts.dart b/lib/providers/local_accounts.dart index c2be457..14aa85b 100644 --- a/lib/providers/local_accounts.dart +++ b/lib/providers/local_accounts.dart @@ -51,33 +51,6 @@ class LocalAccounts extends _$LocalAccounts state = AsyncValue.data(updated); } - /// Make encrypted identitySecret - Future _encryptIdentitySecret( - {required SecretKey identitySecret, - required CryptoKind cryptoKind, - EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, - String encryptionKey = ''}) async { - final veilid = await eventualVeilid.future; - - late final Uint8List identitySecretBytes; - switch (encryptionKeyType) { - case EncryptionKeyType.none: - identitySecretBytes = identitySecret.decode(); - case EncryptionKeyType.pin: - case EncryptionKeyType.password: - final cs = await veilid.getCryptoSystem(cryptoKind); - final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey)); - final nonce = await cs.randomNonce(); - final identitySecretSaltBytes = nonce.decode(); - final sharedSecret = - await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes); - identitySecretBytes = (await cs.cryptNoAuth( - identitySecret.decode(), nonce, sharedSecret)) - ..addAll(identitySecretSaltBytes); - } - return identitySecretBytes; - } - /// Creates a new Account associated with master identity /// Adds a logged-out LocalAccount to track its existence on this device Future newLocalAccount( @@ -97,8 +70,8 @@ class LocalAccounts extends _$LocalAccounts ); // Encrypt identitySecret with key - final identitySecretBytes = await _encryptIdentitySecret( - identitySecret: identitySecret, + final identitySecretBytes = await encryptSecretToBytes( + secret: identitySecret, cryptoKind: identityMaster.identityRecordKey.kind, encryptionKey: encryptionKey, encryptionKeyType: encryptionKeyType); diff --git a/lib/providers/local_accounts.g.dart b/lib/providers/local_accounts.g.dart index fb18af3..df9bc1d 100644 --- a/lib/providers/local_accounts.g.dart +++ b/lib/providers/local_accounts.g.dart @@ -112,7 +112,7 @@ class FetchLocalAccountProvider } } -String _$localAccountsHash() => r'a9a1e1765188556858ec982c9e99f780756ade1e'; +String _$localAccountsHash() => r'80485dab3a2d1024fb5ffe29d9272dc4f3db2dff'; /// See also [LocalAccounts]. @ProviderFor(LocalAccounts) diff --git a/lib/providers/logins.dart b/lib/providers/logins.dart index 15d7fd5..8448e1f 100644 --- a/lib/providers/logins.dart +++ b/lib/providers/logins.dart @@ -121,18 +121,10 @@ class Logins extends _$Logins with AsyncTableDBBacked { } final cs = await veilid .getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind); - final encryptionKeyBytes = Uint8List.fromList(utf8.encode(encryptionKey)); - final identitySecretKeyBytes = - localAccount.identitySecretBytes.sublist(0, SecretKey.decodedLength()); - final identitySecretSaltBytes = - localAccount.identitySecretBytes.sublist(SecretKey.decodedLength()); - - final nonce = Nonce.fromBytes(identitySecretSaltBytes); - final sharedSecret = await cs.deriveSharedSecret( - encryptionKeyBytes, identitySecretSaltBytes); final identitySecret = SecretKey.fromBytes( - await cs.cryptNoAuth(identitySecretKeyBytes, nonce, sharedSecret)); + await cs.decryptNoAuthWithPassword( + localAccount.identitySecretBytes, encryptionKey)); // Validate this secret with the identity public key and log in return _loginCommon(localAccount.identityMaster, identitySecret); diff --git a/lib/providers/logins.g.dart b/lib/providers/logins.g.dart index 7bbb28e..b7332f6 100644 --- a/lib/providers/logins.g.dart +++ b/lib/providers/logins.g.dart @@ -111,7 +111,7 @@ class FetchLoginProvider extends AutoDisposeFutureProvider { } } -String _$loginsHash() => r'5720eaacf858b2e1d69ebf9d2a981173a30f8592'; +String _$loginsHash() => r'fdabd035aaa7ae2521ed4b7d984b6ff41576f0ba'; /// See also [Logins]. @ProviderFor(Logins) diff --git a/lib/tools/radix_generator.dart b/lib/tools/radix_generator.dart index 9abec7e..07d6f41 100644 --- a/lib/tools/radix_generator.dart +++ b/lib/tools/radix_generator.dart @@ -472,50 +472,48 @@ RadixScheme _radixScheme(Brightness brightness, RadixThemeColor themeColor) { return radixScheme; } -ExtendedColorScheme _radixColorScheme( - Brightness brightness, RadixThemeColor themeColor) { - final radix = _radixScheme(brightness, themeColor); - - return ExtendedColorScheme( - scaleScheme: radix.toScale(), - brightness: brightness, - primary: radix.primaryScale.step9, - onPrimary: radix.primaryScale.step12, - primaryContainer: radix.primaryScale.step4, - onPrimaryContainer: radix.primaryScale.step11, - secondary: radix.secondaryScale.step9, - onSecondary: radix.secondaryScale.step12, - secondaryContainer: radix.secondaryScale.step3, - onSecondaryContainer: radix.secondaryScale.step11, - tertiary: radix.tertiaryScale.step9, - onTertiary: radix.tertiaryScale.step12, - tertiaryContainer: radix.tertiaryScale.step3, - onTertiaryContainer: radix.tertiaryScale.step11, - error: radix.errorScale.step9, - onError: radix.errorScale.step12, - errorContainer: radix.errorScale.step3, - onErrorContainer: radix.errorScale.step11, - background: radix.grayScale.step1, - onBackground: radix.grayScale.step11, - surface: radix.primaryScale.step1, - onSurface: radix.primaryScale.step12, - surfaceVariant: radix.secondaryScale.step2, - onSurfaceVariant: radix.secondaryScale.step11, - outline: radix.primaryScale.step7, - outlineVariant: radix.primaryScale.step6, - shadow: RadixColors.dark.gray.step1, - scrim: radix.primaryScale.step9, - inverseSurface: radix.primaryScale.step11, - onInverseSurface: radix.primaryScale.step2, - inversePrimary: radix.primaryScale.step10, - surfaceTint: radix.primaryAlphaScale.step4, - ); -} +ColorScheme _radixColorScheme(Brightness brightness, RadixScheme radix) => + ColorScheme( + brightness: brightness, + primary: radix.primaryScale.step9, + onPrimary: radix.primaryScale.step12, + primaryContainer: radix.primaryScale.step4, + onPrimaryContainer: radix.primaryScale.step11, + secondary: radix.secondaryScale.step9, + onSecondary: radix.secondaryScale.step12, + secondaryContainer: radix.secondaryScale.step3, + onSecondaryContainer: radix.secondaryScale.step11, + tertiary: radix.tertiaryScale.step9, + onTertiary: radix.tertiaryScale.step12, + tertiaryContainer: radix.tertiaryScale.step3, + onTertiaryContainer: radix.tertiaryScale.step11, + error: radix.errorScale.step9, + onError: radix.errorScale.step12, + errorContainer: radix.errorScale.step3, + onErrorContainer: radix.errorScale.step11, + background: radix.grayScale.step1, + onBackground: radix.grayScale.step11, + surface: radix.primaryScale.step1, + onSurface: radix.primaryScale.step12, + surfaceVariant: radix.secondaryScale.step2, + onSurfaceVariant: radix.secondaryScale.step11, + outline: radix.primaryScale.step7, + outlineVariant: radix.primaryScale.step6, + shadow: RadixColors.dark.gray.step1, + scrim: radix.primaryScale.step9, + inverseSurface: radix.primaryScale.step11, + onInverseSurface: radix.primaryScale.step2, + inversePrimary: radix.primaryScale.step10, + surfaceTint: radix.primaryAlphaScale.step4, + ); ThemeData radixGenerator(Brightness brightness, RadixThemeColor themeColor) { TextTheme? textTheme; + final radix = _radixScheme(brightness, themeColor); + final colorScheme = _radixColorScheme(brightness, radix); return ThemeData.from( - colorScheme: _radixColorScheme(brightness, themeColor), - textTheme: textTheme, - useMaterial3: true); + colorScheme: colorScheme, textTheme: textTheme, useMaterial3: true) + .copyWith(extensions: >[ + radix.toScale(), + ]); } diff --git a/lib/tools/secret_crypto.dart b/lib/tools/secret_crypto.dart new file mode 100644 index 0000000..244fa5c --- /dev/null +++ b/lib/tools/secret_crypto.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import '../entities/local_account.dart'; +import '../veilid_support/veilid_support.dart'; + +Future encryptSecretToBytes( + {required SecretKey secret, + required CryptoKind cryptoKind, + EncryptionKeyType encryptionKeyType = EncryptionKeyType.none, + String encryptionKey = ''}) async { + final veilid = await eventualVeilid.future; + + late final Uint8List identitySecretBytes; + switch (encryptionKeyType) { + case EncryptionKeyType.none: + identitySecretBytes = secret.decode(); + case EncryptionKeyType.pin: + case EncryptionKeyType.password: + final cs = await veilid.getCryptoSystem(cryptoKind); + + identitySecretBytes = + await cs.encryptNoAuthWithPassword(secret.decode(), encryptionKey); + } + return identitySecretBytes; +} diff --git a/lib/tools/theme_service.dart b/lib/tools/theme_service.dart index 110111e..678828e 100644 --- a/lib/tools/theme_service.dart +++ b/lib/tools/theme_service.dart @@ -2,59 +2,13 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../entities/preferences.dart'; import 'radix_generator.dart'; -@immutable -class ExtendedColorScheme extends ColorScheme { - const ExtendedColorScheme({ - required this.scaleScheme, - required super.brightness, - required super.primary, - required super.onPrimary, - super.primaryContainer, - super.onPrimaryContainer, - required super.secondary, - required super.onSecondary, - super.secondaryContainer, - super.onSecondaryContainer, - super.tertiary, - super.onTertiary, - super.tertiaryContainer, - super.onTertiaryContainer, - required super.error, - required super.onError, - super.errorContainer, - super.onErrorContainer, - required super.background, - required super.onBackground, - required super.surface, - required super.onSurface, - super.surfaceVariant, - super.onSurfaceVariant, - super.outline, - super.outlineVariant, - super.shadow, - super.scrim, - super.inverseSurface, - super.onInverseSurface, - super.inversePrimary, - super.surfaceTint, - }); - - final ScaleScheme scaleScheme; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('scales', scaleScheme)); - } -} - class ScaleColor { ScaleColor({ required this.appBackground, @@ -83,9 +37,69 @@ class ScaleColor { Color hoverBackground; Color subtleText; Color text; + + ScaleColor copyWith( + {Color? appBackground, + Color? subtleBackground, + Color? elementBackground, + Color? hoverElementBackground, + Color? activedElementBackground, + Color? subtleBorder, + Color? border, + Color? hoverBorder, + Color? background, + Color? hoverBackground, + Color? subtleText, + Color? text}) => + ScaleColor( + appBackground: appBackground ?? this.appBackground, + subtleBackground: subtleBackground ?? this.subtleBackground, + elementBackground: elementBackground ?? this.elementBackground, + hoverElementBackground: + hoverElementBackground ?? this.hoverElementBackground, + activedElementBackground: + activedElementBackground ?? this.activedElementBackground, + subtleBorder: subtleBorder ?? this.subtleBorder, + border: border ?? this.border, + hoverBorder: hoverBorder ?? this.hoverBorder, + background: background ?? this.background, + hoverBackground: hoverBackground ?? this.hoverBackground, + subtleText: subtleText ?? this.subtleText, + text: text ?? this.text, + ); + + // ignore: prefer_constructors_over_static_methods + static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( + appBackground: Color.lerp(a.appBackground, b.appBackground, t) ?? + const Color(0x00000000), + subtleBackground: + Color.lerp(a.subtleBackground, b.subtleBackground, t) ?? + const Color(0x00000000), + elementBackground: + Color.lerp(a.elementBackground, b.elementBackground, t) ?? + const Color(0x00000000), + hoverElementBackground: + Color.lerp(a.hoverElementBackground, b.hoverElementBackground, t) ?? + const Color(0x00000000), + activedElementBackground: Color.lerp( + a.activedElementBackground, b.activedElementBackground, t) ?? + const Color(0x00000000), + subtleBorder: Color.lerp(a.subtleBorder, b.subtleBorder, t) ?? + const Color(0x00000000), + border: Color.lerp(a.border, b.border, t) ?? const Color(0x00000000), + hoverBorder: Color.lerp(a.hoverBorder, b.hoverBorder, t) ?? + const Color(0x00000000), + background: Color.lerp(a.background, b.background, t) ?? + const Color(0x00000000), + hoverBackground: Color.lerp(a.hoverBackground, b.hoverBackground, t) ?? + const Color(0x00000000), + subtleText: Color.lerp(a.subtleText, b.subtleText, t) ?? + const Color(0x00000000), + text: Color.lerp(a.text, b.text, t) ?? const Color(0x00000000), + ); } -class ScaleScheme { +class ScaleScheme extends ThemeExtension { ScaleScheme( {required this.primaryScale, required this.primaryAlphaScale, @@ -94,15 +108,66 @@ class ScaleScheme { required this.grayScale, required this.errorScale}); - ScaleColor primaryScale; - ScaleColor primaryAlphaScale; - ScaleColor secondaryScale; - ScaleColor tertiaryScale; - ScaleColor grayScale; - ScaleColor errorScale; + final ScaleColor primaryScale; + final ScaleColor primaryAlphaScale; + final ScaleColor secondaryScale; + final ScaleColor tertiaryScale; + final ScaleColor grayScale; + final ScaleColor errorScale; - static ScaleScheme of(BuildContext context) => - (Theme.of(context).colorScheme as ExtendedColorScheme).scaleScheme; + @override + ScaleScheme copyWith( + {ScaleColor? primaryScale, + ScaleColor? primaryAlphaScale, + ScaleColor? secondaryScale, + ScaleColor? tertiaryScale, + ScaleColor? grayScale, + ScaleColor? errorScale}) => + ScaleScheme( + primaryScale: primaryScale ?? this.primaryScale, + primaryAlphaScale: primaryAlphaScale ?? this.primaryAlphaScale, + secondaryScale: secondaryScale ?? this.secondaryScale, + tertiaryScale: tertiaryScale ?? this.tertiaryScale, + grayScale: grayScale ?? this.grayScale, + errorScale: errorScale ?? this.errorScale, + ); + + @override + ScaleScheme lerp(ScaleScheme? other, double t) { + if (other is! ScaleScheme) { + return this; + } + return ScaleScheme( + primaryScale: ScaleColor.lerp(primaryScale, other.primaryScale, t), + primaryAlphaScale: + ScaleColor.lerp(primaryAlphaScale, other.primaryAlphaScale, t), + secondaryScale: ScaleColor.lerp(secondaryScale, other.secondaryScale, t), + tertiaryScale: ScaleColor.lerp(tertiaryScale, other.tertiaryScale, t), + grayScale: ScaleColor.lerp(grayScale, other.grayScale, t), + errorScale: ScaleColor.lerp(errorScale, other.errorScale, t), + ); + } + + ChatTheme toChatTheme() => DefaultChatTheme( + primaryColor: primaryScale.background, + secondaryColor: secondaryScale.background, + backgroundColor: grayScale.subtleBackground, + inputBackgroundColor: primaryScale.appBackground, + inputBorderRadius: BorderRadius.zero, + inputTextDecoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: primaryScale.subtleBorder), + borderRadius: const BorderRadius.all(Radius.circular(16))), + ), + inputContainerDecoration: BoxDecoration(color: grayScale.appBackground), + inputPadding: EdgeInsets.all(5), + inputTextStyle: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + height: 1, + ), + attachmentButtonIcon: Icon(Icons.attach_file), + ); } //////////////////////////////////////////////////////////////////////// diff --git a/lib/tools/tools.dart b/lib/tools/tools.dart index 6c7b3d4..91e6364 100644 --- a/lib/tools/tools.dart +++ b/lib/tools/tools.dart @@ -5,5 +5,6 @@ export 'phono_byte.dart'; export 'protobuf_tools.dart'; export 'radix_generator.dart'; export 'responsive.dart'; +export 'secret_crypto.dart'; export 'theme_service.dart'; export 'widget_helpers.dart'; diff --git a/lib/tools/widget_helpers.dart b/lib/tools/widget_helpers.dart index c790f10..a8c08cf 100644 --- a/lib/tools/widget_helpers.dart +++ b/lib/tools/widget_helpers.dart @@ -1,8 +1,11 @@ import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:motion_toast/motion_toast.dart'; import 'package:quickalert/quickalert.dart'; +import 'theme_service.dart'; + extension BorderExt on Widget { DecoratedBox debugBorder() => DecoratedBox( decoration: BoxDecoration(border: Border.all(color: Colors.redAccent)), @@ -10,19 +13,27 @@ extension BorderExt on Widget { } extension ModalProgressExt on Widget { - BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) => - BlurryModalProgressHUD( - inAsyncCall: isLoading, - blurEffectIntensity: 4, - progressIndicator: buildProgressIndicator(context), - color: Theme.of(context).shadowColor, - child: this); + BlurryModalProgressHUD withModalHUD(BuildContext context, bool isLoading) { + final theme = Theme.of(context); + final scale = theme.extension()!; + + return BlurryModalProgressHUD( + inAsyncCall: isLoading, + blurEffectIntensity: 4, + progressIndicator: buildProgressIndicator(context), + color: scale.tertiaryScale.appBackground.withAlpha(64), + child: this); + } } -Widget buildProgressIndicator(BuildContext context) => SpinKitFoldingCube( - color: Theme.of(context).highlightColor, - size: 90, - ); +Widget buildProgressIndicator(BuildContext context) { + final theme = Theme.of(context); + final scale = theme.extension()!; + return SpinKitFoldingCube( + color: scale.tertiaryScale.background, + size: 80, + ); +} Widget waitingPage(BuildContext context) => ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, @@ -40,3 +51,10 @@ Future showErrorModal( //textColor: Colors.white, ); } + +void showErrorToast(BuildContext context, String message) { + MotionToast.error( + title: Text("Error"), + description: Text(message), + ).show(context); +} diff --git a/lib/veilid_support/dht_support/dht_record.dart b/lib/veilid_support/dht_support/dht_record.dart index 5b605e9..c01217d 100644 --- a/lib/veilid_support/dht_support/dht_record.dart +++ b/lib/veilid_support/dht_support/dht_record.dart @@ -19,7 +19,7 @@ class DHTRecord { _recordDescriptor = recordDescriptor, _defaultSubkey = defaultSubkey, _writer = writer, - _open = false, + _open = true, _valid = true, _subkeySeqCache = {}; final VeilidRoutingContext _routingContext; diff --git a/lib/veilid_support/dht_support/dht_record_crypto.dart b/lib/veilid_support/dht_support/dht_record_crypto.dart index c1e37c2..c2c536f 100644 --- a/lib/veilid_support/dht_support/dht_record_crypto.dart +++ b/lib/veilid_support/dht_support/dht_record_crypto.dart @@ -36,28 +36,12 @@ class DHTRecordCryptoPrivate implements DHTRecordCrypto { } @override - FutureOr encrypt(Uint8List data, int subkey) async { - // generate nonce - final nonce = await _cryptoSystem.randomNonce(); - // crypt and append nonce - final b = BytesBuilder() - ..add(await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey)) - ..add(nonce.decode()); - return b.toBytes(); - } + FutureOr encrypt(Uint8List data, int subkey) => + _cryptoSystem.encryptNoAuthWithNonce(data, _secretKey); @override - FutureOr decrypt(Uint8List data, int subkey) async { - // split off nonce from end - if (data.length <= Nonce.decodedLength()) { - throw const FormatException('not enough data to decrypt'); - } - final nonce = - Nonce.fromBytes(data.sublist(data.length - Nonce.decodedLength())); - final encryptedData = data.sublist(0, data.length - Nonce.decodedLength()); - // decrypt - return await _cryptoSystem.cryptNoAuth(encryptedData, nonce, _secretKey); - } + FutureOr decrypt(Uint8List data, int subkey) => + _cryptoSystem.decryptNoAuthWithNonce(data, _secretKey); } //////////////////////////////////// diff --git a/lib/veilid_support/dht_support/dht_record_pool.dart b/lib/veilid_support/dht_support/dht_record_pool.dart index e636bbe..49cb6c2 100644 --- a/lib/veilid_support/dht_support/dht_record_pool.dart +++ b/lib/veilid_support/dht_support/dht_record_pool.dart @@ -10,8 +10,10 @@ part 'dht_record_pool.g.dart'; @freezed class DHTRecordPoolAllocations with _$DHTRecordPoolAllocations { const factory DHTRecordPoolAllocations({ - required IMap> childrenByParent, - required IMap parentByChild, + required IMap> + childrenByParent, // String key due to IMap<> json unsupported in key + required IMap + parentByChild, // String key due to IMap<> json unsupported in key }) = _DHTRecordPoolAllocations; factory DHTRecordPoolAllocations.fromJson(dynamic json) => @@ -100,13 +102,14 @@ class DHTRecordPool with AsyncTableDBBacked { final nextDep = currentDeps.removeLast(); // Remove this child from its parent - _removeDependency(nextDep); + await _removeDependency(nextDep); // Ensure all records are closed before delete assert(!_opened.containsKey(nextDep), 'should not delete opened record'); allDeps.add(nextDep); - final childDeps = _state.childrenByParent[nextDep]?.toList() ?? []; + final childDeps = + _state.childrenByParent[nextDep.toJson()]?.toList() ?? []; currentDeps.addAll(childDeps); } @@ -118,41 +121,45 @@ class DHTRecordPool with AsyncTableDBBacked { await Future.wait(allFutures); } - void _addDependency(TypedKey parent, TypedKey child) { + Future _addDependency(TypedKey parent, TypedKey child) async { final childrenOfParent = - _state.childrenByParent[parent] ?? ISet(); + _state.childrenByParent[parent.toJson()] ?? ISet(); if (childrenOfParent.contains(child)) { - throw StateError('Dependency added twice: $parent -> $child'); + // Dependency already added (consecutive opens, etc) + return; } - if (_state.parentByChild.containsKey(child)) { + if (_state.parentByChild.containsKey(child.toJson())) { throw StateError('Child has two parents: $child <- $parent'); } - if (_state.childrenByParent.containsKey(child)) { + if (_state.childrenByParent.containsKey(child.toJson())) { // dependencies should be opened after their parents throw StateError('Child is not a leaf: $child'); } - _state = _state.copyWith( - childrenByParent: - _state.childrenByParent.add(parent, childrenOfParent.add(child)), - parentByChild: _state.parentByChild.add(child, parent)); + _state = await store(_state.copyWith( + childrenByParent: _state.childrenByParent + .add(parent.toJson(), childrenOfParent.add(child)), + parentByChild: _state.parentByChild.add(child.toJson(), parent))); } - void _removeDependency(TypedKey child) { - final parent = _state.parentByChild[child]; + Future _removeDependency(TypedKey child) async { + final parent = _state.parentByChild[child.toJson()]; if (parent == null) { return; } - final children = _state.childrenByParent[parent]!.remove(child); + final children = _state.childrenByParent[parent.toJson()]!.remove(child); + late final DHTRecordPoolAllocations newState; if (children.isEmpty) { - _state = _state.copyWith( - childrenByParent: _state.childrenByParent.remove(parent), - parentByChild: _state.parentByChild.remove(child)); + newState = _state.copyWith( + childrenByParent: _state.childrenByParent.remove(parent.toJson()), + parentByChild: _state.parentByChild.remove(child.toJson())); } else { - _state = _state.copyWith( - childrenByParent: _state.childrenByParent.add(parent, children), - parentByChild: _state.parentByChild.remove(child)); + newState = _state.copyWith( + childrenByParent: + _state.childrenByParent.add(parent.toJson(), children), + parentByChild: _state.parentByChild.remove(child.toJson())); } + _state = await store(newState); } /////////////////////////////////////////////////////////////////////// @@ -177,7 +184,7 @@ class DHTRecordPool with AsyncTableDBBacked { recordDescriptor.ownerTypedKeyPair()!)); if (parent != null) { - _addDependency(parent, rec.key); + await _addDependency(parent, rec.key); } _recordOpened(rec); @@ -192,7 +199,7 @@ class DHTRecordPool with AsyncTableDBBacked { DHTRecordCrypto? crypto}) async { // If we are opening a key that already exists // make sure we are using the same parent if one was specified - final existingParent = _state.parentByChild[recordKey]; + final existingParent = _state.parentByChild[recordKey.toJson()]; assert(existingParent == parent, 'wrong parent for opened key'); // Open from the veilid api @@ -206,7 +213,7 @@ class DHTRecordPool with AsyncTableDBBacked { // Register the dependency if specified if (parent != null) { - _addDependency(parent, rec.key); + await _addDependency(parent, rec.key); } _recordOpened(rec); @@ -224,7 +231,7 @@ class DHTRecordPool with AsyncTableDBBacked { }) async { // If we are opening a key that already exists // make sure we are using the same parent if one was specified - final existingParent = _state.parentByChild[recordKey]; + final existingParent = _state.parentByChild[recordKey.toJson()]; assert(existingParent == parent, 'wrong parent for opened key'); // Open from the veilid api @@ -241,7 +248,7 @@ class DHTRecordPool with AsyncTableDBBacked { // Register the dependency if specified if (parent != null) { - _addDependency(parent, rec.key); + await _addDependency(parent, rec.key); } _recordOpened(rec); diff --git a/lib/veilid_support/dht_support/dht_record_pool.freezed.dart b/lib/veilid_support/dht_support/dht_record_pool.freezed.dart index c517e5b..ad600be 100644 --- a/lib/veilid_support/dht_support/dht_record_pool.freezed.dart +++ b/lib/veilid_support/dht_support/dht_record_pool.freezed.dart @@ -21,10 +21,10 @@ DHTRecordPoolAllocations _$DHTRecordPoolAllocationsFromJson( /// @nodoc mixin _$DHTRecordPoolAllocations { - IMap, ISet>> - get childrenByParent => throw _privateConstructorUsedError; - IMap, Typed> - get parentByChild => throw _privateConstructorUsedError; + IMap>> get childrenByParent => + throw _privateConstructorUsedError; // String key due to IMap<> json unsupported in key + IMap> get parentByChild => + throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -39,10 +39,8 @@ abstract class $DHTRecordPoolAllocationsCopyWith<$Res> { _$DHTRecordPoolAllocationsCopyWithImpl<$Res, DHTRecordPoolAllocations>; @useResult $Res call( - {IMap, ISet>> - childrenByParent, - IMap, Typed> - parentByChild}); + {IMap>> childrenByParent, + IMap> parentByChild}); } /// @nodoc @@ -66,12 +64,11 @@ class _$DHTRecordPoolAllocationsCopyWithImpl<$Res, childrenByParent: null == childrenByParent ? _value.childrenByParent : childrenByParent // ignore: cast_nullable_to_non_nullable - as IMap, - ISet>>, + as IMap>>, parentByChild: null == parentByChild ? _value.parentByChild : parentByChild // ignore: cast_nullable_to_non_nullable - as IMap, Typed>, + as IMap>, ) as $Val); } } @@ -86,10 +83,8 @@ abstract class _$$_DHTRecordPoolAllocationsCopyWith<$Res> @override @useResult $Res call( - {IMap, ISet>> - childrenByParent, - IMap, Typed> - parentByChild}); + {IMap>> childrenByParent, + IMap> parentByChild}); } /// @nodoc @@ -111,12 +106,11 @@ class __$$_DHTRecordPoolAllocationsCopyWithImpl<$Res> childrenByParent: null == childrenByParent ? _value.childrenByParent : childrenByParent // ignore: cast_nullable_to_non_nullable - as IMap, - ISet>>, + as IMap>>, parentByChild: null == parentByChild ? _value.parentByChild : parentByChild // ignore: cast_nullable_to_non_nullable - as IMap, Typed>, + as IMap>, )); } } @@ -131,11 +125,10 @@ class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations { _$$_DHTRecordPoolAllocationsFromJson(json); @override - final IMap, ISet>> - childrenByParent; + final IMap>> childrenByParent; +// String key due to IMap<> json unsupported in key @override - final IMap, Typed> - parentByChild; + final IMap> parentByChild; @override String toString() { @@ -174,22 +167,18 @@ class _$_DHTRecordPoolAllocations implements _DHTRecordPoolAllocations { abstract class _DHTRecordPoolAllocations implements DHTRecordPoolAllocations { const factory _DHTRecordPoolAllocations( - {required final IMap, - ISet>> + {required final IMap>> childrenByParent, - required final IMap, - Typed> + required final IMap> parentByChild}) = _$_DHTRecordPoolAllocations; factory _DHTRecordPoolAllocations.fromJson(Map json) = _$_DHTRecordPoolAllocations.fromJson; @override - IMap, ISet>> - get childrenByParent; - @override - IMap, Typed> - get parentByChild; + IMap>> get childrenByParent; + @override // String key due to IMap<> json unsupported in key + IMap> get parentByChild; @override @JsonKey(ignore: true) _$$_DHTRecordPoolAllocationsCopyWith<_$_DHTRecordPoolAllocations> diff --git a/lib/veilid_support/dht_support/dht_record_pool.g.dart b/lib/veilid_support/dht_support/dht_record_pool.g.dart index 46ac52b..fd5af1b 100644 --- a/lib/veilid_support/dht_support/dht_record_pool.g.dart +++ b/lib/veilid_support/dht_support/dht_record_pool.g.dart @@ -9,16 +9,15 @@ part of 'dht_record_pool.dart'; _$_DHTRecordPoolAllocations _$$_DHTRecordPoolAllocationsFromJson( Map json) => _$_DHTRecordPoolAllocations( - childrenByParent: IMap, - ISet>>.fromJson( - json['children_by_parent'] as Map, - (value) => Typed.fromJson(value), - (value) => ISet>.fromJson( - value, (value) => Typed.fromJson(value))), - parentByChild: IMap, - Typed>.fromJson( + childrenByParent: + IMap>>.fromJson( + json['children_by_parent'] as Map, + (value) => value as String, + (value) => ISet>.fromJson(value, + (value) => Typed.fromJson(value))), + parentByChild: IMap>.fromJson( json['parent_by_child'] as Map, - (value) => Typed.fromJson(value), + (value) => value as String, (value) => Typed.fromJson(value)), ); @@ -26,13 +25,13 @@ Map _$$_DHTRecordPoolAllocationsToJson( _$_DHTRecordPoolAllocations instance) => { 'children_by_parent': instance.childrenByParent.toJson( - (value) => value.toJson(), + (value) => value, (value) => value.toJson( (value) => value.toJson(), ), ), 'parent_by_child': instance.parentByChild.toJson( - (value) => value.toJson(), + (value) => value, (value) => value.toJson(), ), }; diff --git a/lib/veilid_support/identity_master.dart b/lib/veilid_support/identity_master.dart index 72c5d9b..65bd982 100644 --- a/lib/veilid_support/identity_master.dart +++ b/lib/veilid_support/identity_master.dart @@ -26,50 +26,52 @@ class IdentityMasterWithSecrets { return (await pool.create(crypto: const DHTRecordCryptoPublic())) .deleteScope((masterRec) async { // Identity record is private - final identityRec = await pool.create(parent: masterRec.key); - // Make IdentityMaster - final masterRecordKey = masterRec.key; - final masterOwner = masterRec.ownerKeyPair!; - final masterSigBuf = BytesBuilder() - ..add(masterRecordKey.decode()) - ..add(masterOwner.key.decode()); + return (await pool.create(parent: masterRec.key)) + .scope((identityRec) async { + // Make IdentityMaster + final masterRecordKey = masterRec.key; + final masterOwner = masterRec.ownerKeyPair!; + final masterSigBuf = BytesBuilder() + ..add(masterRecordKey.decode()) + ..add(masterOwner.key.decode()); - final identityRecordKey = identityRec.key; - final identityOwner = identityRec.ownerKeyPair!; - final identitySigBuf = BytesBuilder() - ..add(identityRecordKey.decode()) - ..add(identityOwner.key.decode()); + final identityRecordKey = identityRec.key; + final identityOwner = identityRec.ownerKeyPair!; + final identitySigBuf = BytesBuilder() + ..add(identityRecordKey.decode()) + ..add(identityOwner.key.decode()); - assert(masterRecordKey.kind == identityRecordKey.kind, - 'new master and identity should have same cryptosystem'); - final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind); + assert(masterRecordKey.kind == identityRecordKey.kind, + 'new master and identity should have same cryptosystem'); + final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind); - final identitySignature = - await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes()); - final masterSignature = - await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes()); + final identitySignature = + await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes()); + final masterSignature = + await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes()); - final identityMaster = IdentityMaster( - identityRecordKey: identityRecordKey, - identityPublicKey: identityOwner.key, - masterRecordKey: masterRecordKey, - masterPublicKey: masterOwner.key, - identitySignature: identitySignature, - masterSignature: masterSignature); + final identityMaster = IdentityMaster( + identityRecordKey: identityRecordKey, + identityPublicKey: identityOwner.key, + masterRecordKey: masterRecordKey, + masterPublicKey: masterOwner.key, + identitySignature: identitySignature, + masterSignature: masterSignature); - // Write identity master to master dht key - await masterRec.eventualWriteJson(identityMaster); + // Write identity master to master dht key + await masterRec.eventualWriteJson(identityMaster); - // Make empty identity - const identity = Identity(accountRecords: IMapConst({})); + // Make empty identity + const identity = Identity(accountRecords: IMapConst({})); - // Write empty identity to identity dht key - await identityRec.eventualWriteJson(identity); + // Write empty identity to identity dht key + await identityRec.eventualWriteJson(identity); - return IdentityMasterWithSecrets._( - identityMaster: identityMaster, - masterSecret: masterOwner.secret, - identitySecret: identityOwner.secret); + return IdentityMasterWithSecrets._( + identityMaster: identityMaster, + masterSecret: masterOwner.secret, + identitySecret: identityOwner.secret); + }); }); } diff --git a/lib/veilid_support/table_db.dart b/lib/veilid_support/table_db.dart index 118d13e..0e2e851 100644 --- a/lib/veilid_support/table_db.dart +++ b/lib/veilid_support/table_db.dart @@ -47,9 +47,10 @@ abstract mixin class AsyncTableDBBacked { } /// Store things to storage - Future store(T obj) async { + Future store(T obj) async { await tableScope(tableName(), (tdb) async { await tdb.storeStringJson(0, tableKeyName(), valueToJson(obj)); }); + return obj; } } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 02f41df..95a69d0 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,6 +7,7 @@ #include "generated_plugin_registrant.h" #include +#include #include #include #include @@ -15,6 +16,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) smart_auth_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin"); + smart_auth_plugin_register_with_registrar(smart_auth_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 9eabee6..5e0bcf2 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever + smart_auth url_launcher_linux veilid window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 36ab4fb..e4bd3f6 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import path_provider_foundation import screen_retriever import share_plus import shared_preferences_foundation +import smart_auth import sqflite import url_launcher_macos import veilid @@ -19,6 +20,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VeilidPlugin.register(with: registry.registrar(forPlugin: "VeilidPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index f30bd2d..cdb4268 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,9 +8,13 @@ PODS: - FlutterMacOS - screen_retriever (0.0.1): - FlutterMacOS + - share_plus (0.0.1): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - smart_auth (0.0.1): + - FlutterMacOS - sqflite (0.0.2): - FlutterMacOS - FMDB (>= 2.7.5) @@ -25,7 +29,9 @@ DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`) - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - veilid (from `Flutter/ephemeral/.symlinks/plugins/veilid/macos`) @@ -42,8 +48,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin screen_retriever: :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + smart_auth: + :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos sqflite: :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos url_launcher_macos: @@ -58,7 +68,9 @@ SPEC CHECKSUMS: FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 veilid: a54f57b7bcf0e4e072fe99272d76ca126b2026d0 diff --git a/pubspec.lock b/pubspec.lock index 623afdd..4fd6242 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -757,6 +757,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + motion_toast: + dependency: "direct main" + description: + name: motion_toast + sha256: f27cfcd39c6a0c433670fb20e4add55c42f925a5382b25f58e917c054d47a624 + url: "https://pub.dev" + source: hosted + version: "2.7.8" octo_image: dependency: transitive description: @@ -853,6 +861,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.14.0" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "543da5bfdefd9e06914a12100f8c9156f84cef3efc14bca507c49e966c5b813b" + url: "https://pub.dev" + source: hosted + version: "2.3.0" platform: dependency: transitive description: @@ -1090,6 +1106,14 @@ packages: description: flutter source: sdk version: "0.0.99" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6 + url: "https://pub.dev" + source: hosted + version: "1.1.1" source_gen: dependency: transitive description: @@ -1250,6 +1274,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" url_launcher: dependency: transitive description: @@ -1360,7 +1392,7 @@ packages: path: "../veilid/veilid-flutter" relative: true source: path - version: "0.1.6" + version: "0.1.7" visibility_detector: dependency: transitive description: @@ -1427,4 +1459,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.5 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.10.6" diff --git a/pubspec.yaml b/pubspec.yaml index abec7bb..1abea90 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,8 +40,10 @@ dependencies: intl: ^0.18.0 json_annotation: ^4.8.1 loggy: ^2.0.3 + motion_toast: ^2.7.8 path: ^1.8.2 path_provider: ^2.0.11 + pinput: ^2.3.0 protobuf: ^3.0.0 quickalert: ^1.0.1 radix_colors: ^1.0.4 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index d7c7d25..a741455 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -17,6 +18,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + SmartAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SmartAuthPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); VeilidPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 0124f46..84b6fcc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_retriever share_plus + smart_auth url_launcher_windows veilid window_manager