Skip to content

Commit

Permalink
refactor contacts repo, update dependencies, extend connection reques…
Browse files Browse the repository at this point in the history
…t tests
  • Loading branch information
LGro committed Jul 24, 2024
1 parent 32f1db3 commit d71fe52
Show file tree
Hide file tree
Showing 250 changed files with 14,727 additions and 3,160 deletions.
5 changes: 2 additions & 3 deletions lib/data/providers/distributed_storage/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ abstract class DistributedStorage {

Future<void> watchDHTRecord(String key);

Future<bool> isUpToDateSharingDHT(CoagContact contact);

Future<CoagContact> updateContactSharingDHT(CoagContact contact);
Future<CoagContact> updateContactSharingDHT(CoagContact contact,
{Future<String> Function()? pskGenerator});

Future<CoagContact> updateContactReceivingDHT(CoagContact contact);
}
121 changes: 59 additions & 62 deletions lib/data/providers/distributed_storage/dht.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,43 +10,49 @@ import '../../models/coag_contact.dart';
import 'base.dart';

class VeilidDhtStorage extends DistributedStorage {
final Map<Typed<FixedEncodedString43>, DHTRecord> _openedRecords = {};

/// Create an empty DHT record, return key and writer in string representation
@override
Future<(String, String)> createDHTRecord() async {
final pool = DHTRecordPool.instance;
final record = await pool.create(
debugName: 'coag::create', crypto: const DHTRecordCryptoPublic());
await record.close();
final record = await pool.createRecord(
debugName: 'coag::create', crypto: const VeilidCryptoPublic());
_openedRecords[record.key] = record;
return (record.key.toString(), record.writer!.toString());
}

/// Read DHT record for given key and secret, return decrypted content
@override
Future<String> readPasswordEncryptedDHTRecord(
{required String recordKey, required String secret}) async {
final pool = DHTRecordPool.instance;
// TODO: Handle VeilidAPIExceptionKeyNotFound, VeilidAPIExceptionTryAgain
final record = await pool.openRead(
debugName: 'coag::read',
Typed<FixedEncodedString43>.fromString(recordKey),
crypto: const DHTRecordCryptoPublic());
// TODO: What is the onlyUpdates argument for?
final raw = await record.get(forceRefresh: true);
final _key = Typed<FixedEncodedString43>.fromString(recordKey);
final DHTRecord record;
if (_openedRecords.containsKey(_key)) {
record = _openedRecords[_key]!;
} else {
// TODO: Handle VeilidAPIExceptionKeyNotFound, VeilidAPIExceptionTryAgain
record = await DHTRecordPool.instance.openRecordRead(
debugName: 'coag::read',
Typed<FixedEncodedString43>.fromString(recordKey),
crypto: const VeilidCryptoPublic());
final defaultSubkey = record.subkeyOrDefault(-1);
await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]);
_openedRecords[record.key] = record;
}
final raw = await record.get(refreshMode: DHTRecordRefreshMode.network);
if (raw == null) {
await record.close();
return '';
}

// TODO: Detect if secret is pubkey and use asymmetric encryption here?
// TODO: Error handling
final cs = await pool.veilid.bestCryptoSystem();
final bodyBytes = raw!.sublist(0, raw.length - Nonce.decodedLength());
final cs = await DHTRecordPool.instance.veilid.bestCryptoSystem();
final bodyBytes = raw.sublist(0, raw.length - Nonce.decodedLength());
final saltBytes = raw.sublist(raw.length - Nonce.decodedLength());
final decrypted = await cs.decryptAead(bodyBytes,
Nonce.fromBytes(saltBytes), SharedSecret.fromString(secret), null);

await record.close();

return utf8.decode(decrypted);
}

Expand All @@ -57,15 +63,21 @@ class VeilidDhtStorage extends DistributedStorage {
required String recordWriter,
required String secret,
required String content}) async {
final pool = DHTRecordPool.instance;
final record = await pool.openWrite(
debugName: 'coag::update',
Typed<FixedEncodedString43>.fromString(recordKey),
KeyPair.fromString(recordWriter),
crypto: const DHTRecordCryptoPublic());
final _key = Typed<FixedEncodedString43>.fromString(recordKey);
final DHTRecord record;
if (_openedRecords.containsKey(_key)) {
record = _openedRecords[_key]!;
} else {
record = await DHTRecordPool.instance.openRecordWrite(
debugName: 'coag::update',
Typed<FixedEncodedString43>.fromString(recordKey),
KeyPair.fromString(recordWriter),
crypto: const VeilidCryptoPublic());
_openedRecords[record.key] = record;
}

// TODO: Detect if secret is pubkey and use asymmetric encryption here?
final cs = await pool.veilid.bestCryptoSystem();
final cs = await DHTRecordPool.instance.veilid.bestCryptoSystem();
final nonce = await cs.randomNonce();
final saltBytes = nonce.decode();
final encrypted = Uint8List.fromList((await cs.encryptAead(
Expand All @@ -76,38 +88,33 @@ class VeilidDhtStorage extends DistributedStorage {
saltBytes);

await record.tryWriteBytes(encrypted);
await record.close();
}

@override
Future<void> watchDHTRecord(String key) async {
final pool = DHTRecordPool.instance;
final record = await pool.openRead(
debugName: 'coag::read',
Typed<FixedEncodedString43>.fromString(key),
crypto: const DHTRecordCryptoPublic());
final _key = Typed<FixedEncodedString43>.fromString(key);
final DHTRecord record;
if (_openedRecords.containsKey(_key)) {
record = _openedRecords[_key]!;
} else {
record = await DHTRecordPool.instance.openRecordRead(
debugName: 'coag::read-to-watch',
Typed<FixedEncodedString43>.fromString(key),
crypto: const VeilidCryptoPublic());
_openedRecords[record.key] = record;
}
final defaultSubkey = record.subkeyOrDefault(-1);
await record.watch(subkeys: [ValueSubkeyRange.single(defaultSubkey)]);
await record.close();
}

@override
Future<bool> isUpToDateSharingDHT(CoagContact contact) async {
if (contact.dhtSettingsForSharing?.psk == null ||
contact.sharedProfile == null) {
return true;
}

final record = await readPasswordEncryptedDHTRecord(
recordKey: contact.dhtSettingsForSharing!.key,
secret: contact.dhtSettingsForSharing!.psk!);
return record != contact.sharedProfile;
}

// TODO: Can we update the sharedProfile here as well or not because we're lacking the profile contact?
@override
Future<CoagContact> updateContactSharingDHT(CoagContact contact) async {
final cs = await DHTRecordPool.instance.veilid.bestCryptoSystem();
Future<CoagContact> updateContactSharingDHT(CoagContact contact,
{Future<String> Function()? pskGenerator}) async {
pskGenerator ??= () async {
final cs = await DHTRecordPool.instance.veilid.bestCryptoSystem();
return cs.randomSharedSecret().then((v) => v.toString());
};

if (contact.dhtSettingsForSharing?.writer == null) {
final (key, writer) = await createDHTRecord();
Expand All @@ -119,9 +126,7 @@ class VeilidDhtStorage extends DistributedStorage {
final (key, writer) = await createDHTRecord();
contact = contact.copyWith(
dhtSettingsForReceiving: ContactDHTSettings(
key: key,
writer: writer,
psk: await cs.randomSharedSecret().then((v) => v.toString())));
key: key, writer: writer, psk: await pskGenerator()));
// Write once, to make sure it's created and published on the network
await updatePasswordEncryptedDHTRecord(
recordKey: key,
Expand All @@ -132,8 +137,8 @@ class VeilidDhtStorage extends DistributedStorage {

if (contact.dhtSettingsForSharing!.psk == null) {
contact = contact.copyWith(
dhtSettingsForSharing: contact.dhtSettingsForSharing!.copyWith(
psk: await cs.randomSharedSecret().then((v) => v.toString())));
dhtSettingsForSharing: contact.dhtSettingsForSharing!
.copyWith(psk: await pskGenerator()));
}

if (contact.sharedProfile != null) {
Expand All @@ -156,22 +161,12 @@ class VeilidDhtStorage extends DistributedStorage {
return contact;
}

Future<bool> _isAvailableAndWritable(
ContactDHTSettings? dhtSettingsForSharing) async {
try {
// TODO: Try to open in write mode
return true;
// TODO: Which other exceptions are relevant?
} on VeilidAPIExceptionKeyNotFound {
return false;
}
}

// TODO: Schema version check and migration for backwards compatibility
// TODO: set last checked timestamp inside this function?
@override
Future<CoagContact> updateContactReceivingDHT(CoagContact contact) async {
if (contact.dhtSettingsForReceiving?.psk == null) {
if (contact.dhtSettingsForReceiving?.psk == null ||
contact.dhtSettingsForReceiving?.psk == null) {
return contact;
}
try {
Expand Down Expand Up @@ -200,3 +195,5 @@ class VeilidDhtStorage extends DistributedStorage {
}
}
}

// TODO: Close all records before going to background / closing the app?
2 changes: 2 additions & 0 deletions lib/data/providers/persistent_storage/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ abstract class PersistentStorage {

Future<String?> getProfileContactId();

Future<void> removeProfileContactId();

Future<void> removeContact(String coagContactId);

Future<List<ContactUpdate>> getUpdates();
Expand Down
4 changes: 4 additions & 0 deletions lib/data/providers/persistent_storage/hive.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class HiveStorage extends PersistentStorage {
Future<String?> getProfileContactId() async =>
(await _lazyGetSettingsBox()).get('profile_contact_id');

@override
Future<void> removeProfileContactId() async =>
(await _lazyGetSettingsBox()).delete('profile_contact_id');

@override
Future<void> removeContact(String coagContactId) async =>
(await _lazyGetSettingsBox()).delete(coagContactId);
Expand Down
4 changes: 4 additions & 0 deletions lib/data/providers/persistent_storage/shared_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ class SharedPreferencesStorage extends PersistentStorage {
Future<String?> getProfileContactId() async =>
(await SharedPreferences.getInstance()).getString('profile_contact_id');

@override
Future<void> removeProfileContactId() async =>
(await SharedPreferences.getInstance()).remove('profile_contact_id');

@override
Future<void> removeContact(String coagContactId) async =>
(await SharedPreferences.getInstance()).remove(coagContactId);
Expand Down
4 changes: 4 additions & 0 deletions lib/data/providers/persistent_storage/sqlite.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,10 @@ class SqliteStorage extends PersistentStorage {
Future<String?> getProfileContactId() async =>
(await SharedPreferences.getInstance()).getString('profile_contact_id');

@override
Future<void> removeProfileContactId() async =>
(await SharedPreferences.getInstance()).remove('profile_contact_id');

@override
Future<void> removeContact(String coagContactId) async =>
(await SharedPreferences.getInstance()).remove(coagContactId);
Expand Down
9 changes: 9 additions & 0 deletions lib/data/providers/system_contacts/base.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2024 The Coagulate Authors. All rights reserved.
// SPDX-License-Identifier: MPL-2.0

import 'dart:convert';

import 'package:flutter_contacts/flutter_contacts.dart';

abstract class SystemContactsBase {
Expand All @@ -10,3 +12,10 @@ abstract class SystemContactsBase {
Future<Contact> insertContact(Contact contact);
Future<bool> requestPermission();
}

/// Compare contacts, ignoring differences wrt thumbnail or photo
bool systemContactsEqual(Contact c1, Contact c2) {
final c1Json = jsonEncode(c1.toJson(withThumbnail: false, withPhoto: false));
final c2Json = jsonEncode(c2.toJson(withThumbnail: false, withPhoto: false));
return c1Json == c2Json;
}
Loading

0 comments on commit d71fe52

Please sign in to comment.