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

@ -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;
}