diff --git a/lib/entities/identity.dart b/lib/entities/identity.dart index 7433bdb..ecaafda 100644 --- a/lib/entities/identity.dart +++ b/lib/entities/identity.dart @@ -81,6 +81,9 @@ extension IdentityMasterExtension on IdentityMaster { KeyPair masterWriter(SecretKey secret) => KeyPair(key: masterPublicKey, secret: secret); + TypedKey identityPublicTypedKey() => + TypedKey(kind: identityRecordKey.kind, value: identityPublicKey); + Future readAccountFromIdentity( {required SharedSecret identitySecret}) async { // Read the identity key to get the account keys diff --git a/lib/entities/veilidchat.proto b/lib/entities/veilidchat.proto index 027bdae..ca11446 100644 --- a/lib/entities/veilidchat.proto +++ b/lib/entities/veilidchat.proto @@ -182,7 +182,7 @@ message Conversation { Profile profile = 1; // Identity master (JSON) to publish to friend string identity_master_json = 2; - // Messages DHTLog + // Messages DHTLog (xxx for now DHTShortArray) OwnedDHTRecordPointer messages = 3; } diff --git a/lib/providers/contact.dart b/lib/providers/contact.dart index fed41dc..f44a4c9 100644 --- a/lib/providers/contact.dart +++ b/lib/providers/contact.dart @@ -43,7 +43,7 @@ Future createContact({ parent: accountRecordKey)) .scope((contactList) async { if (await contactList.tryAddItem(contact.writeToBuffer()) == false) { - throw StateError('Failed to add contact'); + throw Exception('Failed to add contact'); } }); } @@ -65,7 +65,7 @@ Future deleteContact( final item = await contactList.getItemProtobuf(proto.Contact.fromBuffer, i); if (item == null) { - throw StateError('Failed to get contact'); + throw Exception('Failed to get contact'); } if (item.remoteConversationKey == contact.remoteConversationKey) { await contactList.tryRemoveItem(i); @@ -105,7 +105,7 @@ Future?> fetchContactList(FetchContactListRef ref) async { for (var i = 0; i < cList.length; i++) { final cir = await cList.getItem(i); if (cir == null) { - throw StateError('Failed to get contact'); + throw Exception('Failed to get contact'); } out = out.add(Contact.fromBuffer(cir)); } diff --git a/lib/providers/contact_invite.dart b/lib/providers/contact_invite.dart index 0b22b0f..5d55f87 100644 --- a/lib/providers/contact_invite.dart +++ b/lib/providers/contact_invite.dart @@ -88,32 +88,38 @@ Future checkAcceptRejectContact( // 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 null; - } + await cs.verify(contactIdentityMaster.identityPublicKey, + contactResponseBytes, signature); // Pull profile from remote conversation key final remoteConversationKey = proto.TypedKeyProto.fromProto(contactResponse.remoteConversationKey); final remoteConversation = await readRemoteConversation( activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), remoteConversationKey: remoteConversationKey); if (remoteConversation == null) { - log.error('Remote conversation could not be read'); + log.info('Remote conversation could not be read. Waiting...'); return null; } - final localConversation = proto.OwnedDHTRecordPointerProto.fromProto( + // Complete the local conversation now that we have the remote profile + final localConversationOwned = proto.OwnedDHTRecordPointerProto.fromProto( contactInvitationRecord.localConversation); - return AcceptedOrRejectedContact( - acceptedContact: AcceptedContact( - profile: remoteConversation.profile, - remoteIdentity: contactIdentityMaster, - remoteConversationKey: remoteConversationKey, - localConversation: localConversation)); + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: + contactIdentityMaster.identityPublicTypedKey(), + existingConversationOwned: localConversationOwned, + // ignore: prefer_expression_function_bodies + callback: (localConversation) async { + return AcceptedOrRejectedContact( + acceptedContact: AcceptedContact( + profile: remoteConversation.profile, + remoteIdentity: contactIdentityMaster, + remoteConversationKey: remoteConversationKey, + localConversation: localConversationOwned)); + }); }); if (acceptReject == null) { @@ -129,6 +135,13 @@ Future checkAcceptRejectContact( return acceptReject; } on Exception catch (e) { log.error('Exception in checkAcceptRejectContact: $e'); + + // Attempt to clean up. All this needs better lifetime management + await deleteContactInvitation( + accepted: false, + activeAccountInfo: activeAccountInfo, + contactInvitationRecord: contactInvitationRecord); + return null; } } @@ -151,7 +164,7 @@ Future deleteContactInvitation( final item = await cirList.getItemProtobuf( proto.ContactInvitationRecord.fromBuffer, i); if (item == null) { - throw StateError('Failed to get contact invitation record'); + throw Exception('Failed to get contact invitation record'); } if (item.contactRequestInbox.recordKey == contactInvitationRecord.contactRequestInbox.recordKey) { @@ -266,7 +279,7 @@ Future createContactInvitation( parent: accountRecordKey)) .scope((cirList) async { if (await cirList.tryAddItem(cinvrec.writeToBuffer()) == false) { - throw StateError('Failed to add contact invitation record'); + throw Exception('Failed to add contact invitation record'); } }); }); @@ -363,55 +376,58 @@ Future acceptContactInvitation( ActiveAccountInfo activeAccountInfo, ValidContactInvitation validContactInvitation) async { final pool = await DHTRecordPool.instance(); - final accountRecordKey = - activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + try { + return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, + validContactInvitation.writer)) + // ignore: prefer_expression_function_bodies + .deleteScope((contactRequestInbox) async { + // Create local conversation key for this + // contact and send via contact response + return createConversation( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: validContactInvitation.contactIdentityMaster + .identityPublicTypedKey(), + callback: (localConversation) async { + final contactResponse = ContactResponse() + ..accept = true + ..remoteConversationKey = localConversation.key.toProto() + ..identityMasterRecordKey = activeAccountInfo + .localAccount.identityMaster.masterRecordKey + .toProto(); + final contactResponseBytes = contactResponse.writeToBuffer(); - return (await pool.openWrite(validContactInvitation.contactRequestInboxKey, - validContactInvitation.writer)) - .deleteScope((contactRequestInbox) async { - final cs = await pool.veilid - .getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind); + final cs = await pool.veilid.getCryptoSystem( + validContactInvitation.contactRequestInboxKey.kind); - // Create local conversation key for this - // contact and send via contact response - return (await pool.create(parent: accountRecordKey)) - .deleteScope((localConversation) async { - final contactResponse = ContactResponse() - ..accept = true - ..remoteConversationKey = localConversation.key.toProto() - ..identityMasterRecordKey = activeAccountInfo - .localAccount.identityMaster.masterRecordKey - .toProto(); - final contactResponseBytes = contactResponse.writeToBuffer(); + final identitySignature = await cs.sign( + activeAccountInfo.localAccount.identityMaster.identityPublicKey, + activeAccountInfo.userLogin.identitySecret.value, + contactResponseBytes); - final identitySignature = await cs.sign( - activeAccountInfo.localAccount.identityMaster.identityPublicKey, - activeAccountInfo.userLogin.identitySecret.value, - contactResponseBytes); + final signedContactResponse = SignedContactResponse() + ..contactResponse = contactResponseBytes + ..identitySignature = identitySignature.toProto(); - final signedContactResponse = SignedContactResponse() - ..contactResponse = contactResponseBytes - ..identitySignature = identitySignature.toProto(); - - // Write the acceptance to the inbox - if (await contactRequestInbox.tryWriteProtobuf( - SignedContactResponse.fromBuffer, signedContactResponse, - subkey: 1) != - null) { - log.error('failed to accept contact invitation'); - await localConversation.delete(); - await contactRequestInbox.delete(); - return null; - } - return AcceptedContact( - profile: validContactInvitation.contactRequestPrivate.profile, - remoteIdentity: validContactInvitation.contactIdentityMaster, - remoteConversationKey: proto.TypedKeyProto.fromProto( - validContactInvitation.contactRequestPrivate.chatRecordKey), - localConversation: localConversation.ownedDHTRecordPointer, - ); + // Write the acceptance to the inbox + if (await contactRequestInbox.tryWriteProtobuf( + SignedContactResponse.fromBuffer, signedContactResponse, + subkey: 1) != + null) { + throw Exception('failed to accept contact invitation'); + } + return AcceptedContact( + profile: validContactInvitation.contactRequestPrivate.profile, + remoteIdentity: validContactInvitation.contactIdentityMaster, + remoteConversationKey: proto.TypedKeyProto.fromProto( + validContactInvitation.contactRequestPrivate.chatRecordKey), + localConversation: localConversation.ownedDHTRecordPointer, + ); + }); }); - }); + } on Exception catch (e) { + log.error('exception: $e'); + return null; + } } Future rejectContactInvitation(ActiveAccountInfo activeAccountInfo, @@ -473,7 +489,7 @@ Future?> fetchContactInvitationRecords( for (var i = 0; i < cirList.length; i++) { final cir = await cirList.getItem(i); if (cir == null) { - throw StateError('Failed to get contact invitation record'); + throw Exception('Failed to get contact invitation record'); } out = out.add(ContactInvitationRecord.fromBuffer(cir)); } diff --git a/lib/providers/conversation.dart b/lib/providers/conversation.dart index f8f6ba3..8988088 100644 --- a/lib/providers/conversation.dart +++ b/lib/providers/conversation.dart @@ -5,7 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../entities/identity.dart'; import '../entities/proto.dart' as proto; -import '../entities/proto.dart' show Conversation, Contact; +import '../entities/proto.dart' show Conversation; import '../veilid_support/veilid_support.dart'; import 'account.dart'; @@ -14,27 +14,80 @@ import 'account.dart'; Future getConversationCrypto({ required ActiveAccountInfo activeAccountInfo, - required Contact contact, + required TypedKey remoteIdentityPublicKey, }) async { final veilid = await eventualVeilid.future; final identitySecret = activeAccountInfo.userLogin.identitySecret; final cs = await veilid.getCryptoSystem(identitySecret.kind); - final remoteIdentityPublicKey = - proto.TypedKeyProto.fromProto(contact.identityPublicKey); final sharedSecret = await cs.cachedDH(remoteIdentityPublicKey.value, identitySecret.value); return DHTRecordCryptoPrivate.fromSecret(identitySecret.kind, sharedSecret); } +// Create a conversation +// If we were the initator of the conversation there may be an +// incomplete 'existingConversationRecord' that we need to fill +// in now that we have the remote identity key +Future createConversation( + {required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, + required FutureOr Function(DHTRecord) callback, + OwnedDHTRecordPointer? existingConversationOwned}) async { + final pool = await DHTRecordPool.instance(); + final accountRecordKey = + activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; + + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + + late final DHTRecord localConversationRecord; + if (existingConversationOwned != null) { + localConversationRecord = await pool.openOwned(existingConversationOwned, + parent: accountRecordKey, crypto: crypto); + } else { + localConversationRecord = + await pool.create(parent: accountRecordKey, crypto: crypto); + } + return localConversationRecord + // ignore: prefer_expression_function_bodies + .deleteScope((localConversation) async { + // Make messages log + return (await DHTShortArray.create( + parent: localConversation.key, crypto: crypto)) + .deleteScope((messages) async { + // Write local conversation key + final conversation = Conversation() + ..profile = activeAccountInfo.account.profile + ..identityMasterJson = + jsonEncode(activeAccountInfo.localAccount.identityMaster.toJson()) + ..messages = messages.record.ownedDHTRecordPointer.toProto(); + + // + final update = await localConversation.tryWriteProtobuf( + Conversation.fromBuffer, conversation); + if (update != null) { + throw Exception('Failed to write local conversation'); + } + return await callback(localConversation); + }); + }); +} + Future readRemoteConversation({ required ActiveAccountInfo activeAccountInfo, + required TypedKey remoteIdentityPublicKey, required TypedKey remoteConversationKey, }) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final pool = await DHTRecordPool.instance(); - return (await pool.openRead(remoteConversationKey, parent: accountRecordKey)) + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + return (await pool.openRead(remoteConversationKey, + parent: accountRecordKey, crypto: crypto)) .scope((remoteConversation) async { // final conversation = @@ -46,14 +99,19 @@ Future readRemoteConversation({ Future writeLocalConversation({ required ActiveAccountInfo activeAccountInfo, required OwnedDHTRecordPointer localConversationOwned, + required TypedKey remoteIdentityPublicKey, required Conversation conversation, }) async { final accountRecordKey = activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; final pool = await DHTRecordPool.instance(); + final crypto = await getConversationCrypto( + activeAccountInfo: activeAccountInfo, + remoteIdentityPublicKey: remoteIdentityPublicKey); + return (await pool.openOwned(localConversationOwned, - parent: accountRecordKey)) + parent: accountRecordKey, crypto: crypto)) .scope((localConversation) async { // final update = await localConversation.tryWriteProtobuf( @@ -87,7 +145,7 @@ Future writeLocalConversation({ // for (var i = 0; i < cList.length; i++) { // final cir = await cList.getItem(i); // if (cir == null) { -// throw StateError('Failed to get contact'); +// throw Exception('Failed to get contact'); // } // out = out.add(Contact.fromBuffer(cir)); // }