Skip to content

Commit

Permalink
feat(cat-voices): keychain and session (#995)
Browse files Browse the repository at this point in the history
* feat(vat-voices): keychain

* docs(cat-voices): typo

* feat: dummy states

* refactor: code review feedback

* refactor: cleanup

* fix: dummy states

* refactor: cleanup
  • Loading branch information
dtscalac authored Oct 14, 2024
1 parent f789198 commit 15f6013
Show file tree
Hide file tree
Showing 22 changed files with 467 additions and 123 deletions.
22 changes: 17 additions & 5 deletions catalyst_voices/lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,34 @@ import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

final class App extends StatelessWidget {
final class App extends StatefulWidget {
final RouterConfig<Object> routerConfig;

const App({
super.key,
required this.routerConfig,
});

@override
State<App> createState() => _AppState();
}

class _AppState extends State<App> {
/// A singleton bloc that manages the user session.
final SessionBloc _sessionBloc = Dependencies.instance.get();

@override
void initState() {
super.initState();
_sessionBloc.add(const RestoreSessionEvent());
}

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: _multiBlocProviders(),
child: AppContent(
routerConfig: routerConfig,
routerConfig: widget.routerConfig,
),
);
}
Expand All @@ -30,9 +44,7 @@ final class App extends StatelessWidget {
BlocProvider<LoginBloc>(
create: (_) => Dependencies.instance.get<LoginBloc>(),
),
BlocProvider<SessionBloc>(
create: (_) => Dependencies.instance.get<SessionBloc>(),
),
BlocProvider<SessionBloc>.value(value: _sessionBloc),
];
}
}
4 changes: 2 additions & 2 deletions catalyst_voices/lib/configs/app_bloc_observer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'package:catalyst_voices_shared/catalyst_voices_shared.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

final class AppBlocObserver extends BlocObserver {
AppBlocObserver();

final _logger = Logger('AppBlocObserver');

AppBlocObserver();

@override
void onChange(
BlocBase<dynamic> bloc,
Expand Down
14 changes: 13 additions & 1 deletion catalyst_voices/lib/dependency/dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ final class Dependencies extends DependencyProvider {
authenticationRepository: get(),
),
)
..registerLazySingleton<SessionBloc>(SessionBloc.new)
..registerLazySingleton<SessionBloc>(
() => SessionBloc(get<Keychain>()),
)
// Factory will rebuild it each time needed
..registerFactory<RegistrationCubit>(() {
return RegistrationCubit(
Expand Down Expand Up @@ -59,9 +61,19 @@ final class Dependencies extends DependencyProvider {
);
registerLazySingleton<Downloader>(Downloader.new);
registerLazySingleton<CatalystCardano>(() => CatalystCardano.instance);

registerLazySingleton<KeyDerivation>(KeyDerivation.new);
registerLazySingleton<Keychain>(
() => Keychain(
get<KeyDerivation>(),
get<Vault>(),
),
);
registerLazySingleton<RegistrationService>(
() => RegistrationService(
get<TransactionConfigRepository>(),
get<Keychain>(),
get<KeyDerivation>(),
get<CatalystCardano>(),
),
);
Expand Down
7 changes: 6 additions & 1 deletion catalyst_voices/lib/pages/account/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart';
import 'package:catalyst_voices/widgets/list/bullet_list.dart';
import 'package:catalyst_voices/widgets/modals/voices_dialog.dart';
import 'package:catalyst_voices_assets/catalyst_voices_assets.dart';
import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';

final class AccountPage extends StatelessWidget {
Expand Down Expand Up @@ -43,7 +45,10 @@ final class AccountPage extends StatelessWidget {
final confirmed =
await DeleteKeychainDialog.show(context);
if (confirmed && context.mounted) {
// TODO(Jakub): remove keychain
context
.read<SessionBloc>()
.add(const RemoveKeychainSessionEvent());

await VoicesDialog.show<void>(
context: context,
builder: (context) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,101 @@
import 'package:catalyst_voices_blocs/src/session/session_event.dart';
import 'package:catalyst_voices_blocs/src/session/session_state.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:catalyst_voices_services/catalyst_voices_services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// TODO(dtscalac): unlock session
/// Manages the user session.
final class SessionBloc extends Bloc<SessionEvent, SessionState> {
SessionBloc() : super(const VisitorSessionState()) {
on<SessionEvent>(_handleSessionEvent);
final Keychain _keychain;

SessionBloc(this._keychain) : super(const VisitorSessionState()) {
on<RestoreSessionEvent>(_onRestoreSessionEvent);
on<NextStateSessionEvent>(_onNextStateEvent);
on<VisitorSessionEvent>(_onVisitorEvent);
on<GuestSessionEvent>(_onGuestEvent);
on<ActiveUserSessionEvent>(_onActiveUserEvent);
on<RemoveKeychainSessionEvent>(_onRemoveKeychainEvent);
}

Future<void> _onRestoreSessionEvent(
RestoreSessionEvent event,
Emitter<SessionState> emit,
) async {
if (!await _keychain.hasSeedPhrase) {
emit(const VisitorSessionState());
} else if (await _keychain.isUnlocked) {
emit(ActiveUserSessionState(user: _dummyUser));
} else {
emit(const GuestSessionState());
}
}

void _handleSessionEvent(
SessionEvent event,
void _onNextStateEvent(
NextStateSessionEvent event,
Emitter<SessionState> emit,
) {
final nextState = switch (event) {
NextStateSessionEvent() => switch (state) {
VisitorSessionState() => const GuestSessionState(),
GuestSessionState() => const ActiveUserSessionState(
user: User(name: 'Account'),
),
ActiveUserSessionState() => const VisitorSessionState(),
},
VisitorSessionEvent() => const VisitorSessionState(),
GuestSessionEvent() => const GuestSessionState(),
ActiveUserSessionEvent() => const ActiveUserSessionState(
user: User(name: 'Account'),
),
final nextState = switch (state) {
VisitorSessionState() => const GuestSessionState(),
GuestSessionState() => ActiveUserSessionState(user: _dummyUser),
ActiveUserSessionState() => const VisitorSessionState(),
};

emit(nextState);
}

Future<void> _onVisitorEvent(
VisitorSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychain.clearAndLock();

emit(const VisitorSessionState());
}

Future<void> _onGuestEvent(
GuestSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychain.setLockAndBeginWith(
seedPhrase: _dummySeedPhrase,
unlockFactor: _dummyUnlockFactor,
unlocked: false,
);

emit(const GuestSessionState());
}

Future<void> _onActiveUserEvent(
ActiveUserSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychain.setLockAndBeginWith(
seedPhrase: _dummySeedPhrase,
unlockFactor: _dummyUnlockFactor,
unlocked: true,
);

emit(ActiveUserSessionState(user: _dummyUser));
}

Future<void> _onRemoveKeychainEvent(
RemoveKeychainSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychain.clearAndLock();
emit(const VisitorSessionState());
}

/// Temporary implementation for testing purposes.
User get _dummyUser => const User(name: 'Account');

/// Temporary implementation for testing purposes.
SeedPhrase get _dummySeedPhrase => SeedPhrase.fromMnemonic(
'few loyal swift champion rug peace dinosaur'
' erase bacon tone install universe',
);

/// Temporary implementation for testing purposes.
LockFactor get _dummyUnlockFactor => const PasswordLockFactor('Test1234');
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ sealed class SessionEvent extends Equatable {
const SessionEvent();
}

/// An event to restore the last session after the app is restarted.
final class RestoreSessionEvent extends SessionEvent {
const RestoreSessionEvent();

@override
List<Object?> get props => [];
}

/// Dummy implementation of session management,
/// just toggles the next session state or reset to the initial one.
final class NextStateSessionEvent extends SessionEvent {
Expand Down Expand Up @@ -37,3 +45,11 @@ final class ActiveUserSessionEvent extends SessionEvent {
@override
List<Object?> get props => [];
}

/// An event which triggers keychain deletion.
final class RemoveKeychainSessionEvent extends SessionEvent {
const RemoveKeychainSessionEvent();

@override
List<Object?> get props => [];
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
enum AccountRole {
voter,
proposer,
drep,
voter(roleNumber: 0),

// TODO(dtscalac): the RBAC specification doesn't define yet the role number
// for the proposer, replace this arbitrary number when it's specified.
proposer(roleNumber: 1),

// TODO(dtscalac): the RBAC specification doesn't define yet the role number
// for the drep, replace this arbitrary number when it's specified.
drep(roleNumber: 2);

/// The RBAC specified role number.
final int roleNumber;

const AccountRole({required this.roleNumber});

/// Returns the role which is assigned to every user.
static AccountRole get root => voter;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import 'dart:typed_data';

import 'package:bip39/bip39.dart' as bip39;
import 'package:bip39/src/wordlists/english.dart';
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:ed25519_hd_key/ed25519_hd_key.dart';
import 'package:equatable/equatable.dart';

/// Represent singular mnemonic words which keeps data as well as index of
Expand Down Expand Up @@ -138,27 +136,6 @@ final class SeedPhrase extends Equatable {
return [...mnemonicWords]..shuffle();
}

/// Derives an Ed25519 key pair from a seed.
///
/// Throws a [RangeError] If the provided [offset] is negative or exceeds
/// the length of the seed (64).
///
/// [offset]: The offset is applied
/// to the seed to adjust where key derivation starts. It defaults to 0.
Future<Ed25519KeyPair> deriveKeyPair([int offset = 0]) async {
final modifiedSeed = uint8ListSeed.sublist(offset);

final masterKey = await ED25519_HD_KEY.getMasterKeyFromSeed(modifiedSeed);
final privateKey = masterKey.key;

final publicKey = await ED25519_HD_KEY.getPublicKey(privateKey, false);

return Ed25519KeyPair(
publicKey: Ed25519PublicKey.fromBytes(publicKey),
privateKey: Ed25519PrivateKey.fromBytes(privateKey),
);
}

@override
String toString() => 'SeedPhrase(${mnemonic.hashCode})';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ dependencies:
catalyst_cardano_web: ^0.3.0
collection: ^1.18.0
convert: ^3.1.1
ed25519_hd_key: ^2.3.0
equatable: ^2.0.5
meta: ^1.10.0
password_strength: ^0.2.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,6 @@ void main() {
expect(words, expectedWords);
});

test('should generate key pair with different valid offsets', () async {
for (final offset in [0, 4, 28, 32, 64]) {
final keyPair = await SeedPhrase().deriveKeyPair(offset);

expect(keyPair, isNotNull);
}
});

test('should throw an error for key pair with out of range offset',
() async {
for (final offset in [-1, 65]) {
expect(
() async => SeedPhrase().deriveKeyPair(offset),
throwsA(isA<RangeError>()),
);
}
});

test('toString should return hashed mnemonic', () {
final seedPhrase = SeedPhrase();
final mnemonic = seedPhrase.mnemonic;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export 'downloader/downloader.dart';
export 'keychain/key_derivation.dart';
export 'keychain/keychain.dart';
export 'registration/registration_service.dart';
export 'registration/registration_transaction_builder.dart';
export 'storage/dummy_auth_storage.dart';
Expand Down
Loading

0 comments on commit 15f6013

Please sign in to comment.