mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-02-22 15:59:50 -05:00
checkpoint
This commit is contained in:
parent
3af819e28b
commit
c047ae05c5
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
@ -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';
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user