contact reject

This commit is contained in:
Christien Rioux 2023-08-05 19:34:00 -04:00
parent 9be3d100e4
commit b12cbcf684
11 changed files with 267 additions and 76 deletions

View File

@ -34,9 +34,10 @@
3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret
## Receiving an accept/reject
1. Decrypt with writer secret
2. Get DHT record for contact's AccountMaster
3. Validate the SignedContactResponse signature
1. Open and get SignedContactResponse from ContactRequest unicaseinbox DHT record
2. Decrypt with writer secret
3. Get DHT record for contact's AccountMaster
4. Validate the SignedContactResponse signature
If accept == false:
1. Announce rejection

View File

@ -57,6 +57,7 @@ class ContactInvitationItemWidget extends ConsumerWidget {
await ref.read(fetchActiveAccountProvider.future);
if (activeAccountInfo != null) {
await deleteContactInvitation(
accepted: false,
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
ref.invalidate(fetchContactInvitationRecordsProvider);

View File

@ -1575,7 +1575,7 @@ class ContactResponse extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'accept')
..aOM<TypedKey>(2, _omitFieldNames ? '' : 'accountMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: TypedKey.create)
..aOM<TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationKey', subBuilder: TypedKey.create)
..hasRequiredFields = false
;
@ -1611,15 +1611,15 @@ class ContactResponse extends $pb.GeneratedMessage {
void clearAccept() => clearField(1);
@$pb.TagNumber(2)
TypedKey get accountMasterRecordKey => $_getN(1);
TypedKey get identityMasterRecordKey => $_getN(1);
@$pb.TagNumber(2)
set accountMasterRecordKey(TypedKey v) { setField(2, v); }
set identityMasterRecordKey(TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasAccountMasterRecordKey() => $_has(1);
$core.bool hasIdentityMasterRecordKey() => $_has(1);
@$pb.TagNumber(2)
void clearAccountMasterRecordKey() => clearField(2);
void clearIdentityMasterRecordKey() => clearField(2);
@$pb.TagNumber(2)
TypedKey ensureAccountMasterRecordKey() => $_ensure(1);
TypedKey ensureIdentityMasterRecordKey() => $_ensure(1);
@$pb.TagNumber(3)
TypedKey get remoteConversationKey => $_getN(2);

View File

@ -427,17 +427,17 @@ const ContactResponse$json = {
'1': 'ContactResponse',
'2': [
{'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'},
{'1': 'account_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'accountMasterRecordKey'},
{'1': 'identity_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.TypedKey', '10': 'identityMasterRecordKey'},
{'1': 'remote_conversation_key', '3': 3, '4': 1, '5': 11, '6': '.TypedKey', '10': 'remoteConversationKey'},
],
};
/// Descriptor for `ContactResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode(
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSRAoZYWNjb3VudF9tYX'
'N0ZXJfcmVjb3JkX2tleRgCIAEoCzIJLlR5cGVkS2V5UhZhY2NvdW50TWFzdGVyUmVjb3JkS2V5'
'EkEKF3JlbW90ZV9jb252ZXJzYXRpb25fa2V5GAMgASgLMgkuVHlwZWRLZXlSFXJlbW90ZUNvbn'
'ZlcnNhdGlvbktleQ==');
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSRgoaaWRlbnRpdHlfbW'
'FzdGVyX3JlY29yZF9rZXkYAiABKAsyCS5UeXBlZEtleVIXaWRlbnRpdHlNYXN0ZXJSZWNvcmRL'
'ZXkSQQoXcmVtb3RlX2NvbnZlcnNhdGlvbl9rZXkYAyABKAsyCS5UeXBlZEtleVIVcmVtb3RlQ2'
'9udmVyc2F0aW9uS2V5');
@$core.Deprecated('Use signedContactResponseDescriptor instead')
const SignedContactResponse$json = {

View File

@ -307,7 +307,7 @@ message ContactRequestPrivate {
CryptoKey writer_key = 1;
// Snapshot of profile
Profile profile = 2;
// Identity master dht key
// Identity master DHT record key
TypedKey identity_master_record_key = 3;
// Local chat DHT record key
TypedKey chat_record_key = 4;
@ -319,8 +319,8 @@ message ContactRequestPrivate {
message ContactResponse {
// Accept or reject
bool accept = 1;
// Account master record key
TypedKey account_master_record_key = 2;
// Remote identity master DHT record key
TypedKey identity_master_record_key = 2;
// Remote chat DHT record key if accepted
TypedKey remote_conversation_key = 3;
}

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -5,10 +7,13 @@ import 'package:split_view/split_view.dart';
import 'package:signal_strength_indicator/signal_strength_indicator.dart';
import '../components/chat_component.dart';
import '../providers/account.dart';
import '../providers/contact.dart';
import '../providers/local_accounts.dart';
import '../providers/logins.dart';
import '../providers/window_control.dart';
import '../tools/tools.dart';
import '../veilid_support/dht_support/dht_record_pool.dart';
import 'main_pager/main_pager.dart';
class HomePage extends ConsumerStatefulWidget {
@ -19,10 +24,17 @@ class HomePage extends ConsumerStatefulWidget {
HomePageState createState() => HomePageState();
}
// XXX Eliminate this when we have ValueChanged
const int ticksPerContactInvitationCheck = 5;
class HomePageState extends ConsumerState<HomePage>
with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
Timer? _homeTickTimer;
bool _inHomeTick = false;
int _contactInvitationCheckTick = 0;
@override
void initState() {
super.initState();
@ -31,15 +43,69 @@ class HomePageState extends ConsumerState<HomePage>
setState(() {});
await ref.read(windowControlProvider.notifier).changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
_homeTickTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!_inHomeTick) {
unawaited(_onHomeTick());
}
});
});
}
@override
void dispose() {
final homeTickTimer = _homeTickTimer;
if (homeTickTimer != null) {
homeTickTimer.cancel();
}
_unfocusNode.dispose();
super.dispose();
}
Future<void> _onHomeTick() async {
_inHomeTick = true;
try {
// Check extant contact invitations once every 5 seconds
_contactInvitationCheckTick += 1;
if (_contactInvitationCheckTick >= ticksPerContactInvitationCheck) {
_contactInvitationCheckTick = 0;
await _doContactInvitationCheck();
}
} finally {
_inHomeTick = false;
}
}
Future<void> _doContactInvitationCheck() async {
final contactInvitationRecords =
await ref.read(fetchContactInvitationRecordsProvider.future);
final activeAccountInfo = await ref.read(fetchActiveAccountProvider.future);
if (contactInvitationRecords == null || activeAccountInfo == null) {
return;
}
final allChecks = <Future<void>>[];
for (final contactInvitationRecord in contactInvitationRecords) {
allChecks.add(() async {
final acceptReject = await checkAcceptRejectContact(
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
if (acceptReject != null) {
if (acceptReject) {
// Accept
ref
..invalidate(fetchContactInvitationRecordsProvider)
..invalidate(fetchContactListProvider);
} else {
// Reject
ref.invalidate(fetchContactInvitationRecordsProvider);
}
}
}());
}
await Future.wait(allChecks);
}
// ignore: prefer_expression_function_bodies
Widget buildPhone(BuildContext context) {
//

View File

@ -14,8 +14,8 @@ import '../entities/proto.dart'
ContactInvitationRecord,
ContactRequest,
ContactRequestPrivate,
SignedContactInvitation,
ContactResponse,
SignedContactInvitation,
SignedContactResponse;
import '../log/loggy.dart';
import '../tools/tools.dart';
@ -24,9 +24,90 @@ import 'account.dart';
part 'contact.g.dart';
Future<void> deleteContactInvitation(
Future<bool?> checkAcceptRejectContact(
{required ActiveAccountInfo activeAccountInfo,
required ContactInvitationRecord contactInvitationRecord}) async {
// Open the contact request inbox
try {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final writerKey =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerKey);
final writerSecret =
proto.CryptoKeyProto.fromProto(contactInvitationRecord.writerSecret);
final writer = TypedKeyPair(
kind: contactInvitationRecord.contactRequestInbox.recordKey.kind,
key: writerKey,
secret: writerSecret);
final acceptReject = await (await pool.openRead(
proto.TypedKeyProto.fromProto(
contactInvitationRecord.contactRequestInbox.recordKey),
crypto: await DHTRecordCryptoPrivate.fromTypedKeyPair(writer),
parent: accountRecordKey,
defaultSubkey: 1))
.scope((contactRequestInbox) async {
//
final signedContactResponse = await contactRequestInbox
.getProtobuf(SignedContactResponse.fromBuffer, forceRefresh: true);
if (signedContactResponse == null) {
return null;
}
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse = ContactResponse.fromBuffer(contactResponseBytes);
final contactIdentityMasterRecordKey = proto.TypedKeyProto.fromProto(
contactResponse.identityMasterRecordKey);
final cs = await pool.veilid.getCryptoSystem(
contactInvitationRecord.contactRequestInbox.recordKey.kind);
// Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
// Verify
final signature = proto.SignatureProto.fromProto(
signedContactResponse.identitySignature);
try {
await cs.verify(contactIdentityMaster.identityPublicKey,
contactResponseBytes, signature);
} on Exception catch (e) {
log.error('Bad identity used, failed to verify: $e');
return false;
}
return contactResponse.accept;
});
if (acceptReject == null) {
return null;
}
// Add contact if accepted
if (acceptReject) {
//
await deleteContactInvitation(
accepted: true,
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
return true;
} else {
await deleteContactInvitation(
accepted: false,
activeAccountInfo: activeAccountInfo,
contactInvitationRecord: contactInvitationRecord);
return false;
}
} on Exception catch (e) {
log.error('Exception in checkAcceptRejectContact: $e');
return null;
}
}
Future<void> deleteContactInvitation(
{required bool accepted,
required ActiveAccountInfo activeAccountInfo,
required ContactInvitationRecord contactInvitationRecord}) async {
final pool = await DHTRecordPool.instance();
final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -53,12 +134,18 @@ Future<void> deleteContactInvitation(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.contactRequestInbox),
parent: accountRecordKey))
.delete();
await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.localConversation),
parent: accountRecordKey))
.delete();
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
await contactRequestInbox.delete();
});
if (!accepted) {
await (await pool.openOwned(
proto.OwnedDHTRecordPointerProto.fromProto(
contactInvitationRecord.localConversation),
parent: accountRecordKey))
.delete();
}
});
}
@ -255,7 +342,7 @@ Future<void> acceptContactInvitation(ActiveAccountInfo activeAccountInfo,
// xxx
final contactResponse = ContactResponse()
..accept = false
..accountMasterRecordKey = activeAccountInfo
..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
@ -290,7 +377,7 @@ Future<void> rejectContactInvitation(ActiveAccountInfo activeAccountInfo,
final contactResponse = ContactResponse()
..accept = false
..accountMasterRecordKey = activeAccountInfo
..identityMasterRecordKey = activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart';
import 'package:veilid/veilid.dart';
import '../../tools/tools.dart';
import '../veilid_support.dart';
@ -51,7 +50,7 @@ class DHTRecord {
}
final pool = await DHTRecordPool.instance();
await _routingContext.closeDHTRecord(_recordDescriptor.key);
pool.recordClosed(this);
pool.recordClosed(_recordDescriptor.key);
_open = false;
}
@ -71,7 +70,9 @@ class DHTRecord {
try {
return await scopeFunction(this);
} finally {
await close();
if (_valid) {
await close();
}
}
}
@ -79,10 +80,14 @@ class DHTRecord {
FutureOr<T> Function(DHTRecord) scopeFunction) async {
try {
final out = await scopeFunction(this);
await close();
if (_valid && _open) {
await close();
}
return out;
} on Exception catch (_) {
await delete();
if (_valid) {
await delete();
}
rethrow;
}
}

View File

@ -1,5 +1,6 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:mutex/mutex.dart';
import '../../log/loggy.dart';
import '../veilid_support.dart';
@ -38,14 +39,14 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = DHTRecordPoolAllocations(
childrenByParent: IMap(), parentByChild: IMap()),
_opened = <TypedKey, DHTRecord>{},
_opened = <TypedKey, Mutex>{},
_routingContext = routingContext,
_veilid = veilid;
// Persistent DHT record list
DHTRecordPoolAllocations _state;
// Which DHT records are currently open
final Map<TypedKey, DHTRecord> _opened;
final Map<TypedKey, Mutex> _opened;
// Default routing context to use for new keys
final VeilidRoutingContext _routingContext;
// Convenience accessor
@ -89,14 +90,20 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
Veilid get veilid => _veilid;
void _recordOpened(DHTRecord record) {
assert(!_opened.containsKey(record.key), 'record already opened');
_opened[record.key] = record;
Future<void> _recordOpened(TypedKey key) async {
// no race because dart is single threaded until async breaks
final m = _opened[key] ?? Mutex();
_opened[key] = m;
await m.acquire();
_opened[key] = m;
}
void recordClosed(DHTRecord record) {
assert(_opened.containsKey(record.key), 'record already closed');
_opened.remove(record.key);
void recordClosed(TypedKey key) {
final m = _opened.remove(key);
if (m == null) {
throw StateError('record already closed');
}
m.release();
}
Future<void> deleteDeep(TypedKey parent) async {
@ -191,7 +198,8 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
if (parent != null) {
await _addDependency(parent, rec.key);
}
_recordOpened(rec);
await _recordOpened(rec.key);
return rec;
}
@ -202,25 +210,32 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
TypedKey? parent,
int defaultSubkey = 0,
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.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
await _recordOpened(recordKey);
// Open from the veilid api
final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null);
final rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
crypto: crypto ?? const DHTRecordCryptoPublic());
late final DHTRecord rec;
try {
// 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.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
// Register the dependency if specified
if (parent != null) {
await _addDependency(parent, rec.key);
// Open from the veilid api
final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
crypto: crypto ?? const DHTRecordCryptoPublic());
// Register the dependency if specified
if (parent != null) {
await _addDependency(parent, rec.key);
}
} on Exception catch (_) {
recordClosed(recordKey);
rethrow;
}
_recordOpened(rec);
return rec;
}
@ -234,28 +249,35 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
int defaultSubkey = 0,
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.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
await _recordOpened(recordKey);
// Open from the veilid api
final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer);
final rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
writer: writer,
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
late final DHTRecord rec;
try {
// 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.toJson()];
assert(existingParent == parent, 'wrong parent for opened key');
// Register the dependency if specified
if (parent != null) {
await _addDependency(parent, rec.key);
// Open from the veilid api
final dhtctx = routingContext ?? _routingContext;
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
writer: writer,
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
// Register the dependency if specified
if (parent != null) {
await _addDependency(parent, rec.key);
}
} on Exception catch (_) {
recordClosed(recordKey);
rethrow;
}
_recordOpened(rec);
return rec;
}

View File

@ -781,6 +781,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.7.8"
mutex:
dependency: "direct main"
description:
name: mutex
sha256: "03116a4e46282a671b46c12de649d72c0ed18188ffe12a8d0fc63e83f4ad88f4"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
octo_image:
dependency: transitive
description:

View File

@ -44,6 +44,7 @@ dependencies:
json_annotation: ^4.8.1
loggy: ^2.0.3
motion_toast: ^2.7.8
mutex: ^3.0.1
path: ^1.8.2
path_provider: ^2.0.11
pinput: ^2.3.0