mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2024-10-01 06:55:46 -04:00
xfer
This commit is contained in:
parent
9d8b609844
commit
bc3ed79cc2
@ -11,7 +11,7 @@ part 'user_login.g.dart';
|
||||
@freezed
|
||||
class UserLogin with _$UserLogin {
|
||||
const factory UserLogin({
|
||||
// Master public key for the user used to index the local accounts table
|
||||
// Master record key for the user used to index the local accounts table
|
||||
required TypedKey accountMasterKey,
|
||||
// The identity secret as unlocked from the local accounts table
|
||||
required TypedSecret secretKey,
|
||||
@ -30,7 +30,7 @@ class ActiveLogins with _$ActiveLogins {
|
||||
const factory ActiveLogins({
|
||||
// The list of current logged in accounts
|
||||
required IList<UserLogin> userLogins,
|
||||
// The current selected account indexed by master key
|
||||
// The current selected account indexed by master record key
|
||||
TypedKey? activeUserLogin,
|
||||
}) = _ActiveLogins;
|
||||
|
||||
|
@ -20,8 +20,8 @@ UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UserLogin {
|
||||
// Master public key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterKey =>
|
||||
// Master record key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey =>
|
||||
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> get secretKey =>
|
||||
throw _privateConstructorUsedError; // The time this login was most recently used
|
||||
@ -63,7 +63,7 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
accountMasterKey: null == accountMasterKey
|
||||
? _value.accountMasterKey
|
||||
? _value.accountMasterRecordKey
|
||||
: accountMasterKey // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
secretKey: null == secretKey
|
||||
@ -107,8 +107,8 @@ class __$$_UserLoginCopyWithImpl<$Res>
|
||||
Object? lastActive = null,
|
||||
}) {
|
||||
return _then(_$_UserLogin(
|
||||
accountMasterKey: null == accountMasterKey
|
||||
? _value.accountMasterKey
|
||||
accountMasterRecordKey: null == accountMasterKey
|
||||
? _value.accountMasterRecordKey
|
||||
: accountMasterKey // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
secretKey: null == secretKey
|
||||
@ -127,16 +127,16 @@ class __$$_UserLoginCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$_UserLogin implements _UserLogin {
|
||||
const _$_UserLogin(
|
||||
{required this.accountMasterKey,
|
||||
{required this.accountMasterRecordKey,
|
||||
required this.secretKey,
|
||||
required this.lastActive});
|
||||
|
||||
factory _$_UserLogin.fromJson(Map<String, dynamic> json) =>
|
||||
_$$_UserLoginFromJson(json);
|
||||
|
||||
// Master public key for the user used to index the local accounts table
|
||||
// Master record key for the user used to index the local accounts table
|
||||
@override
|
||||
final Typed<FixedEncodedString43> accountMasterKey;
|
||||
final Typed<FixedEncodedString43> accountMasterRecordKey;
|
||||
// The identity secret as unlocked from the local accounts table
|
||||
@override
|
||||
final Typed<FixedEncodedString43> secretKey;
|
||||
@ -146,7 +146,7 @@ class _$_UserLogin implements _UserLogin {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserLogin(accountMasterKey: $accountMasterKey, secretKey: $secretKey, lastActive: $lastActive)';
|
||||
return 'UserLogin(accountMasterKey: $accountMasterRecordKey, secretKey: $secretKey, lastActive: $lastActive)';
|
||||
}
|
||||
|
||||
@override
|
||||
@ -154,8 +154,8 @@ class _$_UserLogin implements _UserLogin {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_UserLogin &&
|
||||
(identical(other.accountMasterKey, accountMasterKey) ||
|
||||
other.accountMasterKey == accountMasterKey) &&
|
||||
(identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
|
||||
other.accountMasterRecordKey == accountMasterRecordKey) &&
|
||||
(identical(other.secretKey, secretKey) ||
|
||||
other.secretKey == secretKey) &&
|
||||
(identical(other.lastActive, lastActive) ||
|
||||
@ -165,7 +165,7 @@ class _$_UserLogin implements _UserLogin {
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, accountMasterKey, secretKey, lastActive);
|
||||
Object.hash(runtimeType, accountMasterRecordKey, secretKey, lastActive);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@ -190,8 +190,8 @@ abstract class _UserLogin implements UserLogin {
|
||||
factory _UserLogin.fromJson(Map<String, dynamic> json) =
|
||||
_$_UserLogin.fromJson;
|
||||
|
||||
@override // Master public key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterKey;
|
||||
@override // Master record key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey;
|
||||
@override // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> get secretKey;
|
||||
@override // The time this login was most recently used
|
||||
@ -210,7 +210,7 @@ ActiveLogins _$ActiveLoginsFromJson(Map<String, dynamic> json) {
|
||||
mixin _$ActiveLogins {
|
||||
// The list of current logged in accounts
|
||||
IList<UserLogin> get userLogins =>
|
||||
throw _privateConstructorUsedError; // The current selected account indexed by master key
|
||||
throw _privateConstructorUsedError; // The current selected account indexed by master record key
|
||||
Typed<FixedEncodedString43>? get activeUserLogin =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
@ -311,7 +311,7 @@ class _$_ActiveLogins implements _ActiveLogins {
|
||||
// The list of current logged in accounts
|
||||
@override
|
||||
final IList<UserLogin> userLogins;
|
||||
// The current selected account indexed by master key
|
||||
// The current selected account indexed by master record key
|
||||
@override
|
||||
final Typed<FixedEncodedString43>? activeUserLogin;
|
||||
|
||||
@ -360,7 +360,7 @@ abstract class _ActiveLogins implements ActiveLogins {
|
||||
|
||||
@override // The list of current logged in accounts
|
||||
IList<UserLogin> get userLogins;
|
||||
@override // The current selected account indexed by master key
|
||||
@override // The current selected account indexed by master record key
|
||||
Typed<FixedEncodedString43>? get activeUserLogin;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
|
@ -7,7 +7,7 @@ part of 'user_login.dart';
|
||||
// **************************************************************************
|
||||
|
||||
_$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
|
||||
accountMasterKey:
|
||||
accountMasterRecordKey:
|
||||
Typed<FixedEncodedString43>.fromJson(json['account_master_key']),
|
||||
secretKey: Typed<FixedEncodedString43>.fromJson(json['secret_key']),
|
||||
lastActive: Timestamp.fromJson(json['last_active']),
|
||||
@ -15,7 +15,7 @@ _$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
|
||||
|
||||
Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) =>
|
||||
<String, dynamic>{
|
||||
'account_master_key': instance.accountMasterKey.toJson(),
|
||||
'account_master_key': instance.accountMasterRecordKey.toJson(),
|
||||
'secret_key': instance.secretKey.toJson(),
|
||||
'last_active': instance.lastActive.toJson(),
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ import 'package:veilid/veilid.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../tools/tools.dart';
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import '../entities/entities.dart';
|
||||
import '../entities/proto.dart' as proto;
|
||||
|
||||
@ -14,31 +15,28 @@ part 'local_accounts.g.dart';
|
||||
|
||||
// Local account manager
|
||||
@riverpod
|
||||
abstract class LocalAccounts extends _$LocalAccounts {
|
||||
static const localAccountManagerTable = "local_account_manager";
|
||||
static const localAccountsKey = "local_accounts";
|
||||
class LocalAccounts extends _$LocalAccounts
|
||||
with AsyncTableDBBacked<IList<LocalAccount>> {
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// AsyncTableDBBacked
|
||||
@override
|
||||
String tableName() => "local_account_manager";
|
||||
@override
|
||||
String tableKeyName() => "local_accounts";
|
||||
@override
|
||||
IList<LocalAccount> reviveJson(Object? obj) => obj != null
|
||||
? IList<LocalAccount>.fromJson(
|
||||
obj, genericFromJson(LocalAccount.fromJson))
|
||||
: IList<LocalAccount>();
|
||||
|
||||
/// Get all local account information
|
||||
@override
|
||||
FutureOr<IList<LocalAccount>> build() async {
|
||||
// Load accounts from tabledb
|
||||
final localAccounts =
|
||||
await tableScope(localAccountManagerTable, (tdb) async {
|
||||
final localAccountsJson = await tdb.loadStringJson(0, localAccountsKey);
|
||||
return localAccountsJson != null
|
||||
? IList<LocalAccount>.fromJson(
|
||||
localAccountsJson, genericFromJson(LocalAccount.fromJson))
|
||||
: IList<LocalAccount>();
|
||||
});
|
||||
return localAccounts;
|
||||
return await load();
|
||||
}
|
||||
|
||||
/// Store things back to storage
|
||||
Future<void> flush(IList<LocalAccount> localAccounts) async {
|
||||
await tableScope(localAccountManagerTable, (tdb) async {
|
||||
await tdb.storeStringJson(0, localAccountsKey, localAccounts);
|
||||
});
|
||||
}
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
|
||||
@ -47,7 +45,11 @@ abstract class LocalAccounts extends _$LocalAccounts {
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
return (await DHTRecord.create(dhtctx)).deleteScope((masterRec) async {
|
||||
// IdentityMaster DHT record is public/unencrypted
|
||||
return (await DHTRecord.create(dhtctx,
|
||||
crypto: const DHTRecordCryptoPublic()))
|
||||
.deleteScope((masterRec) async {
|
||||
// Identity record is private
|
||||
return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async {
|
||||
// Make IdentityMaster
|
||||
final masterRecordKey = masterRec.key();
|
||||
@ -100,19 +102,31 @@ abstract class LocalAccounts extends _$LocalAccounts {
|
||||
final localAccounts = state.requireValue;
|
||||
|
||||
// Encrypt identitySecret with key
|
||||
final cs = await Veilid.instance.bestCryptoSystem();
|
||||
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
|
||||
final nonce = await cs.randomNonce();
|
||||
final eksalt = nonce.decode();
|
||||
SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt);
|
||||
final identitySecretBytes =
|
||||
await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret);
|
||||
late final Uint8List identitySecretBytes;
|
||||
late final Uint8List identitySecretSaltBytes;
|
||||
|
||||
switch (encryptionKeyType) {
|
||||
case EncryptionKeyType.none:
|
||||
identitySecretBytes = identitySecret.decode();
|
||||
identitySecretSaltBytes = Uint8List(0);
|
||||
case EncryptionKeyType.pin:
|
||||
case EncryptionKeyType.password:
|
||||
final cs = await Veilid.instance
|
||||
.getCryptoSystem(identityMaster.identityRecordKey.kind);
|
||||
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
|
||||
final nonce = await cs.randomNonce();
|
||||
identitySecretSaltBytes = nonce.decode();
|
||||
SharedSecret sharedSecret =
|
||||
await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes);
|
||||
identitySecretBytes =
|
||||
await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret);
|
||||
}
|
||||
|
||||
// Create local account object
|
||||
final localAccount = LocalAccount(
|
||||
identityMaster: identityMaster,
|
||||
identitySecretKeyBytes: identitySecretBytes,
|
||||
identitySecretSaltBytes: eksalt,
|
||||
identitySecretSaltBytes: identitySecretSaltBytes,
|
||||
encryptionKeyType: encryptionKeyType,
|
||||
biometricsEnabled: false,
|
||||
hiddenAccount: false,
|
||||
@ -126,7 +140,7 @@ abstract class LocalAccounts extends _$LocalAccounts {
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
// Open identity key for writing
|
||||
(await DHTRecord.open(dhtctx, identityMaster.identityRecordKey,
|
||||
(await DHTRecord.openWrite(dhtctx, identityMaster.identityRecordKey,
|
||||
identityMaster.identityWriter(identitySecret)))
|
||||
.scope((identityRec) async {
|
||||
// Create new account to insert into identity
|
||||
@ -135,9 +149,8 @@ abstract class LocalAccounts extends _$LocalAccounts {
|
||||
await accountRec.eventualWriteProtobuf(account);
|
||||
|
||||
// Update identity key to include account
|
||||
final newAccountRecordOwner = accountRec.ownerKeyPair()!;
|
||||
final newAccountRecordInfo = AccountRecordInfo(
|
||||
key: accountRec.key(), owner: newAccountRecordOwner);
|
||||
key: accountRec.key(), owner: accountRec.ownerKeyPair()!);
|
||||
|
||||
await identityRec.eventualUpdateJson(Identity.fromJson,
|
||||
(oldIdentity) async {
|
||||
@ -151,7 +164,7 @@ abstract class LocalAccounts extends _$LocalAccounts {
|
||||
|
||||
// Add local account object to internal store
|
||||
final newLocalAccounts = localAccounts.add(localAccount);
|
||||
await flush(newLocalAccounts);
|
||||
await store(newLocalAccounts);
|
||||
state = AsyncValue.data(newLocalAccounts);
|
||||
|
||||
// Return local account object
|
@ -6,7 +6,7 @@ part of 'local_accounts.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$localAccountsHash() => r'27e90f03be60aa9f3d534456b5b697de669a20fe';
|
||||
String _$localAccountsHash() => r'41f70b78e71c8b3faa2cd1f2a96084fbb373324c';
|
||||
|
||||
/// See also [LocalAccounts].
|
||||
@ProviderFor(LocalAccounts)
|
170
lib/providers/logins.dart
Normal file
170
lib/providers/logins.dart
Normal file
@ -0,0 +1,170 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilidchat/providers/repositories.dart';
|
||||
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import '../entities/entities.dart';
|
||||
|
||||
part 'logins.g.dart';
|
||||
|
||||
// Local account manager
|
||||
@riverpod
|
||||
class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// AsyncTableDBBacked
|
||||
@override
|
||||
String tableName() => "local_account_manager";
|
||||
@override
|
||||
String tableKeyName() => "active_logins";
|
||||
@override
|
||||
ActiveLogins reviveJson(Object? obj) => obj != null
|
||||
? ActiveLogins.fromJson(obj as Map<String, dynamic>)
|
||||
: ActiveLogins.empty();
|
||||
|
||||
/// Get all local account information
|
||||
@override
|
||||
FutureOr<ActiveLogins> build() async {
|
||||
return await load();
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
Future<void> setActiveUserLogin(TypedKey accountMasterKey) async {
|
||||
final current = state.requireValue;
|
||||
for (final userLogin in current.userLogins) {
|
||||
if (userLogin.accountMasterRecordKey == accountMasterKey) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(activeUserLogin: accountMasterKey));
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception("User not found");
|
||||
}
|
||||
|
||||
Future<bool> loginWithNone(TypedKey accountMasterRecordKey) async {
|
||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
||||
|
||||
// Get account, throws if not found
|
||||
final localAccount = localAccounts.firstWhere(
|
||||
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
||||
|
||||
// Log in with this local account
|
||||
|
||||
// Derive key from password
|
||||
if (localAccount.encryptionKeyType != EncryptionKeyType.none) {
|
||||
throw Exception("Wrong authentication type");
|
||||
}
|
||||
|
||||
final identitySecret =
|
||||
SecretKey.fromBytes(localAccount.identitySecretKeyBytes);
|
||||
|
||||
// Validate this secret with the identity public key
|
||||
final cs = await Veilid.instance
|
||||
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
|
||||
final keyOk = await cs.validateKeyPair(
|
||||
localAccount.identityMaster.identityPublicKey, identitySecret);
|
||||
if (!keyOk) {
|
||||
throw Exception("Identity is corrupted");
|
||||
}
|
||||
|
||||
// Add to user logins and select it
|
||||
final current = state.requireValue;
|
||||
final now = Veilid.instance.now();
|
||||
final updated = current.copyWith(
|
||||
userLogins: current.userLogins.replaceFirstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey,
|
||||
(ul) => ul != null
|
||||
? ul.copyWith(lastActive: now)
|
||||
: UserLogin(
|
||||
accountMasterKey: accountMasterRecordKey,
|
||||
secretKey:
|
||||
TypedSecret(kind: cs.kind(), value: identitySecret),
|
||||
lastActive: now),
|
||||
addIfNotFound: true),
|
||||
activeUserLogin: accountMasterRecordKey);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> loginWithPasswordOrPin(
|
||||
TypedKey accountMasterRecordKey, String encryptionKey) async {
|
||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
||||
|
||||
// Get account, throws if not found
|
||||
final localAccount = localAccounts.firstWhere(
|
||||
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
||||
|
||||
// Log in with this local account
|
||||
|
||||
// Derive key from password
|
||||
if (localAccount.encryptionKeyType != EncryptionKeyType.password ||
|
||||
localAccount.encryptionKeyType != EncryptionKeyType.pin) {
|
||||
throw Exception("Wrong authentication type");
|
||||
}
|
||||
final cs = await Veilid.instance
|
||||
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
|
||||
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
|
||||
final eksalt = localAccount.identitySecretSaltBytes;
|
||||
final nonce = Nonce.fromBytes(eksalt);
|
||||
SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt);
|
||||
final identitySecret = SecretKey.fromBytes(await cs.cryptNoAuth(
|
||||
localAccount.identitySecretKeyBytes, nonce, sharedSecret));
|
||||
|
||||
// Validate this secret with the identity public key
|
||||
final keyOk = await cs.validateKeyPair(
|
||||
localAccount.identityMaster.identityPublicKey, identitySecret);
|
||||
if (!keyOk) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add to user logins and select it
|
||||
final current = state.requireValue;
|
||||
final now = Veilid.instance.now();
|
||||
final updated = current.copyWith(
|
||||
userLogins: current.userLogins.replaceFirstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey,
|
||||
(ul) => ul != null
|
||||
? ul.copyWith(lastActive: now)
|
||||
: UserLogin(
|
||||
accountMasterKey: accountMasterRecordKey,
|
||||
secretKey:
|
||||
TypedSecret(kind: cs.kind(), value: identitySecret),
|
||||
lastActive: now),
|
||||
addIfNotFound: true),
|
||||
activeUserLogin: accountMasterRecordKey);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
final current = state.requireValue;
|
||||
if (current.activeUserLogin == null) {
|
||||
return;
|
||||
}
|
||||
final updated = current.copyWith(
|
||||
activeUserLogin: null,
|
||||
userLogins: current.userLogins.removeWhere(
|
||||
(ul) => ul.accountMasterRecordKey == current.activeUserLogin));
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
||||
Future<void> switchToAccount(TypedKey accountMasterRecordKey) async {
|
||||
final current = state.requireValue;
|
||||
final userLogin = current.userLogins.firstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
|
||||
final updated =
|
||||
current.copyWith(activeUserLogin: userLogin.accountMasterRecordKey);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
}
|
24
lib/providers/logins.g.dart
Normal file
24
lib/providers/logins.g.dart
Normal file
@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'logins.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$loginsHash() => r'379514b8b6623d59cb89b4f128be5953d5ea62a6';
|
||||
|
||||
/// See also [Logins].
|
||||
@ProviderFor(Logins)
|
||||
final loginsProvider =
|
||||
AutoDisposeAsyncNotifierProvider<Logins, ActiveLogins>.internal(
|
||||
Logins.new,
|
||||
name: r'loginsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$loginsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Logins = AutoDisposeAsyncNotifier<ActiveLogins>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
3
lib/providers/providers.dart
Normal file
3
lib/providers/providers.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'local_accounts.dart';
|
||||
export 'connection_state.dart';
|
||||
export 'logins.dart';
|
@ -1,3 +0,0 @@
|
||||
export 'local_accounts.dart';
|
||||
|
||||
// xxx rename this probably
|
@ -1,107 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
import '../entities/entities.dart';
|
||||
|
||||
part 'active_logins_state.g.dart';
|
||||
|
||||
@riverpod
|
||||
class ActiveLoginsState extends _$ActiveLoginsState {
|
||||
VeilidTableDB? _userLoginsTable;
|
||||
ActiveLogins _activeLogins;
|
||||
|
||||
ActiveLoginsState() : _activeLogins = ActiveLogins.empty();
|
||||
|
||||
@override
|
||||
FutureOr<ActiveLogins> build() async {
|
||||
_userLoginsTable ??= await Veilid.instance.openTableDB("login_state", 1);
|
||||
_activeLogins =
|
||||
(await _userLoginsTable!.loadStringJson(0, "active_logins") ??
|
||||
ActiveLogins.empty()) as ActiveLogins;
|
||||
_persistenceRefreshLogic();
|
||||
return _activeLogins;
|
||||
}
|
||||
|
||||
/// Log out of active user
|
||||
Future<void> logout() async {
|
||||
// If no user is active, then logout does nothing
|
||||
if (_activeLogins.activeUserLogin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove userlogin and set the active user to logged out
|
||||
final newUserLogins = _activeLogins.userLogins.removeWhere(
|
||||
(ul) => _activeLogins.activeUserLogin == ul.accountMasterKey);
|
||||
_activeLogins = _activeLogins.copyWith(
|
||||
activeUserLogin: null, userLogins: newUserLogins);
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Log all users
|
||||
Future<void> logoutAll() async {
|
||||
// If no user is active, then logout does nothing
|
||||
if (_activeLogins.activeUserLogin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all userlogins and set the active user to logged out
|
||||
_activeLogins = ActiveLogins.empty();
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Log out specific user identified by its master public key
|
||||
Future<void> logoutUser(TypedKey user) async {
|
||||
// Remove userlogin and set the active user to logged out
|
||||
final newUserLogins = _activeLogins.userLogins
|
||||
.removeWhere((ul) => user == ul.accountMasterKey);
|
||||
final newActiveUserLogin = _activeLogins.activeUserLogin == user
|
||||
? null
|
||||
: _activeLogins.activeUserLogin;
|
||||
_activeLogins = ActiveLogins(
|
||||
userLogins: newUserLogins, activeUserLogin: newActiveUserLogin);
|
||||
|
||||
// Report changed state
|
||||
state = AsyncValue.data(_activeLogins);
|
||||
}
|
||||
|
||||
/// Attempt a login and if successful make that user active
|
||||
Future<void> login(String publicKey, String password) async {
|
||||
state = await AsyncValue.guard<User?>(() async {
|
||||
return Future.delayed(
|
||||
networkRoundTripTime,
|
||||
() => _dummyUser,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Internal method used to listen authentication state changes.
|
||||
/// When the auth object is in a loading state, nothing happens.
|
||||
/// When the auth object is in a error state, we choose to remove the token
|
||||
/// Otherwise, we expect the current auth value to be reflected in our persistence API
|
||||
void _persistenceRefreshLogic() {
|
||||
ref.listenSelf((_, next) {
|
||||
if (next.isLoading) return;
|
||||
if (next.hasError) {
|
||||
sharedPreferences.remove(_sharedPrefsKey);
|
||||
return;
|
||||
}
|
||||
|
||||
final val = next.requireValue;
|
||||
final isAuthenticated = val == null;
|
||||
|
||||
isAuthenticated
|
||||
? sharedPreferences.remove(_sharedPrefsKey)
|
||||
: sharedPreferences.setString(_sharedPrefsKey, val.publicKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedException implements Exception {
|
||||
final String message;
|
||||
const UnauthorizedException(this.message);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'active_logins_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$activeLoginsStateHash() => r'9b8795055e21f15f8fbf13534365725591311cf4';
|
||||
|
||||
/// See also [ActiveLoginsState].
|
||||
@ProviderFor(ActiveLoginsState)
|
||||
final activeLoginsStateProvider =
|
||||
AutoDisposeAsyncNotifierProvider<ActiveLoginsState, ActiveLogins>.internal(
|
||||
ActiveLoginsState.new,
|
||||
name: r'activeLoginsStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$activeLoginsStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ActiveLoginsState = AutoDisposeAsyncNotifier<ActiveLogins>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
@ -1,2 +0,0 @@
|
||||
export 'connection_state.dart';
|
||||
export 'active_logins_state.dart';
|
@ -1,53 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'tools.dart';
|
||||
|
||||
typedef DHTRecordEncryptionFactory = DHTRecordEncryption Function(DHTRecord);
|
||||
|
||||
abstract class DHTRecordEncryption {
|
||||
factory DHTRecordEncryption.private(DHTRecord record) {
|
||||
return _DHTRecordEncryptionPrivate(record);
|
||||
}
|
||||
factory DHTRecordEncryption.public(DHTRecord record) {
|
||||
return _DHTRecordEncryptionPublic(record);
|
||||
}
|
||||
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey);
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey);
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
/// Private DHT Record: Encrypted with the owner's secret key
|
||||
class _DHTRecordEncryptionPrivate implements DHTRecordEncryption {
|
||||
_DHTRecordEncryptionPrivate(DHTRecord record) {
|
||||
// xxx derive key from record
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) {}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
/// Public DHT Record: No encryption
|
||||
class _DHTRecordEncryptionPublic implements DHTRecordEncryption {
|
||||
_DHTRecordEncryptionPublic(DHTRecord record) {
|
||||
//
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) {
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) {
|
||||
return data;
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
|
@ -1,7 +1,4 @@
|
||||
export 'external_stream_state.dart';
|
||||
export 'dht_record.dart';
|
||||
export 'dht_record_encryption.dart';
|
||||
export 'json_tools.dart';
|
||||
export 'phono_byte.dart';
|
||||
export 'protobuf_tools.dart';
|
||||
export 'table_db.dart';
|
||||
|
@ -1,60 +1,68 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'dart:typed_data';
|
||||
import 'tools.dart';
|
||||
|
||||
import 'veilid_support.dart';
|
||||
import '../tools/tools.dart';
|
||||
|
||||
class DHTRecord {
|
||||
final VeilidRoutingContext _dhtctx;
|
||||
final DHTRecordDescriptor _recordDescriptor;
|
||||
final int _defaultSubkey;
|
||||
final KeyPair? _writer;
|
||||
late final DHTRecordEncryption _encryption;
|
||||
DHTRecordCrypto _crypto;
|
||||
|
||||
static Future<DHTRecord> create(VeilidRoutingContext dhtctx,
|
||||
{DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
|
||||
int defaultSubkey = 0,
|
||||
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async {
|
||||
DHTRecordCrypto? crypto}) async {
|
||||
DHTRecordDescriptor recordDescriptor = await dhtctx.createDHTRecord(schema);
|
||||
|
||||
final rec = DHTRecord(
|
||||
dhtctx: dhtctx,
|
||||
recordDescriptor: recordDescriptor,
|
||||
defaultSubkey: defaultSubkey,
|
||||
writer: recordDescriptor.ownerKeyPair());
|
||||
|
||||
rec._encryption = crypto(rec);
|
||||
writer: recordDescriptor.ownerKeyPair(),
|
||||
crypto: crypto ??
|
||||
await DHTRecordCryptoPrivate.fromTypedKeyPair(
|
||||
recordDescriptor.ownerTypedKeyPair()!));
|
||||
|
||||
return rec;
|
||||
}
|
||||
|
||||
static Future<DHTRecord> openRead(
|
||||
VeilidRoutingContext dhtctx, TypedKey recordKey,
|
||||
{int defaultSubkey = 0,
|
||||
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async {
|
||||
{int defaultSubkey = 0, DHTRecordCrypto? crypto}) async {
|
||||
DHTRecordDescriptor recordDescriptor =
|
||||
await dhtctx.openDHTRecord(recordKey, null);
|
||||
final rec = DHTRecord(
|
||||
dhtctx: dhtctx,
|
||||
recordDescriptor: recordDescriptor,
|
||||
defaultSubkey: defaultSubkey,
|
||||
writer: null);
|
||||
writer: null,
|
||||
crypto: crypto ?? const DHTRecordCryptoPublic());
|
||||
|
||||
rec._encryption = crypto(rec);
|
||||
return rec;
|
||||
}
|
||||
|
||||
static Future<DHTRecord> openWrite(
|
||||
VeilidRoutingContext dhtctx, TypedKey recordKey, KeyPair writer,
|
||||
{int defaultSubkey = 0,
|
||||
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async {
|
||||
VeilidRoutingContext dhtctx,
|
||||
TypedKey recordKey,
|
||||
KeyPair writer, {
|
||||
int defaultSubkey = 0,
|
||||
DHTRecordCrypto? crypto,
|
||||
}) async {
|
||||
DHTRecordDescriptor recordDescriptor =
|
||||
await dhtctx.openDHTRecord(recordKey, writer);
|
||||
final rec = DHTRecord(
|
||||
dhtctx: dhtctx,
|
||||
recordDescriptor: recordDescriptor,
|
||||
defaultSubkey: defaultSubkey,
|
||||
writer: writer);
|
||||
rec._encryption = crypto(rec);
|
||||
writer: writer,
|
||||
crypto: crypto ??
|
||||
await DHTRecordCryptoPrivate.fromTypedKeyPair(
|
||||
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
|
||||
return rec;
|
||||
}
|
||||
|
||||
@ -62,11 +70,13 @@ class DHTRecord {
|
||||
{required VeilidRoutingContext dhtctx,
|
||||
required DHTRecordDescriptor recordDescriptor,
|
||||
int defaultSubkey = 0,
|
||||
KeyPair? writer})
|
||||
KeyPair? writer,
|
||||
DHTRecordCrypto crypto = const DHTRecordCryptoPublic()})
|
||||
: _dhtctx = dhtctx,
|
||||
_recordDescriptor = recordDescriptor,
|
||||
_defaultSubkey = defaultSubkey,
|
||||
_writer = writer;
|
||||
_writer = writer,
|
||||
_crypto = crypto;
|
||||
|
||||
int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey;
|
||||
|
||||
@ -86,6 +96,10 @@ class DHTRecord {
|
||||
return _writer;
|
||||
}
|
||||
|
||||
void setCrypto(DHTRecordCrypto crypto) {
|
||||
_crypto = crypto;
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
await _dhtctx.closeDHTRecord(_recordDescriptor.key);
|
||||
}
|
||||
@ -120,7 +134,7 @@ class DHTRecord {
|
||||
if (valueData == null) {
|
||||
return null;
|
||||
}
|
||||
return _encryption.decrypt(valueData.data, subkey);
|
||||
return _crypto.decrypt(valueData.data, subkey);
|
||||
}
|
||||
|
||||
Future<T?> getJson<T>(T Function(Map<String, dynamic>) fromJson,
|
||||
@ -134,7 +148,7 @@ class DHTRecord {
|
||||
|
||||
Future<void> eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async {
|
||||
subkey = subkeyOrDefault(subkey);
|
||||
newValue = await _encryption.encrypt(newValue, subkey);
|
||||
newValue = await _crypto.encrypt(newValue, subkey);
|
||||
// Get existing identity key
|
||||
ValueData? valueData;
|
||||
do {
|
||||
@ -165,9 +179,9 @@ class DHTRecord {
|
||||
}
|
||||
|
||||
// Update the data
|
||||
final oldData = await _encryption.decrypt(valueData.data, subkey);
|
||||
final oldData = await _crypto.decrypt(valueData.data, subkey);
|
||||
final updatedData = await update(oldData);
|
||||
final newData = await _encryption.encrypt(updatedData, subkey);
|
||||
final newData = await _crypto.encrypt(updatedData, subkey);
|
||||
|
||||
// Set it back
|
||||
valueData =
|
73
lib/veilid_support/dht_record_crypto.dart
Normal file
73
lib/veilid_support/dht_record_crypto.dart
Normal file
@ -0,0 +1,73 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
abstract class DHTRecordCrypto {
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey);
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey);
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
/// Private DHT Record: Encrypted for a specific symmetric key
|
||||
class DHTRecordCryptoPrivate implements DHTRecordCrypto {
|
||||
final VeilidCryptoSystem _cryptoSystem;
|
||||
final SharedSecret _secretKey;
|
||||
|
||||
DHTRecordCryptoPrivate._(
|
||||
VeilidCryptoSystem cryptoSystem, SharedSecret secretKey)
|
||||
: _cryptoSystem = cryptoSystem,
|
||||
_secretKey = secretKey;
|
||||
|
||||
static Future<DHTRecordCryptoPrivate> fromTypedKeyPair(
|
||||
TypedKeyPair typedKeyPair) async {
|
||||
final cryptoSystem =
|
||||
await Veilid.instance.getCryptoSystem(typedKeyPair.kind);
|
||||
final secretKey = typedKeyPair.secret;
|
||||
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
|
||||
}
|
||||
|
||||
static Future<DHTRecordCryptoPrivate> fromSecret(
|
||||
CryptoKind kind, SharedSecret secretKey) async {
|
||||
final cryptoSystem = await Veilid.instance.getCryptoSystem(kind);
|
||||
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) async {
|
||||
// generate nonce
|
||||
final nonce = await _cryptoSystem.randomNonce();
|
||||
// crypt and append nonce
|
||||
return (await _cryptoSystem.cryptNoAuth(data, nonce, _secretKey))
|
||||
..addAll(nonce.decode());
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) async {
|
||||
// split off nonce from end
|
||||
if (data.length <= Nonce.decodedLength()) {
|
||||
throw const FormatException("not enough data to decrypt");
|
||||
}
|
||||
final nonce =
|
||||
Nonce.fromBytes(data.sublist(data.length - Nonce.decodedLength()));
|
||||
final encryptedData = data.sublist(0, data.length - Nonce.decodedLength());
|
||||
// decrypt
|
||||
return await _cryptoSystem.cryptNoAuth(encryptedData, nonce, _secretKey);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////
|
||||
/// Public DHT Record: No encryption
|
||||
class DHTRecordCryptoPublic implements DHTRecordCrypto {
|
||||
const DHTRecordCryptoPublic();
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey) {
|
||||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey) {
|
||||
return data;
|
||||
}
|
||||
}
|
58
lib/veilid_support/identity_master.dart
Normal file
58
lib/veilid_support/identity_master.dart
Normal file
@ -0,0 +1,58 @@
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
|
||||
import '../entities/identity.dart';
|
||||
import 'veilid_support.dart';
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
|
||||
final crypto = await Veilid.instance.bestCryptoSystem();
|
||||
final dhtctx = (await Veilid.instance.routingContext())
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
// IdentityMaster DHT record is public/unencrypted
|
||||
return (await DHTRecord.create(dhtctx, crypto: const DHTRecordCryptoPublic()))
|
||||
.deleteScope((masterRec) async {
|
||||
// Identity record is private
|
||||
return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async {
|
||||
// Make IdentityMaster
|
||||
final masterRecordKey = masterRec.key();
|
||||
final masterOwner = masterRec.ownerKeyPair()!;
|
||||
final masterSigBuf = masterRecordKey.decode()
|
||||
..addAll(masterOwner.key.decode());
|
||||
|
||||
final identityRecordKey = identityRec.key();
|
||||
final identityOwner = identityRec.ownerKeyPair()!;
|
||||
final identitySigBuf = identityRecordKey.decode()
|
||||
..addAll(identityOwner.key.decode());
|
||||
|
||||
final identitySignature =
|
||||
await crypto.signWithKeyPair(masterOwner, identitySigBuf);
|
||||
final masterSignature =
|
||||
await crypto.signWithKeyPair(identityOwner, masterSigBuf);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
@ -3,7 +3,7 @@ import 'package:veilid/veilid.dart';
|
||||
import 'config.dart';
|
||||
import 'veilid_log.dart';
|
||||
import '../log/log.dart';
|
||||
import '../state/state.dart';
|
||||
import '../providers/providers.dart';
|
||||
|
||||
class Processor {
|
||||
String _veilidVersion = "";
|
||||
|
@ -28,3 +28,25 @@ Future<T> transactionScope<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract mixin class AsyncTableDBBacked<T> {
|
||||
String tableName();
|
||||
String tableKeyName();
|
||||
T reviveJson(Object? obj);
|
||||
|
||||
/// Load things from storage
|
||||
Future<T> load() async {
|
||||
final obj = await tableScope(tableName(), (tdb) async {
|
||||
final objJson = await tdb.loadStringJson(0, tableKeyName());
|
||||
return reviveJson(objJson);
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
/// Store things to storage
|
||||
Future<void> store(T obj) async {
|
||||
await tableScope(tableName(), (tdb) async {
|
||||
await tdb.storeStringJson(0, tableKeyName(), obj);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
import 'package:veilid/veilid_encoding.dart';
|
||||
|
||||
//xxx
|
@ -1,5 +1,8 @@
|
||||
export 'config.dart';
|
||||
export 'processor.dart';
|
||||
export 'tools.dart';
|
||||
export 'veilid_log.dart';
|
||||
export 'init.dart';
|
||||
export 'dht_record.dart';
|
||||
export 'dht_record_crypto.dart';
|
||||
export 'table_db.dart';
|
||||
export 'identity_master.dart';
|
||||
|
Loading…
Reference in New Issue
Block a user