big identity refactor

This commit is contained in:
Christien Rioux 2024-06-07 14:42:04 -04:00
parent bae58d5f5c
commit ddc02f6771
46 changed files with 2143 additions and 1300 deletions

View File

@ -31,7 +31,8 @@
"cancel": "Cancel",
"delete": "Delete",
"accept": "Accept",
"reject": "Reject"
"reject": "Reject",
"waiting_for_network": "Waiting For Network"
},
"toast": {
"error": "Error",

View File

@ -16,27 +16,27 @@
2. Get the ContactRequest record unicastinbox DHT record owner subkey from the network
3. Decrypt the writer secret with the password if necessary
4. Decrypt the ContactRequestPrivate chunk with the writer secret
5. Get the contact's AccountMaster record key
5. Get the contact's SuperIdentity record key
6. Verify identity signature on the SignedContactInvitation
7. Verify expiration
8. Display the profile and ask if the user wants to accept or reject the invitation
## Accepting an invitation
1. Create a Local Chat DHT record (no content yet, will be encrypted with DH of contact identity key)
2. Create ContactResponse with chat dht record and account master
2. Create ContactResponse with chat dht record and superidentity
3. Create SignedContactResponse with accept=true signed with identity
4. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret
5. Add a local contact with the remote chat dht record, updating from the remote profile in it
## Rejecting an invitation
1. Create ContactResponse with account master
1. Create ContactResponse with superidentity
2. Create SignedContactResponse with accept=false signed with identity
3. Set ContactRequest unicastinbox DHT record writer subkey with SignedContactResponse, encrypted with writer secret
## Receiving an accept/reject
1. Open and get SignedContactResponse from ContactRequest unicastinbox DHT record
2. Decrypt with writer secret
3. Get DHT record for contact's AccountMaster
3. Get DHT record for contact's SuperIdentity
4. Validate the SignedContactResponse signature
If accept == false:

View File

@ -14,14 +14,18 @@ class ActiveAccountInfo {
});
//
TypedKey get superIdentityRecordKey => localAccount.superIdentity.recordKey;
TypedKey get accountRecordKey =>
userLogin.accountRecordInfo.accountRecord.recordKey;
KeyPair get conversationWriter {
final identityKey = localAccount.identityMaster.identityPublicKey;
final identitySecret = userLogin.identitySecret;
return KeyPair(key: identityKey, secret: identitySecret.value);
}
TypedKey get identityTypedPublicKey =>
localAccount.superIdentity.currentInstance.typedPublicKey;
PublicKey get identityPublicKey =>
localAccount.superIdentity.currentInstance.publicKey;
SecretKey get identitySecretKey => userLogin.identitySecret.value;
KeyPair get identityWriter =>
KeyPair(key: identityPublicKey, secret: identitySecretKey);
Future<VeilidCryptoSystem> get identityCryptoSystem =>
localAccount.superIdentity.currentInstance.cryptoSystem;
Future<VeilidCrypto> makeConversationCrypto(
TypedKey remoteIdentityPublicKey) async {

View File

@ -11,25 +11,31 @@ part 'local_account.freezed.dart';
// Local Accounts are stored in a table locally and not backed by a DHT key
// and represents the accounts that have been added/imported
// on the current device.
// Stores a copy of the IdentityMaster associated with the account
// Stores a copy of the most recent SuperIdentity associated with the account
// and the identitySecretKey optionally encrypted by an unlock code
// This is the root of the account information tree for VeilidChat
//
@freezed
class LocalAccount with _$LocalAccount {
const factory LocalAccount({
// The master key record for the account, containing the identityPublicKey
required IdentityMaster identityMaster,
// The encrypted identity secret that goes with
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
required SuperIdentity superIdentity,
// The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter() required Uint8List identitySecretBytes,
// The kind of encryption input used on the account
required EncryptionKeyType encryptionKeyType,
// If account is not hidden, password can be retrieved via
required bool biometricsEnabled,
// Keep account hidden unless account password is entered
// (tries all hidden accounts with auth method (no biometrics))
required bool hiddenAccount,
// Display name for account until it is unlocked
required String name,
}) = _LocalAccount;

View File

@ -20,9 +20,10 @@ LocalAccount _$LocalAccountFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$LocalAccount {
// The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster =>
throw _privateConstructorUsedError; // The encrypted identity secret that goes with
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
SuperIdentity get superIdentity =>
throw _privateConstructorUsedError; // The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes =>
@ -49,14 +50,14 @@ abstract class $LocalAccountCopyWith<$Res> {
_$LocalAccountCopyWithImpl<$Res, LocalAccount>;
@useResult
$Res call(
{IdentityMaster identityMaster,
{SuperIdentity superIdentity,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
bool hiddenAccount,
String name});
$IdentityMasterCopyWith<$Res> get identityMaster;
$SuperIdentityCopyWith<$Res> get superIdentity;
}
/// @nodoc
@ -72,7 +73,7 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? superIdentity = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
@ -80,10 +81,10 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
Object? name = null,
}) {
return _then(_value.copyWith(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
superIdentity: null == superIdentity
? _value.superIdentity
: superIdentity // ignore: cast_nullable_to_non_nullable
as SuperIdentity,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
@ -109,9 +110,9 @@ class _$LocalAccountCopyWithImpl<$Res, $Val extends LocalAccount>
@override
@pragma('vm:prefer-inline')
$IdentityMasterCopyWith<$Res> get identityMaster {
return $IdentityMasterCopyWith<$Res>(_value.identityMaster, (value) {
return _then(_value.copyWith(identityMaster: value) as $Val);
$SuperIdentityCopyWith<$Res> get superIdentity {
return $SuperIdentityCopyWith<$Res>(_value.superIdentity, (value) {
return _then(_value.copyWith(superIdentity: value) as $Val);
});
}
}
@ -125,7 +126,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res>
@override
@useResult
$Res call(
{IdentityMaster identityMaster,
{SuperIdentity superIdentity,
@Uint8ListJsonConverter() Uint8List identitySecretBytes,
EncryptionKeyType encryptionKeyType,
bool biometricsEnabled,
@ -133,7 +134,7 @@ abstract class _$$LocalAccountImplCopyWith<$Res>
String name});
@override
$IdentityMasterCopyWith<$Res> get identityMaster;
$SuperIdentityCopyWith<$Res> get superIdentity;
}
/// @nodoc
@ -147,7 +148,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityMaster = null,
Object? superIdentity = null,
Object? identitySecretBytes = null,
Object? encryptionKeyType = null,
Object? biometricsEnabled = null,
@ -155,10 +156,10 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
Object? name = null,
}) {
return _then(_$LocalAccountImpl(
identityMaster: null == identityMaster
? _value.identityMaster
: identityMaster // ignore: cast_nullable_to_non_nullable
as IdentityMaster,
superIdentity: null == superIdentity
? _value.superIdentity
: superIdentity // ignore: cast_nullable_to_non_nullable
as SuperIdentity,
identitySecretBytes: null == identitySecretBytes
? _value.identitySecretBytes
: identitySecretBytes // ignore: cast_nullable_to_non_nullable
@ -187,7 +188,7 @@ class __$$LocalAccountImplCopyWithImpl<$Res>
@JsonSerializable()
class _$LocalAccountImpl implements _LocalAccount {
const _$LocalAccountImpl(
{required this.identityMaster,
{required this.superIdentity,
@Uint8ListJsonConverter() required this.identitySecretBytes,
required this.encryptionKeyType,
required this.biometricsEnabled,
@ -197,10 +198,11 @@ class _$LocalAccountImpl implements _LocalAccount {
factory _$LocalAccountImpl.fromJson(Map<String, dynamic> json) =>
_$$LocalAccountImplFromJson(json);
// The master key record for the account, containing the identityPublicKey
// The super identity key record for the account,
// containing the publicKey in the currentIdentity
@override
final IdentityMaster identityMaster;
// The encrypted identity secret that goes with
final SuperIdentity superIdentity;
// The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@override
@Uint8ListJsonConverter()
@ -221,7 +223,7 @@ class _$LocalAccountImpl implements _LocalAccount {
@override
String toString() {
return 'LocalAccount(identityMaster: $identityMaster, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)';
return 'LocalAccount(superIdentity: $superIdentity, identitySecretBytes: $identitySecretBytes, encryptionKeyType: $encryptionKeyType, biometricsEnabled: $biometricsEnabled, hiddenAccount: $hiddenAccount, name: $name)';
}
@override
@ -229,8 +231,8 @@ class _$LocalAccountImpl implements _LocalAccount {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LocalAccountImpl &&
(identical(other.identityMaster, identityMaster) ||
other.identityMaster == identityMaster) &&
(identical(other.superIdentity, superIdentity) ||
other.superIdentity == superIdentity) &&
const DeepCollectionEquality()
.equals(other.identitySecretBytes, identitySecretBytes) &&
(identical(other.encryptionKeyType, encryptionKeyType) ||
@ -246,7 +248,7 @@ class _$LocalAccountImpl implements _LocalAccount {
@override
int get hashCode => Object.hash(
runtimeType,
identityMaster,
superIdentity,
const DeepCollectionEquality().hash(identitySecretBytes),
encryptionKeyType,
biometricsEnabled,
@ -269,7 +271,7 @@ class _$LocalAccountImpl implements _LocalAccount {
abstract class _LocalAccount implements LocalAccount {
const factory _LocalAccount(
{required final IdentityMaster identityMaster,
{required final SuperIdentity superIdentity,
@Uint8ListJsonConverter() required final Uint8List identitySecretBytes,
required final EncryptionKeyType encryptionKeyType,
required final bool biometricsEnabled,
@ -279,9 +281,10 @@ abstract class _LocalAccount implements LocalAccount {
factory _LocalAccount.fromJson(Map<String, dynamic> json) =
_$LocalAccountImpl.fromJson;
@override // The master key record for the account, containing the identityPublicKey
IdentityMaster get identityMaster;
@override // The encrypted identity secret that goes with
@override // The super identity key record for the account,
// containing the publicKey in the currentIdentity
SuperIdentity get superIdentity;
@override // The encrypted currentIdentity secret that goes with
// the identityPublicKey with appended salt
@Uint8ListJsonConverter()
Uint8List get identitySecretBytes;

View File

@ -8,7 +8,7 @@ part of 'local_account.dart';
_$LocalAccountImpl _$$LocalAccountImplFromJson(Map<String, dynamic> json) =>
_$LocalAccountImpl(
identityMaster: IdentityMaster.fromJson(json['identity_master']),
superIdentity: SuperIdentity.fromJson(json['super_identity']),
identitySecretBytes: const Uint8ListJsonConverter()
.fromJson(json['identity_secret_bytes']),
encryptionKeyType:
@ -20,7 +20,7 @@ _$LocalAccountImpl _$$LocalAccountImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$LocalAccountImplToJson(_$LocalAccountImpl instance) =>
<String, dynamic>{
'identity_master': instance.identityMaster.toJson(),
'super_identity': instance.superIdentity.toJson(),
'identity_secret_bytes':
const Uint8ListJsonConverter().toJson(instance.identitySecretBytes),
'encryption_key_type': instance.encryptionKeyType.toJson(),

View File

@ -7,12 +7,13 @@ part 'user_login.g.dart';
// Represents a currently logged in account
// User logins are stored in the user_logins tablestore table
// indexed by the accountMasterKey
// indexed by the accountSuperIdentityRecordKey
@freezed
class UserLogin with _$UserLogin {
const factory UserLogin({
// Master record key for the user used to index the local accounts table
required TypedKey accountMasterRecordKey,
// SuperIdentity record key for the user
// used to index the local accounts table
required TypedKey superIdentityRecordKey,
// The identity secret as unlocked from the local accounts table
required TypedSecret identitySecret,
// The account record key, owner key and secret pulled from the identity

View File

@ -20,8 +20,9 @@ UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$UserLogin {
// Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey =>
// SuperIdentity record key for the user
// used to index the local accounts table
Typed<FixedEncodedString43> get superIdentityRecordKey =>
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret =>
throw _privateConstructorUsedError; // The account record key, owner key and secret pulled from the identity
@ -41,7 +42,7 @@ abstract class $UserLoginCopyWith<$Res> {
_$UserLoginCopyWithImpl<$Res, UserLogin>;
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
{Typed<FixedEncodedString43> superIdentityRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
@ -62,15 +63,15 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? superIdentityRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_value.copyWith(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
superIdentityRecordKey: null == superIdentityRecordKey
? _value.superIdentityRecordKey
: superIdentityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
@ -105,7 +106,7 @@ abstract class _$$UserLoginImplCopyWith<$Res>
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> accountMasterRecordKey,
{Typed<FixedEncodedString43> superIdentityRecordKey,
Typed<FixedEncodedString43> identitySecret,
AccountRecordInfo accountRecordInfo,
Timestamp lastActive});
@ -125,15 +126,15 @@ class __$$UserLoginImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountMasterRecordKey = null,
Object? superIdentityRecordKey = null,
Object? identitySecret = null,
Object? accountRecordInfo = null,
Object? lastActive = null,
}) {
return _then(_$UserLoginImpl(
accountMasterRecordKey: null == accountMasterRecordKey
? _value.accountMasterRecordKey
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
superIdentityRecordKey: null == superIdentityRecordKey
? _value.superIdentityRecordKey
: superIdentityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identitySecret: null == identitySecret
? _value.identitySecret
@ -155,7 +156,7 @@ class __$$UserLoginImplCopyWithImpl<$Res>
@JsonSerializable()
class _$UserLoginImpl implements _UserLogin {
const _$UserLoginImpl(
{required this.accountMasterRecordKey,
{required this.superIdentityRecordKey,
required this.identitySecret,
required this.accountRecordInfo,
required this.lastActive});
@ -163,9 +164,10 @@ class _$UserLoginImpl implements _UserLogin {
factory _$UserLoginImpl.fromJson(Map<String, dynamic> json) =>
_$$UserLoginImplFromJson(json);
// Master record key for the user used to index the local accounts table
// SuperIdentity record key for the user
// used to index the local accounts table
@override
final Typed<FixedEncodedString43> accountMasterRecordKey;
final Typed<FixedEncodedString43> superIdentityRecordKey;
// The identity secret as unlocked from the local accounts table
@override
final Typed<FixedEncodedString43> identitySecret;
@ -178,7 +180,7 @@ class _$UserLoginImpl implements _UserLogin {
@override
String toString() {
return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)';
return 'UserLogin(superIdentityRecordKey: $superIdentityRecordKey, identitySecret: $identitySecret, accountRecordInfo: $accountRecordInfo, lastActive: $lastActive)';
}
@override
@ -186,8 +188,8 @@ class _$UserLoginImpl implements _UserLogin {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$UserLoginImpl &&
(identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
other.accountMasterRecordKey == accountMasterRecordKey) &&
(identical(other.superIdentityRecordKey, superIdentityRecordKey) ||
other.superIdentityRecordKey == superIdentityRecordKey) &&
(identical(other.identitySecret, identitySecret) ||
other.identitySecret == identitySecret) &&
(identical(other.accountRecordInfo, accountRecordInfo) ||
@ -198,7 +200,7 @@ class _$UserLoginImpl implements _UserLogin {
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountMasterRecordKey,
int get hashCode => Object.hash(runtimeType, superIdentityRecordKey,
identitySecret, accountRecordInfo, lastActive);
@JsonKey(ignore: true)
@ -217,7 +219,7 @@ class _$UserLoginImpl implements _UserLogin {
abstract class _UserLogin implements UserLogin {
const factory _UserLogin(
{required final Typed<FixedEncodedString43> accountMasterRecordKey,
{required final Typed<FixedEncodedString43> superIdentityRecordKey,
required final Typed<FixedEncodedString43> identitySecret,
required final AccountRecordInfo accountRecordInfo,
required final Timestamp lastActive}) = _$UserLoginImpl;
@ -225,8 +227,9 @@ abstract class _UserLogin implements UserLogin {
factory _UserLogin.fromJson(Map<String, dynamic> json) =
_$UserLoginImpl.fromJson;
@override // Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterRecordKey;
@override // SuperIdentity record key for the user
// used to index the local accounts table
Typed<FixedEncodedString43> get superIdentityRecordKey;
@override // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get identitySecret;
@override // The account record key, owner key and secret pulled from the identity

View File

@ -8,8 +8,8 @@ part of 'user_login.dart';
_$UserLoginImpl _$$UserLoginImplFromJson(Map<String, dynamic> json) =>
_$UserLoginImpl(
accountMasterRecordKey: Typed<FixedEncodedString43>.fromJson(
json['account_master_record_key']),
superIdentityRecordKey: Typed<FixedEncodedString43>.fromJson(
json['super_identity_record_key']),
identitySecret:
Typed<FixedEncodedString43>.fromJson(json['identity_secret']),
accountRecordInfo:
@ -19,7 +19,7 @@ _$UserLoginImpl _$$UserLoginImplFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$$UserLoginImplToJson(_$UserLoginImpl instance) =>
<String, dynamic>{
'account_master_record_key': instance.accountMasterRecordKey.toJson(),
'super_identity_record_key': instance.superIdentityRecordKey.toJson(),
'identity_secret': instance.identitySecret.toJson(),
'account_record_info': instance.accountRecordInfo.toJson(),
'last_active': instance.lastActive.toJson(),

View File

@ -87,30 +87,30 @@ class AccountRepository {
: fetchUserLogin(activeLocalAccount);
}
LocalAccount? fetchLocalAccount(TypedKey accountMasterRecordKey) {
LocalAccount? fetchLocalAccount(TypedKey accountSuperIdentityRecordKey) {
final localAccounts = _localAccounts.value;
final idx = localAccounts.indexWhere(
(e) => e.identityMaster.masterRecordKey == accountMasterRecordKey);
(e) => e.superIdentity.recordKey == accountSuperIdentityRecordKey);
if (idx == -1) {
return null;
}
return localAccounts[idx];
}
UserLogin? fetchUserLogin(TypedKey accountMasterRecordKey) {
UserLogin? fetchUserLogin(TypedKey superIdentityRecordKey) {
final userLogins = _userLogins.value;
final idx = userLogins
.indexWhere((e) => e.accountMasterRecordKey == accountMasterRecordKey);
.indexWhere((e) => e.superIdentityRecordKey == superIdentityRecordKey);
if (idx == -1) {
return null;
}
return userLogins[idx];
}
AccountInfo getAccountInfo(TypedKey? accountMasterRecordKey) {
AccountInfo getAccountInfo(TypedKey? superIdentityRecordKey) {
// Get active account if we have one
final activeLocalAccount = getActiveLocalAccount();
if (accountMasterRecordKey == null) {
if (superIdentityRecordKey == null) {
if (activeLocalAccount == null) {
// No user logged in
return const AccountInfo(
@ -118,12 +118,12 @@ class AccountRepository {
active: false,
activeAccountInfo: null);
}
accountMasterRecordKey = activeLocalAccount;
superIdentityRecordKey = activeLocalAccount;
}
final active = accountMasterRecordKey == activeLocalAccount;
final active = superIdentityRecordKey == activeLocalAccount;
// Get which local account we want to fetch the profile for
final localAccount = fetchLocalAccount(accountMasterRecordKey);
final localAccount = fetchLocalAccount(superIdentityRecordKey);
if (localAccount == null) {
// account does not exist
return AccountInfo(
@ -133,7 +133,7 @@ class AccountRepository {
}
// See if we've logged into this account or if it is locked
final userLogin = fetchUserLogin(accountMasterRecordKey);
final userLogin = fetchUserLogin(superIdentityRecordKey);
if (userLogin == null) {
// Account was locked
return AccountInfo(
@ -165,33 +165,32 @@ class AccountRepository {
_streamController.add(AccountRepositoryChange.localAccounts);
}
/// Creates a new master identity, an account associated with the master
/// identity, stores the account in the identity key and then logs into
/// that account with no password set at this time
Future<void> createWithNewMasterIdentity(
NewProfileSpec newProfileSpec) async {
log.debug('Creating master identity');
final imws = await IdentityMasterWithSecrets.create();
/// Creates a new super identity, an identity instance, an account associated
/// with the identity instance, stores the account in the identity key and
/// then logs into that account with no password set at this time
Future<void> createWithNewSuperIdentity(NewProfileSpec newProfileSpec) async {
log.debug('Creating super identity');
final wsi = await WritableSuperIdentity.create();
try {
final localAccount = await _newLocalAccount(
identityMaster: imws.identityMaster,
identitySecret: imws.identitySecret,
superIdentity: wsi.superIdentity,
identitySecret: wsi.identitySecret,
newProfileSpec: newProfileSpec);
// Log in the new account by default with no pin
final ok = await login(localAccount.identityMaster.masterRecordKey,
EncryptionKeyType.none, '');
final ok = await login(
localAccount.superIdentity.recordKey, EncryptionKeyType.none, '');
assert(ok, 'login with none should never fail');
} on Exception catch (_) {
await imws.delete();
await wsi.delete();
rethrow;
}
}
/// Creates a new Account associated with master identity
/// Creates a new Account associated with the current instance of the identity
/// Adds a logged-out LocalAccount to track its existence on this device
Future<LocalAccount> _newLocalAccount(
{required IdentityMaster identityMaster,
{required SuperIdentity superIdentity,
required SecretKey identitySecret,
required NewProfileSpec newProfileSpec,
EncryptionKeyType encryptionKeyType = EncryptionKeyType.none,
@ -201,8 +200,9 @@ class AccountRepository {
final localAccounts = await _localAccounts.get();
// Add account with profile to DHT
await identityMaster.addAccountToIdentity(
identitySecret: identitySecret,
await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey,
createAccountCallback: (parent) async {
// Make empty contact list
@ -235,13 +235,13 @@ class AccountRepository {
..contactList = contactList.toProto()
..contactInvitationRecords = contactInvitationRecords.toProto()
..chatList = chatRecords.toProto();
return account;
return account.writeToBuffer();
});
// Encrypt identitySecret with key
final identitySecretBytes = await encryptionKeyType.encryptSecretToBytes(
secret: identitySecret,
cryptoKind: identityMaster.identityRecordKey.kind,
cryptoKind: superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
@ -250,7 +250,7 @@ class AccountRepository {
// as that is not to be persisted, and only pulled from the identity key
// and optionally decrypted with the unlock password
final localAccount = LocalAccount(
identityMaster: identityMaster,
superIdentity: superIdentity,
identitySecretBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
@ -269,12 +269,12 @@ class AccountRepository {
}
/// Remove an account and wipe the messages for this account from this device
Future<bool> deleteLocalAccount(TypedKey accountMasterRecordKey) async {
await logout(accountMasterRecordKey);
Future<bool> deleteLocalAccount(TypedKey superIdentityRecordKey) async {
await logout(superIdentityRecordKey);
final localAccounts = await _localAccounts.get();
final newLocalAccounts = localAccounts.removeWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
(la) => la.superIdentity.recordKey == superIdentityRecordKey);
await _localAccounts.set(newLocalAccounts);
_streamController.add(AccountRepositoryChange.localAccounts);
@ -290,31 +290,35 @@ class AccountRepository {
/// Delete an account from all devices
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
final activeLocalAccount = await _activeLocalAccount.get();
if (activeLocalAccount == accountMasterRecordKey) {
if (activeLocalAccount == superIdentityRecordKey) {
// Nothing to do
return;
}
if (accountMasterRecordKey != null) {
if (superIdentityRecordKey != null) {
// Assert the specified record key can be found, will throw if not
final _ = _userLogins.value.firstWhere(
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
(ul) => ul.superIdentityRecordKey == superIdentityRecordKey);
}
await _activeLocalAccount.set(accountMasterRecordKey);
await _activeLocalAccount.set(superIdentityRecordKey);
_streamController.add(AccountRepositoryChange.activeLocalAccount);
}
Future<bool> _decryptedLogin(
IdentityMaster identityMaster, SecretKey identitySecret) async {
SuperIdentity superIdentity, SecretKey identitySecret) async {
// Verify identity secret works and return the valid cryptosystem
final cs = await identityMaster.validateIdentitySecret(identitySecret);
final cs = await superIdentity.currentInstance
.validateIdentitySecret(identitySecret);
// Read the identity key to get the account keys
final accountRecordInfoList = await identityMaster.readAccountsFromIdentity(
identitySecret: identitySecret, accountKey: veilidChatAccountKey);
final accountRecordInfoList = await superIdentity.currentInstance
.readAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey);
if (accountRecordInfoList.length > 1) {
throw IdentityException.limitExceeded;
} else if (accountRecordInfoList.isEmpty) {
@ -326,11 +330,11 @@ class AccountRepository {
final userLogins = await _userLogins.get();
final now = Veilid.instance.now();
final newUserLogins = userLogins.replaceFirstWhere(
(ul) => ul.accountMasterRecordKey == identityMaster.masterRecordKey,
(ul) => ul.superIdentityRecordKey == superIdentity.recordKey,
(ul) => ul != null
? ul.copyWith(lastActive: now)
: UserLogin(
accountMasterRecordKey: identityMaster.masterRecordKey,
superIdentityRecordKey: superIdentity.recordKey,
identitySecret:
TypedSecret(kind: cs.kind(), value: identitySecret),
accountRecordInfo: accountRecordInfo,
@ -338,7 +342,7 @@ class AccountRepository {
addIfNotFound: true);
await _userLogins.set(newUserLogins);
await _activeLocalAccount.set(identityMaster.masterRecordKey);
await _activeLocalAccount.set(superIdentity.recordKey);
_streamController
..add(AccountRepositoryChange.userLogins)
@ -347,13 +351,13 @@ class AccountRepository {
return true;
}
Future<bool> login(TypedKey accountMasterRecordKey,
Future<bool> login(TypedKey accountSuperRecordKey,
EncryptionKeyType encryptionKeyType, String encryptionKey) async {
final localAccounts = await _localAccounts.get();
// Get account, throws if not found
final localAccount = localAccounts.firstWhere(
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
(la) => la.superIdentity.recordKey == accountSuperRecordKey);
// Log in with this local account
@ -365,12 +369,12 @@ class AccountRepository {
final identitySecret =
await localAccount.encryptionKeyType.decryptSecretFromBytes(
secretBytes: localAccount.identitySecretBytes,
cryptoKind: localAccount.identityMaster.identityRecordKey.kind,
cryptoKind: localAccount.superIdentity.currentInstance.recordKey.kind,
encryptionKey: encryptionKey,
);
// Validate this secret with the identity public key and log in
return _decryptedLogin(localAccount.identityMaster, identitySecret);
return _decryptedLogin(localAccount.superIdentity, identitySecret);
}
Future<void> logout(TypedKey? accountMasterRecordKey) async {
@ -390,20 +394,20 @@ class AccountRepository {
// Remove user from active logins list
final newUserLogins = (await _userLogins.get())
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser);
.removeWhere((ul) => ul.superIdentityRecordKey == logoutUser);
await _userLogins.set(newUserLogins);
_streamController.add(AccountRepositoryChange.userLogins);
}
Future<DHTRecord> openAccountRecord(UserLogin userLogin) async {
final localAccount = fetchLocalAccount(userLogin.accountMasterRecordKey)!;
final localAccount = fetchLocalAccount(userLogin.superIdentityRecordKey)!;
// Record not yet open, do it
final pool = DHTRecordPool.instance;
final record = await pool.openRecordOwned(
userLogin.accountRecordInfo.accountRecord,
debugName: 'AccountRepository::openAccountRecord::AccountRecord',
parent: localAccount.identityMaster.identityRecordKey);
parent: localAccount.superIdentity.currentInstance.recordKey);
return record;
}

View File

@ -1,16 +1,17 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:go_router/go_router.dart';
import '../../../layout/default_app_bar.dart';
import '../../../theme/theme.dart';
import '../../../tools/tools.dart';
import '../../../veilid_processor/veilid_processor.dart';
import '../../account_manager.dart';
import '../../layout/default_app_bar.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import '../../veilid_processor/veilid_processor.dart';
import '../account_manager.dart';
class NewAccountPage extends StatefulWidget {
const NewAccountPage({super.key});
@ -36,9 +37,17 @@ class NewAccountPageState extends State<NewAccountPage> {
}
Widget _newAccountForm(BuildContext context,
{required Future<void> Function(GlobalKey<FormBuilderState>)
onSubmit}) =>
FormBuilder(
{required Future<void> Function(GlobalKey<FormBuilderState>) onSubmit}) {
final networkReady = context
.watch<ConnectionStateCubit>()
.state
.asData
?.value
.isPublicInternetReady ??
false;
final canSubmit = networkReady;
return FormBuilder(
key: _formKey,
child: ListView(
children: [
@ -60,8 +69,8 @@ class NewAccountPageState extends State<NewAccountPage> {
FormBuilderTextField(
name: formFieldPronouns,
maxLength: 64,
decoration: InputDecoration(
labelText: translate('account.form_pronouns')),
decoration:
InputDecoration(labelText: translate('account.form_pronouns')),
textInputAction: TextInputAction.next,
),
Row(children: [
@ -72,7 +81,9 @@ class NewAccountPageState extends State<NewAccountPage> {
const Spacer(),
]).paddingSymmetric(vertical: 4),
ElevatedButton(
onPressed: () async {
onPressed: !canSubmit
? null
: () async {
if (_formKey.currentState?.saveAndValidate() ?? false) {
setState(() {
isInAsyncCall = true;
@ -88,11 +99,14 @@ class NewAccountPageState extends State<NewAccountPage> {
}
}
},
child: Text(translate('new_account_page.create')),
child: Text(translate(!networkReady
? 'button.waiting_for_network'
: 'new_account_page.create')),
).paddingSymmetric(vertical: 4).alignAtCenterRight(),
],
),
);
}
@override
Widget build(BuildContext context) {
@ -127,7 +141,7 @@ class NewAccountPageState extends State<NewAccountPage> {
NewProfileSpec(name: name, pronouns: pronouns);
await AccountRepository.instance
.createWithNewMasterIdentity(newProfileSpec);
.createWithNewSuperIdentity(newProfileSpec);
} on Exception catch (e) {
if (context.mounted) {
await showErrorModal(context, translate('new_account_page.error'),

View File

@ -1,2 +1,2 @@
export 'new_account_page/new_account_page.dart';
export 'new_account_page.dart';
export 'profile_widget.dart';

View File

@ -1,4 +1,5 @@
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:async_tools/async_tools.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@ -7,6 +8,7 @@ import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:form_builder_validators/form_builder_validators.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import 'account_manager/account_manager.dart';
import 'init.dart';
@ -22,6 +24,10 @@ class ReloadThemeIntent extends Intent {
const ReloadThemeIntent();
}
class AttachDetachThemeIntent extends Intent {
const AttachDetachThemeIntent();
}
class VeilidChatApp extends StatelessWidget {
const VeilidChatApp({
required this.initialThemeData,
@ -37,6 +43,29 @@ class VeilidChatApp extends StatelessWidget {
final theme =
PreferencesRepository.instance.value.themePreferences.themeData();
ThemeSwitcher.of(context).changeTheme(theme: theme);
// Hack to reload translations
final localizationDelegate = LocalizedApp.of(context).delegate;
singleFuture(this, () async {
await LocalizationDelegate.create(
fallbackLocale: localizationDelegate.fallbackLocale.toString(),
supportedLocales: localizationDelegate.supportedLocales
.map((x) => x.toString())
.toList());
});
}
void _attachDetachTheme(BuildContext context) {
singleFuture(this, () async {
if (ProcessorRepository.instance.processorConnectionState.isAttached) {
log.info('Detaching');
await Veilid.instance.detach();
} else if (ProcessorRepository
.instance.processorConnectionState.isDetached) {
log.info('Attaching');
await Veilid.instance.attach();
}
});
}
Widget _buildShortcuts(
@ -48,10 +77,16 @@ class VeilidChatApp extends StatelessWidget {
LogicalKeySet(
LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR):
const ReloadThemeIntent(),
LogicalKeySet(
LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD):
const AttachDetachThemeIntent(),
},
child: Actions(actions: <Type, Action<Intent>>{
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
onInvoke: (intent) => _reloadTheme(context)),
AttachDetachThemeIntent:
CallbackAction<AttachDetachThemeIntent>(
onInvoke: (intent) => _attachDetachTheme(context)),
}, child: Focus(autofocus: true, child: builder(context)))));
@override

View File

@ -49,8 +49,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
required ActiveConversationState activeConversationState,
required SingleContactMessagesCubit messagesCubit}) {
// Make local 'User'
final localUserIdentityKey =
activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey();
final localUserIdentityKey = activeAccountInfo.identityTypedPublicKey;
final localUser = types.User(
id: localUserIdentityKey.toString(),
firstName: accountRecordInfo.profile.name,

View File

@ -114,13 +114,12 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_conversationCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
_senderMessageIntegrity = await MessageIntegrity.create(
author: _activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey());
author: _activeAccountInfo.identityTypedPublicKey);
}
// Open local messages key
Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.conversationWriter;
final writer = _activeAccountInfo.identityWriter;
_sentMessagesCubit = DHTLogCubit(
open: () async => DHTLog.openWrite(_localMessagesRecordKey, writer,
@ -190,7 +189,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
{int? tail, int? count, bool? follow, bool forceRefresh = false}) async {
await _initWait();
print('setWindow: tail=$tail count=$count, follow=$follow');
// print('setWindow: tail=$tail count=$count, follow=$follow');
await _reconciledMessagesCubit!.setWindow(
tail: tail, count: count, follow: follow, forceRefresh: forceRefresh);
@ -241,10 +240,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
return;
}
_reconciliation.reconcileMessages(
_activeAccountInfo.localAccount.identityMaster.identityPublicTypedKey(),
sentMessages,
_sentMessagesCubit!);
_reconciliation.reconcileMessages(_activeAccountInfo.identityTypedPublicKey,
sentMessages, _sentMessagesCubit!);
// Update the view
_renderState();
@ -281,7 +278,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Now sign it
await _senderMessageIntegrity.signMessage(
message, _activeAccountInfo.userLogin.identitySecret.value);
message, _activeAccountInfo.identitySecretKey);
}
// Async process to send messages in the background
@ -334,8 +331,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
for (final m in reconciledMessages.windowElements) {
final isLocal = m.content.author.toVeilid() ==
_activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey();
_activeAccountInfo.identityTypedPublicKey;
final reconciledTimestamp = Timestamp.fromInt64(m.reconciledTime);
final sm =
isLocal ? sentMessagesMap[m.content.authorUniqueIdString] : null;
@ -373,9 +369,7 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Add common fields
// id and signature will get set by _processMessageToSend
message
..author = _activeAccountInfo.localAccount.identityMaster
.identityPublicTypedKey()
.toProto()
..author = _activeAccountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64();
// Put in the queue

View File

@ -68,14 +68,16 @@ class ContactInvitationListCubit
final pool = DHTRecordPool.instance;
// Generate writer keypair to share with new contact
final cs = await pool.veilid.bestCryptoSystem();
final contactRequestWriter = await cs.generateKeyPair();
final conversationWriter = _activeAccountInfo.conversationWriter;
final crcs = await pool.veilid.bestCryptoSystem();
final contactRequestWriter = await crcs.generateKeyPair();
final idcs = await _activeAccountInfo.identityCryptoSystem;
final identityWriter = _activeAccountInfo.identityWriter;
// Encrypt the writer secret with the encryption key
final encryptedSecret = await encryptionKeyType.encryptSecretToBytes(
secret: contactRequestWriter.secret,
cryptoKind: cs.kind(),
cryptoKind: crcs.kind(),
encryptionKey: encryptionKey,
);
@ -89,21 +91,21 @@ class ContactInvitationListCubit
debugName: 'ContactInvitationListCubit::createInvitation::'
'LocalConversation',
parent: _activeAccountInfo.accountRecordKey,
schema: DHTSchema.smpl(oCnt: 0, members: [
DHTSchemaMember(mKey: conversationWriter.key, mCnt: 1)
])))
schema: DHTSchema.smpl(
oCnt: 0,
members: [DHTSchemaMember(mKey: identityWriter.key, mCnt: 1)])))
.deleteScope((localConversation) async {
// dont bother reopening localConversation with writer
// Make ContactRequestPrivate and encrypt with the writer secret
final crpriv = proto.ContactRequestPrivate()
..writerKey = contactRequestWriter.key.toProto()
..profile = _account.profile
..identityMasterRecordKey =
_activeAccountInfo.userLogin.accountMasterRecordKey.toProto()
..superIdentityRecordKey =
_activeAccountInfo.userLogin.superIdentityRecordKey.toProto()
..chatRecordKey = localConversation.key.toProto()
..expiration = expiration?.toInt64() ?? Int64.ZERO;
final crprivbytes = crpriv.writeToBuffer();
final encryptedContactRequestPrivate = await cs.encryptAeadWithNonce(
final encryptedContactRequestPrivate = await crcs.encryptAeadWithNonce(
crprivbytes, contactRequestWriter.secret);
// Create ContactRequest and embed contactrequestprivate
@ -140,9 +142,8 @@ class ContactInvitationListCubit
final cinvbytes = cinv.writeToBuffer();
final scinv = proto.SignedContactInvitation()
..contactInvitation = cinvbytes
..identitySignature = (await cs.sign(
conversationWriter.key, conversationWriter.secret, cinvbytes))
.toProto();
..identitySignature =
(await idcs.signWithKeyPair(identityWriter, cinvbytes)).toProto();
signedContactInvitationBytes = scinv.writeToBuffer();
// Create ContactInvitationRecord
@ -237,8 +238,6 @@ class ContactInvitationListCubit
ValidContactInvitation? out;
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
// Compare the invitation's contact request
// inbox with our list of extant invitations
// If we're chatting to ourselves,
@ -257,6 +256,8 @@ class ContactInvitationListCubit
final contactRequest = await contactRequestInbox
.getProtobuf(proto.ContactRequest.fromBuffer);
final cs = await pool.veilid.getCryptoSystem(contactRequestInboxKey.kind);
// Decrypt contact request private
final encryptionKeyType =
EncryptionKeyType.fromProto(contactRequest!.encryptionKeyType);
@ -276,16 +277,17 @@ class ContactInvitationListCubit
final contactRequestPrivate =
proto.ContactRequestPrivate.fromBuffer(contactRequestPrivateBytes);
final contactIdentityMasterRecordKey =
contactRequestPrivate.identityMasterRecordKey.toVeilid();
final contactSuperIdentityRecordKey =
contactRequestPrivate.superIdentityRecordKey.toVeilid();
// Fetch the account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
final contactSuperIdentity = await SuperIdentity.open(
superRecordKey: contactSuperIdentityRecordKey);
// Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
final signature = signedContactInvitation.identitySignature.toVeilid();
await cs.verify(contactIdentityMaster.identityPublicKey,
await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactInvitationBytes, signature);
final writer = KeyPair(
@ -297,7 +299,7 @@ class ContactInvitationListCubit
account: _account,
contactRequestInboxKey: contactRequestInboxKey,
contactRequestPrivate: contactRequestPrivate,
contactIdentityMaster: contactIdentityMaster,
contactSuperIdentity: contactSuperIdentity,
writer: writer);
});

View File

@ -43,23 +43,21 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
if (signedContactResponse == null) {
return const AsyncValue.loading();
}
final pool = DHTRecordPool.instance;
final contactResponseBytes =
Uint8List.fromList(signedContactResponse.contactResponse);
final contactResponse =
proto.ContactResponse.fromBuffer(contactResponseBytes);
final contactIdentityMasterRecordKey =
contactResponse.identityMasterRecordKey.toVeilid();
final cs =
await pool.veilid.getCryptoSystem(contactIdentityMasterRecordKey.kind);
contactResponse.superIdentityRecordKey.toVeilid();
// Fetch the remote contact's account master
final contactIdentityMaster = await openIdentityMaster(
identityMasterRecordKey: contactIdentityMasterRecordKey);
final contactSuperIdentity = await SuperIdentity.open(
superRecordKey: contactIdentityMasterRecordKey);
// Verify
final idcs = await contactSuperIdentity.currentInstance.cryptoSystem;
final signature = signedContactResponse.identitySignature.toVeilid();
await cs.verify(contactIdentityMaster.identityPublicKey,
await idcs.verify(contactSuperIdentity.currentInstance.publicKey,
contactResponseBytes, signature);
// Check for rejection
@ -74,7 +72,8 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
final conversation = ConversationCubit(
activeAccountInfo: activeAccountInfo,
remoteIdentityPublicKey: contactIdentityMaster.identityPublicTypedKey(),
remoteIdentityPublicKey:
contactSuperIdentity.currentInstance.typedPublicKey,
remoteConversationRecordKey: remoteConversationRecordKey);
// wait for remote conversation for up to 20 seconds
@ -106,7 +105,7 @@ class WaitingInvitationCubit extends AsyncTransformerCubit<InvitationStatus,
return AsyncValue.data(InvitationStatus(
acceptedContact: AcceptedContact(
remoteProfile: remoteProfile,
remoteIdentity: contactIdentityMaster,
remoteIdentity: contactSuperIdentity,
remoteConversationRecordKey: remoteConversationRecordKey,
localConversationRecordKey: localConversationRecordKey)));
});

View File

@ -14,7 +14,7 @@ class AcceptedContact extends Equatable {
});
final proto.Profile remoteProfile;
final IdentityMaster remoteIdentity;
final SuperIdentity remoteIdentity;
final TypedKey remoteConversationRecordKey;
final TypedKey localConversationRecordKey;

View File

@ -17,13 +17,13 @@ class ValidContactInvitation {
required proto.Account account,
required TypedKey contactRequestInboxKey,
required proto.ContactRequestPrivate contactRequestPrivate,
required IdentityMaster contactIdentityMaster,
required SuperIdentity contactSuperIdentity,
required KeyPair writer})
: _activeAccountInfo = activeAccountInfo,
_account = account,
_contactRequestInboxKey = contactRequestInboxKey,
_contactRequestPrivate = contactRequestPrivate,
_contactIdentityMaster = contactIdentityMaster,
_contactSuperIdentity = contactSuperIdentity,
_writer = writer;
proto.Profile get remoteProfile => _contactRequestPrivate.profile;
@ -33,8 +33,8 @@ class ValidContactInvitation {
try {
// Ensure we don't delete this if we're trying to chat to self
// The initiating side will delete the records in deleteInvitation()
final isSelf = _contactIdentityMaster.identityPublicKey ==
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
@ -48,24 +48,23 @@ class ValidContactInvitation {
final conversation = ConversationCubit(
activeAccountInfo: _activeAccountInfo,
remoteIdentityPublicKey:
_contactIdentityMaster.identityPublicTypedKey());
_contactSuperIdentity.currentInstance.typedPublicKey);
return conversation.initLocalConversation(
profile: _account.profile,
callback: (localConversation) async {
final contactResponse = proto.ContactResponse()
..accept = true
..remoteConversationRecordKey = localConversation.key.toProto()
..identityMasterRecordKey = _activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final cs = await pool.veilid
.getCryptoSystem(_contactRequestInboxKey.kind);
final identitySignature = await cs.sign(
_activeAccountInfo.conversationWriter.key,
_activeAccountInfo.conversationWriter.secret,
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
@ -78,7 +77,7 @@ class ValidContactInvitation {
return AcceptedContact(
remoteProfile: _contactRequestPrivate.profile,
remoteIdentity: _contactIdentityMaster,
remoteIdentity: _contactSuperIdentity,
remoteConversationRecordKey:
_contactRequestPrivate.chatRecordKey.toVeilid(),
localConversationRecordKey: localConversation.key,
@ -95,10 +94,9 @@ class ValidContactInvitation {
final pool = DHTRecordPool.instance;
// Ensure we don't delete this if we're trying to chat to self
final isSelf = _contactIdentityMaster.identityPublicKey ==
_activeAccountInfo.localAccount.identityMaster.identityPublicKey;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final isSelf = _contactSuperIdentity.currentInstance.publicKey ==
_activeAccountInfo.identityPublicKey;
final accountRecordKey = _activeAccountInfo.accountRecordKey;
return (await pool.openRecordWrite(_contactRequestInboxKey, _writer,
debugName: 'ValidContactInvitation::reject::'
@ -110,14 +108,13 @@ class ValidContactInvitation {
final contactResponse = proto.ContactResponse()
..accept = false
..identityMasterRecordKey = _activeAccountInfo
.localAccount.identityMaster.masterRecordKey
.toProto();
..superIdentityRecordKey =
_activeAccountInfo.superIdentityRecordKey.toProto();
final contactResponseBytes = contactResponse.writeToBuffer();
final identitySignature = await cs.sign(
_activeAccountInfo.conversationWriter.key,
_activeAccountInfo.conversationWriter.secret,
_activeAccountInfo.identityWriter.key,
_activeAccountInfo.identityWriter.secret,
contactResponseBytes);
final signedContactResponse = proto.SignedContactResponse()
@ -135,7 +132,7 @@ class ValidContactInvitation {
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
final TypedKey _contactRequestInboxKey;
final IdentityMaster _contactIdentityMaster;
final SuperIdentity _contactSuperIdentity;
final KeyPair _writer;
final proto.ContactRequestPrivate _contactRequestPrivate;
}

View File

@ -86,13 +86,12 @@ class InvitationDialogState extends State<InvitationDialog> {
if (acceptedContact != null) {
// initiator when accept is received will create
// contact in the case of a 'note to self'
final isSelf =
activeAccountInfo.localAccount.identityMaster.identityPublicKey ==
acceptedContact.remoteIdentity.identityPublicKey;
final isSelf = activeAccountInfo.identityPublicKey ==
acceptedContact.remoteIdentity.currentInstance.publicKey;
if (!isSelf) {
await contactList.createContact(
remoteProfile: acceptedContact.remoteProfile,
remoteIdentity: acceptedContact.remoteIdentity,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:

View File

@ -36,7 +36,7 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
Future<void> createContact({
required proto.Profile remoteProfile,
required IdentityMaster remoteIdentity,
required SuperIdentity remoteSuperIdentity,
required TypedKey remoteConversationRecordKey,
required TypedKey localConversationRecordKey,
}) async {
@ -44,11 +44,9 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
final contact = proto.Contact()
..editedProfile = remoteProfile
..remoteProfile = remoteProfile
..identityMasterJson = jsonEncode(remoteIdentity.toJson())
..identityPublicKey = TypedKey(
kind: remoteIdentity.identityRecordKey.kind,
value: remoteIdentity.identityPublicKey)
.toProto()
..superIdentityJson = jsonEncode(remoteSuperIdentity.toJson())
..identityPublicKey =
remoteSuperIdentity.currentInstance.typedPublicKey.toProto()
..remoteConversationRecordKey = remoteConversationRecordKey.toProto()
..localConversationRecordKey = localConversationRecordKey.toProto()
..showAvailability = false;

View File

@ -47,7 +47,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
// Open local record key if it is specified
final pool = DHTRecordPool.instance;
final crypto = await _cachedConversationCrypto();
final writer = _activeAccountInfo.conversationWriter;
final writer = _activeAccountInfo.identityWriter;
final record = await pool.openRecordWrite(
_localConversationRecordKey!, writer,
debugName: 'ConversationCubit::LocalConversation',
@ -221,7 +221,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
final crypto = await _cachedConversationCrypto();
final writer = _activeAccountInfo.conversationWriter;
final writer = _activeAccountInfo.identityWriter;
// Open with SMPL scheme for identity writer
late final DHTRecord localConversationRecord;
@ -254,8 +254,8 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
// Create initial local conversation key contents
final conversation = proto.Conversation()
..profile = profile
..identityMasterJson = jsonEncode(
_activeAccountInfo.localAccount.identityMaster.toJson())
..superIdentityJson = jsonEncode(
_activeAccountInfo.localAccount.superIdentity.toJson())
..messages = messages.recordKey.toProto();
// Write initial conversation to record
@ -289,7 +289,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
}) async {
final crypto =
await activeAccountInfo.makeConversationCrypto(remoteIdentityPublicKey);
final writer = activeAccountInfo.conversationWriter;
final writer = activeAccountInfo.identityWriter;
return (await DHTLog.create(
debugName: 'ConversationCubit::initLocalMessages::LocalMessages',

View File

@ -91,7 +91,7 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
// Accept
await contactListCubit.createContact(
remoteProfile: acceptedContact.remoteProfile,
remoteIdentity: acceptedContact.remoteIdentity,
remoteSuperIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:

View File

@ -1026,7 +1026,7 @@ class Conversation extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Conversation', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<Profile>(1, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOS(2, _omitFieldNames ? '' : 'identityMasterJson')
..aOS(2, _omitFieldNames ? '' : 'superIdentityJson')
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'messages', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@ -1064,13 +1064,13 @@ class Conversation extends $pb.GeneratedMessage {
Profile ensureProfile() => $_ensure(0);
@$pb.TagNumber(2)
$core.String get identityMasterJson => $_getSZ(1);
$core.String get superIdentityJson => $_getSZ(1);
@$pb.TagNumber(2)
set identityMasterJson($core.String v) { $_setString(1, v); }
set superIdentityJson($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2)
$core.bool hasIdentityMasterJson() => $_has(1);
$core.bool hasSuperIdentityJson() => $_has(1);
@$pb.TagNumber(2)
void clearIdentityMasterJson() => clearField(2);
void clearSuperIdentityJson() => clearField(2);
@$pb.TagNumber(3)
$0.TypedKey get messages => $_getN(2);
@ -1427,7 +1427,7 @@ class Contact extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'Contact', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<Profile>(1, _omitFieldNames ? '' : 'editedProfile', subBuilder: Profile.create)
..aOM<Profile>(2, _omitFieldNames ? '' : 'remoteProfile', subBuilder: Profile.create)
..aOS(3, _omitFieldNames ? '' : 'identityMasterJson')
..aOS(3, _omitFieldNames ? '' : 'superIdentityJson')
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'identityPublicKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(5, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(6, _omitFieldNames ? '' : 'localConversationRecordKey', subBuilder: $0.TypedKey.create)
@ -1479,13 +1479,13 @@ class Contact extends $pb.GeneratedMessage {
Profile ensureRemoteProfile() => $_ensure(1);
@$pb.TagNumber(3)
$core.String get identityMasterJson => $_getSZ(2);
$core.String get superIdentityJson => $_getSZ(2);
@$pb.TagNumber(3)
set identityMasterJson($core.String v) { $_setString(2, v); }
set superIdentityJson($core.String v) { $_setString(2, v); }
@$pb.TagNumber(3)
$core.bool hasIdentityMasterJson() => $_has(2);
$core.bool hasSuperIdentityJson() => $_has(2);
@$pb.TagNumber(3)
void clearIdentityMasterJson() => clearField(3);
void clearSuperIdentityJson() => clearField(3);
@$pb.TagNumber(4)
$0.TypedKey get identityPublicKey => $_getN(3);
@ -1699,7 +1699,7 @@ class ContactRequestPrivate extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactRequestPrivate', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOM<$0.CryptoKey>(1, _omitFieldNames ? '' : 'writerKey', subBuilder: $0.CryptoKey.create)
..aOM<Profile>(2, _omitFieldNames ? '' : 'profile', subBuilder: Profile.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'superIdentityRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(4, _omitFieldNames ? '' : 'chatRecordKey', subBuilder: $0.TypedKey.create)
..a<$fixnum.Int64>(5, _omitFieldNames ? '' : 'expiration', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
..hasRequiredFields = false
@ -1749,15 +1749,15 @@ class ContactRequestPrivate extends $pb.GeneratedMessage {
Profile ensureProfile() => $_ensure(1);
@$pb.TagNumber(3)
$0.TypedKey get identityMasterRecordKey => $_getN(2);
$0.TypedKey get superIdentityRecordKey => $_getN(2);
@$pb.TagNumber(3)
set identityMasterRecordKey($0.TypedKey v) { setField(3, v); }
set superIdentityRecordKey($0.TypedKey v) { setField(3, v); }
@$pb.TagNumber(3)
$core.bool hasIdentityMasterRecordKey() => $_has(2);
$core.bool hasSuperIdentityRecordKey() => $_has(2);
@$pb.TagNumber(3)
void clearIdentityMasterRecordKey() => clearField(3);
void clearSuperIdentityRecordKey() => clearField(3);
@$pb.TagNumber(3)
$0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(2);
$0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(2);
@$pb.TagNumber(4)
$0.TypedKey get chatRecordKey => $_getN(3);
@ -1788,7 +1788,7 @@ class ContactResponse extends $pb.GeneratedMessage {
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'ContactResponse', package: const $pb.PackageName(_omitMessageNames ? '' : 'veilidchat'), createEmptyInstance: create)
..aOB(1, _omitFieldNames ? '' : 'accept')
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'identityMasterRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(2, _omitFieldNames ? '' : 'superIdentityRecordKey', subBuilder: $0.TypedKey.create)
..aOM<$0.TypedKey>(3, _omitFieldNames ? '' : 'remoteConversationRecordKey', subBuilder: $0.TypedKey.create)
..hasRequiredFields = false
;
@ -1824,15 +1824,15 @@ class ContactResponse extends $pb.GeneratedMessage {
void clearAccept() => clearField(1);
@$pb.TagNumber(2)
$0.TypedKey get identityMasterRecordKey => $_getN(1);
$0.TypedKey get superIdentityRecordKey => $_getN(1);
@$pb.TagNumber(2)
set identityMasterRecordKey($0.TypedKey v) { setField(2, v); }
set superIdentityRecordKey($0.TypedKey v) { setField(2, v); }
@$pb.TagNumber(2)
$core.bool hasIdentityMasterRecordKey() => $_has(1);
$core.bool hasSuperIdentityRecordKey() => $_has(1);
@$pb.TagNumber(2)
void clearIdentityMasterRecordKey() => clearField(2);
void clearSuperIdentityRecordKey() => clearField(2);
@$pb.TagNumber(2)
$0.TypedKey ensureIdentityMasterRecordKey() => $_ensure(1);
$0.TypedKey ensureSuperIdentityRecordKey() => $_ensure(1);
@$pb.TagNumber(3)
$0.TypedKey get remoteConversationRecordKey => $_getN(2);

View File

@ -309,7 +309,7 @@ const Conversation$json = {
'1': 'Conversation',
'2': [
{'1': 'profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'identity_master_json', '3': 2, '4': 1, '5': 9, '10': 'identityMasterJson'},
{'1': 'super_identity_json', '3': 2, '4': 1, '5': 9, '10': 'superIdentityJson'},
{'1': 'messages', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'messages'},
],
};
@ -317,8 +317,8 @@ const Conversation$json = {
/// Descriptor for `Conversation`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List conversationDescriptor = $convert.base64Decode(
'CgxDb252ZXJzYXRpb24SLQoHcHJvZmlsZRgBIAEoCzITLnZlaWxpZGNoYXQuUHJvZmlsZVIHcH'
'JvZmlsZRIwChRpZGVudGl0eV9tYXN0ZXJfanNvbhgCIAEoCVISaWRlbnRpdHlNYXN0ZXJKc29u'
'EiwKCG1lc3NhZ2VzGAMgASgLMhAudmVpbGlkLlR5cGVkS2V5UghtZXNzYWdlcw==');
'JvZmlsZRIuChNzdXBlcl9pZGVudGl0eV9qc29uGAIgASgJUhFzdXBlcklkZW50aXR5SnNvbhIs'
'CghtZXNzYWdlcxgDIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIIbWVzc2FnZXM=');
@$core.Deprecated('Use chatDescriptor instead')
const Chat$json = {
@ -411,7 +411,7 @@ const Contact$json = {
'2': [
{'1': 'edited_profile', '3': 1, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'editedProfile'},
{'1': 'remote_profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'remoteProfile'},
{'1': 'identity_master_json', '3': 3, '4': 1, '5': 9, '10': 'identityMasterJson'},
{'1': 'super_identity_json', '3': 3, '4': 1, '5': 9, '10': 'superIdentityJson'},
{'1': 'identity_public_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityPublicKey'},
{'1': 'remote_conversation_record_key', '3': 5, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
{'1': 'local_conversation_record_key', '3': 6, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'localConversationRecordKey'},
@ -423,13 +423,13 @@ const Contact$json = {
final $typed_data.Uint8List contactDescriptor = $convert.base64Decode(
'CgdDb250YWN0EjoKDmVkaXRlZF9wcm9maWxlGAEgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxlUg'
'1lZGl0ZWRQcm9maWxlEjoKDnJlbW90ZV9wcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9m'
'aWxlUg1yZW1vdGVQcm9maWxlEjAKFGlkZW50aXR5X21hc3Rlcl9qc29uGAMgASgJUhJpZGVudG'
'l0eU1hc3Rlckpzb24SQAoTaWRlbnRpdHlfcHVibGljX2tleRgEIAEoCzIQLnZlaWxpZC5UeXBl'
'ZEtleVIRaWRlbnRpdHlQdWJsaWNLZXkSVQoecmVtb3RlX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2'
'V5GAUgASgLMhAudmVpbGlkLlR5cGVkS2V5UhtyZW1vdGVDb252ZXJzYXRpb25SZWNvcmRLZXkS'
'UwodbG9jYWxfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYBiABKAsyEC52ZWlsaWQuVHlwZWRLZX'
'lSGmxvY2FsQ29udmVyc2F0aW9uUmVjb3JkS2V5EisKEXNob3dfYXZhaWxhYmlsaXR5GAcgASgI'
'UhBzaG93QXZhaWxhYmlsaXR5');
'aWxlUg1yZW1vdGVQcm9maWxlEi4KE3N1cGVyX2lkZW50aXR5X2pzb24YAyABKAlSEXN1cGVySW'
'RlbnRpdHlKc29uEkAKE2lkZW50aXR5X3B1YmxpY19rZXkYBCABKAsyEC52ZWlsaWQuVHlwZWRL'
'ZXlSEWlkZW50aXR5UHVibGljS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleR'
'gFIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5ElMK'
'HWxvY2FsX2NvbnZlcnNhdGlvbl9yZWNvcmRfa2V5GAYgASgLMhAudmVpbGlkLlR5cGVkS2V5Uh'
'psb2NhbENvbnZlcnNhdGlvblJlY29yZEtleRIrChFzaG93X2F2YWlsYWJpbGl0eRgHIAEoCFIQ'
'c2hvd0F2YWlsYWJpbGl0eQ==');
@$core.Deprecated('Use contactInvitationDescriptor instead')
const ContactInvitation$json = {
@ -482,7 +482,7 @@ const ContactRequestPrivate$json = {
'2': [
{'1': 'writer_key', '3': 1, '4': 1, '5': 11, '6': '.veilid.CryptoKey', '10': 'writerKey'},
{'1': 'profile', '3': 2, '4': 1, '5': 11, '6': '.veilidchat.Profile', '10': 'profile'},
{'1': 'identity_master_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityMasterRecordKey'},
{'1': 'super_identity_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'superIdentityRecordKey'},
{'1': 'chat_record_key', '3': 4, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'chatRecordKey'},
{'1': 'expiration', '3': 5, '4': 1, '5': 4, '10': 'expiration'},
],
@ -492,27 +492,27 @@ const ContactRequestPrivate$json = {
final $typed_data.Uint8List contactRequestPrivateDescriptor = $convert.base64Decode(
'ChVDb250YWN0UmVxdWVzdFByaXZhdGUSMAoKd3JpdGVyX2tleRgBIAEoCzIRLnZlaWxpZC5Dcn'
'lwdG9LZXlSCXdyaXRlcktleRItCgdwcm9maWxlGAIgASgLMhMudmVpbGlkY2hhdC5Qcm9maWxl'
'Ugdwcm9maWxlEk0KGmlkZW50aXR5X21hc3Rlcl9yZWNvcmRfa2V5GAMgASgLMhAudmVpbGlkLl'
'R5cGVkS2V5UhdpZGVudGl0eU1hc3RlclJlY29yZEtleRI4Cg9jaGF0X3JlY29yZF9rZXkYBCAB'
'KAsyEC52ZWlsaWQuVHlwZWRLZXlSDWNoYXRSZWNvcmRLZXkSHgoKZXhwaXJhdGlvbhgFIAEoBF'
'IKZXhwaXJhdGlvbg==');
'Ugdwcm9maWxlEksKGXN1cGVyX2lkZW50aXR5X3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaWQuVH'
'lwZWRLZXlSFnN1cGVySWRlbnRpdHlSZWNvcmRLZXkSOAoPY2hhdF9yZWNvcmRfa2V5GAQgASgL'
'MhAudmVpbGlkLlR5cGVkS2V5Ug1jaGF0UmVjb3JkS2V5Eh4KCmV4cGlyYXRpb24YBSABKARSCm'
'V4cGlyYXRpb24=');
@$core.Deprecated('Use contactResponseDescriptor instead')
const ContactResponse$json = {
'1': 'ContactResponse',
'2': [
{'1': 'accept', '3': 1, '4': 1, '5': 8, '10': 'accept'},
{'1': 'identity_master_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'identityMasterRecordKey'},
{'1': 'super_identity_record_key', '3': 2, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'superIdentityRecordKey'},
{'1': 'remote_conversation_record_key', '3': 3, '4': 1, '5': 11, '6': '.veilid.TypedKey', '10': 'remoteConversationRecordKey'},
],
};
/// Descriptor for `ContactResponse`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List contactResponseDescriptor = $convert.base64Decode(
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSTQoaaWRlbnRpdHlfbW'
'FzdGVyX3JlY29yZF9rZXkYAiABKAsyEC52ZWlsaWQuVHlwZWRLZXlSF2lkZW50aXR5TWFzdGVy'
'UmVjb3JkS2V5ElUKHnJlbW90ZV9jb252ZXJzYXRpb25fcmVjb3JkX2tleRgDIAEoCzIQLnZlaW'
'xpZC5UeXBlZEtleVIbcmVtb3RlQ29udmVyc2F0aW9uUmVjb3JkS2V5');
'Cg9Db250YWN0UmVzcG9uc2USFgoGYWNjZXB0GAEgASgIUgZhY2NlcHQSSwoZc3VwZXJfaWRlbn'
'RpdHlfcmVjb3JkX2tleRgCIAEoCzIQLnZlaWxpZC5UeXBlZEtleVIWc3VwZXJJZGVudGl0eVJl'
'Y29yZEtleRJVCh5yZW1vdGVfY29udmVyc2F0aW9uX3JlY29yZF9rZXkYAyABKAsyEC52ZWlsaW'
'QuVHlwZWRLZXlSG3JlbW90ZUNvbnZlcnNhdGlvblJlY29yZEtleQ==');
@$core.Deprecated('Use signedContactResponseDescriptor instead')
const SignedContactResponse$json = {

View File

@ -237,8 +237,8 @@ message ReconciledMessage {
message Conversation {
// Profile to publish to friend
Profile profile = 1;
// Identity master (JSON) to publish to friend or chat room
string identity_master_json = 2;
// SuperIdentity (JSON) to publish to friend or chat room
string super_identity_json = 2;
// Messages DHTLog
veilid.TypedKey messages = 3;
}
@ -327,8 +327,8 @@ message Contact {
Profile edited_profile = 1;
// Copy of friend's profile from remote conversation
Profile remote_profile = 2;
// Copy of friend's IdentityMaster in JSON from remote conversation
string identity_master_json = 3;
// Copy of friend's SuperIdentity in JSON from remote conversation
string super_identity_json = 3;
// Copy of friend's most recent identity public key from their identityMaster
veilid.TypedKey identity_public_key = 4;
// Remote conversation key to sync from friend
@ -378,8 +378,8 @@ message ContactRequestPrivate {
veilid.CryptoKey writer_key = 1;
// Snapshot of profile
Profile profile = 2;
// Identity master DHT record key
veilid.TypedKey identity_master_record_key = 3;
// SuperIdentity DHT record key
veilid.TypedKey super_identity_record_key = 3;
// Local chat DHT record key
veilid.TypedKey chat_record_key = 4;
// Expiration timestamp
@ -390,8 +390,8 @@ message ContactRequestPrivate {
message ContactResponse {
// Accept or reject
bool accept = 1;
// Remote identity master DHT record key
veilid.TypedKey identity_master_record_key = 2;
// Remote SuperIdentity DHT record key
veilid.TypedKey super_identity_record_key = 2;
// Remote chat DHT record key if accepted
veilid.TypedKey remote_conversation_record_key = 3;
}

View File

@ -15,5 +15,7 @@ class ProcessorConnectionState with _$ProcessorConnectionState {
attachment.state == AttachmentState.detaching ||
attachment.state == AttachmentState.attaching);
bool get isDetached => attachment.state == AttachmentState.detached;
bool get isPublicInternetReady => attachment.publicInternetReady;
}

View File

@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import '../veilid_support.dart';
part 'account_record_info.freezed.dart';
part 'account_record_info.g.dart';
/// AccountRecordInfo is the key and owner info for the account dht record that
/// is stored in the identity instance record
@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>);
}

View File

@ -0,0 +1,170 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'account_record_info.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AccountRecordInfo _$AccountRecordInfoFromJson(Map<String, dynamic> json) {
return _AccountRecordInfo.fromJson(json);
}
/// @nodoc
mixin _$AccountRecordInfo {
// Top level account keys and secrets
OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AccountRecordInfoCopyWith<AccountRecordInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AccountRecordInfoCopyWith<$Res> {
factory $AccountRecordInfoCopyWith(
AccountRecordInfo value, $Res Function(AccountRecordInfo) then) =
_$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>;
@useResult
$Res call({OwnedDHTRecordPointer accountRecord});
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord;
}
/// @nodoc
class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo>
implements $AccountRecordInfoCopyWith<$Res> {
_$AccountRecordInfoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecord = null,
}) {
return _then(_value.copyWith(
accountRecord: null == accountRecord
? _value.accountRecord
: accountRecord // ignore: cast_nullable_to_non_nullable
as OwnedDHTRecordPointer,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord {
return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) {
return _then(_value.copyWith(accountRecord: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AccountRecordInfoImplCopyWith<$Res>
implements $AccountRecordInfoCopyWith<$Res> {
factory _$$AccountRecordInfoImplCopyWith(_$AccountRecordInfoImpl value,
$Res Function(_$AccountRecordInfoImpl) then) =
__$$AccountRecordInfoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({OwnedDHTRecordPointer accountRecord});
@override
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord;
}
/// @nodoc
class __$$AccountRecordInfoImplCopyWithImpl<$Res>
extends _$AccountRecordInfoCopyWithImpl<$Res, _$AccountRecordInfoImpl>
implements _$$AccountRecordInfoImplCopyWith<$Res> {
__$$AccountRecordInfoImplCopyWithImpl(_$AccountRecordInfoImpl _value,
$Res Function(_$AccountRecordInfoImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecord = null,
}) {
return _then(_$AccountRecordInfoImpl(
accountRecord: null == accountRecord
? _value.accountRecord
: accountRecord // ignore: cast_nullable_to_non_nullable
as OwnedDHTRecordPointer,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AccountRecordInfoImpl implements _AccountRecordInfo {
const _$AccountRecordInfoImpl({required this.accountRecord});
factory _$AccountRecordInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$AccountRecordInfoImplFromJson(json);
// Top level account keys and secrets
@override
final OwnedDHTRecordPointer accountRecord;
@override
String toString() {
return 'AccountRecordInfo(accountRecord: $accountRecord)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AccountRecordInfoImpl &&
(identical(other.accountRecord, accountRecord) ||
other.accountRecord == accountRecord));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountRecord);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith =>
__$$AccountRecordInfoImplCopyWithImpl<_$AccountRecordInfoImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AccountRecordInfoImplToJson(
this,
);
}
}
abstract class _AccountRecordInfo implements AccountRecordInfo {
const factory _AccountRecordInfo(
{required final OwnedDHTRecordPointer accountRecord}) =
_$AccountRecordInfoImpl;
factory _AccountRecordInfo.fromJson(Map<String, dynamic> json) =
_$AccountRecordInfoImpl.fromJson;
@override // Top level account keys and secrets
OwnedDHTRecordPointer get accountRecord;
@override
@JsonKey(ignore: true)
_$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'account_record_info.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson(
Map<String, dynamic> json) =>
_$AccountRecordInfoImpl(
accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']),
);
Map<String, dynamic> _$$AccountRecordInfoImplToJson(
_$AccountRecordInfoImpl instance) =>
<String, dynamic>{
'account_record': instance.accountRecord.toJson(),
};

View File

@ -0,0 +1,13 @@
/// 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';
}

View File

@ -0,0 +1,25 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'account_record_info.dart';
part 'identity.freezed.dart';
part 'identity.g.dart';
/// Identity points to accounts associated with this IdentityInstance
/// accountRecords field has a map of bundle id or uuid to account key pairs
/// DHT Schema: DFLT(1)
/// DHT Key (Private): IdentityInstance.recordKey
/// DHT Owner Key: IdentityInstance.publicKey
/// DHT Secret: IdentityInstance Secret Key (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>);
}

View File

@ -0,0 +1,156 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'identity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
Identity _$IdentityFromJson(Map<String, dynamic> json) {
return _Identity.fromJson(json);
}
/// @nodoc
mixin _$Identity {
// Top level account keys and secrets
IMap<String, ISet<AccountRecordInfo>> get accountRecords =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IdentityCopyWith<Identity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IdentityCopyWith<$Res> {
factory $IdentityCopyWith(Identity value, $Res Function(Identity) then) =
_$IdentityCopyWithImpl<$Res, Identity>;
@useResult
$Res call({IMap<String, ISet<AccountRecordInfo>> accountRecords});
}
/// @nodoc
class _$IdentityCopyWithImpl<$Res, $Val extends Identity>
implements $IdentityCopyWith<$Res> {
_$IdentityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecords = null,
}) {
return _then(_value.copyWith(
accountRecords: null == accountRecords
? _value.accountRecords
: accountRecords // ignore: cast_nullable_to_non_nullable
as IMap<String, ISet<AccountRecordInfo>>,
) as $Val);
}
}
/// @nodoc
abstract class _$$IdentityImplCopyWith<$Res>
implements $IdentityCopyWith<$Res> {
factory _$$IdentityImplCopyWith(
_$IdentityImpl value, $Res Function(_$IdentityImpl) then) =
__$$IdentityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({IMap<String, ISet<AccountRecordInfo>> accountRecords});
}
/// @nodoc
class __$$IdentityImplCopyWithImpl<$Res>
extends _$IdentityCopyWithImpl<$Res, _$IdentityImpl>
implements _$$IdentityImplCopyWith<$Res> {
__$$IdentityImplCopyWithImpl(
_$IdentityImpl _value, $Res Function(_$IdentityImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecords = null,
}) {
return _then(_$IdentityImpl(
accountRecords: null == accountRecords
? _value.accountRecords
: accountRecords // ignore: cast_nullable_to_non_nullable
as IMap<String, ISet<AccountRecordInfo>>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IdentityImpl implements _Identity {
const _$IdentityImpl({required this.accountRecords});
factory _$IdentityImpl.fromJson(Map<String, dynamic> json) =>
_$$IdentityImplFromJson(json);
// Top level account keys and secrets
@override
final IMap<String, ISet<AccountRecordInfo>> accountRecords;
@override
String toString() {
return 'Identity(accountRecords: $accountRecords)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IdentityImpl &&
(identical(other.accountRecords, accountRecords) ||
other.accountRecords == accountRecords));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountRecords);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IdentityImplCopyWith<_$IdentityImpl> get copyWith =>
__$$IdentityImplCopyWithImpl<_$IdentityImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IdentityImplToJson(
this,
);
}
}
abstract class _Identity implements Identity {
const factory _Identity(
{required final IMap<String, ISet<AccountRecordInfo>>
accountRecords}) = _$IdentityImpl;
factory _Identity.fromJson(Map<String, dynamic> json) =
_$IdentityImpl.fromJson;
@override // Top level account keys and secrets
IMap<String, ISet<AccountRecordInfo>> get accountRecords;
@override
@JsonKey(ignore: true)
_$$IdentityImplCopyWith<_$IdentityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'identity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$IdentityImpl _$$IdentityImplFromJson(Map<String, dynamic> json) =>
_$IdentityImpl(
accountRecords: IMap<String, ISet<AccountRecordInfo>>.fromJson(
json['account_records'] as Map<String, dynamic>,
(value) => value as String,
(value) => ISet<AccountRecordInfo>.fromJson(
value, (value) => AccountRecordInfo.fromJson(value))),
);
Map<String, dynamic> _$$IdentityImplToJson(_$IdentityImpl instance) =>
<String, dynamic>{
'account_records': instance.accountRecords.toJson(
(value) => value,
(value) => value.toJson(
(value) => value.toJson(),
),
),
};

View File

@ -0,0 +1,274 @@
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../src/veilid_log.dart';
import '../veilid_support.dart';
import 'exceptions.dart';
part 'identity_instance.freezed.dart';
part 'identity_instance.g.dart';
@freezed
class IdentityInstance with _$IdentityInstance {
const factory IdentityInstance({
// Private DHT record storing identity account mapping
required TypedKey recordKey,
// Public key of identity instance
required PublicKey publicKey,
// Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt
// Used to recover accounts without generating a new instance
@Uint8ListJsonConverter() required Uint8List encryptedSecretKey,
// Signature of SuperInstance recordKey and SuperInstance publicKey
// by publicKey
required Signature superSignature,
// Signature of recordKey, publicKey, encryptedSecretKey, and superSignature
// by SuperIdentity publicKey
required Signature signature,
}) = _IdentityInstance;
factory IdentityInstance.fromJson(dynamic json) =>
_$IdentityInstanceFromJson(json as Map<String, dynamic>);
const IdentityInstance._();
////////////////////////////////////////////////////////////////////////////
// Public interface
/// Delete this identity instance record
/// Only deletes from the local machine not the DHT
Future<void> delete() async {
final pool = DHTRecordPool.instance;
await pool.deleteRecord(recordKey);
}
Future<VeilidCryptoSystem> get cryptoSystem =>
Veilid.instance.getCryptoSystem(recordKey.kind);
Future<VeilidCrypto> getPrivateCrypto(SecretKey secretKey) async =>
DHTRecordPool.privateCryptoFromTypedSecret(
TypedKey(kind: recordKey.kind, value: secretKey));
KeyPair writer(SecretKey secret) => KeyPair(key: publicKey, secret: secret);
TypedKey get typedPublicKey =>
TypedKey(kind: recordKey.kind, value: publicKey);
Future<VeilidCryptoSystem> validateIdentitySecret(SecretKey secretKey) async {
final cs = await cryptoSystem;
final keyOk = await cs.validateKeyPair(publicKey, secretKey);
if (!keyOk) {
throw IdentityException.invalid;
}
return cs;
}
/// Read the account record info for a specific accountKey from the identity
/// instance record using the identity instance secret key to decrypt
Future<List<AccountRecordInfo>> readAccount(
{required TypedKey superRecordKey,
required SecretKey secretKey,
required String accountKey}) async {
// Read the identity key to get the account keys
final pool = DHTRecordPool.instance;
final identityRecordCrypto = await getPrivateCrypto(secretKey);
late final List<AccountRecordInfo> accountRecordInfo;
await (await pool.openRecordRead(recordKey,
debugName: 'IdentityInstance::readAccounts::IdentityRecord',
parent: superRecordKey,
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 super identity and store it in the
/// identity instance record.
Future<AccountRecordInfo> addAccount({
required TypedKey superRecordKey,
required SecretKey secretKey,
required String accountKey,
required Future<Uint8List> 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(recordKey, writer(secretKey),
debugName: 'IdentityInstance::addAccount::IdentityRecord',
parent: superRecordKey))
.scope((identityRec) async {
// Create new account to insert into identity
veilidLoggy.debug('Creating new account');
return (await pool.createRecord(
debugName:
'IdentityInstance::addAccount::IdentityRecord::AccountRecord',
parent: identityRec.key))
.deleteScope((accountRec) async {
final account = await createAccountCallback(accountRec.key);
// Write account key
veilidLoggy.debug('Writing account record');
await accountRec.eventualWriteBytes(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;
});
});
}
////////////////////////////////////////////////////////////////////////////
// Internal implementation
Future<bool> validateIdentityInstance(
{required TypedKey superRecordKey,
required PublicKey superPublicKey}) async {
final sigValid = await IdentityInstance.validateIdentitySignature(
recordKey: recordKey,
publicKey: publicKey,
encryptedSecretKey: encryptedSecretKey,
superSignature: superSignature,
superPublicKey: superPublicKey,
signature: signature);
if (!sigValid) {
return false;
}
final superSigValid = await IdentityInstance.validateSuperSignature(
superRecordKey: superRecordKey,
superPublicKey: superPublicKey,
publicKey: publicKey,
superSignature: superSignature);
if (!superSigValid) {
return false;
}
return true;
}
static Uint8List signatureBytes({
required TypedKey recordKey,
required PublicKey publicKey,
required Uint8List encryptedSecretKey,
required Signature superSignature,
}) {
final sigBuf = BytesBuilder()
..add(recordKey.decode())
..add(publicKey.decode())
..add(encryptedSecretKey)
..add(superSignature.decode());
return sigBuf.toBytes();
}
static Future<bool> validateIdentitySignature({
required TypedKey recordKey,
required PublicKey publicKey,
required Uint8List encryptedSecretKey,
required Signature superSignature,
required PublicKey superPublicKey,
required Signature signature,
}) async {
final cs = await Veilid.instance.getCryptoSystem(recordKey.kind);
final identitySigBytes = IdentityInstance.signatureBytes(
recordKey: recordKey,
publicKey: publicKey,
encryptedSecretKey: encryptedSecretKey,
superSignature: superSignature);
return cs.verify(superPublicKey, identitySigBytes, signature);
}
static Future<Signature> createIdentitySignature({
required TypedKey recordKey,
required PublicKey publicKey,
required Uint8List encryptedSecretKey,
required Signature superSignature,
required PublicKey superPublicKey,
required SecretKey superSecret,
}) async {
final cs = await Veilid.instance.getCryptoSystem(recordKey.kind);
final identitySigBytes = IdentityInstance.signatureBytes(
recordKey: recordKey,
publicKey: publicKey,
encryptedSecretKey: encryptedSecretKey,
superSignature: superSignature);
return cs.sign(superPublicKey, superSecret, identitySigBytes);
}
static Uint8List superSignatureBytes({
required TypedKey superRecordKey,
required PublicKey superPublicKey,
}) {
final superSigBuf = BytesBuilder()
..add(superRecordKey.decode())
..add(superPublicKey.decode());
return superSigBuf.toBytes();
}
static Future<bool> validateSuperSignature({
required TypedKey superRecordKey,
required PublicKey superPublicKey,
required PublicKey publicKey,
required Signature superSignature,
}) async {
final cs = await Veilid.instance.getCryptoSystem(superRecordKey.kind);
final superSigBytes = IdentityInstance.superSignatureBytes(
superRecordKey: superRecordKey,
superPublicKey: superPublicKey,
);
return cs.verify(publicKey, superSigBytes, superSignature);
}
static Future<Signature> createSuperSignature({
required TypedKey superRecordKey,
required PublicKey superPublicKey,
required PublicKey publicKey,
required SecretKey secretKey,
}) async {
final cs = await Veilid.instance.getCryptoSystem(superRecordKey.kind);
final superSigBytes = IdentityInstance.superSignatureBytes(
superRecordKey: superRecordKey,
superPublicKey: superPublicKey,
);
return cs.sign(publicKey, secretKey, superSigBytes);
}
}

View File

@ -0,0 +1,274 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'identity_instance.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
IdentityInstance _$IdentityInstanceFromJson(Map<String, dynamic> json) {
return _IdentityInstance.fromJson(json);
}
/// @nodoc
mixin _$IdentityInstance {
// Private DHT record storing identity account mapping
Typed<FixedEncodedString43> get recordKey =>
throw _privateConstructorUsedError; // Public key of identity instance
FixedEncodedString43 get publicKey =>
throw _privateConstructorUsedError; // Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt
// Used to recover accounts without generating a new instance
@Uint8ListJsonConverter()
Uint8List get encryptedSecretKey =>
throw _privateConstructorUsedError; // Signature of SuperInstance recordKey and SuperInstance publicKey
// by publicKey
FixedEncodedString86 get superSignature =>
throw _privateConstructorUsedError; // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature
// by SuperIdentity publicKey
FixedEncodedString86 get signature => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IdentityInstanceCopyWith<IdentityInstance> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IdentityInstanceCopyWith<$Res> {
factory $IdentityInstanceCopyWith(
IdentityInstance value, $Res Function(IdentityInstance) then) =
_$IdentityInstanceCopyWithImpl<$Res, IdentityInstance>;
@useResult
$Res call(
{Typed<FixedEncodedString43> recordKey,
FixedEncodedString43 publicKey,
@Uint8ListJsonConverter() Uint8List encryptedSecretKey,
FixedEncodedString86 superSignature,
FixedEncodedString86 signature});
}
/// @nodoc
class _$IdentityInstanceCopyWithImpl<$Res, $Val extends IdentityInstance>
implements $IdentityInstanceCopyWith<$Res> {
_$IdentityInstanceCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? recordKey = null,
Object? publicKey = null,
Object? encryptedSecretKey = null,
Object? superSignature = null,
Object? signature = null,
}) {
return _then(_value.copyWith(
recordKey: null == recordKey
? _value.recordKey
: recordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
publicKey: null == publicKey
? _value.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
encryptedSecretKey: null == encryptedSecretKey
? _value.encryptedSecretKey
: encryptedSecretKey // ignore: cast_nullable_to_non_nullable
as Uint8List,
superSignature: null == superSignature
? _value.superSignature
: superSignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
signature: null == signature
? _value.signature
: signature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
) as $Val);
}
}
/// @nodoc
abstract class _$$IdentityInstanceImplCopyWith<$Res>
implements $IdentityInstanceCopyWith<$Res> {
factory _$$IdentityInstanceImplCopyWith(_$IdentityInstanceImpl value,
$Res Function(_$IdentityInstanceImpl) then) =
__$$IdentityInstanceImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> recordKey,
FixedEncodedString43 publicKey,
@Uint8ListJsonConverter() Uint8List encryptedSecretKey,
FixedEncodedString86 superSignature,
FixedEncodedString86 signature});
}
/// @nodoc
class __$$IdentityInstanceImplCopyWithImpl<$Res>
extends _$IdentityInstanceCopyWithImpl<$Res, _$IdentityInstanceImpl>
implements _$$IdentityInstanceImplCopyWith<$Res> {
__$$IdentityInstanceImplCopyWithImpl(_$IdentityInstanceImpl _value,
$Res Function(_$IdentityInstanceImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? recordKey = null,
Object? publicKey = null,
Object? encryptedSecretKey = null,
Object? superSignature = null,
Object? signature = null,
}) {
return _then(_$IdentityInstanceImpl(
recordKey: null == recordKey
? _value.recordKey
: recordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
publicKey: null == publicKey
? _value.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
encryptedSecretKey: null == encryptedSecretKey
? _value.encryptedSecretKey
: encryptedSecretKey // ignore: cast_nullable_to_non_nullable
as Uint8List,
superSignature: null == superSignature
? _value.superSignature
: superSignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
signature: null == signature
? _value.signature
: signature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IdentityInstanceImpl extends _IdentityInstance {
const _$IdentityInstanceImpl(
{required this.recordKey,
required this.publicKey,
@Uint8ListJsonConverter() required this.encryptedSecretKey,
required this.superSignature,
required this.signature})
: super._();
factory _$IdentityInstanceImpl.fromJson(Map<String, dynamic> json) =>
_$$IdentityInstanceImplFromJson(json);
// Private DHT record storing identity account mapping
@override
final Typed<FixedEncodedString43> recordKey;
// Public key of identity instance
@override
final FixedEncodedString43 publicKey;
// Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt
// Used to recover accounts without generating a new instance
@override
@Uint8ListJsonConverter()
final Uint8List encryptedSecretKey;
// Signature of SuperInstance recordKey and SuperInstance publicKey
// by publicKey
@override
final FixedEncodedString86 superSignature;
// Signature of recordKey, publicKey, encryptedSecretKey, and superSignature
// by SuperIdentity publicKey
@override
final FixedEncodedString86 signature;
@override
String toString() {
return 'IdentityInstance(recordKey: $recordKey, publicKey: $publicKey, encryptedSecretKey: $encryptedSecretKey, superSignature: $superSignature, signature: $signature)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IdentityInstanceImpl &&
(identical(other.recordKey, recordKey) ||
other.recordKey == recordKey) &&
(identical(other.publicKey, publicKey) ||
other.publicKey == publicKey) &&
const DeepCollectionEquality()
.equals(other.encryptedSecretKey, encryptedSecretKey) &&
(identical(other.superSignature, superSignature) ||
other.superSignature == superSignature) &&
(identical(other.signature, signature) ||
other.signature == signature));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
recordKey,
publicKey,
const DeepCollectionEquality().hash(encryptedSecretKey),
superSignature,
signature);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith =>
__$$IdentityInstanceImplCopyWithImpl<_$IdentityInstanceImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IdentityInstanceImplToJson(
this,
);
}
}
abstract class _IdentityInstance extends IdentityInstance {
const factory _IdentityInstance(
{required final Typed<FixedEncodedString43> recordKey,
required final FixedEncodedString43 publicKey,
@Uint8ListJsonConverter() required final Uint8List encryptedSecretKey,
required final FixedEncodedString86 superSignature,
required final FixedEncodedString86 signature}) = _$IdentityInstanceImpl;
const _IdentityInstance._() : super._();
factory _IdentityInstance.fromJson(Map<String, dynamic> json) =
_$IdentityInstanceImpl.fromJson;
@override // Private DHT record storing identity account mapping
Typed<FixedEncodedString43> get recordKey;
@override // Public key of identity instance
FixedEncodedString43 get publicKey;
@override // Secret key of identity instance
// Encrypted with DH(publicKey, SuperIdentity.secret) with appended salt
// Used to recover accounts without generating a new instance
@Uint8ListJsonConverter()
Uint8List get encryptedSecretKey;
@override // Signature of SuperInstance recordKey and SuperInstance publicKey
// by publicKey
FixedEncodedString86 get superSignature;
@override // Signature of recordKey, publicKey, encryptedSecretKey, and superSignature
// by SuperIdentity publicKey
FixedEncodedString86 get signature;
@override
@JsonKey(ignore: true)
_$$IdentityInstanceImplCopyWith<_$IdentityInstanceImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,29 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'identity_instance.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$IdentityInstanceImpl _$$IdentityInstanceImplFromJson(
Map<String, dynamic> json) =>
_$IdentityInstanceImpl(
recordKey: Typed<FixedEncodedString43>.fromJson(json['record_key']),
publicKey: FixedEncodedString43.fromJson(json['public_key']),
encryptedSecretKey:
const Uint8ListJsonConverter().fromJson(json['encrypted_secret_key']),
superSignature: FixedEncodedString86.fromJson(json['super_signature']),
signature: FixedEncodedString86.fromJson(json['signature']),
);
Map<String, dynamic> _$$IdentityInstanceImplToJson(
_$IdentityInstanceImpl instance) =>
<String, dynamic>{
'record_key': instance.recordKey.toJson(),
'public_key': instance.publicKey.toJson(),
'encrypted_secret_key':
const Uint8ListJsonConverter().toJson(instance.encryptedSecretKey),
'super_signature': instance.superSignature.toJson(),
'signature': instance.signature.toJson(),
};

View File

@ -0,0 +1,6 @@
export 'account_record_info.dart';
export 'exceptions.dart';
export 'identity.dart';
export 'identity_instance.dart';
export 'super_identity.dart';
export 'writable_super_identity.dart';

View File

@ -0,0 +1,174 @@
import 'dart:typed_data';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../veilid_support.dart';
part 'super_identity.freezed.dart';
part 'super_identity.g.dart';
/// SuperIdentity key structure for created account
///
/// SuperIdentity key allows for regeneration of identity DHT record
/// Bidirectional Super<->Instance signature allows for
/// chain of identity ownership for account recovery process
///
/// Backed by a DHT key at superRecordKey, the secret is kept
/// completely offline and only written to upon account recovery
///
/// DHT Schema: DFLT(1)
/// DHT Record Key (Public): SuperIdentity.recordKey
/// DHT Owner Key: SuperIdentity.publicKey
/// DHT Owner Secret: SuperIdentity Secret Key (kept offline)
/// Encryption: None
@freezed
class SuperIdentity with _$SuperIdentity {
const factory SuperIdentity({
/// Public DHT record storing this structure for account recovery
/// changing this can migrate/forward the SuperIdentity to a new DHT record
/// Instances should not hash this recordKey, rather the actual record
/// key used to store the superIdentity, as this may change.
required TypedKey recordKey,
/// Public key of the SuperIdentity used to sign identity keys for recovery
/// This must match the owner of the superRecord DHT record and can not be
/// changed without changing the record
required PublicKey publicKey,
/// Current identity instance
/// The most recently generated identity instance for this SuperIdentity
required IdentityInstance currentInstance,
/// Deprecated identity instances
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
required List<IdentityInstance> deprecatedInstances,
/// Deprecated superRecords
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
required List<TypedKey> deprecatedSuperRecordKeys,
/// Signature of recordKey, currentInstance signature,
/// signatures of deprecatedInstances, and deprecatedSuperRecordKeys
/// by publicKey
required Signature signature,
}) = _SuperIdentity;
////////////////////////////////////////////////////////////////////////////
// Constructors
factory SuperIdentity.fromJson(dynamic json) =>
_$SuperIdentityFromJson(json as Map<String, dynamic>);
const SuperIdentity._();
/// Opens an existing super identity and validates it
static Future<SuperIdentity> open({required TypedKey superRecordKey}) async {
final pool = DHTRecordPool.instance;
// SuperIdentity DHT record is public/unencrypted
return (await pool.openRecordRead(superRecordKey,
debugName: 'SuperIdentity::openSuperIdentity::SuperIdentityRecord'))
.deleteScope((superRec) async {
final superIdentity = (await superRec.getJson(SuperIdentity.fromJson,
refreshMode: DHTRecordRefreshMode.network))!;
// Validate current IdentityInstance
if (!await superIdentity.currentInstance.validateIdentityInstance(
superRecordKey: superRecordKey,
superPublicKey: superIdentity.publicKey)) {
// Invalid current IdentityInstance signature(s)
throw IdentityException.invalid;
}
// Validate deprecated IdentityInstances
for (final deprecatedInstance in superIdentity.deprecatedInstances) {
if (!await deprecatedInstance.validateIdentityInstance(
superRecordKey: superRecordKey,
superPublicKey: superIdentity.publicKey)) {
// Invalid deprecated IdentityInstance signature(s)
throw IdentityException.invalid;
}
}
// Validate SuperIdentity
final deprecatedInstancesSignatures =
superIdentity.deprecatedInstances.map((x) => x.signature).toList();
if (!await _validateSuperIdentitySignature(
recordKey: superIdentity.recordKey,
currentInstanceSignature: superIdentity.currentInstance.signature,
deprecatedInstancesSignatures: deprecatedInstancesSignatures,
deprecatedSuperRecordKeys: superIdentity.deprecatedSuperRecordKeys,
publicKey: superIdentity.publicKey,
signature: superIdentity.signature)) {
// Invalid SuperIdentity signature
throw IdentityException.invalid;
}
return superIdentity;
});
}
////////////////////////////////////////////////////////////////////////////
// Public Interface
/// Deletes a super identity and the identity instance records under it
/// Only deletes from the local machine not the DHT
Future<void> delete() async {
final pool = DHTRecordPool.instance;
await pool.deleteRecord(recordKey);
}
Future<VeilidCryptoSystem> get cryptoSystem =>
Veilid.instance.getCryptoSystem(recordKey.kind);
KeyPair writer(SecretKey secretKey) =>
KeyPair(key: publicKey, secret: secretKey);
TypedKey get typedPublicKey =>
TypedKey(kind: recordKey.kind, value: publicKey);
Future<VeilidCryptoSystem> validateSecret(SecretKey secretKey) async {
final cs = await cryptoSystem;
final keyOk = await cs.validateKeyPair(publicKey, secretKey);
if (!keyOk) {
throw IdentityException.invalid;
}
return cs;
}
////////////////////////////////////////////////////////////////////////////
// Internal implementation
static Uint8List signatureBytes({
required TypedKey recordKey,
required Signature currentInstanceSignature,
required List<Signature> deprecatedInstancesSignatures,
required List<TypedKey> deprecatedSuperRecordKeys,
}) {
final sigBuf = BytesBuilder()
..add(recordKey.decode())
..add(currentInstanceSignature.decode())
..add(deprecatedInstancesSignatures.expand((s) => s.decode()).toList())
..add(deprecatedSuperRecordKeys.expand((s) => s.decode()).toList());
return sigBuf.toBytes();
}
static Future<bool> _validateSuperIdentitySignature({
required TypedKey recordKey,
required Signature currentInstanceSignature,
required List<Signature> deprecatedInstancesSignatures,
required List<TypedKey> deprecatedSuperRecordKeys,
required PublicKey publicKey,
required Signature signature,
}) async {
final cs = await Veilid.instance.getCryptoSystem(recordKey.kind);
final sigBytes = SuperIdentity.signatureBytes(
recordKey: recordKey,
currentInstanceSignature: currentInstanceSignature,
deprecatedInstancesSignatures: deprecatedInstancesSignatures,
deprecatedSuperRecordKeys: deprecatedSuperRecordKeys);
return cs.verify(publicKey, sigBytes, signature);
}
}

View File

@ -0,0 +1,380 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'super_identity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
SuperIdentity _$SuperIdentityFromJson(Map<String, dynamic> json) {
return _SuperIdentity.fromJson(json);
}
/// @nodoc
mixin _$SuperIdentity {
/// Public DHT record storing this structure for account recovery
/// changing this can migrate/forward the SuperIdentity to a new DHT record
/// Instances should not hash this recordKey, rather the actual record
/// key used to store the superIdentity, as this may change.
Typed<FixedEncodedString43> get recordKey =>
throw _privateConstructorUsedError;
/// Public key of the SuperIdentity used to sign identity keys for recovery
/// This must match the owner of the superRecord DHT record and can not be
/// changed without changing the record
FixedEncodedString43 get publicKey => throw _privateConstructorUsedError;
/// Current identity instance
/// The most recently generated identity instance for this SuperIdentity
IdentityInstance get currentInstance => throw _privateConstructorUsedError;
/// Deprecated identity instances
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
List<IdentityInstance> get deprecatedInstances =>
throw _privateConstructorUsedError;
/// Deprecated superRecords
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
List<Typed<FixedEncodedString43>> get deprecatedSuperRecordKeys =>
throw _privateConstructorUsedError;
/// Signature of recordKey, currentInstance signature,
/// signatures of deprecatedInstances, and deprecatedSuperRecordKeys
/// by publicKey
FixedEncodedString86 get signature => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SuperIdentityCopyWith<SuperIdentity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SuperIdentityCopyWith<$Res> {
factory $SuperIdentityCopyWith(
SuperIdentity value, $Res Function(SuperIdentity) then) =
_$SuperIdentityCopyWithImpl<$Res, SuperIdentity>;
@useResult
$Res call(
{Typed<FixedEncodedString43> recordKey,
FixedEncodedString43 publicKey,
IdentityInstance currentInstance,
List<IdentityInstance> deprecatedInstances,
List<Typed<FixedEncodedString43>> deprecatedSuperRecordKeys,
FixedEncodedString86 signature});
$IdentityInstanceCopyWith<$Res> get currentInstance;
}
/// @nodoc
class _$SuperIdentityCopyWithImpl<$Res, $Val extends SuperIdentity>
implements $SuperIdentityCopyWith<$Res> {
_$SuperIdentityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? recordKey = null,
Object? publicKey = null,
Object? currentInstance = null,
Object? deprecatedInstances = null,
Object? deprecatedSuperRecordKeys = null,
Object? signature = null,
}) {
return _then(_value.copyWith(
recordKey: null == recordKey
? _value.recordKey
: recordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
publicKey: null == publicKey
? _value.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
currentInstance: null == currentInstance
? _value.currentInstance
: currentInstance // ignore: cast_nullable_to_non_nullable
as IdentityInstance,
deprecatedInstances: null == deprecatedInstances
? _value.deprecatedInstances
: deprecatedInstances // ignore: cast_nullable_to_non_nullable
as List<IdentityInstance>,
deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys
? _value.deprecatedSuperRecordKeys
: deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable
as List<Typed<FixedEncodedString43>>,
signature: null == signature
? _value.signature
: signature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$IdentityInstanceCopyWith<$Res> get currentInstance {
return $IdentityInstanceCopyWith<$Res>(_value.currentInstance, (value) {
return _then(_value.copyWith(currentInstance: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$SuperIdentityImplCopyWith<$Res>
implements $SuperIdentityCopyWith<$Res> {
factory _$$SuperIdentityImplCopyWith(
_$SuperIdentityImpl value, $Res Function(_$SuperIdentityImpl) then) =
__$$SuperIdentityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> recordKey,
FixedEncodedString43 publicKey,
IdentityInstance currentInstance,
List<IdentityInstance> deprecatedInstances,
List<Typed<FixedEncodedString43>> deprecatedSuperRecordKeys,
FixedEncodedString86 signature});
@override
$IdentityInstanceCopyWith<$Res> get currentInstance;
}
/// @nodoc
class __$$SuperIdentityImplCopyWithImpl<$Res>
extends _$SuperIdentityCopyWithImpl<$Res, _$SuperIdentityImpl>
implements _$$SuperIdentityImplCopyWith<$Res> {
__$$SuperIdentityImplCopyWithImpl(
_$SuperIdentityImpl _value, $Res Function(_$SuperIdentityImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? recordKey = null,
Object? publicKey = null,
Object? currentInstance = null,
Object? deprecatedInstances = null,
Object? deprecatedSuperRecordKeys = null,
Object? signature = null,
}) {
return _then(_$SuperIdentityImpl(
recordKey: null == recordKey
? _value.recordKey
: recordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
publicKey: null == publicKey
? _value.publicKey
: publicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
currentInstance: null == currentInstance
? _value.currentInstance
: currentInstance // ignore: cast_nullable_to_non_nullable
as IdentityInstance,
deprecatedInstances: null == deprecatedInstances
? _value._deprecatedInstances
: deprecatedInstances // ignore: cast_nullable_to_non_nullable
as List<IdentityInstance>,
deprecatedSuperRecordKeys: null == deprecatedSuperRecordKeys
? _value._deprecatedSuperRecordKeys
: deprecatedSuperRecordKeys // ignore: cast_nullable_to_non_nullable
as List<Typed<FixedEncodedString43>>,
signature: null == signature
? _value.signature
: signature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
));
}
}
/// @nodoc
@JsonSerializable()
class _$SuperIdentityImpl extends _SuperIdentity {
const _$SuperIdentityImpl(
{required this.recordKey,
required this.publicKey,
required this.currentInstance,
required final List<IdentityInstance> deprecatedInstances,
required final List<Typed<FixedEncodedString43>>
deprecatedSuperRecordKeys,
required this.signature})
: _deprecatedInstances = deprecatedInstances,
_deprecatedSuperRecordKeys = deprecatedSuperRecordKeys,
super._();
factory _$SuperIdentityImpl.fromJson(Map<String, dynamic> json) =>
_$$SuperIdentityImplFromJson(json);
/// Public DHT record storing this structure for account recovery
/// changing this can migrate/forward the SuperIdentity to a new DHT record
/// Instances should not hash this recordKey, rather the actual record
/// key used to store the superIdentity, as this may change.
@override
final Typed<FixedEncodedString43> recordKey;
/// Public key of the SuperIdentity used to sign identity keys for recovery
/// This must match the owner of the superRecord DHT record and can not be
/// changed without changing the record
@override
final FixedEncodedString43 publicKey;
/// Current identity instance
/// The most recently generated identity instance for this SuperIdentity
@override
final IdentityInstance currentInstance;
/// Deprecated identity instances
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
final List<IdentityInstance> _deprecatedInstances;
/// Deprecated identity instances
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
@override
List<IdentityInstance> get deprecatedInstances {
if (_deprecatedInstances is EqualUnmodifiableListView)
return _deprecatedInstances;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_deprecatedInstances);
}
/// Deprecated superRecords
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
final List<Typed<FixedEncodedString43>> _deprecatedSuperRecordKeys;
/// Deprecated superRecords
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
@override
List<Typed<FixedEncodedString43>> get deprecatedSuperRecordKeys {
if (_deprecatedSuperRecordKeys is EqualUnmodifiableListView)
return _deprecatedSuperRecordKeys;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_deprecatedSuperRecordKeys);
}
/// Signature of recordKey, currentInstance signature,
/// signatures of deprecatedInstances, and deprecatedSuperRecordKeys
/// by publicKey
@override
final FixedEncodedString86 signature;
@override
String toString() {
return 'SuperIdentity(recordKey: $recordKey, publicKey: $publicKey, currentInstance: $currentInstance, deprecatedInstances: $deprecatedInstances, deprecatedSuperRecordKeys: $deprecatedSuperRecordKeys, signature: $signature)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SuperIdentityImpl &&
(identical(other.recordKey, recordKey) ||
other.recordKey == recordKey) &&
(identical(other.publicKey, publicKey) ||
other.publicKey == publicKey) &&
(identical(other.currentInstance, currentInstance) ||
other.currentInstance == currentInstance) &&
const DeepCollectionEquality()
.equals(other._deprecatedInstances, _deprecatedInstances) &&
const DeepCollectionEquality().equals(
other._deprecatedSuperRecordKeys, _deprecatedSuperRecordKeys) &&
(identical(other.signature, signature) ||
other.signature == signature));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
recordKey,
publicKey,
currentInstance,
const DeepCollectionEquality().hash(_deprecatedInstances),
const DeepCollectionEquality().hash(_deprecatedSuperRecordKeys),
signature);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith =>
__$$SuperIdentityImplCopyWithImpl<_$SuperIdentityImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SuperIdentityImplToJson(
this,
);
}
}
abstract class _SuperIdentity extends SuperIdentity {
const factory _SuperIdentity(
{required final Typed<FixedEncodedString43> recordKey,
required final FixedEncodedString43 publicKey,
required final IdentityInstance currentInstance,
required final List<IdentityInstance> deprecatedInstances,
required final List<Typed<FixedEncodedString43>>
deprecatedSuperRecordKeys,
required final FixedEncodedString86 signature}) = _$SuperIdentityImpl;
const _SuperIdentity._() : super._();
factory _SuperIdentity.fromJson(Map<String, dynamic> json) =
_$SuperIdentityImpl.fromJson;
@override
/// Public DHT record storing this structure for account recovery
/// changing this can migrate/forward the SuperIdentity to a new DHT record
/// Instances should not hash this recordKey, rather the actual record
/// key used to store the superIdentity, as this may change.
Typed<FixedEncodedString43> get recordKey;
@override
/// Public key of the SuperIdentity used to sign identity keys for recovery
/// This must match the owner of the superRecord DHT record and can not be
/// changed without changing the record
FixedEncodedString43 get publicKey;
@override
/// Current identity instance
/// The most recently generated identity instance for this SuperIdentity
IdentityInstance get currentInstance;
@override
/// Deprecated identity instances
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
List<IdentityInstance> get deprecatedInstances;
@override
/// Deprecated superRecords
/// These may be compromised and should not be considered valid for
/// new signatures, but may be used to validate old signatures
List<Typed<FixedEncodedString43>> get deprecatedSuperRecordKeys;
@override
/// Signature of recordKey, currentInstance signature,
/// signatures of deprecatedInstances, and deprecatedSuperRecordKeys
/// by publicKey
FixedEncodedString86 get signature;
@override
@JsonKey(ignore: true)
_$$SuperIdentityImplCopyWith<_$SuperIdentityImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'super_identity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$SuperIdentityImpl _$$SuperIdentityImplFromJson(Map<String, dynamic> json) =>
_$SuperIdentityImpl(
recordKey: Typed<FixedEncodedString43>.fromJson(json['record_key']),
publicKey: FixedEncodedString43.fromJson(json['public_key']),
currentInstance: IdentityInstance.fromJson(json['current_instance']),
deprecatedInstances: (json['deprecated_instances'] as List<dynamic>)
.map(IdentityInstance.fromJson)
.toList(),
deprecatedSuperRecordKeys:
(json['deprecated_super_record_keys'] as List<dynamic>)
.map(Typed<FixedEncodedString43>.fromJson)
.toList(),
signature: FixedEncodedString86.fromJson(json['signature']),
);
Map<String, dynamic> _$$SuperIdentityImplToJson(_$SuperIdentityImpl instance) =>
<String, dynamic>{
'record_key': instance.recordKey.toJson(),
'public_key': instance.publicKey.toJson(),
'current_instance': instance.currentInstance.toJson(),
'deprecated_instances':
instance.deprecatedInstances.map((e) => e.toJson()).toList(),
'deprecated_super_record_keys':
instance.deprecatedSuperRecordKeys.map((e) => e.toJson()).toList(),
'signature': instance.signature.toJson(),
};

View File

@ -0,0 +1,158 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import '../src/veilid_log.dart';
import '../veilid_support.dart';
Uint8List identityCryptoDomain = utf8.encode('identity');
/// SuperIdentity creator with secret
/// Not freezed because we never persist this class in its entirety.
class WritableSuperIdentity {
WritableSuperIdentity._({
required this.superIdentity,
required this.superSecret,
required this.identitySecret,
});
static Future<WritableSuperIdentity> create() async {
final pool = DHTRecordPool.instance;
// SuperIdentity DHT record is public/unencrypted
veilidLoggy.debug('Creating super identity record');
return (await pool.createRecord(
debugName: 'WritableSuperIdentity::create::SuperIdentityRecord',
crypto: const VeilidCryptoPublic()))
.deleteScope((superRec) async {
final superRecordKey = superRec.key;
final superPublicKey = superRec.ownerKeyPair!.key;
final superSecret = superRec.ownerKeyPair!.secret;
return _createIdentityInstance(
superRecordKey: superRecordKey,
superPublicKey: superPublicKey,
superSecret: superSecret,
closure: (identityInstance, identitySecret) async {
final signature = await _createSuperIdentitySignature(
recordKey: superRecordKey,
publicKey: superPublicKey,
secretKey: superSecret,
currentInstanceSignature: identityInstance.signature,
deprecatedInstancesSignatures: [],
deprecatedSuperRecordKeys: [],
);
final superIdentity = SuperIdentity(
recordKey: superRecordKey,
publicKey: superPublicKey,
currentInstance: identityInstance,
deprecatedInstances: [],
deprecatedSuperRecordKeys: [],
signature: signature);
// Write superidentity to dht record
await superRec.eventualWriteJson(superIdentity);
return WritableSuperIdentity._(
superIdentity: superIdentity,
superSecret: superSecret,
identitySecret: identitySecret);
});
});
}
////////////////////////////////////////////////////////////////////////////
// Public Interface
/// Delete a super identity with secrets
Future<void> delete() async => superIdentity.delete();
/// xxx: migration support, new identities, reveal identity secret etc
////////////////////////////////////////////////////////////////////////////
/// Private Implementation
static Future<Signature> _createSuperIdentitySignature({
required TypedKey recordKey,
required Signature currentInstanceSignature,
required List<Signature> deprecatedInstancesSignatures,
required List<TypedKey> deprecatedSuperRecordKeys,
required PublicKey publicKey,
required SecretKey secretKey,
}) async {
final cs = await Veilid.instance.getCryptoSystem(recordKey.kind);
final sigBytes = SuperIdentity.signatureBytes(
recordKey: recordKey,
currentInstanceSignature: currentInstanceSignature,
deprecatedInstancesSignatures: deprecatedInstancesSignatures,
deprecatedSuperRecordKeys: deprecatedSuperRecordKeys);
return cs.sign(publicKey, secretKey, sigBytes);
}
static Future<T> _createIdentityInstance<T>({
required TypedKey superRecordKey,
required PublicKey superPublicKey,
required SecretKey superSecret,
required Future<T> Function(IdentityInstance, SecretKey) closure,
}) async {
final pool = DHTRecordPool.instance;
veilidLoggy.debug('Creating identity instance record');
// Identity record is private
return (await pool.createRecord(
debugName: 'SuperIdentityWithSecrets::create::IdentityRecord',
parent: superRecordKey))
.deleteScope((identityRec) async {
final identityRecordKey = identityRec.key;
assert(superRecordKey.kind == identityRecordKey.kind,
'new super and identity should have same cryptosystem');
final identityPublicKey = identityRec.ownerKeyPair!.key;
final identitySecretKey = identityRec.ownerKeyPair!.secret;
// Make encrypted secret key
final cs = await Veilid.instance.getCryptoSystem(identityRecordKey.kind);
final encryptionKey = await cs.generateSharedSecret(
identityPublicKey, superSecret, identityCryptoDomain);
final encryptedSecretKey = await cs.encryptNoAuthWithNonce(
identitySecretKey.decode(), encryptionKey);
// Make supersignature
final superSigBuf = BytesBuilder()
..add(superRecordKey.decode())
..add(superPublicKey.decode());
final superSignature = await cs.signWithKeyPair(
identityRec.ownerKeyPair!, superSigBuf.toBytes());
// Make signature
final signature = await IdentityInstance.createIdentitySignature(
recordKey: identityRecordKey,
publicKey: identityPublicKey,
encryptedSecretKey: encryptedSecretKey,
superSignature: superSignature,
superPublicKey: superPublicKey,
superSecret: superSecret);
// Make empty identity
const identity = Identity(accountRecords: IMapConst({}));
// Write empty identity to identity dht key
await identityRec.eventualWriteJson(identity);
final identityInstance = IdentityInstance(
recordKey: identityRecordKey,
publicKey: identityPublicKey,
encryptedSecretKey: encryptedSecretKey,
superSignature: superSignature,
signature: signature);
return closure(identityInstance, identitySecretKey);
});
}
SuperIdentity superIdentity;
SecretKey superSecret;
SecretKey identitySecret;
}

View File

@ -1,333 +0,0 @@
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 SecretKey identitySecret, required String accountKey}) async {
// Read the identity key to get the account keys
final pool = DHTRecordPool.instance;
final identityRecordCrypto =
await DHTRecordPool.privateCryptoFromTypedSecret(
TypedKey(kind: identityRecordKey.kind, value: 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 SecretKey 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 VeilidCryptoPublic()))
.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;
});
}

View File

@ -1,579 +0,0 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'identity.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
AccountRecordInfo _$AccountRecordInfoFromJson(Map<String, dynamic> json) {
return _AccountRecordInfo.fromJson(json);
}
/// @nodoc
mixin _$AccountRecordInfo {
// Top level account keys and secrets
OwnedDHTRecordPointer get accountRecord => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$AccountRecordInfoCopyWith<AccountRecordInfo> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $AccountRecordInfoCopyWith<$Res> {
factory $AccountRecordInfoCopyWith(
AccountRecordInfo value, $Res Function(AccountRecordInfo) then) =
_$AccountRecordInfoCopyWithImpl<$Res, AccountRecordInfo>;
@useResult
$Res call({OwnedDHTRecordPointer accountRecord});
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord;
}
/// @nodoc
class _$AccountRecordInfoCopyWithImpl<$Res, $Val extends AccountRecordInfo>
implements $AccountRecordInfoCopyWith<$Res> {
_$AccountRecordInfoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecord = null,
}) {
return _then(_value.copyWith(
accountRecord: null == accountRecord
? _value.accountRecord
: accountRecord // ignore: cast_nullable_to_non_nullable
as OwnedDHTRecordPointer,
) as $Val);
}
@override
@pragma('vm:prefer-inline')
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord {
return $OwnedDHTRecordPointerCopyWith<$Res>(_value.accountRecord, (value) {
return _then(_value.copyWith(accountRecord: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$AccountRecordInfoImplCopyWith<$Res>
implements $AccountRecordInfoCopyWith<$Res> {
factory _$$AccountRecordInfoImplCopyWith(_$AccountRecordInfoImpl value,
$Res Function(_$AccountRecordInfoImpl) then) =
__$$AccountRecordInfoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({OwnedDHTRecordPointer accountRecord});
@override
$OwnedDHTRecordPointerCopyWith<$Res> get accountRecord;
}
/// @nodoc
class __$$AccountRecordInfoImplCopyWithImpl<$Res>
extends _$AccountRecordInfoCopyWithImpl<$Res, _$AccountRecordInfoImpl>
implements _$$AccountRecordInfoImplCopyWith<$Res> {
__$$AccountRecordInfoImplCopyWithImpl(_$AccountRecordInfoImpl _value,
$Res Function(_$AccountRecordInfoImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecord = null,
}) {
return _then(_$AccountRecordInfoImpl(
accountRecord: null == accountRecord
? _value.accountRecord
: accountRecord // ignore: cast_nullable_to_non_nullable
as OwnedDHTRecordPointer,
));
}
}
/// @nodoc
@JsonSerializable()
class _$AccountRecordInfoImpl implements _AccountRecordInfo {
const _$AccountRecordInfoImpl({required this.accountRecord});
factory _$AccountRecordInfoImpl.fromJson(Map<String, dynamic> json) =>
_$$AccountRecordInfoImplFromJson(json);
// Top level account keys and secrets
@override
final OwnedDHTRecordPointer accountRecord;
@override
String toString() {
return 'AccountRecordInfo(accountRecord: $accountRecord)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AccountRecordInfoImpl &&
(identical(other.accountRecord, accountRecord) ||
other.accountRecord == accountRecord));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountRecord);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith =>
__$$AccountRecordInfoImplCopyWithImpl<_$AccountRecordInfoImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$AccountRecordInfoImplToJson(
this,
);
}
}
abstract class _AccountRecordInfo implements AccountRecordInfo {
const factory _AccountRecordInfo(
{required final OwnedDHTRecordPointer accountRecord}) =
_$AccountRecordInfoImpl;
factory _AccountRecordInfo.fromJson(Map<String, dynamic> json) =
_$AccountRecordInfoImpl.fromJson;
@override // Top level account keys and secrets
OwnedDHTRecordPointer get accountRecord;
@override
@JsonKey(ignore: true)
_$$AccountRecordInfoImplCopyWith<_$AccountRecordInfoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
Identity _$IdentityFromJson(Map<String, dynamic> json) {
return _Identity.fromJson(json);
}
/// @nodoc
mixin _$Identity {
// Top level account keys and secrets
IMap<String, ISet<AccountRecordInfo>> get accountRecords =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IdentityCopyWith<Identity> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IdentityCopyWith<$Res> {
factory $IdentityCopyWith(Identity value, $Res Function(Identity) then) =
_$IdentityCopyWithImpl<$Res, Identity>;
@useResult
$Res call({IMap<String, ISet<AccountRecordInfo>> accountRecords});
}
/// @nodoc
class _$IdentityCopyWithImpl<$Res, $Val extends Identity>
implements $IdentityCopyWith<$Res> {
_$IdentityCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecords = null,
}) {
return _then(_value.copyWith(
accountRecords: null == accountRecords
? _value.accountRecords
: accountRecords // ignore: cast_nullable_to_non_nullable
as IMap<String, ISet<AccountRecordInfo>>,
) as $Val);
}
}
/// @nodoc
abstract class _$$IdentityImplCopyWith<$Res>
implements $IdentityCopyWith<$Res> {
factory _$$IdentityImplCopyWith(
_$IdentityImpl value, $Res Function(_$IdentityImpl) then) =
__$$IdentityImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({IMap<String, ISet<AccountRecordInfo>> accountRecords});
}
/// @nodoc
class __$$IdentityImplCopyWithImpl<$Res>
extends _$IdentityCopyWithImpl<$Res, _$IdentityImpl>
implements _$$IdentityImplCopyWith<$Res> {
__$$IdentityImplCopyWithImpl(
_$IdentityImpl _value, $Res Function(_$IdentityImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? accountRecords = null,
}) {
return _then(_$IdentityImpl(
accountRecords: null == accountRecords
? _value.accountRecords
: accountRecords // ignore: cast_nullable_to_non_nullable
as IMap<String, ISet<AccountRecordInfo>>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IdentityImpl implements _Identity {
const _$IdentityImpl({required this.accountRecords});
factory _$IdentityImpl.fromJson(Map<String, dynamic> json) =>
_$$IdentityImplFromJson(json);
// Top level account keys and secrets
@override
final IMap<String, ISet<AccountRecordInfo>> accountRecords;
@override
String toString() {
return 'Identity(accountRecords: $accountRecords)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IdentityImpl &&
(identical(other.accountRecords, accountRecords) ||
other.accountRecords == accountRecords));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(runtimeType, accountRecords);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IdentityImplCopyWith<_$IdentityImpl> get copyWith =>
__$$IdentityImplCopyWithImpl<_$IdentityImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IdentityImplToJson(
this,
);
}
}
abstract class _Identity implements Identity {
const factory _Identity(
{required final IMap<String, ISet<AccountRecordInfo>>
accountRecords}) = _$IdentityImpl;
factory _Identity.fromJson(Map<String, dynamic> json) =
_$IdentityImpl.fromJson;
@override // Top level account keys and secrets
IMap<String, ISet<AccountRecordInfo>> get accountRecords;
@override
@JsonKey(ignore: true)
_$$IdentityImplCopyWith<_$IdentityImpl> get copyWith =>
throw _privateConstructorUsedError;
}
IdentityMaster _$IdentityMasterFromJson(Map<String, dynamic> json) {
return _IdentityMaster.fromJson(json);
}
/// @nodoc
mixin _$IdentityMaster {
// Private DHT record storing identity account mapping
Typed<FixedEncodedString43> get identityRecordKey =>
throw _privateConstructorUsedError; // Public key of identity
FixedEncodedString43 get identityPublicKey =>
throw _privateConstructorUsedError; // Public DHT record storing this structure for account recovery
Typed<FixedEncodedString43> get masterRecordKey =>
throw _privateConstructorUsedError; // Public key of master identity used to sign identity keys for recovery
FixedEncodedString43 get masterPublicKey =>
throw _privateConstructorUsedError; // Signature of identityRecordKey and identityPublicKey by masterPublicKey
FixedEncodedString86 get identitySignature =>
throw _privateConstructorUsedError; // Signature of masterRecordKey and masterPublicKey by identityPublicKey
FixedEncodedString86 get masterSignature =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$IdentityMasterCopyWith<IdentityMaster> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $IdentityMasterCopyWith<$Res> {
factory $IdentityMasterCopyWith(
IdentityMaster value, $Res Function(IdentityMaster) then) =
_$IdentityMasterCopyWithImpl<$Res, IdentityMaster>;
@useResult
$Res call(
{Typed<FixedEncodedString43> identityRecordKey,
FixedEncodedString43 identityPublicKey,
Typed<FixedEncodedString43> masterRecordKey,
FixedEncodedString43 masterPublicKey,
FixedEncodedString86 identitySignature,
FixedEncodedString86 masterSignature});
}
/// @nodoc
class _$IdentityMasterCopyWithImpl<$Res, $Val extends IdentityMaster>
implements $IdentityMasterCopyWith<$Res> {
_$IdentityMasterCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityRecordKey = null,
Object? identityPublicKey = null,
Object? masterRecordKey = null,
Object? masterPublicKey = null,
Object? identitySignature = null,
Object? masterSignature = null,
}) {
return _then(_value.copyWith(
identityRecordKey: null == identityRecordKey
? _value.identityRecordKey
: identityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identityPublicKey: null == identityPublicKey
? _value.identityPublicKey
: identityPublicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
masterRecordKey: null == masterRecordKey
? _value.masterRecordKey
: masterRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
masterPublicKey: null == masterPublicKey
? _value.masterPublicKey
: masterPublicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
identitySignature: null == identitySignature
? _value.identitySignature
: identitySignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
masterSignature: null == masterSignature
? _value.masterSignature
: masterSignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
) as $Val);
}
}
/// @nodoc
abstract class _$$IdentityMasterImplCopyWith<$Res>
implements $IdentityMasterCopyWith<$Res> {
factory _$$IdentityMasterImplCopyWith(_$IdentityMasterImpl value,
$Res Function(_$IdentityMasterImpl) then) =
__$$IdentityMasterImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> identityRecordKey,
FixedEncodedString43 identityPublicKey,
Typed<FixedEncodedString43> masterRecordKey,
FixedEncodedString43 masterPublicKey,
FixedEncodedString86 identitySignature,
FixedEncodedString86 masterSignature});
}
/// @nodoc
class __$$IdentityMasterImplCopyWithImpl<$Res>
extends _$IdentityMasterCopyWithImpl<$Res, _$IdentityMasterImpl>
implements _$$IdentityMasterImplCopyWith<$Res> {
__$$IdentityMasterImplCopyWithImpl(
_$IdentityMasterImpl _value, $Res Function(_$IdentityMasterImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? identityRecordKey = null,
Object? identityPublicKey = null,
Object? masterRecordKey = null,
Object? masterPublicKey = null,
Object? identitySignature = null,
Object? masterSignature = null,
}) {
return _then(_$IdentityMasterImpl(
identityRecordKey: null == identityRecordKey
? _value.identityRecordKey
: identityRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
identityPublicKey: null == identityPublicKey
? _value.identityPublicKey
: identityPublicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
masterRecordKey: null == masterRecordKey
? _value.masterRecordKey
: masterRecordKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
masterPublicKey: null == masterPublicKey
? _value.masterPublicKey
: masterPublicKey // ignore: cast_nullable_to_non_nullable
as FixedEncodedString43,
identitySignature: null == identitySignature
? _value.identitySignature
: identitySignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
masterSignature: null == masterSignature
? _value.masterSignature
: masterSignature // ignore: cast_nullable_to_non_nullable
as FixedEncodedString86,
));
}
}
/// @nodoc
@JsonSerializable()
class _$IdentityMasterImpl implements _IdentityMaster {
const _$IdentityMasterImpl(
{required this.identityRecordKey,
required this.identityPublicKey,
required this.masterRecordKey,
required this.masterPublicKey,
required this.identitySignature,
required this.masterSignature});
factory _$IdentityMasterImpl.fromJson(Map<String, dynamic> json) =>
_$$IdentityMasterImplFromJson(json);
// Private DHT record storing identity account mapping
@override
final Typed<FixedEncodedString43> identityRecordKey;
// Public key of identity
@override
final FixedEncodedString43 identityPublicKey;
// Public DHT record storing this structure for account recovery
@override
final Typed<FixedEncodedString43> masterRecordKey;
// Public key of master identity used to sign identity keys for recovery
@override
final FixedEncodedString43 masterPublicKey;
// Signature of identityRecordKey and identityPublicKey by masterPublicKey
@override
final FixedEncodedString86 identitySignature;
// Signature of masterRecordKey and masterPublicKey by identityPublicKey
@override
final FixedEncodedString86 masterSignature;
@override
String toString() {
return 'IdentityMaster(identityRecordKey: $identityRecordKey, identityPublicKey: $identityPublicKey, masterRecordKey: $masterRecordKey, masterPublicKey: $masterPublicKey, identitySignature: $identitySignature, masterSignature: $masterSignature)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$IdentityMasterImpl &&
(identical(other.identityRecordKey, identityRecordKey) ||
other.identityRecordKey == identityRecordKey) &&
(identical(other.identityPublicKey, identityPublicKey) ||
other.identityPublicKey == identityPublicKey) &&
(identical(other.masterRecordKey, masterRecordKey) ||
other.masterRecordKey == masterRecordKey) &&
(identical(other.masterPublicKey, masterPublicKey) ||
other.masterPublicKey == masterPublicKey) &&
(identical(other.identitySignature, identitySignature) ||
other.identitySignature == identitySignature) &&
(identical(other.masterSignature, masterSignature) ||
other.masterSignature == masterSignature));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
identityRecordKey,
identityPublicKey,
masterRecordKey,
masterPublicKey,
identitySignature,
masterSignature);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$IdentityMasterImplCopyWith<_$IdentityMasterImpl> get copyWith =>
__$$IdentityMasterImplCopyWithImpl<_$IdentityMasterImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$IdentityMasterImplToJson(
this,
);
}
}
abstract class _IdentityMaster implements IdentityMaster {
const factory _IdentityMaster(
{required final Typed<FixedEncodedString43> identityRecordKey,
required final FixedEncodedString43 identityPublicKey,
required final Typed<FixedEncodedString43> masterRecordKey,
required final FixedEncodedString43 masterPublicKey,
required final FixedEncodedString86 identitySignature,
required final FixedEncodedString86 masterSignature}) =
_$IdentityMasterImpl;
factory _IdentityMaster.fromJson(Map<String, dynamic> json) =
_$IdentityMasterImpl.fromJson;
@override // Private DHT record storing identity account mapping
Typed<FixedEncodedString43> get identityRecordKey;
@override // Public key of identity
FixedEncodedString43 get identityPublicKey;
@override // Public DHT record storing this structure for account recovery
Typed<FixedEncodedString43> get masterRecordKey;
@override // Public key of master identity used to sign identity keys for recovery
FixedEncodedString43 get masterPublicKey;
@override // Signature of identityRecordKey and identityPublicKey by masterPublicKey
FixedEncodedString86 get identitySignature;
@override // Signature of masterRecordKey and masterPublicKey by identityPublicKey
FixedEncodedString86 get masterSignature;
@override
@JsonKey(ignore: true)
_$$IdentityMasterImplCopyWith<_$IdentityMasterImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -1,63 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'identity.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$AccountRecordInfoImpl _$$AccountRecordInfoImplFromJson(
Map<String, dynamic> json) =>
_$AccountRecordInfoImpl(
accountRecord: OwnedDHTRecordPointer.fromJson(json['account_record']),
);
Map<String, dynamic> _$$AccountRecordInfoImplToJson(
_$AccountRecordInfoImpl instance) =>
<String, dynamic>{
'account_record': instance.accountRecord.toJson(),
};
_$IdentityImpl _$$IdentityImplFromJson(Map<String, dynamic> json) =>
_$IdentityImpl(
accountRecords: IMap<String, ISet<AccountRecordInfo>>.fromJson(
json['account_records'] as Map<String, dynamic>,
(value) => value as String,
(value) => ISet<AccountRecordInfo>.fromJson(
value, (value) => AccountRecordInfo.fromJson(value))),
);
Map<String, dynamic> _$$IdentityImplToJson(_$IdentityImpl instance) =>
<String, dynamic>{
'account_records': instance.accountRecords.toJson(
(value) => value,
(value) => value.toJson(
(value) => value.toJson(),
),
),
};
_$IdentityMasterImpl _$$IdentityMasterImplFromJson(Map<String, dynamic> json) =>
_$IdentityMasterImpl(
identityRecordKey:
Typed<FixedEncodedString43>.fromJson(json['identity_record_key']),
identityPublicKey:
FixedEncodedString43.fromJson(json['identity_public_key']),
masterRecordKey:
Typed<FixedEncodedString43>.fromJson(json['master_record_key']),
masterPublicKey: FixedEncodedString43.fromJson(json['master_public_key']),
identitySignature:
FixedEncodedString86.fromJson(json['identity_signature']),
masterSignature: FixedEncodedString86.fromJson(json['master_signature']),
);
Map<String, dynamic> _$$IdentityMasterImplToJson(
_$IdentityMasterImpl instance) =>
<String, dynamic>{
'identity_record_key': instance.identityRecordKey.toJson(),
'identity_public_key': instance.identityPublicKey.toJson(),
'master_record_key': instance.masterRecordKey.toJson(),
'master_public_key': instance.masterPublicKey.toJson(),
'identity_signature': instance.identitySignature.toJson(),
'master_signature': instance.masterSignature.toJson(),
};

View File

@ -6,8 +6,8 @@ library veilid_support;
export 'package:veilid/veilid.dart';
export 'dht_support/dht_support.dart';
export 'identity_support/identity_support.dart';
export 'src/config.dart';
export 'src/identity.dart';
export 'src/json_tools.dart';
export 'src/memory_tools.dart';
export 'src/online_element_state.dart';