mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-12-24 23:29:32 -05:00
encryption and dht work
This commit is contained in:
parent
7831deabd3
commit
c63eee26fd
@ -83,15 +83,21 @@
|
|||||||
"copy_invitation": "Copy Invitation",
|
"copy_invitation": "Copy Invitation",
|
||||||
"invitation_copied": "Invitation Copied"
|
"invitation_copied": "Invitation Copied"
|
||||||
},
|
},
|
||||||
|
"contact_invite": {
|
||||||
|
"message_from_contact": "Message from contact",
|
||||||
|
"validating": "Validating...",
|
||||||
|
"failed_to_accept": "Failed to accept contact invite",
|
||||||
|
"failed_to_reject": "Failed to reject contact invite",
|
||||||
|
"invalid_invitation": "Invalid invitation",
|
||||||
|
"protected_with_pin": "Contact invite is protected with a PIN",
|
||||||
|
"protected_with_password": "Contact invite is protected with a password",
|
||||||
|
"invalid_pin": "Invalid PIN",
|
||||||
|
"invalid_password": "Invalid password"
|
||||||
|
},
|
||||||
"paste_invite_dialog": {
|
"paste_invite_dialog": {
|
||||||
"title": "Paste Contact Invite",
|
"title": "Paste Contact Invite",
|
||||||
"paste_invite_here": "Paste your contact invite here:",
|
"paste_invite_here": "Paste your contact invite here:",
|
||||||
"paste": "Paste",
|
"paste": "Paste"
|
||||||
"message_from_contact": "Message from contact",
|
|
||||||
"validating": "Validating...",
|
|
||||||
"invalid_invitation": "Invalid invitation",
|
|
||||||
"failed_to_accept": "Failed to accept contact invite",
|
|
||||||
"failed_to_reject": "Failed to reject contact invite"
|
|
||||||
},
|
},
|
||||||
"enter_pin_dialog": {
|
"enter_pin_dialog": {
|
||||||
"enter_pin": "Enter PIN",
|
"enter_pin": "Enter PIN",
|
||||||
|
@ -33,9 +33,6 @@ class PasteInviteDialog extends ConsumerStatefulWidget {
|
|||||||
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
||||||
final _pasteTextController = TextEditingController();
|
final _pasteTextController = TextEditingController();
|
||||||
|
|
||||||
EncryptionKeyType _encryptionKeyType = EncryptionKeyType.none;
|
|
||||||
String _encryptionKey = '';
|
|
||||||
Timestamp? _expiration;
|
|
||||||
ValidContactInvitation? _validInvitation;
|
ValidContactInvitation? _validInvitation;
|
||||||
bool _validatingPaste = false;
|
bool _validatingPaste = false;
|
||||||
bool _isAccepting = false;
|
bool _isAccepting = false;
|
||||||
@ -45,61 +42,6 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Future<void> _onNoneEncryptionSelected(bool selected) async {
|
|
||||||
// setState(() {
|
|
||||||
// if (selected) {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.none;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> _onPinEncryptionSelected(bool selected) async {
|
|
||||||
// final description = translate('receive_invite_dialog.pin_description');
|
|
||||||
// final pin = await showDialog<String>(
|
|
||||||
// context: context,
|
|
||||||
// builder: (context) => EnterPinDialog(description: description));
|
|
||||||
// if (pin == null) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// // ignore: use_build_context_synchronously
|
|
||||||
// if (!context.mounted) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// final matchpin = await showDialog<String>(
|
|
||||||
// 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('receive_invite_dialog.pin_does_not_match'));
|
|
||||||
// setState(() {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.none;
|
|
||||||
// _encryptionKey = '';
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Future<void> _onPasswordEncryptionSelected(bool selected) async {
|
|
||||||
// setState(() {
|
|
||||||
// if (selected) {
|
|
||||||
// _encryptionKeyType = EncryptionKeyType.password;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
Future<void> _onAccept() async {
|
Future<void> _onAccept() async {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
|
|
||||||
@ -133,7 +75,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
..invalidate(fetchContactListProvider);
|
..invalidate(fetchContactListProvider);
|
||||||
} else {
|
} else {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showErrorToast(context, 'paste_invite_dialog.failed_to_accept');
|
showErrorToast(context, 'contact_invite.failed_to_accept');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +105,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
// do nothing right now
|
// do nothing right now
|
||||||
} else {
|
} else {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
showErrorToast(context, 'paste_invite_dialog.failed_to_reject');
|
showErrorToast(context, 'contact_invite.failed_to_reject');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,23 +145,74 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
||||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||||
|
|
||||||
|
final activeAccountInfo =
|
||||||
|
await ref.read(fetchActiveAccountProvider.future);
|
||||||
|
if (activeAccountInfo == null) {
|
||||||
|
setState(() {
|
||||||
|
_validatingPaste = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_validatingPaste = true;
|
_validatingPaste = true;
|
||||||
_validInvitation = null;
|
_validInvitation = null;
|
||||||
});
|
});
|
||||||
final validatedContactInvitation = await validateContactInvitation(
|
final validatedContactInvitation = await validateContactInvitation(
|
||||||
inviteData, (encryptionKeyType, encryptedSecret) async {
|
activeAccountInfo: activeAccountInfo,
|
||||||
switch (encryptionKeyType) {
|
inviteData: inviteData,
|
||||||
case EncryptionKeyType.none:
|
getEncryptionKeyCallback:
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
(cs, encryptionKeyType, encryptedSecret) async {
|
||||||
case EncryptionKeyType.pin:
|
String encryptionKey;
|
||||||
//xxx
|
switch (encryptionKeyType) {
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
case EncryptionKeyType.none:
|
||||||
case EncryptionKeyType.password:
|
encryptionKey = '';
|
||||||
//xxx
|
case EncryptionKeyType.pin:
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
final description =
|
||||||
}
|
translate('contact_invite.protected_with_pin');
|
||||||
});
|
if (!context.mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final pin = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
EnterPinDialog(description: description));
|
||||||
|
if (pin == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
encryptionKey = pin;
|
||||||
|
case EncryptionKeyType.password:
|
||||||
|
final description =
|
||||||
|
translate('contact_invite.protected_with_pin');
|
||||||
|
if (!context.mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final password = await showDialog<String>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
EnterPinDialog(description: description));
|
||||||
|
if (password == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
encryptionKey = password;
|
||||||
|
}
|
||||||
|
return decryptSecretFromBytes(
|
||||||
|
secretBytes: encryptedSecret,
|
||||||
|
cryptoKind: cs.kind(),
|
||||||
|
encryptionKeyType: encryptionKeyType,
|
||||||
|
encryptionKey: encryptionKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if validation was cancelled
|
||||||
|
if (validatedContactInvitation == null) {
|
||||||
|
setState(() {
|
||||||
|
_validatingPaste = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Verify expiration
|
// Verify expiration
|
||||||
// xxx
|
// xxx
|
||||||
|
|
||||||
@ -227,11 +220,29 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
_validatingPaste = false;
|
_validatingPaste = false;
|
||||||
_validInvitation = validatedContactInvitation;
|
_validInvitation = validatedContactInvitation;
|
||||||
});
|
});
|
||||||
|
} on ContactInviteInvalidKeyException catch (e) {
|
||||||
|
String errorText;
|
||||||
|
switch (e.type) {
|
||||||
|
case EncryptionKeyType.none:
|
||||||
|
errorText = translate('contact_invite.invalid_invitation');
|
||||||
|
case EncryptionKeyType.password:
|
||||||
|
errorText = translate('contact_invite.invalid_pin');
|
||||||
|
case EncryptionKeyType.pin:
|
||||||
|
errorText = translate('contact_invite.invalid_password');
|
||||||
|
}
|
||||||
|
if (context.mounted) {
|
||||||
|
showErrorToast(context, errorText);
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_validatingPaste = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validatingPaste = false;
|
_validatingPaste = false;
|
||||||
_validInvitation = null;
|
_validInvitation = null;
|
||||||
});
|
});
|
||||||
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -277,7 +288,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
)).paddingLTRB(0, 0, 0, 8),
|
)).paddingLTRB(0, 0, 0, 8),
|
||||||
if (_validatingPaste)
|
if (_validatingPaste)
|
||||||
Column(children: [
|
Column(children: [
|
||||||
Text(translate('paste_invite_dialog.validating'))
|
Text(translate('contact_invite.validating'))
|
||||||
.paddingLTRB(0, 0, 0, 8),
|
.paddingLTRB(0, 0, 0, 8),
|
||||||
buildProgressIndicator(context),
|
buildProgressIndicator(context),
|
||||||
]).paddingAll(16).toCenter(),
|
]).paddingAll(16).toCenter(),
|
||||||
@ -285,7 +296,7 @@ class PasteInviteDialogState extends ConsumerState<PasteInviteDialog> {
|
|||||||
!_validatingPaste &&
|
!_validatingPaste &&
|
||||||
_pasteTextController.text.isNotEmpty)
|
_pasteTextController.text.isNotEmpty)
|
||||||
Column(children: [
|
Column(children: [
|
||||||
Text(translate('paste_invite_dialog.invalid_invitation')),
|
Text(translate('contact_invite.invalid_invitation')),
|
||||||
const Icon(Icons.error)
|
const Icon(Icons.error)
|
||||||
]).paddingAll(16).toCenter(),
|
]).paddingAll(16).toCenter(),
|
||||||
if (_validInvitation != null && !_validatingPaste)
|
if (_validInvitation != null && !_validatingPaste)
|
||||||
|
@ -196,30 +196,42 @@ class ScanInviteDialogState extends ConsumerState<ScanInviteDialog> {
|
|||||||
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
final inviteDataBase64 = lines.sublist(firstline, lastline).join();
|
||||||
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
final inviteData = base64UrlNoPadDecode(inviteDataBase64);
|
||||||
|
|
||||||
|
final activeAccountInfo =
|
||||||
|
await ref.read(fetchActiveAccountProvider.future);
|
||||||
|
if (activeAccountInfo == null) {
|
||||||
|
setState(() {
|
||||||
|
_validatingPaste = false;
|
||||||
|
_validInvitation = null;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_validatingPaste = true;
|
_validatingPaste = true;
|
||||||
_validInvitation = null;
|
_validInvitation = null;
|
||||||
});
|
});
|
||||||
final validatedContactInvitation = await validateContactInvitation(
|
// final validatedContactInvitation = await validateContactInvitation(
|
||||||
inviteData, (encryptionKeyType, encryptedSecret) async {
|
// activeAccountInfo: activeAccountInfo,
|
||||||
switch (encryptionKeyType) {
|
// inviteData: inviteData,
|
||||||
case EncryptionKeyType.none:
|
// getEncryptionKeyCallback: (encryptionKeyType, encryptedSecret) async {
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
// switch (encryptionKeyType) {
|
||||||
case EncryptionKeyType.pin:
|
// case EncryptionKeyType.none:
|
||||||
//xxx
|
// return SecretKey.fromBytes(encryptedSecret);
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
// case EncryptionKeyType.pin:
|
||||||
case EncryptionKeyType.password:
|
// //xxx
|
||||||
//xxx
|
// return SecretKey.fromBytes(encryptedSecret);
|
||||||
return SecretKey.fromBytes(encryptedSecret);
|
// case EncryptionKeyType.password:
|
||||||
}
|
// //xxx
|
||||||
});
|
// return SecretKey.fromBytes(encryptedSecret);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
// Verify expiration
|
// Verify expiration
|
||||||
// xxx
|
// xxx
|
||||||
|
|
||||||
setState(() {
|
// setState(() {
|
||||||
_validatingPaste = false;
|
// _validatingPaste = false;
|
||||||
_validInvitation = validatedContactInvitation;
|
// _validInvitation = validatedContactInvitation;
|
||||||
});
|
// });
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_validatingPaste = false;
|
_validatingPaste = false;
|
||||||
|
@ -9,6 +9,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
|
|
||||||
import '../components/default_app_bar.dart';
|
import '../components/default_app_bar.dart';
|
||||||
import '../components/signal_strength_meter.dart';
|
import '../components/signal_strength_meter.dart';
|
||||||
|
import '../entities/entities.dart';
|
||||||
import '../providers/local_accounts.dart';
|
import '../providers/local_accounts.dart';
|
||||||
import '../providers/logins.dart';
|
import '../providers/logins.dart';
|
||||||
import '../providers/window_control.dart';
|
import '../providers/window_control.dart';
|
||||||
@ -59,9 +60,9 @@ class NewAccountPageState extends ConsumerState<NewAccountPage> {
|
|||||||
title: title);
|
title: title);
|
||||||
|
|
||||||
// Log in the new account by default with no pin
|
// Log in the new account by default with no pin
|
||||||
final ok = await logins
|
final ok = await logins.login(localAccount.identityMaster.masterRecordKey,
|
||||||
.loginWithNone(localAccount.identityMaster.masterRecordKey);
|
EncryptionKeyType.none, '');
|
||||||
assert(ok == true, 'login with none should never fail');
|
assert(ok, 'login with none should never fail');
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
await imws.delete();
|
await imws.delete();
|
||||||
rethrow;
|
rethrow;
|
||||||
|
@ -24,6 +24,11 @@ import 'conversation.dart';
|
|||||||
|
|
||||||
part 'contact_invite.g.dart';
|
part 'contact_invite.g.dart';
|
||||||
|
|
||||||
|
class ContactInviteInvalidKeyException implements Exception {
|
||||||
|
const ContactInviteInvalidKeyException(this.type) : super();
|
||||||
|
final EncryptionKeyType type;
|
||||||
|
}
|
||||||
|
|
||||||
class AcceptedContact {
|
class AcceptedContact {
|
||||||
AcceptedContact({
|
AcceptedContact({
|
||||||
required this.profile,
|
required this.profile,
|
||||||
@ -238,8 +243,8 @@ Future<Uint8List> createContactInvitation(
|
|||||||
..chatRecordKey = localConversation.key.toProto()
|
..chatRecordKey = localConversation.key.toProto()
|
||||||
..expiration = expiration?.toInt64() ?? Int64.ZERO;
|
..expiration = expiration?.toInt64() ?? Int64.ZERO;
|
||||||
final crprivbytes = crpriv.writeToBuffer();
|
final crprivbytes = crpriv.writeToBuffer();
|
||||||
final encryptedContactRequestPrivate = await cs.encryptNoAuthWithNonce(
|
final encryptedContactRequestPrivate =
|
||||||
crprivbytes, contactRequestWriter.secret);
|
await cs.encryptAeadWithNonce(crprivbytes, contactRequestWriter.secret);
|
||||||
|
|
||||||
// Create ContactRequest and embed contactrequestprivate
|
// Create ContactRequest and embed contactrequestprivate
|
||||||
final creq = ContactRequest()
|
final creq = ContactRequest()
|
||||||
@ -315,11 +320,18 @@ class ValidContactInvitation {
|
|||||||
KeyPair writer;
|
KeyPair writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef GetEncryptionKeyCallback = Future<SecretKey> Function(
|
typedef GetEncryptionKeyCallback = Future<SecretKey?> Function(
|
||||||
EncryptionKeyType encryptionKeyType, Uint8List encryptedSecret);
|
VeilidCryptoSystem cs,
|
||||||
|
EncryptionKeyType encryptionKeyType,
|
||||||
|
Uint8List encryptedSecret);
|
||||||
|
|
||||||
|
Future<ValidContactInvitation?> validateContactInvitation(
|
||||||
|
{required ActiveAccountInfo activeAccountInfo,
|
||||||
|
required Uint8List inviteData,
|
||||||
|
required GetEncryptionKeyCallback getEncryptionKeyCallback}) async {
|
||||||
|
final accountRecordKey =
|
||||||
|
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||||
|
|
||||||
Future<ValidContactInvitation> validateContactInvitation(Uint8List inviteData,
|
|
||||||
GetEncryptionKeyCallback getEncryptionKeyCallback) async {
|
|
||||||
final signedContactInvitation =
|
final signedContactInvitation =
|
||||||
proto.SignedContactInvitation.fromBuffer(inviteData);
|
proto.SignedContactInvitation.fromBuffer(inviteData);
|
||||||
|
|
||||||
@ -334,19 +346,27 @@ Future<ValidContactInvitation> validateContactInvitation(Uint8List inviteData,
|
|||||||
late final ValidContactInvitation out;
|
late final ValidContactInvitation out;
|
||||||
|
|
||||||
final pool = await DHTRecordPool.instance();
|
final pool = await DHTRecordPool.instance();
|
||||||
await (await pool.openRead(contactRequestInboxKey))
|
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
|
||||||
.deleteScope((contactRequestInbox) async {
|
|
||||||
|
// See if we're chatting to ourselves, if so, don't delete it here
|
||||||
|
final ownKey = pool.getParentRecord(contactRequestInboxKey) != null;
|
||||||
|
|
||||||
|
await (await pool.openRead(contactRequestInboxKey, parent: accountRecordKey))
|
||||||
|
.maybeDeleteScope(!ownKey, (contactRequestInbox) async {
|
||||||
//
|
//
|
||||||
final contactRequest =
|
final contactRequest =
|
||||||
await contactRequestInbox.getProtobuf(proto.ContactRequest.fromBuffer);
|
await contactRequestInbox.getProtobuf(proto.ContactRequest.fromBuffer);
|
||||||
|
|
||||||
// Decrypt contact request private
|
// Decrypt contact request private
|
||||||
final encryptionKeyType =
|
final encryptionKeyType =
|
||||||
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
|
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
|
||||||
final writerSecret = await getEncryptionKeyCallback(
|
final writerSecret = await getEncryptionKeyCallback(cs, encryptionKeyType,
|
||||||
encryptionKeyType, Uint8List.fromList(contactInvitation.writerSecret));
|
Uint8List.fromList(contactInvitation.writerSecret));
|
||||||
|
if (writerSecret == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
|
final contactRequestPrivateBytes = await cs.decryptAeadWithNonce(
|
||||||
final contactRequestPrivateBytes = await cs.decryptNoAuthWithNonce(
|
|
||||||
Uint8List.fromList(contactRequest.private), writerSecret);
|
Uint8List.fromList(contactRequest.private), writerSecret);
|
||||||
final contactRequestPrivate =
|
final contactRequestPrivate =
|
||||||
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
|
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
|
||||||
@ -360,8 +380,12 @@ Future<ValidContactInvitation> validateContactInvitation(Uint8List inviteData,
|
|||||||
// Verify
|
// Verify
|
||||||
final signature = proto.SignatureProto.fromProto(
|
final signature = proto.SignatureProto.fromProto(
|
||||||
signedContactInvitation.identitySignature);
|
signedContactInvitation.identitySignature);
|
||||||
await cs.verify(contactIdentityMaster.identityPublicKey,
|
try {
|
||||||
contactInvitationBytes, signature);
|
await cs.verify(contactIdentityMaster.identityPublicKey,
|
||||||
|
contactInvitationBytes, signature);
|
||||||
|
} on Exception catch (_) {
|
||||||
|
throw ContactInviteInvalidKeyException(encryptionKeyType);
|
||||||
|
}
|
||||||
|
|
||||||
final writer = KeyPair(
|
final writer = KeyPair(
|
||||||
key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey),
|
key: proto.CryptoKeyProto.fromProto(contactRequestPrivate.writerKey),
|
||||||
@ -385,10 +409,18 @@ Future<AcceptedContact?> acceptContactInvitation(
|
|||||||
ValidContactInvitation validContactInvitation) async {
|
ValidContactInvitation validContactInvitation) async {
|
||||||
final pool = await DHTRecordPool.instance();
|
final pool = await DHTRecordPool.instance();
|
||||||
try {
|
try {
|
||||||
|
// Ensure we don't delete this if we're trying to chat to self
|
||||||
|
final ownKey =
|
||||||
|
pool.getParentRecord(validContactInvitation.contactRequestInboxKey) !=
|
||||||
|
null;
|
||||||
|
final accountRecordKey =
|
||||||
|
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||||
|
|
||||||
return (await pool.openWrite(validContactInvitation.contactRequestInboxKey,
|
return (await pool.openWrite(validContactInvitation.contactRequestInboxKey,
|
||||||
validContactInvitation.writer))
|
validContactInvitation.writer,
|
||||||
|
parent: accountRecordKey))
|
||||||
// ignore: prefer_expression_function_bodies
|
// ignore: prefer_expression_function_bodies
|
||||||
.deleteScope((contactRequestInbox) async {
|
.maybeDeleteScope(!ownKey, (contactRequestInbox) async {
|
||||||
// Create local conversation key for this
|
// Create local conversation key for this
|
||||||
// contact and send via contact response
|
// contact and send via contact response
|
||||||
return createConversation(
|
return createConversation(
|
||||||
@ -441,9 +473,18 @@ Future<AcceptedContact?> acceptContactInvitation(
|
|||||||
Future<bool> rejectContactInvitation(ActiveAccountInfo activeAccountInfo,
|
Future<bool> rejectContactInvitation(ActiveAccountInfo activeAccountInfo,
|
||||||
ValidContactInvitation validContactInvitation) async {
|
ValidContactInvitation validContactInvitation) async {
|
||||||
final pool = await DHTRecordPool.instance();
|
final pool = await DHTRecordPool.instance();
|
||||||
|
|
||||||
|
// Ensure we don't delete this if we're trying to chat to self
|
||||||
|
final ownKey =
|
||||||
|
pool.getParentRecord(validContactInvitation.contactRequestInboxKey) !=
|
||||||
|
null;
|
||||||
|
final accountRecordKey =
|
||||||
|
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
|
||||||
|
|
||||||
return (await pool.openWrite(validContactInvitation.contactRequestInboxKey,
|
return (await pool.openWrite(validContactInvitation.contactRequestInboxKey,
|
||||||
validContactInvitation.writer))
|
validContactInvitation.writer,
|
||||||
.deleteScope((contactRequestInbox) async {
|
parent: accountRecordKey))
|
||||||
|
.maybeDeleteScope(!ownKey, (contactRequestInbox) async {
|
||||||
final cs = await pool.veilid
|
final cs = await pool.veilid
|
||||||
.getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind);
|
.getCryptoSystem(validContactInvitation.contactRequestInboxKey.kind);
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
|
|
||||||
import '../entities/entities.dart';
|
import '../entities/entities.dart';
|
||||||
import '../log/loggy.dart';
|
import '../log/loggy.dart';
|
||||||
|
import '../tools/tools.dart';
|
||||||
import '../veilid_support/veilid_support.dart';
|
import '../veilid_support/veilid_support.dart';
|
||||||
import 'local_accounts.dart';
|
import 'local_accounts.dart';
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||||||
state = AsyncValue.data(updated);
|
state = AsyncValue.data(updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _loginCommon(
|
Future<bool> _decryptedLogin(
|
||||||
IdentityMaster identityMaster, SecretKey identitySecret) async {
|
IdentityMaster identityMaster, SecretKey identitySecret) async {
|
||||||
final veilid = await eventualVeilid.future;
|
final veilid = await eventualVeilid.future;
|
||||||
final cs =
|
final cs =
|
||||||
@ -89,7 +90,8 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> loginWithNone(TypedKey accountMasterRecordKey) async {
|
Future<bool> login(TypedKey accountMasterRecordKey,
|
||||||
|
EncryptionKeyType encryptionKeyType, String encryptionKey) async {
|
||||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
||||||
|
|
||||||
// Get account, throws if not found
|
// Get account, throws if not found
|
||||||
@ -99,42 +101,19 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||||||
// Log in with this local account
|
// Log in with this local account
|
||||||
|
|
||||||
// Derive key from password
|
// Derive key from password
|
||||||
if (localAccount.encryptionKeyType != EncryptionKeyType.none) {
|
if (localAccount.encryptionKeyType != encryptionKeyType) {
|
||||||
throw Exception('Wrong authentication type');
|
throw Exception('Wrong authentication type');
|
||||||
}
|
}
|
||||||
|
|
||||||
final identitySecret =
|
final identitySecret = await decryptSecretFromBytes(
|
||||||
SecretKey.fromBytes(localAccount.identitySecretBytes);
|
secretBytes: localAccount.identitySecretBytes,
|
||||||
|
cryptoKind: localAccount.identityMaster.identityRecordKey.kind,
|
||||||
|
encryptionKeyType: localAccount.encryptionKeyType,
|
||||||
|
encryptionKey: encryptionKey,
|
||||||
|
);
|
||||||
|
|
||||||
// Validate this secret with the identity public key and log in
|
// Validate this secret with the identity public key and log in
|
||||||
return _loginCommon(localAccount.identityMaster, identitySecret);
|
return _decryptedLogin(localAccount.identityMaster, identitySecret);
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> loginWithPasswordOrPin(
|
|
||||||
TypedKey accountMasterRecordKey, String encryptionKey) async {
|
|
||||||
final veilid = await eventualVeilid.future;
|
|
||||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
|
||||||
|
|
||||||
// Get account, throws if not found
|
|
||||||
final localAccount = localAccounts.firstWhere(
|
|
||||||
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
|
||||||
|
|
||||||
// Log in with this local account
|
|
||||||
|
|
||||||
// Derive key from password
|
|
||||||
if (localAccount.encryptionKeyType != EncryptionKeyType.password ||
|
|
||||||
localAccount.encryptionKeyType != EncryptionKeyType.pin) {
|
|
||||||
throw Exception('Wrong authentication type');
|
|
||||||
}
|
|
||||||
final cs = await veilid
|
|
||||||
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
|
|
||||||
|
|
||||||
final identitySecret = SecretKey.fromBytes(
|
|
||||||
await cs.decryptNoAuthWithPassword(
|
|
||||||
localAccount.identitySecretBytes, encryptionKey));
|
|
||||||
|
|
||||||
// Validate this secret with the identity public key and log in
|
|
||||||
return _loginCommon(localAccount.identityMaster, identitySecret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout(TypedKey? accountMasterRecordKey) async {
|
Future<void> logout(TypedKey? accountMasterRecordKey) async {
|
||||||
|
@ -9,16 +9,37 @@ Future<Uint8List> encryptSecretToBytes(
|
|||||||
String encryptionKey = ''}) async {
|
String encryptionKey = ''}) async {
|
||||||
final veilid = await eventualVeilid.future;
|
final veilid = await eventualVeilid.future;
|
||||||
|
|
||||||
late final Uint8List identitySecretBytes;
|
late final Uint8List secretBytes;
|
||||||
switch (encryptionKeyType) {
|
switch (encryptionKeyType) {
|
||||||
case EncryptionKeyType.none:
|
case EncryptionKeyType.none:
|
||||||
identitySecretBytes = secret.decode();
|
secretBytes = secret.decode();
|
||||||
case EncryptionKeyType.pin:
|
case EncryptionKeyType.pin:
|
||||||
case EncryptionKeyType.password:
|
case EncryptionKeyType.password:
|
||||||
final cs = await veilid.getCryptoSystem(cryptoKind);
|
final cs = await veilid.getCryptoSystem(cryptoKind);
|
||||||
|
|
||||||
identitySecretBytes =
|
secretBytes =
|
||||||
await cs.encryptNoAuthWithPassword(secret.decode(), encryptionKey);
|
await cs.encryptAeadWithPassword(secret.decode(), encryptionKey);
|
||||||
}
|
}
|
||||||
return identitySecretBytes;
|
return secretBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SecretKey> decryptSecretFromBytes(
|
||||||
|
{required Uint8List secretBytes,
|
||||||
|
required CryptoKind cryptoKind,
|
||||||
|
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
|
||||||
|
String encryptionKey = ''}) async {
|
||||||
|
final veilid = await eventualVeilid.future;
|
||||||
|
|
||||||
|
late final SecretKey secret;
|
||||||
|
switch (encryptionKeyType) {
|
||||||
|
case EncryptionKeyType.none:
|
||||||
|
secret = SecretKey.fromBytes(secretBytes);
|
||||||
|
case EncryptionKeyType.pin:
|
||||||
|
case EncryptionKeyType.password:
|
||||||
|
final cs = await veilid.getCryptoSystem(cryptoKind);
|
||||||
|
|
||||||
|
secret = SecretKey.fromBytes(
|
||||||
|
await cs.decryptAeadWithPassword(secretBytes, encryptionKey));
|
||||||
|
}
|
||||||
|
return secret;
|
||||||
}
|
}
|
||||||
|
@ -91,6 +91,15 @@ class DHTRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T> maybeDeleteScope<T>(
|
||||||
|
bool delete, Future<T> Function(DHTRecord) scopeFunction) async {
|
||||||
|
if (delete) {
|
||||||
|
return deleteScope(scopeFunction);
|
||||||
|
} else {
|
||||||
|
return scope(scopeFunction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<Uint8List?> get(
|
Future<Uint8List?> get(
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
bool forceRefresh = false,
|
bool forceRefresh = false,
|
||||||
|
@ -120,12 +120,12 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
|
|||||||
while (currentDeps.isNotEmpty) {
|
while (currentDeps.isNotEmpty) {
|
||||||
final nextDep = currentDeps.removeLast();
|
final nextDep = currentDeps.removeLast();
|
||||||
|
|
||||||
|
// Ensure we get the exclusive lock on this record
|
||||||
|
await _recordOpened(nextDep);
|
||||||
|
|
||||||
// Remove this child from its parent
|
// Remove this child from its parent
|
||||||
await _removeDependency(nextDep);
|
await _removeDependency(nextDep);
|
||||||
|
|
||||||
// Ensure all records are closed before delete
|
|
||||||
assert(!_opened.containsKey(nextDep), 'should not delete opened record');
|
|
||||||
|
|
||||||
allDeps.add(nextDep);
|
allDeps.add(nextDep);
|
||||||
final childDeps =
|
final childDeps =
|
||||||
_state.childrenByParent[nextDep.toJson()]?.toList() ?? [];
|
_state.childrenByParent[nextDep.toJson()]?.toList() ?? [];
|
||||||
@ -136,6 +136,7 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
|
|||||||
final allFutures = <Future<void>>[];
|
final allFutures = <Future<void>>[];
|
||||||
for (final dep in allDeps) {
|
for (final dep in allDeps) {
|
||||||
allFutures.add(_routingContext.deleteDHTRecord(dep));
|
allFutures.add(_routingContext.deleteDHTRecord(dep));
|
||||||
|
recordClosed(dep);
|
||||||
}
|
}
|
||||||
await Future.wait(allFutures);
|
await Future.wait(allFutures);
|
||||||
}
|
}
|
||||||
@ -324,4 +325,10 @@ class DHTRecordPool with AsyncTableDBBacked<DHTRecordPoolAllocations> {
|
|||||||
defaultSubkey: defaultSubkey,
|
defaultSubkey: defaultSubkey,
|
||||||
crypto: crypto,
|
crypto: crypto,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Get the parent of a DHTRecord key if it exists
|
||||||
|
TypedKey? getParentRecord(TypedKey child) {
|
||||||
|
final childJson = child.toJson();
|
||||||
|
return _state.parentByChild[childJson];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user