identity work

This commit is contained in:
Christien Rioux 2023-07-09 00:07:21 -04:00
parent 9eff8f0cb4
commit ac58e1dea3
15 changed files with 814 additions and 147 deletions

View file

@ -0,0 +1,107 @@
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

@ -0,0 +1,25 @@
// 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,110 +0,0 @@
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class User {
final String publicKey;
final String secretKey;
const User(this.publicKey, this.secretKey);
}
/// A mock of an Authenticated User
const _dummyUser = User("", "");
/// XXXX THIS IS TOTALLY BOGUS FOR NOW
/// This notifier holds and handles the authentication state of the application
class AuthNotifier extends AutoDisposeAsyncNotifier<User?> {
late SharedPreferences sharedPreferences;
static const _sharedPrefsKey = 'token';
/// Mock of the duration of a network request
@override
FutureOr<User?> build() async {
sharedPreferences = await SharedPreferences.getInstance();
_persistenceRefreshLogic();
return await _loginRecoveryAttempt();
}
/// Tries to perform a login with the saved token on the persistant storage.
/// If _anything_ goes wrong, deletes the internal token and returns a [User.signedOut].
Future<User?> _loginRecoveryAttempt() async {
try {
final savedToken = sharedPreferences.getString(_sharedPrefsKey);
if (savedToken == null) {
throw const UnauthorizedException(
"Couldn't find the authentication token");
}
return await _loginWithToken(savedToken);
} catch (_, __) {
await sharedPreferences.remove(_sharedPrefsKey);
return null;
}
}
/// Mock of a request performed on logout (might be common, or not, whatevs).
Future<void> logout() async {
await Future.delayed(networkRoundTripTime);
state = const AsyncValue.data(null);
}
/// Mock of a successful login attempt, which results come from the network.
Future<void> login(String publicKey, String password) async {
state = await AsyncValue.guard<User?>(() async {
return Future.delayed(
networkRoundTripTime,
() => _dummyUser,
);
});
}
/// Mock of a login request performed with a saved token.
/// If such request fails, this method will throw an [UnauthorizedException].
Future<User> _loginWithToken(String token) async {
final logInAttempt = await Future.delayed(
networkRoundTripTime,
() => true,
);
if (logInAttempt) return _dummyUser;
throw const UnauthorizedException('401 Unauthorized or something');
}
/// 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);
});
}
}
final authNotifierProvider =
AutoDisposeAsyncNotifierProvider<AuthNotifier, User?>(() {
return AuthNotifier();
});
class UnauthorizedException implements Exception {
final String message;
const UnauthorizedException(this.message);
}
/// Mock of the duration of a network request
const networkRoundTripTime = Duration(milliseconds: 750);

View file

@ -0,0 +1,109 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:veilid/veilid.dart';
import '../entities/entities.dart';
import '../entities/proto.dart' as proto;
part 'local_account_manager.g.dart';
// Local account manager
class LocalAccountManager {
final VeilidTableDB _localAccountsTable;
final IList<LocalAccount> _localAccounts;
const LocalAccountManager(
{required VeilidTableDB localAccountsTable,
required IList<LocalAccount> localAccounts})
: _localAccountsTable = localAccountsTable,
_localAccounts = localAccounts;
/// Gets or creates a local account manager
static Future<LocalAccountManager> open() async {
final localAccountsTable =
await Veilid.instance.openTableDB("local_account_manager", 1);
final localAccounts =
(await localAccountsTable.loadStringJson(0, "local_accounts") ??
const IListConst([])) as IList<LocalAccount>;
return LocalAccountManager(
localAccountsTable: localAccountsTable, localAccounts: localAccounts);
}
/// Flush things to storage
Future<void> flush() async {}
/// Creates a new master identity and returns it with its secrets
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
final dhtctx = (await Veilid.instance.routingContext())
.withPrivacy()
.withSequencing(Sequencing.ensureOrdered);
final masterRec =
await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
final identityRec =
await dhtctx.createDHTRecord(const DHTSchema.dflt(oCnt: 1));
final crypto = await Veilid.instance.bestCryptoSystem();
assert(masterRec.key.kind == crypto.kind());
assert(identityRec.key.kind == crypto.kind());
// IdentityMaster
final masterRecordKey = masterRec.key;
final masterPublicKey = masterRec.owner;
final masterSecret = masterRec.ownerSecret!;
final masterSigBuf = masterRecordKey.decode()
..addAll(masterPublicKey.decode());
final identityRecordKey = identityRec.key;
final identityPublicKey = identityRec.owner;
final identitySecret = identityRec.ownerSecret!;
final identitySigBuf = identityRecordKey.decode()
..addAll(identityPublicKey.decode());
final identitySignature =
await crypto.sign(masterPublicKey, masterSecret, identitySigBuf);
final masterSignature =
await crypto.sign(identityPublicKey, identitySecret, masterSigBuf);
final identityMaster = IdentityMaster(
identityRecordKey: identityRecordKey,
identityPublicKey: identityPublicKey,
masterRecordKey: masterRecordKey,
masterPublicKey: masterPublicKey,
identitySignature: identitySignature,
masterSignature: masterSignature);
// Write identity master to master dht key
final identityMasterBytes =
Uint8List.fromList(utf8.encode(jsonEncode(identityMaster)));
await dhtctx.setDHTValue(masterRecordKey, 0, identityMasterBytes);
return IdentityMasterWithSecrets(
identityMaster: identityMaster,
masterSecret: masterSecret,
identitySecret: identitySecret);
}
/// Creates a new account associated with master identity
Future<LocalAccount> newAccount(
IdentityMaster identityMaster,
SecretKey identitySecret,
EncryptionKeyType encryptionKeyType,
String encryptionKey) async {
//
return LocalAccount(
identityMaster: identityMaster,
identitySecretKeyBytes: identitySecretBytes,
encryptionKeyType: encryptionKeyType,
biometricsEnabled: false,
hiddenAccount: false,
);
}
}
@riverpod
Future<LocalAccountManager> localAccountManager(LocalAccountManagerRef ref) {
return LocalAccountManager.open();
}

View file

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