mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-02-04 09:15:19 -05:00
333 lines
12 KiB
Dart
333 lines
12 KiB
Dart
import 'dart:typed_data';
|
|
|
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
import 'package:protobuf/protobuf.dart';
|
|
|
|
import '../veilid_support.dart';
|
|
import 'veilid_log.dart';
|
|
|
|
part 'identity.freezed.dart';
|
|
part 'identity.g.dart';
|
|
|
|
// Identity errors
|
|
enum IdentityException implements Exception {
|
|
readError('identity could not be read'),
|
|
noAccount('no account record info'),
|
|
limitExceeded('too many items for the limit'),
|
|
invalid('identity is corrupted or secret is invalid');
|
|
|
|
const IdentityException(this.message);
|
|
final String message;
|
|
|
|
@override
|
|
String toString() => 'IdentityException($name): $message';
|
|
}
|
|
|
|
// AccountOwnerInfo is the key and owner info for the account dht key that is
|
|
// stored in the identity key
|
|
@freezed
|
|
class AccountRecordInfo with _$AccountRecordInfo {
|
|
const factory AccountRecordInfo({
|
|
// Top level account keys and secrets
|
|
required OwnedDHTRecordPointer accountRecord,
|
|
}) = _AccountRecordInfo;
|
|
|
|
factory AccountRecordInfo.fromJson(dynamic json) =>
|
|
_$AccountRecordInfoFromJson(json as Map<String, dynamic>);
|
|
}
|
|
|
|
// Identity Key points to accounts associated with this identity
|
|
// accounts field has a map of bundle id or uuid to account key pairs
|
|
// DHT Schema: DFLT(1)
|
|
// DHT Key (Private): identityRecordKey
|
|
// DHT Owner Key: identityPublicKey
|
|
// DHT Secret: identitySecretKey (stored encrypted
|
|
// with unlock code in local table store)
|
|
@freezed
|
|
class Identity with _$Identity {
|
|
const factory Identity({
|
|
// Top level account keys and secrets
|
|
required IMap<String, ISet<AccountRecordInfo>> accountRecords,
|
|
}) = _Identity;
|
|
|
|
factory Identity.fromJson(dynamic json) =>
|
|
_$IdentityFromJson(json as Map<String, dynamic>);
|
|
}
|
|
|
|
// 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
|
|
//
|
|
// Backed by a DHT key at masterRecordKey, the secret is kept
|
|
// completely offline and only written to upon account recovery
|
|
//
|
|
// DHT Schema: DFLT(1)
|
|
// DHT Record Key (Public): masterRecordKey
|
|
// DHT Owner Key: masterPublicKey
|
|
// DHT Owner Secret: masterSecretKey (kept offline)
|
|
// Encryption: None
|
|
@freezed
|
|
class IdentityMaster with _$IdentityMaster {
|
|
const factory IdentityMaster(
|
|
{
|
|
// 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
|
|
required Signature identitySignature,
|
|
// Signature of masterRecordKey and masterPublicKey by identityPublicKey
|
|
required Signature masterSignature}) = _IdentityMaster;
|
|
|
|
factory IdentityMaster.fromJson(dynamic json) =>
|
|
_$IdentityMasterFromJson(json as Map<String, dynamic>);
|
|
}
|
|
|
|
extension IdentityMasterExtension on IdentityMaster {
|
|
/// Deletes a master identity and the identity record under it
|
|
Future<void> delete() async {
|
|
final pool = DHTRecordPool.instance;
|
|
await pool.deleteRecord(masterRecordKey);
|
|
}
|
|
|
|
Future<VeilidCryptoSystem> get identityCrypto =>
|
|
Veilid.instance.getCryptoSystem(identityRecordKey.kind);
|
|
|
|
Future<VeilidCryptoSystem> get masterCrypto =>
|
|
Veilid.instance.getCryptoSystem(masterRecordKey.kind);
|
|
|
|
KeyPair identityWriter(SecretKey secret) =>
|
|
KeyPair(key: identityPublicKey, secret: secret);
|
|
|
|
KeyPair masterWriter(SecretKey secret) =>
|
|
KeyPair(key: masterPublicKey, secret: secret);
|
|
|
|
TypedKey identityPublicTypedKey() =>
|
|
TypedKey(kind: identityRecordKey.kind, value: identityPublicKey);
|
|
|
|
TypedKey masterPublicTypedKey() =>
|
|
TypedKey(kind: identityRecordKey.kind, value: masterPublicKey);
|
|
|
|
Future<VeilidCryptoSystem> validateIdentitySecret(
|
|
SecretKey identitySecret) async {
|
|
final cs = await identityCrypto;
|
|
final keyOk = await cs.validateKeyPair(identityPublicKey, identitySecret);
|
|
if (!keyOk) {
|
|
throw IdentityException.invalid;
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
Future<List<AccountRecordInfo>> readAccountsFromIdentity(
|
|
{required SharedSecret identitySecret,
|
|
required String accountKey}) async {
|
|
// Read the identity key to get the account keys
|
|
final pool = DHTRecordPool.instance;
|
|
|
|
final identityRecordCrypto = await DHTRecordCryptoPrivate.fromSecret(
|
|
identityRecordKey.kind, identitySecret);
|
|
|
|
late final List<AccountRecordInfo> accountRecordInfo;
|
|
await (await pool.openRecordRead(identityRecordKey,
|
|
debugName:
|
|
'IdentityMaster::readAccountsFromIdentity::IdentityRecord',
|
|
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 IdentityException.readError;
|
|
}
|
|
final accountRecords = IMapOfSets.from(identity.accountRecords);
|
|
final vcAccounts = accountRecords.get(accountKey);
|
|
|
|
accountRecordInfo = vcAccounts.toList();
|
|
});
|
|
|
|
return accountRecordInfo;
|
|
}
|
|
|
|
/// Creates a new Account associated with master identity and store it in the
|
|
/// identity key.
|
|
Future<AccountRecordInfo> addAccountToIdentity<T extends GeneratedMessage>({
|
|
required SharedSecret identitySecret,
|
|
required String accountKey,
|
|
required Future<T> Function(TypedKey parent) createAccountCallback,
|
|
int maxAccounts = 1,
|
|
}) async {
|
|
final pool = DHTRecordPool.instance;
|
|
|
|
/////// Add account with profile to DHT
|
|
|
|
// Open identity key for writing
|
|
veilidLoggy.debug('Opening identity record');
|
|
return (await pool.openRecordWrite(
|
|
identityRecordKey, identityWriter(identitySecret),
|
|
debugName: 'IdentityMaster::addAccountToIdentity::IdentityRecord',
|
|
parent: masterRecordKey))
|
|
.scope((identityRec) async {
|
|
// Create new account to insert into identity
|
|
veilidLoggy.debug('Creating new account');
|
|
return (await pool.createRecord(
|
|
debugName: 'IdentityMaster::addAccountToIdentity::AccountRecord',
|
|
parent: identityRec.key))
|
|
.deleteScope((accountRec) async {
|
|
final account = await createAccountCallback(accountRec.key);
|
|
// Write account key
|
|
veilidLoggy.debug('Writing account record');
|
|
await accountRec.eventualWriteProtobuf(account);
|
|
|
|
// Update identity key to include account
|
|
final newAccountRecordInfo = AccountRecordInfo(
|
|
accountRecord: OwnedDHTRecordPointer(
|
|
recordKey: accountRec.key, owner: accountRec.ownerKeyPair!));
|
|
|
|
veilidLoggy.debug('Updating identity with new account');
|
|
await identityRec.eventualUpdateJson(Identity.fromJson,
|
|
(oldIdentity) async {
|
|
if (oldIdentity == null) {
|
|
throw IdentityException.readError;
|
|
}
|
|
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
|
|
|
|
if (oldAccountRecords.get(accountKey).length >= maxAccounts) {
|
|
throw IdentityException.limitExceeded;
|
|
}
|
|
final accountRecords =
|
|
oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap();
|
|
return oldIdentity.copyWith(accountRecords: accountRecords);
|
|
});
|
|
|
|
return newAccountRecordInfo;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
// Identity Master with secrets
|
|
// Not freezed because we never persist this class in its entirety
|
|
class IdentityMasterWithSecrets {
|
|
IdentityMasterWithSecrets._(
|
|
{required this.identityMaster,
|
|
required this.masterSecret,
|
|
required this.identitySecret});
|
|
IdentityMaster identityMaster;
|
|
SecretKey masterSecret;
|
|
SecretKey identitySecret;
|
|
|
|
/// Delete a master identity with secrets
|
|
Future<void> delete() async => identityMaster.delete();
|
|
|
|
/// Creates a new master identity and returns it with its secrets
|
|
static Future<IdentityMasterWithSecrets> create() async {
|
|
final pool = DHTRecordPool.instance;
|
|
|
|
// IdentityMaster DHT record is public/unencrypted
|
|
veilidLoggy.debug('Creating master identity record');
|
|
return (await pool.createRecord(
|
|
debugName:
|
|
'IdentityMasterWithSecrets::create::IdentityMasterRecord',
|
|
crypto: const DHTRecordCryptoPublic()))
|
|
.deleteScope((masterRec) async {
|
|
veilidLoggy.debug('Creating identity record');
|
|
// Identity record is private
|
|
return (await pool.createRecord(
|
|
debugName: 'IdentityMasterWithSecrets::create::IdentityRecord',
|
|
parent: masterRec.key))
|
|
.scope((identityRec) async {
|
|
// Make IdentityMaster
|
|
final masterRecordKey = masterRec.key;
|
|
final masterOwner = masterRec.ownerKeyPair!;
|
|
final masterSigBuf = BytesBuilder()
|
|
..add(masterRecordKey.decode())
|
|
..add(masterOwner.key.decode());
|
|
|
|
final identityRecordKey = identityRec.key;
|
|
final identityOwner = identityRec.ownerKeyPair!;
|
|
final identitySigBuf = BytesBuilder()
|
|
..add(identityRecordKey.decode())
|
|
..add(identityOwner.key.decode());
|
|
|
|
assert(masterRecordKey.kind == identityRecordKey.kind,
|
|
'new master and identity should have same cryptosystem');
|
|
final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind);
|
|
|
|
final identitySignature =
|
|
await crypto.signWithKeyPair(masterOwner, identitySigBuf.toBytes());
|
|
final masterSignature =
|
|
await crypto.signWithKeyPair(identityOwner, masterSigBuf.toBytes());
|
|
|
|
final identityMaster = IdentityMaster(
|
|
identityRecordKey: identityRecordKey,
|
|
identityPublicKey: identityOwner.key,
|
|
masterRecordKey: masterRecordKey,
|
|
masterPublicKey: masterOwner.key,
|
|
identitySignature: identitySignature,
|
|
masterSignature: masterSignature);
|
|
|
|
// Write identity master to master dht key
|
|
await masterRec.eventualWriteJson(identityMaster);
|
|
|
|
// Make empty identity
|
|
const identity = Identity(accountRecords: IMapConst({}));
|
|
|
|
// Write empty identity to identity dht key
|
|
await identityRec.eventualWriteJson(identity);
|
|
|
|
return IdentityMasterWithSecrets._(
|
|
identityMaster: identityMaster,
|
|
masterSecret: masterOwner.secret,
|
|
identitySecret: identityOwner.secret);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
/// Opens an existing master identity and validates it
|
|
Future<IdentityMaster> openIdentityMaster(
|
|
{required TypedKey identityMasterRecordKey}) async {
|
|
final pool = DHTRecordPool.instance;
|
|
|
|
// IdentityMaster DHT record is public/unencrypted
|
|
return (await pool.openRecordRead(identityMasterRecordKey,
|
|
debugName:
|
|
'IdentityMaster::openIdentityMaster::IdentityMasterRecord'))
|
|
.deleteScope((masterRec) async {
|
|
final identityMaster = (await masterRec.getJson(IdentityMaster.fromJson,
|
|
refreshMode: DHTRecordRefreshMode.network))!;
|
|
|
|
// Validate IdentityMaster
|
|
final masterRecordKey = masterRec.key;
|
|
final masterOwnerKey = masterRec.owner;
|
|
final masterSigBuf = BytesBuilder()
|
|
..add(masterRecordKey.decode())
|
|
..add(masterOwnerKey.decode());
|
|
final masterSignature = identityMaster.masterSignature;
|
|
|
|
final identityRecordKey = identityMaster.identityRecordKey;
|
|
final identityOwnerKey = identityMaster.identityPublicKey;
|
|
final identitySigBuf = BytesBuilder()
|
|
..add(identityRecordKey.decode())
|
|
..add(identityOwnerKey.decode());
|
|
final identitySignature = identityMaster.identitySignature;
|
|
|
|
assert(masterRecordKey.kind == identityRecordKey.kind,
|
|
'new master and identity should have same cryptosystem');
|
|
final crypto = await pool.veilid.getCryptoSystem(masterRecordKey.kind);
|
|
|
|
await crypto.verify(
|
|
masterOwnerKey, identitySigBuf.toBytes(), identitySignature);
|
|
await crypto.verify(
|
|
identityOwnerKey, masterSigBuf.toBytes(), masterSignature);
|
|
|
|
return identityMaster;
|
|
});
|
|
}
|