This commit is contained in:
Christien Rioux 2023-07-21 21:25:27 -04:00
parent 9d8b609844
commit bc3ed79cc2
23 changed files with 458 additions and 275 deletions

View File

@ -11,7 +11,7 @@ part 'user_login.g.dart';
@freezed @freezed
class UserLogin with _$UserLogin { class UserLogin with _$UserLogin {
const factory 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, required TypedKey accountMasterKey,
// The identity secret as unlocked from the local accounts table // The identity secret as unlocked from the local accounts table
required TypedSecret secretKey, required TypedSecret secretKey,
@ -30,7 +30,7 @@ class ActiveLogins with _$ActiveLogins {
const factory ActiveLogins({ const factory ActiveLogins({
// The list of current logged in accounts // The list of current logged in accounts
required IList<UserLogin> userLogins, required IList<UserLogin> userLogins,
// The current selected account indexed by master key // The current selected account indexed by master record key
TypedKey? activeUserLogin, TypedKey? activeUserLogin,
}) = _ActiveLogins; }) = _ActiveLogins;

View File

@ -20,8 +20,8 @@ UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$UserLogin { mixin _$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
Typed<FixedEncodedString43> get accountMasterKey => Typed<FixedEncodedString43> get accountMasterRecordKey =>
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get secretKey => Typed<FixedEncodedString43> get secretKey =>
throw _privateConstructorUsedError; // The time this login was most recently used throw _privateConstructorUsedError; // The time this login was most recently used
@ -63,7 +63,7 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
accountMasterKey: null == accountMasterKey accountMasterKey: null == accountMasterKey
? _value.accountMasterKey ? _value.accountMasterRecordKey
: accountMasterKey // ignore: cast_nullable_to_non_nullable : accountMasterKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>, as Typed<FixedEncodedString43>,
secretKey: null == secretKey secretKey: null == secretKey
@ -107,8 +107,8 @@ class __$$_UserLoginCopyWithImpl<$Res>
Object? lastActive = null, Object? lastActive = null,
}) { }) {
return _then(_$_UserLogin( return _then(_$_UserLogin(
accountMasterKey: null == accountMasterKey accountMasterRecordKey: null == accountMasterKey
? _value.accountMasterKey ? _value.accountMasterRecordKey
: accountMasterKey // ignore: cast_nullable_to_non_nullable : accountMasterKey // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>, as Typed<FixedEncodedString43>,
secretKey: null == secretKey secretKey: null == secretKey
@ -127,16 +127,16 @@ class __$$_UserLoginCopyWithImpl<$Res>
@JsonSerializable() @JsonSerializable()
class _$_UserLogin implements _UserLogin { class _$_UserLogin implements _UserLogin {
const _$_UserLogin( const _$_UserLogin(
{required this.accountMasterKey, {required this.accountMasterRecordKey,
required this.secretKey, required this.secretKey,
required this.lastActive}); required this.lastActive});
factory _$_UserLogin.fromJson(Map<String, dynamic> json) => factory _$_UserLogin.fromJson(Map<String, dynamic> json) =>
_$$_UserLoginFromJson(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 @override
final Typed<FixedEncodedString43> accountMasterKey; final Typed<FixedEncodedString43> accountMasterRecordKey;
// The identity secret as unlocked from the local accounts table // The identity secret as unlocked from the local accounts table
@override @override
final Typed<FixedEncodedString43> secretKey; final Typed<FixedEncodedString43> secretKey;
@ -146,7 +146,7 @@ class _$_UserLogin implements _UserLogin {
@override @override
String toString() { String toString() {
return 'UserLogin(accountMasterKey: $accountMasterKey, secretKey: $secretKey, lastActive: $lastActive)'; return 'UserLogin(accountMasterKey: $accountMasterRecordKey, secretKey: $secretKey, lastActive: $lastActive)';
} }
@override @override
@ -154,8 +154,8 @@ class _$_UserLogin implements _UserLogin {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$_UserLogin && other is _$_UserLogin &&
(identical(other.accountMasterKey, accountMasterKey) || (identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
other.accountMasterKey == accountMasterKey) && other.accountMasterRecordKey == accountMasterRecordKey) &&
(identical(other.secretKey, secretKey) || (identical(other.secretKey, secretKey) ||
other.secretKey == secretKey) && other.secretKey == secretKey) &&
(identical(other.lastActive, lastActive) || (identical(other.lastActive, lastActive) ||
@ -165,7 +165,7 @@ class _$_UserLogin implements _UserLogin {
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
int get hashCode => int get hashCode =>
Object.hash(runtimeType, accountMasterKey, secretKey, lastActive); Object.hash(runtimeType, accountMasterRecordKey, secretKey, lastActive);
@JsonKey(ignore: true) @JsonKey(ignore: true)
@override @override
@ -190,8 +190,8 @@ abstract class _UserLogin implements UserLogin {
factory _UserLogin.fromJson(Map<String, dynamic> json) = factory _UserLogin.fromJson(Map<String, dynamic> json) =
_$_UserLogin.fromJson; _$_UserLogin.fromJson;
@override // Master public key for the user used to index the local accounts table @override // Master record key for the user used to index the local accounts table
Typed<FixedEncodedString43> get accountMasterKey; Typed<FixedEncodedString43> get accountMasterRecordKey;
@override // The identity secret as unlocked from the local accounts table @override // The identity secret as unlocked from the local accounts table
Typed<FixedEncodedString43> get secretKey; Typed<FixedEncodedString43> get secretKey;
@override // The time this login was most recently used @override // The time this login was most recently used
@ -210,7 +210,7 @@ ActiveLogins _$ActiveLoginsFromJson(Map<String, dynamic> json) {
mixin _$ActiveLogins { mixin _$ActiveLogins {
// The list of current logged in accounts // The list of current logged in accounts
IList<UserLogin> get userLogins => 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 => Typed<FixedEncodedString43>? get activeUserLogin =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -311,7 +311,7 @@ class _$_ActiveLogins implements _ActiveLogins {
// The list of current logged in accounts // The list of current logged in accounts
@override @override
final IList<UserLogin> userLogins; final IList<UserLogin> userLogins;
// The current selected account indexed by master key // The current selected account indexed by master record key
@override @override
final Typed<FixedEncodedString43>? activeUserLogin; final Typed<FixedEncodedString43>? activeUserLogin;
@ -360,7 +360,7 @@ abstract class _ActiveLogins implements ActiveLogins {
@override // The list of current logged in accounts @override // The list of current logged in accounts
IList<UserLogin> get userLogins; 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; Typed<FixedEncodedString43>? get activeUserLogin;
@override @override
@JsonKey(ignore: true) @JsonKey(ignore: true)

View File

@ -7,7 +7,7 @@ part of 'user_login.dart';
// ************************************************************************** // **************************************************************************
_$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin( _$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
accountMasterKey: accountMasterRecordKey:
Typed<FixedEncodedString43>.fromJson(json['account_master_key']), Typed<FixedEncodedString43>.fromJson(json['account_master_key']),
secretKey: Typed<FixedEncodedString43>.fromJson(json['secret_key']), secretKey: Typed<FixedEncodedString43>.fromJson(json['secret_key']),
lastActive: Timestamp.fromJson(json['last_active']), lastActive: Timestamp.fromJson(json['last_active']),
@ -15,7 +15,7 @@ _$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) => Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) =>
<String, dynamic>{ <String, dynamic>{
'account_master_key': instance.accountMasterKey.toJson(), 'account_master_key': instance.accountMasterRecordKey.toJson(),
'secret_key': instance.secretKey.toJson(), 'secret_key': instance.secretKey.toJson(),
'last_active': instance.lastActive.toJson(), 'last_active': instance.lastActive.toJson(),
}; };

View File

@ -7,6 +7,7 @@ import 'package:veilid/veilid.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../tools/tools.dart'; import '../tools/tools.dart';
import '../veilid_support/veilid_support.dart';
import '../entities/entities.dart'; import '../entities/entities.dart';
import '../entities/proto.dart' as proto; import '../entities/proto.dart' as proto;
@ -14,31 +15,28 @@ part 'local_accounts.g.dart';
// Local account manager // Local account manager
@riverpod @riverpod
abstract class LocalAccounts extends _$LocalAccounts { class LocalAccounts extends _$LocalAccounts
static const localAccountManagerTable = "local_account_manager"; with AsyncTableDBBacked<IList<LocalAccount>> {
static const localAccountsKey = "local_accounts"; //////////////////////////////////////////////////////////////
/// 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 /// Get all local account information
@override @override
FutureOr<IList<LocalAccount>> build() async { FutureOr<IList<LocalAccount>> build() async {
// Load accounts from tabledb return await load();
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;
} }
/// Store things back to storage //////////////////////////////////////////////////////////////
Future<void> flush(IList<LocalAccount> localAccounts) async { /// Mutators and Selectors
await tableScope(localAccountManagerTable, (tdb) async {
await tdb.storeStringJson(0, localAccountsKey, localAccounts);
});
}
/// Creates a new master identity and returns it with its secrets /// Creates a new master identity and returns it with its secrets
Future<IdentityMasterWithSecrets> newIdentityMaster() async { Future<IdentityMasterWithSecrets> newIdentityMaster() async {
@ -47,7 +45,11 @@ abstract class LocalAccounts extends _$LocalAccounts {
.withPrivacy() .withPrivacy()
.withSequencing(Sequencing.ensureOrdered); .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 { return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async {
// Make IdentityMaster // Make IdentityMaster
final masterRecordKey = masterRec.key(); final masterRecordKey = masterRec.key();
@ -100,19 +102,31 @@ abstract class LocalAccounts extends _$LocalAccounts {
final localAccounts = state.requireValue; final localAccounts = state.requireValue;
// Encrypt identitySecret with key // Encrypt identitySecret with key
final cs = await Veilid.instance.bestCryptoSystem(); 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 ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
final nonce = await cs.randomNonce(); final nonce = await cs.randomNonce();
final eksalt = nonce.decode(); identitySecretSaltBytes = nonce.decode();
SharedSecret sharedSecret = await cs.deriveSharedSecret(ekbytes, eksalt); SharedSecret sharedSecret =
final identitySecretBytes = await cs.deriveSharedSecret(ekbytes, identitySecretSaltBytes);
identitySecretBytes =
await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret); await cs.cryptNoAuth(identitySecret.decode(), nonce, sharedSecret);
}
// Create local account object // Create local account object
final localAccount = LocalAccount( final localAccount = LocalAccount(
identityMaster: identityMaster, identityMaster: identityMaster,
identitySecretKeyBytes: identitySecretBytes, identitySecretKeyBytes: identitySecretBytes,
identitySecretSaltBytes: eksalt, identitySecretSaltBytes: identitySecretSaltBytes,
encryptionKeyType: encryptionKeyType, encryptionKeyType: encryptionKeyType,
biometricsEnabled: false, biometricsEnabled: false,
hiddenAccount: false, hiddenAccount: false,
@ -126,7 +140,7 @@ abstract class LocalAccounts extends _$LocalAccounts {
.withSequencing(Sequencing.ensureOrdered); .withSequencing(Sequencing.ensureOrdered);
// Open identity key for writing // Open identity key for writing
(await DHTRecord.open(dhtctx, identityMaster.identityRecordKey, (await DHTRecord.openWrite(dhtctx, identityMaster.identityRecordKey,
identityMaster.identityWriter(identitySecret))) identityMaster.identityWriter(identitySecret)))
.scope((identityRec) async { .scope((identityRec) async {
// Create new account to insert into identity // Create new account to insert into identity
@ -135,9 +149,8 @@ abstract class LocalAccounts extends _$LocalAccounts {
await accountRec.eventualWriteProtobuf(account); await accountRec.eventualWriteProtobuf(account);
// Update identity key to include account // Update identity key to include account
final newAccountRecordOwner = accountRec.ownerKeyPair()!;
final newAccountRecordInfo = AccountRecordInfo( final newAccountRecordInfo = AccountRecordInfo(
key: accountRec.key(), owner: newAccountRecordOwner); key: accountRec.key(), owner: accountRec.ownerKeyPair()!);
await identityRec.eventualUpdateJson(Identity.fromJson, await identityRec.eventualUpdateJson(Identity.fromJson,
(oldIdentity) async { (oldIdentity) async {
@ -151,7 +164,7 @@ abstract class LocalAccounts extends _$LocalAccounts {
// Add local account object to internal store // Add local account object to internal store
final newLocalAccounts = localAccounts.add(localAccount); final newLocalAccounts = localAccounts.add(localAccount);
await flush(newLocalAccounts); await store(newLocalAccounts);
state = AsyncValue.data(newLocalAccounts); state = AsyncValue.data(newLocalAccounts);
// Return local account object // Return local account object

View File

@ -6,7 +6,7 @@ part of 'local_accounts.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$localAccountsHash() => r'27e90f03be60aa9f3d534456b5b697de669a20fe'; String _$localAccountsHash() => r'41f70b78e71c8b3faa2cd1f2a96084fbb373324c';
/// See also [LocalAccounts]. /// See also [LocalAccounts].
@ProviderFor(LocalAccounts) @ProviderFor(LocalAccounts)

170
lib/providers/logins.dart Normal file
View 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);
}
}

View 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

View File

@ -0,0 +1,3 @@
export 'local_accounts.dart';
export 'connection_state.dart';
export 'logins.dart';

View File

@ -1,3 +0,0 @@
export 'local_accounts.dart';
// xxx rename this probably

View File

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

View File

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

View File

@ -1,2 +0,0 @@
export 'connection_state.dart';
export 'active_logins_state.dart';

View File

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

View File

@ -1,4 +1,3 @@
import 'package:veilid/veilid.dart';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:convert'; import 'dart:convert';

View File

@ -1,7 +1,4 @@
export 'external_stream_state.dart'; export 'external_stream_state.dart';
export 'dht_record.dart';
export 'dht_record_encryption.dart';
export 'json_tools.dart'; export 'json_tools.dart';
export 'phono_byte.dart'; export 'phono_byte.dart';
export 'protobuf_tools.dart'; export 'protobuf_tools.dart';
export 'table_db.dart';

View File

@ -1,60 +1,68 @@
import 'dart:typed_data';
import 'package:protobuf/protobuf.dart'; import 'package:protobuf/protobuf.dart';
import 'package:veilid/veilid.dart'; import 'package:veilid/veilid.dart';
import 'dart:typed_data';
import 'tools.dart'; import 'veilid_support.dart';
import '../tools/tools.dart';
class DHTRecord { class DHTRecord {
final VeilidRoutingContext _dhtctx; final VeilidRoutingContext _dhtctx;
final DHTRecordDescriptor _recordDescriptor; final DHTRecordDescriptor _recordDescriptor;
final int _defaultSubkey; final int _defaultSubkey;
final KeyPair? _writer; final KeyPair? _writer;
late final DHTRecordEncryption _encryption; DHTRecordCrypto _crypto;
static Future<DHTRecord> create(VeilidRoutingContext dhtctx, static Future<DHTRecord> create(VeilidRoutingContext dhtctx,
{DHTSchema schema = const DHTSchema.dflt(oCnt: 1), {DHTSchema schema = const DHTSchema.dflt(oCnt: 1),
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async { DHTRecordCrypto? crypto}) async {
DHTRecordDescriptor recordDescriptor = await dhtctx.createDHTRecord(schema); DHTRecordDescriptor recordDescriptor = await dhtctx.createDHTRecord(schema);
final rec = DHTRecord( final rec = DHTRecord(
dhtctx: dhtctx, dhtctx: dhtctx,
recordDescriptor: recordDescriptor, recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
writer: recordDescriptor.ownerKeyPair()); writer: recordDescriptor.ownerKeyPair(),
crypto: crypto ??
rec._encryption = crypto(rec); await DHTRecordCryptoPrivate.fromTypedKeyPair(
recordDescriptor.ownerTypedKeyPair()!));
return rec; return rec;
} }
static Future<DHTRecord> openRead( static Future<DHTRecord> openRead(
VeilidRoutingContext dhtctx, TypedKey recordKey, VeilidRoutingContext dhtctx, TypedKey recordKey,
{int defaultSubkey = 0, {int defaultSubkey = 0, DHTRecordCrypto? crypto}) async {
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async {
DHTRecordDescriptor recordDescriptor = DHTRecordDescriptor recordDescriptor =
await dhtctx.openDHTRecord(recordKey, null); await dhtctx.openDHTRecord(recordKey, null);
final rec = DHTRecord( final rec = DHTRecord(
dhtctx: dhtctx, dhtctx: dhtctx,
recordDescriptor: recordDescriptor, recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
writer: null); writer: null,
crypto: crypto ?? const DHTRecordCryptoPublic());
rec._encryption = crypto(rec);
return rec; return rec;
} }
static Future<DHTRecord> openWrite( static Future<DHTRecord> openWrite(
VeilidRoutingContext dhtctx, TypedKey recordKey, KeyPair writer, VeilidRoutingContext dhtctx,
{int defaultSubkey = 0, TypedKey recordKey,
DHTRecordEncryptionFactory crypto = DHTRecordEncryption.private}) async { KeyPair writer, {
int defaultSubkey = 0,
DHTRecordCrypto? crypto,
}) async {
DHTRecordDescriptor recordDescriptor = DHTRecordDescriptor recordDescriptor =
await dhtctx.openDHTRecord(recordKey, writer); await dhtctx.openDHTRecord(recordKey, writer);
final rec = DHTRecord( final rec = DHTRecord(
dhtctx: dhtctx, dhtctx: dhtctx,
recordDescriptor: recordDescriptor, recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey, defaultSubkey: defaultSubkey,
writer: writer); writer: writer,
rec._encryption = crypto(rec); crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
return rec; return rec;
} }
@ -62,11 +70,13 @@ class DHTRecord {
{required VeilidRoutingContext dhtctx, {required VeilidRoutingContext dhtctx,
required DHTRecordDescriptor recordDescriptor, required DHTRecordDescriptor recordDescriptor,
int defaultSubkey = 0, int defaultSubkey = 0,
KeyPair? writer}) KeyPair? writer,
DHTRecordCrypto crypto = const DHTRecordCryptoPublic()})
: _dhtctx = dhtctx, : _dhtctx = dhtctx,
_recordDescriptor = recordDescriptor, _recordDescriptor = recordDescriptor,
_defaultSubkey = defaultSubkey, _defaultSubkey = defaultSubkey,
_writer = writer; _writer = writer,
_crypto = crypto;
int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey; int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey;
@ -86,6 +96,10 @@ class DHTRecord {
return _writer; return _writer;
} }
void setCrypto(DHTRecordCrypto crypto) {
_crypto = crypto;
}
Future<void> close() async { Future<void> close() async {
await _dhtctx.closeDHTRecord(_recordDescriptor.key); await _dhtctx.closeDHTRecord(_recordDescriptor.key);
} }
@ -120,7 +134,7 @@ class DHTRecord {
if (valueData == null) { if (valueData == null) {
return 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, 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 { Future<void> eventualWriteBytes(Uint8List newValue, {int subkey = -1}) async {
subkey = subkeyOrDefault(subkey); subkey = subkeyOrDefault(subkey);
newValue = await _encryption.encrypt(newValue, subkey); newValue = await _crypto.encrypt(newValue, subkey);
// Get existing identity key // Get existing identity key
ValueData? valueData; ValueData? valueData;
do { do {
@ -165,9 +179,9 @@ class DHTRecord {
} }
// Update the data // 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 updatedData = await update(oldData);
final newData = await _encryption.encrypt(updatedData, subkey); final newData = await _crypto.encrypt(updatedData, subkey);
// Set it back // Set it back
valueData = valueData =

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

View 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);
});
});
}

View File

@ -3,7 +3,7 @@ import 'package:veilid/veilid.dart';
import 'config.dart'; import 'config.dart';
import 'veilid_log.dart'; import 'veilid_log.dart';
import '../log/log.dart'; import '../log/log.dart';
import '../state/state.dart'; import '../providers/providers.dart';
class Processor { class Processor {
String _veilidVersion = ""; String _veilidVersion = "";

View File

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

View File

@ -1,3 +0,0 @@
import 'package:veilid/veilid_encoding.dart';
//xxx

View File

@ -1,5 +1,8 @@
export 'config.dart'; export 'config.dart';
export 'processor.dart'; export 'processor.dart';
export 'tools.dart';
export 'veilid_log.dart'; export 'veilid_log.dart';
export 'init.dart'; export 'init.dart';
export 'dht_record.dart';
export 'dht_record_crypto.dart';
export 'table_db.dart';
export 'identity_master.dart';