checkpoint

This commit is contained in:
Christien Rioux 2023-08-05 12:38:03 -04:00
parent 3af819e28b
commit c047ae05c5
5 changed files with 162 additions and 93 deletions

View File

@ -16,6 +16,7 @@ import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart'; import '../veilid_support/veilid_support.dart';
import 'contact_invitation_display.dart'; import 'contact_invitation_display.dart';
import 'enter_pin.dart'; import 'enter_pin.dart';
import 'profile_widget.dart';
class PasteInviteDialog extends ConsumerStatefulWidget { class PasteInviteDialog extends ConsumerStatefulWidget {
const PasteInviteDialog({super.key}); const PasteInviteDialog({super.key});
@ -26,12 +27,12 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> { class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final _pasteTextController = TextEditingController(); final _pasteTextController = TextEditingController();
final _messageTextController = TextEditingController();
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none; EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
String _encryptionKey = ''; String _encryptionKey = '';
Timestamp? _expiration; Timestamp? _expiration;
proto.SignedContactInvitation? _validInvitation; ValidContactInvitation? _validInvitation;
bool _validatingPaste = false;
@override @override
void initState() { void initState() {
@ -93,11 +94,28 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
// }); // });
// } // }
Future<void> _onAccept() async {
Navigator.of(context).pop();
if (_validInvitation != null) {
return acceptContactInvitation(_validInvitation);
}
}
Future<void> _onReject() async {
Navigator.of(context).pop();
if (_validInvitation != null) {
return rejectContactInvitation(_validInvitation);
}
}
Future<void> _onPasteChanged(String text) async { Future<void> _onPasteChanged(String text) async {
try { try {
final lines = text.split('\n'); final lines = text.split('\n');
if (lines.isEmpty) { if (lines.isEmpty) {
_validInvitation = null; setState(() {
_validatingPaste = false;
_validInvitation = null;
});
return; return;
} }
@ -111,74 +129,44 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
lastline = lines.length; lastline = lines.length;
} }
if (lastline <= firstline) { if (lastline <= firstline) {
_validInvitation = null; setState(() {
_validatingPaste = false;
_validInvitation = null;
});
return; return;
} }
final inviteDataBase64 = lines.sublist(firstline, lastline).join(); final inviteDataBase64 = lines.sublist(firstline, lastline).join();
final inviteData = base64UrlNoPadDecode(inviteDataBase64); final inviteData = base64UrlNoPadDecode(inviteDataBase64);
final signedContactInvitation =
proto.SignedContactInvitation.fromBuffer(inviteData);
final contactInvitationBytes = setState(() {
Uint8List.fromList(signedContactInvitation.contactInvitation); _validatingPaste = true;
final contactInvitation = _validInvitation = null;
proto.ContactInvitation.fromBuffer(contactInvitationBytes); });
final validatedContactInvitation = await validateContactInvitation(
final contactRequestInboxKey = proto.TypedKeyProto.fromProto( inviteData, (encryptionKeyType, encryptedSecret) async {
contactInvitation.contactRequestInboxKey);
// xxx should ensure contact request is not from ourselves
// xxx or implement as 'note to self' but this could be done more carefully
// xxx this operation gets the wrong parent. can we allow opening dht records
// xxx that we already have open for readonly?
// xxx test on multiple devices
// Open context request inbox subkey zero to get the contact request object
final pool = await DHTRecordPool.instance();
await (await pool.openRead(contactRequestInboxKey))
.deleteScope((contactRequestInbox) async {
//
final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
late final SecretKey writerSecret;
switch (encryptionKeyType) { switch (encryptionKeyType) {
case EncryptionKeyType.none: case EncryptionKeyType.none:
writerSecret = SecretKey.fromBytes( return SecretKey.fromBytes(encryptedSecret);
Uint8List.fromList(contactInvitation.writerSecret));
case EncryptionKeyType.pin: case EncryptionKeyType.pin:
// //xxx
return SecretKey.fromBytes(encryptedSecret);
case EncryptionKeyType.password: case EncryptionKeyType.password:
// //xxx
return SecretKey.fromBytes(encryptedSecret);
} }
final cs = });
await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind); // Verify expiration
final contactRequestPrivateBytes = await cs.decryptNoAuthWithNonce( // xxx
Uint8List.fromList(contactRequest.private), writerSecret);
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactRequestPrivate.identityMasterRecordKey);
// Fetch the account master setState(() {
final contactIdentityMaster = await openIdentityMaster( _validatingPaste = false;
identityMasterRecordKey: contactIdentityMasterRecordKey); _validInvitation = validatedContactInvitation;
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactInvitation.identitySignature);
await cs.verify(contactIdentityMaster.identityPublicKey,
contactInvitationBytes, signature);
// Verify expiration
//xxx
_validInvitation = signedContactInvitation;
}); });
} on Exception catch (_) { } on Exception catch (_) {
_validInvitation = null; setState(() {
_validatingPaste = false;
_validInvitation = null;
});
} }
} }
@ -188,7 +176,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
final theme = Theme.of(context); final theme = Theme.of(context);
//final scale = theme.extension<ScaleScheme>()!; //final scale = theme.extension<ScaleScheme>()!;
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final height = MediaQuery.of(context).size.height; //final height = MediaQuery.of(context).size.height;
return SizedBox( return SizedBox(
height: 400, height: 400,
@ -204,6 +192,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
Container( Container(
constraints: const BoxConstraints(maxHeight: 200), constraints: const BoxConstraints(maxHeight: 200),
child: TextField( child: TextField(
enabled: !_validatingPaste,
onChanged: _onPasteChanged, onChanged: _onPasteChanged,
style: textTheme.labelSmall! style: textTheme.labelSmall!
.copyWith(fontFamily: 'Victor Mono', fontSize: 11), .copyWith(fontFamily: 'Victor Mono', fontSize: 11),
@ -218,31 +207,28 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
//labelText: translate('paste_invite_dialog.paste') //labelText: translate('paste_invite_dialog.paste')
), ),
).paddingAll(8)), ).paddingAll(8)),
if (_validInvitation != null) if (_validInvitation != null && !_validatingPaste)
Row( Column(children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, ProfileWidget(
children: [ name: _validInvitation!.contactRequestPrivate.profile.name,
ElevatedButton.icon( title:
icon: const Icon(Icons.check_circle), _validInvitation!.contactRequestPrivate.profile.title),
label: Text(translate('button.accept')), Row(
onPressed: () { mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// children: [
}, ElevatedButton.icon(
), icon: const Icon(Icons.check_circle),
ElevatedButton.icon( label: Text(translate('button.accept')),
icon: const Icon(Icons.cancel), onPressed: _onAccept,
label: Text(translate('button.reject')), ),
onPressed: () { ElevatedButton.icon(
// icon: const Icon(Icons.cancel),
}, label: Text(translate('button.reject')),
) onPressed: _onReject,
], )
), ],
TextField( ),
enabled: false, ])
controller: _messageTextController,
style: Theme.of(context).textTheme.bodySmall,
).paddingAll(8),
], ],
), ),
), ),
@ -252,7 +238,5 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<TextEditingController>(
'messageTextController', _messageTextController));
} }
} }

View File

@ -30,11 +30,9 @@ class ProfileWidget extends ConsumerWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
side: BorderSide(color: scale.primaryScale.border))), side: BorderSide(color: scale.primaryScale.border))),
child: Column(mainAxisSize: MainAxisSize.min, children: [ child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(name, style: Theme.of(context).textTheme.headlineSmall) Text(name, style: textTheme.headlineSmall).paddingAll(8),
.paddingAll(8),
if (title != null && title!.isNotEmpty) if (title != null && title!.isNotEmpty)
Text(title!, style: Theme.of(context).textTheme.bodyMedium) Text(title!, style: textTheme.bodyMedium).paddingLTRB(8, 0, 8, 8),
.paddingLTRB(8, 0, 8, 8),
])).paddingAll(8); ])).paddingAll(8);
} }

View File

@ -7,7 +7,7 @@ import 'package:flutter_translate/flutter_translate.dart';
import '../../components/contact_invitation_list_widget.dart'; import '../../components/contact_invitation_list_widget.dart';
import '../../components/contact_list_widget.dart'; import '../../components/contact_list_widget.dart';
import '../../components/profile.dart'; import '../../components/profile_widget.dart';
import '../../entities/local_account.dart'; import '../../entities/local_account.dart';
import '../../entities/proto.dart' as proto; import '../../entities/proto.dart' as proto;
import '../../providers/account.dart'; import '../../providers/account.dart';

View File

@ -4,6 +4,7 @@ import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/identity.dart';
import '../entities/local_account.dart'; import '../entities/local_account.dart';
import '../entities/proto.dart' as proto; import '../entities/proto.dart' as proto;
import '../entities/proto.dart' import '../entities/proto.dart'
@ -155,6 +156,93 @@ Future<Uint8List> createContactInvitation(
return signedContactInvitationBytes; return signedContactInvitationBytes;
} }
class ValidContactInvitation {
ValidContactInvitation(
{required this.signedContactInvitation,
required this.contactInvitation,
required this.contactRequestInboxKey,
required this.contactRequest,
required this.contactRequestPrivate,
required this.contactIdentityMaster});
SignedContactInvitation signedContactInvitation;
ContactInvitation contactInvitation;
TypedKey contactRequestInboxKey;
ContactRequest contactRequest;
ContactRequestPrivate contactRequestPrivate;
IdentityMaster contactIdentityMaster;
}
typedef GetEncryptionKeyCallback = Future<SecretKey> Function(
EncryptionKeyType encryptionKeyType, Uint8List encryptedSecret);
Future<ValidContactInvitation> validateContactInvitation(Uint8List inviteData,
GetEncryptionKeyCallback getEncryptionKeyCallback) async {
final signedContactInvitation =
proto.SignedContactInvitation.fromBuffer(inviteData);
final contactInvitationBytes =
Uint8List.fromList(signedContactInvitation.contactInvitation);
final contactInvitation =
proto.ContactInvitation.fromBuffer(contactInvitationBytes);
final contactRequestInboxKey =
proto.TypedKeyProto.fromProto(contactInvitation.contactRequestInboxKey);
late final ValidContactInvitation out;
final pool = await DHTRecordPool.instance();
await (await pool.openRead(contactRequestInboxKey))
.deleteScope((contactRequestInbox) async {
//
final contactRequest =
await contactRequestInbox.getProtobuf(proto.ContactRequest.fromBuffer);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
final writerSecret = await getEncryptionKeyCallback(
encryptionKeyType, Uint8List.fromList(contactInvitation.writerSecret));
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
final contactRequestPrivateBytes = await cs.decryptNoAuthWithNonce(
Uint8List.fromList(contactRequest.private), writerSecret);
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactRequestPrivate.identityMasterRecordKey);
// Fetch the account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactInvitation.identitySignature);
await cs.verify(contactIdentityMaster.identityPublicKey,
contactInvitationBytes, signature);
out = ValidContactInvitation(
signedContactInvitation: signedContactInvitation,
contactInvitation: contactInvitation,
contactRequestInboxKey: contactRequestInboxKey,
contactRequest: contactRequest,
contactRequestPrivate: contactRequestPrivate,
contactIdentityMaster: contactIdentityMaster);
});
return out;
}
Future<void> acceptContactInvitation(
ValidContactInvitation validContactInvitation) async {
//
}
Future<void> rejectContactInvitation(
ValidContactInvitation validContactInvitation) async {
//
}
/// Get the active account contact invitation list /// Get the active account contact invitation list
@riverpod @riverpod
Future<IList<ContactInvitationRecord>?> fetchContactInvitationRecords( Future<IList<ContactInvitationRecord>?> fetchContactInvitationRecords(

View File

@ -90,9 +90,8 @@ Future<IdentityMaster> openIdentityMaster(
// IdentityMaster DHT record is public/unencrypted // IdentityMaster DHT record is public/unencrypted
return (await pool.openRead(identityMasterRecordKey)) return (await pool.openRead(identityMasterRecordKey))
.deleteScope((masterRec) async { .deleteScope((masterRec) async {
final identityMasterJson = final identityMaster =
(await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!; (await masterRec.getJson(IdentityMaster.fromJson, forceRefresh: true))!;
final identityMaster = IdentityMaster.fromJson(identityMasterJson);
// Validate IdentityMaster // Validate IdentityMaster
final masterRecordKey = masterRec.key; final masterRecordKey = masterRec.key;