veilidchat/lib/entities/identity.dart

176 lines
6.5 KiB
Dart
Raw Normal View History

2023-07-17 22:39:33 -04:00
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
2023-07-07 19:33:28 -04:00
import 'package:freezed_annotation/freezed_annotation.dart';
2023-08-01 00:39:50 -04:00
import '../veilid_support/veilid_support.dart';
import 'proto.dart' as proto;
2023-07-07 19:33:28 -04:00
part 'identity.freezed.dart';
part 'identity.g.dart';
2023-08-01 00:39:50 -04:00
const String veilidChatAccountKey = 'com.veilid.veilidchat';
2023-07-16 21:41:40 -04:00
// AccountOwnerInfo is the key and owner info for the account dht key that is
// stored in the identity key
@freezed
2023-07-17 22:39:33 -04:00
class AccountRecordInfo with _$AccountRecordInfo {
const factory AccountRecordInfo({
2023-07-16 21:41:40 -04:00
// Top level account keys and secrets
2023-08-01 00:39:50 -04:00
required OwnedDHTRecordPointer accountRecord,
2023-07-17 22:39:33 -04:00
}) = _AccountRecordInfo;
2023-07-16 21:41:40 -04:00
2023-07-25 01:04:34 -04:00
factory AccountRecordInfo.fromJson(dynamic json) =>
_$AccountRecordInfoFromJson(json as Map<String, dynamic>);
2023-07-16 21:41:40 -04:00
}
2023-07-07 19:33:28 -04:00
// Identity Key points to accounts associated with this identity
2023-07-25 01:04:34 -04:00
// accounts field has a map of bundle id or uuid to account key pairs
2023-07-07 19:33:28 -04:00
// DHT Schema: DFLT(1)
2023-07-09 00:07:21 -04:00
// DHT Key (Private): identityRecordKey
// DHT Owner Key: identityPublicKey
2023-07-26 17:42:11 -04:00
// DHT Secret: identitySecretKey (stored encrypted
// with unlock code in local table store)
2023-07-07 19:33:28 -04:00
@freezed
class Identity with _$Identity {
const factory Identity({
// Top level account keys and secrets
2023-07-17 22:39:33 -04:00
required IMap<String, ISet<AccountRecordInfo>> accountRecords,
2023-07-07 19:33:28 -04:00
}) = _Identity;
2023-07-25 01:04:34 -04:00
factory Identity.fromJson(dynamic json) =>
_$IdentityFromJson(json as Map<String, dynamic>);
2023-07-07 19:33:28 -04:00
}
// Identity Master key structure for created account
// Master key allows for regeneration of identity DHT record
// Bidirectional Master<->Identity signature allows for
// chain of identity ownership for account recovery process
//
2023-07-09 00:07:21 -04:00
// Backed by a DHT key at masterRecordKey, the secret is kept
2023-07-07 19:33:28 -04:00
// completely offline and only written to upon account recovery
//
// DHT Schema: DFLT(1)
2023-07-09 00:07:21 -04:00
// DHT Record Key (Public): masterRecordKey
// DHT Owner Key: masterPublicKey
// DHT Owner Secret: masterSecretKey (kept offline)
2023-07-07 19:33:28 -04:00
// Encryption: None
@freezed
class IdentityMaster with _$IdentityMaster {
const factory IdentityMaster(
2023-07-09 00:07:21 -04:00
{
// Private DHT record storing identity account mapping
required TypedKey identityRecordKey,
// Public key of identity
required PublicKey identityPublicKey,
// Public DHT record storing this structure for account recovery
required TypedKey masterRecordKey,
// Public key of master identity used to sign identity keys for recovery
required PublicKey masterPublicKey,
// Signature of identityRecordKey and identityPublicKey by masterPublicKey
2023-07-07 19:33:28 -04:00
required Signature identitySignature,
2023-07-09 00:07:21 -04:00
// Signature of masterRecordKey and masterPublicKey by identityPublicKey
2023-07-07 19:33:28 -04:00
required Signature masterSignature}) = _IdentityMaster;
2023-07-25 01:04:34 -04:00
factory IdentityMaster.fromJson(dynamic json) =>
_$IdentityMasterFromJson(json as Map<String, dynamic>);
2023-07-07 19:33:28 -04:00
}
2023-07-09 00:07:21 -04:00
2023-07-16 21:41:40 -04:00
extension IdentityMasterExtension on IdentityMaster {
2023-07-26 17:42:11 -04:00
KeyPair identityWriter(SecretKey secret) =>
KeyPair(key: identityPublicKey, secret: secret);
2023-07-16 21:41:40 -04:00
2023-07-26 17:42:11 -04:00
KeyPair masterWriter(SecretKey secret) =>
KeyPair(key: masterPublicKey, secret: secret);
2023-08-01 00:39:50 -04:00
Future<AccountRecordInfo> readAccountFromIdentity(
{required SharedSecret identitySecret}) async {
// Read the identity key to get the account keys
final pool = await DHTRecordPool.instance();
final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret(
identityRecordKey.kind, identitySecret);
late final AccountRecordInfo accountRecordInfo;
await (await pool.openRead(identityRecordKey,
parent: masterRecordKey, crypto: identityRecordCrypto))
.scope((identityRec) async {
final identity = await identityRec.getJson(Identity.fromJson);
if (identity == null) {
// Identity could not be read or decrypted from DHT
throw StateError('identity could not be read');
}
final accountRecords = IMapOfSets.from(identity.accountRecords);
final vcAccounts = accountRecords.get(veilidChatAccountKey);
if (vcAccounts.length != 1) {
// No veilidchat account, or multiple accounts
// somehow associated with identity
throw StateError('no single veilidchat account');
}
accountRecordInfo = vcAccounts.first;
});
return accountRecordInfo;
}
/// Creates a new Account associated with master identity and store it in the
/// identity key.
Future<void> newAccount({
required SharedSecret identitySecret,
required String name,
required String title,
}) async {
final pool = await DHTRecordPool.instance();
/////// Add account with profile to DHT
// Open identity key for writing
await (await pool.openWrite(
identityRecordKey, identityWriter(identitySecret),
parent: masterRecordKey))
.scope((identityRec) async {
// Create new account to insert into identity
await (await pool.create(parent: identityRec.key))
.deleteScope((accountRec) async {
2023-08-04 01:00:38 -04:00
// Make empty contact list
final contactList =
await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer);
2023-08-02 21:09:28 -04:00
// Make empty contact invitation record list
2023-08-04 01:00:38 -04:00
final contactInvitationRecords =
await (await DHTShortArray.create(parent: accountRec.key))
.scope((r) => r.record.ownedDHTRecordPointer);
2023-08-01 00:39:50 -04:00
// Make account object
final account = proto.Account()
..profile = (proto.Profile()
..name = name
..title = title)
2023-08-04 01:00:38 -04:00
..contactList = contactList.toProto()
2023-08-02 21:09:28 -04:00
..contactInvitationRecords = contactInvitationRecords.toProto();
2023-08-01 00:39:50 -04:00
// Write account key
await accountRec.eventualWriteProtobuf(account);
// Update identity key to include account
final newAccountRecordInfo = AccountRecordInfo(
accountRecord: OwnedDHTRecordPointer(
recordKey: accountRec.key, owner: accountRec.ownerKeyPair!));
await identityRec.eventualUpdateJson(Identity.fromJson,
(oldIdentity) async {
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
// Only allow one account per identity for veilidchat
if (oldAccountRecords.get(veilidChatAccountKey).isNotEmpty) {
throw StateError(
'Only one account per identity allowed for VeilidChat');
}
final accountRecords = oldAccountRecords
.add(veilidChatAccountKey, newAccountRecordInfo)
.asIMap();
return oldIdentity.copyWith(accountRecords: accountRecords);
});
});
});
}
2023-07-16 21:41:40 -04:00
}