Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-voices): keychain and session #995

Merged
merged 8 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion catalyst_voices/lib/app/view/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ final class App extends StatelessWidget {
create: (_) => Dependencies.instance.get<LoginBloc>(),
),
BlocProvider<SessionBloc>(
create: (_) => Dependencies.instance.get<SessionBloc>(),
create: (_) => Dependencies.instance.get<SessionBloc>()
..add(const RestoreSessionEvent()),
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
),
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
];
}
Expand Down
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<KeychainService>()),
)
// 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<KeyDerivationService>(KeyDerivationService.new);
registerLazySingleton<KeychainService>(
() => KeychainService(
get<KeyDerivationService>(),
get<Vault>(),
),
);
registerLazySingleton<RegistrationService>(
() => RegistrationService(
get<TransactionConfigRepository>(),
get<KeychainService>(),
get<KeyDerivationService>(),
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,103 @@
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 KeychainService _keychainService;

SessionBloc(this._keychainService) : 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 _keychainService.hasSeedPhrase) {
emit(const VisitorSessionState());
} else if (await _keychainService.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(
final nextState = switch (state) {
VisitorSessionState() => const GuestSessionState(),
GuestSessionState() => const ActiveUserSessionState(
user: User(name: 'Account'),
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
),
ActiveUserSessionState() => const VisitorSessionState(),
};

emit(nextState);
}

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

emit(const VisitorSessionState());
}

Future<void> _onGuestEvent(
GuestSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychainService.init(
seedPhrase: _dummySeedPhrase,
unlockFactor: _dummyUnlockFactor,
);
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved

emit(const GuestSessionState());
}

Future<void> _onActiveUserEvent(
ActiveUserSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychainService.init(
seedPhrase: _dummySeedPhrase,
unlockFactor: _dummyUnlockFactor,
);

await _keychainService.unlock(_dummyUnlockFactor);

emit(ActiveUserSessionState(user: _dummyUser));
}

Future<void> _onRemoveKeychainEvent(
RemoveKeychainSessionEvent event,
Emitter<SessionState> emit,
) async {
await _keychainService.remove();
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_service.dart';
export 'keychain/keychain_service.dart';
export 'registration/registration_service.dart';
export 'registration/registration_transaction_builder.dart';
export 'storage/dummy_auth_storage.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart';
import 'package:catalyst_voices_models/catalyst_voices_models.dart';
import 'package:ed25519_hd_key/ed25519_hd_key.dart';

/// Derives key pairs from a seed phrase.
class KeyDerivationService {
/// Derives an [Ed25519KeyPair] from a [seedPhrase] and [path].
///
/// Example [path]: m/0'/2147483647'
Future<Ed25519KeyPair> deriveKeyPair({
required SeedPhrase seedPhrase,
required String path,
}) async {
final masterKey = await ED25519_HD_KEY.derivePath(
path,
seedPhrase.uint8ListSeed,
);

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

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

/// Derives the [Ed25519KeyPair] for the [role] from a [seedPhrase].
Future<Ed25519KeyPair> deriveAccountRoleKeyPair({
required SeedPhrase seedPhrase,
required AccountRole role,
}) async {
return deriveKeyPair(
seedPhrase: seedPhrase,
path: _roleKeyDerivationPath(role),
);
}

/// The path feed into key derivation algorithm
/// to generate a key pair from a seed phrase.
///
// TODO(dtscalac): update when RBAC specifies it
static String _roleKeyDerivationPath(AccountRole role) {
damian-molinski marked this conversation as resolved.
Show resolved Hide resolved
return "m/${role.roleNumber}'/1234'";
}
}
Loading
Loading