This commit is contained in:
Dain Nilsson 2022-02-08 15:14:21 +01:00
commit af652b3609
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
13 changed files with 297 additions and 125 deletions

View File

@ -12,12 +12,26 @@ class YubiKeyData with _$YubiKeyData {
_YubiKeyData;
}
const _listEquality = ListEquality();
class DevicePath {
final List<String> segments;
DevicePath(List<String> path) : segments = List.unmodifiable(path);
@override
bool operator ==(Object other) =>
other is DevicePath && _listEquality.equals(segments, other.segments);
@override
int get hashCode => Object.hashAll(segments);
}
@freezed
class DeviceNode with _$DeviceNode {
factory DeviceNode.usbYubiKey(
List<String> path, String name, int pid, DeviceInfo info) =
UsbYubiKeyNode;
factory DeviceNode.nfcReader(List<String> path, String name) = NfcReaderNode;
DevicePath path, String name, int pid, DeviceInfo info) = UsbYubiKeyNode;
factory DeviceNode.nfcReader(DevicePath path, String name) = NfcReaderNode;
}
@freezed

View File

@ -206,7 +206,7 @@ class _$DeviceNodeTearOff {
const _$DeviceNodeTearOff();
UsbYubiKeyNode usbYubiKey(
List<String> path, String name, int pid, DeviceInfo info) {
DevicePath path, String name, int pid, DeviceInfo info) {
return UsbYubiKeyNode(
path,
name,
@ -215,7 +215,7 @@ class _$DeviceNodeTearOff {
);
}
NfcReaderNode nfcReader(List<String> path, String name) {
NfcReaderNode nfcReader(DevicePath path, String name) {
return NfcReaderNode(
path,
name,
@ -228,29 +228,29 @@ const $DeviceNode = _$DeviceNodeTearOff();
/// @nodoc
mixin _$DeviceNode {
List<String> get path => throw _privateConstructorUsedError;
DevicePath get path => throw _privateConstructorUsedError;
String get name => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
List<String> path, String name, int pid, DeviceInfo info)
DevicePath path, String name, int pid, DeviceInfo info)
usbYubiKey,
required TResult Function(List<String> path, String name) nfcReader,
required TResult Function(DevicePath path, String name) nfcReader,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@ -284,7 +284,7 @@ abstract class $DeviceNodeCopyWith<$Res> {
factory $DeviceNodeCopyWith(
DeviceNode value, $Res Function(DeviceNode) then) =
_$DeviceNodeCopyWithImpl<$Res>;
$Res call({List<String> path, String name});
$Res call({DevicePath path, String name});
}
/// @nodoc
@ -304,7 +304,7 @@ class _$DeviceNodeCopyWithImpl<$Res> implements $DeviceNodeCopyWith<$Res> {
path: path == freezed
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as List<String>,
as DevicePath,
name: name == freezed
? _value.name
: name // ignore: cast_nullable_to_non_nullable
@ -320,7 +320,7 @@ abstract class $UsbYubiKeyNodeCopyWith<$Res>
UsbYubiKeyNode value, $Res Function(UsbYubiKeyNode) then) =
_$UsbYubiKeyNodeCopyWithImpl<$Res>;
@override
$Res call({List<String> path, String name, int pid, DeviceInfo info});
$Res call({DevicePath path, String name, int pid, DeviceInfo info});
$DeviceInfoCopyWith<$Res> get info;
}
@ -346,7 +346,7 @@ class _$UsbYubiKeyNodeCopyWithImpl<$Res> extends _$DeviceNodeCopyWithImpl<$Res>
path == freezed
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as List<String>,
as DevicePath,
name == freezed
? _value.name
: name // ignore: cast_nullable_to_non_nullable
@ -376,7 +376,7 @@ class _$UsbYubiKeyNode implements UsbYubiKeyNode {
_$UsbYubiKeyNode(this.path, this.name, this.pid, this.info);
@override
final List<String> path;
final DevicePath path;
@override
final String name;
@override
@ -417,9 +417,9 @@ class _$UsbYubiKeyNode implements UsbYubiKeyNode {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
List<String> path, String name, int pid, DeviceInfo info)
DevicePath path, String name, int pid, DeviceInfo info)
usbYubiKey,
required TResult Function(List<String> path, String name) nfcReader,
required TResult Function(DevicePath path, String name) nfcReader,
}) {
return usbYubiKey(path, name, pid, info);
}
@ -427,9 +427,9 @@ class _$UsbYubiKeyNode implements UsbYubiKeyNode {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
}) {
return usbYubiKey?.call(path, name, pid, info);
}
@ -437,9 +437,9 @@ class _$UsbYubiKeyNode implements UsbYubiKeyNode {
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
required TResult orElse(),
}) {
if (usbYubiKey != null) {
@ -482,11 +482,11 @@ class _$UsbYubiKeyNode implements UsbYubiKeyNode {
abstract class UsbYubiKeyNode implements DeviceNode {
factory UsbYubiKeyNode(
List<String> path, String name, int pid, DeviceInfo info) =
DevicePath path, String name, int pid, DeviceInfo info) =
_$UsbYubiKeyNode;
@override
List<String> get path;
DevicePath get path;
@override
String get name;
int get pid;
@ -504,7 +504,7 @@ abstract class $NfcReaderNodeCopyWith<$Res>
NfcReaderNode value, $Res Function(NfcReaderNode) then) =
_$NfcReaderNodeCopyWithImpl<$Res>;
@override
$Res call({List<String> path, String name});
$Res call({DevicePath path, String name});
}
/// @nodoc
@ -526,7 +526,7 @@ class _$NfcReaderNodeCopyWithImpl<$Res> extends _$DeviceNodeCopyWithImpl<$Res>
path == freezed
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as List<String>,
as DevicePath,
name == freezed
? _value.name
: name // ignore: cast_nullable_to_non_nullable
@ -541,7 +541,7 @@ class _$NfcReaderNode implements NfcReaderNode {
_$NfcReaderNode(this.path, this.name);
@override
final List<String> path;
final DevicePath path;
@override
final String name;
@ -574,9 +574,9 @@ class _$NfcReaderNode implements NfcReaderNode {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(
List<String> path, String name, int pid, DeviceInfo info)
DevicePath path, String name, int pid, DeviceInfo info)
usbYubiKey,
required TResult Function(List<String> path, String name) nfcReader,
required TResult Function(DevicePath path, String name) nfcReader,
}) {
return nfcReader(path, name);
}
@ -584,9 +584,9 @@ class _$NfcReaderNode implements NfcReaderNode {
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
}) {
return nfcReader?.call(path, name);
}
@ -594,9 +594,9 @@ class _$NfcReaderNode implements NfcReaderNode {
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(List<String> path, String name, int pid, DeviceInfo info)?
TResult Function(DevicePath path, String name, int pid, DeviceInfo info)?
usbYubiKey,
TResult Function(List<String> path, String name)? nfcReader,
TResult Function(DevicePath path, String name)? nfcReader,
required TResult orElse(),
}) {
if (nfcReader != null) {
@ -638,10 +638,10 @@ class _$NfcReaderNode implements NfcReaderNode {
}
abstract class NfcReaderNode implements DeviceNode {
factory NfcReaderNode(List<String> path, String name) = _$NfcReaderNode;
factory NfcReaderNode(DevicePath path, String name) = _$NfcReaderNode;
@override
List<String> get path;
DevicePath get path;
@override
String get name;
@override

View File

@ -1,14 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/management/models.dart';
import 'package:collection/collection.dart';
import '../models.dart';
import '../state.dart';
import 'device_avatar.dart';
Function _listEquals = const ListEquality().equals;
class MainActionsDialog extends ConsumerWidget {
const MainActionsDialog({Key? key}) : super(key: key);
@ -20,7 +17,7 @@ class MainActionsDialog extends ConsumerWidget {
final actions = ref.watch(menuActionsProvider)(context);
if (currentNode != null) {
devices.removeWhere((e) => _listEquals(e.path, currentNode.path));
devices.removeWhere((e) => e.path == currentNode.path);
}
return SimpleDialog(

View File

@ -67,7 +67,7 @@ class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
var deviceResult = await _rpc.command('get', path);
var deviceData = deviceResult['data'];
usbDevices.add(DeviceNode.usbYubiKey(
path,
DevicePath(path),
deviceData['name'],
deviceData['pid'],
DeviceInfo.fromJson(deviceData['info']),
@ -131,8 +131,8 @@ class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
log.info('NFC state change', jsonEncode(children));
_nfcState = newState;
state = children.entries
.map((e) =>
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
.map((e) => DeviceNode.nfcReader(
DevicePath(['nfc', e.key]), e.value['name'] as String)
as NfcReaderNode)
.toList();
}
@ -207,7 +207,7 @@ class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
_pollTimer?.cancel();
final node = _deviceNode!;
try {
var result = await _rpc.command('get', node.path);
var result = await _rpc.command('get', node.path.segments);
if (mounted) {
if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'],

View File

@ -15,13 +15,35 @@ import '../state.dart';
final log = Logger('desktop.oath.state');
final _sessionProvider =
Provider.autoDispose.family<RpcNodeSession, List<String>>(
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
(ref, devicePath) =>
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['ccid', 'oath']),
);
// This remembers the key for all devices for the duration of the process.
final _oathLockKeyProvider =
StateNotifierProvider.family<_LockKeyNotifier, String?, DevicePath>(
(ref, devicePath) => _LockKeyNotifier(null));
class _LockKeyNotifier extends StateNotifier<String?> {
_LockKeyNotifier(String? state) : super(state);
setKey(String key) {
state = key;
}
unsetKey() {
state = null;
}
@override
void dispose() {
super.dispose();
}
}
final desktopOathState = StateNotifierProvider.autoDispose
.family<OathStateNotifier, OathState?, List<String>>(
.family<OathStateNotifier, OathState?, DevicePath>(
(ref, devicePath) {
final session = ref.watch(_sessionProvider(devicePath));
final notifier = _DesktopOathStateNotifier(session, ref);
@ -50,13 +72,15 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
var result = await _session.command('get');
log.config('application status', jsonEncode(result));
var oathState = OathState.fromJson(result['data']);
final key = _ref.read(oathLockKeyProvider(_session.devicePath));
final key = _ref.read(_oathLockKeyProvider(_session.devicePath));
if (oathState.locked && key != null) {
final result = await _session.command('validate', params: {'key': key});
if (result['unlocked']) {
if (result['success']) {
oathState = oathState.copyWith(locked: false);
} else {
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref
.read(_oathLockKeyProvider(_session.devicePath).notifier)
.unsetKey();
}
}
if (mounted) {
@ -67,28 +91,32 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
@override
Future<void> reset() async {
await _session.command('reset');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.refresh(_sessionProvider(_session.devicePath));
}
@override
Future<bool> unlock(String password) async {
Future<bool> unlock(String password, {bool remember = false}) async {
var result =
await _session.command('derive', params: {'password': password});
var key = result['key'];
final status = await _session.command('validate', params: {'key': key});
if (mounted && status['unlocked']) {
final status = await _session
.command('validate', params: {'key': key, 'remember': remember});
if (mounted && status['success']) {
log.config('applet unlocked');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
state = state?.copyWith(locked: false);
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
state = state?.copyWith(
locked: false,
remembered: remember || state?.remembered == true,
);
}
return status['unlocked'];
return status['success'];
}
Future<bool> _checkPassword(String password) async {
var result =
await _session.command('derive', params: {'password': password});
return _ref.read(oathLockKeyProvider(_session.devicePath)) == result['key'];
await _session.command('validate', params: {'password': password});
return result['success'];
}
@override
@ -108,7 +136,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
var key = result['key'];
await _session.command('set_key', params: {'key': key});
log.config('OATH key set');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
if (mounted) {
state = state?.copyWith(hasKey: true);
}
@ -123,16 +151,25 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
}
}
await _session.command('unset_key');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
if (mounted) {
state = state?.copyWith(hasKey: false, locked: false);
}
return true;
}
@override
Future<void> forgetPassword() async {
await _session.command('forget');
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
if (mounted) {
state = state?.copyWith(remembered: false);
}
}
}
final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
.family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
(ref, devicePath) {
var notifier = _DesktopCredentialListNotifier(
ref.watch(_sessionProvider(devicePath)),

View File

@ -5,6 +5,7 @@ import 'dart:io';
import 'package:logging/logging.dart';
import 'package:async/async.dart';
import '../app/models.dart';
import 'models.dart';
final log = Logger('rpc');
@ -147,7 +148,7 @@ typedef ErrorHandler = Future<void> Function(RpcError e);
class RpcNodeSession {
final RpcSession _rpc;
final List<String> devicePath;
final DevicePath devicePath;
final List<String> subPath;
final Map<String, ErrorHandler> _errorHandlers = {};
@ -170,7 +171,7 @@ class RpcNodeSession {
try {
return await _rpc.command(
action,
devicePath + subPath + target,
devicePath.segments + subPath + target,
params: params,
signal: signal,
);

View File

@ -55,7 +55,12 @@ class OathPair with _$OathPair {
@freezed
class OathState with _$OathState {
factory OathState(String deviceId, bool hasKey, bool locked) = _OathState;
factory OathState(
String deviceId, {
required bool hasKey,
required bool remembered,
required bool locked,
}) = _OathState;
factory OathState.fromJson(Map<String, dynamic> json) =>
_$OathStateFromJson(json);

View File

@ -652,11 +652,13 @@ OathState _$OathStateFromJson(Map<String, dynamic> json) {
class _$OathStateTearOff {
const _$OathStateTearOff();
_OathState call(String deviceId, bool hasKey, bool locked) {
_OathState call(String deviceId,
{required bool hasKey, required bool remembered, required bool locked}) {
return _OathState(
deviceId,
hasKey,
locked,
hasKey: hasKey,
remembered: remembered,
locked: locked,
);
}
@ -672,6 +674,7 @@ const $OathState = _$OathStateTearOff();
mixin _$OathState {
String get deviceId => throw _privateConstructorUsedError;
bool get hasKey => throw _privateConstructorUsedError;
bool get remembered => throw _privateConstructorUsedError;
bool get locked => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -684,7 +687,7 @@ mixin _$OathState {
abstract class $OathStateCopyWith<$Res> {
factory $OathStateCopyWith(OathState value, $Res Function(OathState) then) =
_$OathStateCopyWithImpl<$Res>;
$Res call({String deviceId, bool hasKey, bool locked});
$Res call({String deviceId, bool hasKey, bool remembered, bool locked});
}
/// @nodoc
@ -699,6 +702,7 @@ class _$OathStateCopyWithImpl<$Res> implements $OathStateCopyWith<$Res> {
$Res call({
Object? deviceId = freezed,
Object? hasKey = freezed,
Object? remembered = freezed,
Object? locked = freezed,
}) {
return _then(_value.copyWith(
@ -710,6 +714,10 @@ class _$OathStateCopyWithImpl<$Res> implements $OathStateCopyWith<$Res> {
? _value.hasKey
: hasKey // ignore: cast_nullable_to_non_nullable
as bool,
remembered: remembered == freezed
? _value.remembered
: remembered // ignore: cast_nullable_to_non_nullable
as bool,
locked: locked == freezed
? _value.locked
: locked // ignore: cast_nullable_to_non_nullable
@ -724,7 +732,7 @@ abstract class _$OathStateCopyWith<$Res> implements $OathStateCopyWith<$Res> {
_OathState value, $Res Function(_OathState) then) =
__$OathStateCopyWithImpl<$Res>;
@override
$Res call({String deviceId, bool hasKey, bool locked});
$Res call({String deviceId, bool hasKey, bool remembered, bool locked});
}
/// @nodoc
@ -740,6 +748,7 @@ class __$OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
$Res call({
Object? deviceId = freezed,
Object? hasKey = freezed,
Object? remembered = freezed,
Object? locked = freezed,
}) {
return _then(_OathState(
@ -747,11 +756,15 @@ class __$OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
? _value.deviceId
: deviceId // ignore: cast_nullable_to_non_nullable
as String,
hasKey == freezed
hasKey: hasKey == freezed
? _value.hasKey
: hasKey // ignore: cast_nullable_to_non_nullable
as bool,
locked == freezed
remembered: remembered == freezed
? _value.remembered
: remembered // ignore: cast_nullable_to_non_nullable
as bool,
locked: locked == freezed
? _value.locked
: locked // ignore: cast_nullable_to_non_nullable
as bool,
@ -762,7 +775,8 @@ class __$OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
/// @nodoc
@JsonSerializable()
class _$_OathState implements _OathState {
_$_OathState(this.deviceId, this.hasKey, this.locked);
_$_OathState(this.deviceId,
{required this.hasKey, required this.remembered, required this.locked});
factory _$_OathState.fromJson(Map<String, dynamic> json) =>
_$$_OathStateFromJson(json);
@ -772,11 +786,13 @@ class _$_OathState implements _OathState {
@override
final bool hasKey;
@override
final bool remembered;
@override
final bool locked;
@override
String toString() {
return 'OathState(deviceId: $deviceId, hasKey: $hasKey, locked: $locked)';
return 'OathState(deviceId: $deviceId, hasKey: $hasKey, remembered: $remembered, locked: $locked)';
}
@override
@ -786,6 +802,8 @@ class _$_OathState implements _OathState {
other is _OathState &&
const DeepCollectionEquality().equals(other.deviceId, deviceId) &&
const DeepCollectionEquality().equals(other.hasKey, hasKey) &&
const DeepCollectionEquality()
.equals(other.remembered, remembered) &&
const DeepCollectionEquality().equals(other.locked, locked));
}
@ -794,6 +812,7 @@ class _$_OathState implements _OathState {
runtimeType,
const DeepCollectionEquality().hash(deviceId),
const DeepCollectionEquality().hash(hasKey),
const DeepCollectionEquality().hash(remembered),
const DeepCollectionEquality().hash(locked));
@JsonKey(ignore: true)
@ -808,7 +827,10 @@ class _$_OathState implements _OathState {
}
abstract class _OathState implements OathState {
factory _OathState(String deviceId, bool hasKey, bool locked) = _$_OathState;
factory _OathState(String deviceId,
{required bool hasKey,
required bool remembered,
required bool locked}) = _$_OathState;
factory _OathState.fromJson(Map<String, dynamic> json) =
_$_OathState.fromJson;
@ -818,6 +840,8 @@ abstract class _OathState implements OathState {
@override
bool get hasKey;
@override
bool get remembered;
@override
bool get locked;
@override
@JsonKey(ignore: true)

View File

@ -48,14 +48,16 @@ Map<String, dynamic> _$$_OathCodeToJson(_$_OathCode instance) =>
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
json['device_id'] as String,
json['has_key'] as bool,
json['locked'] as bool,
hasKey: json['has_key'] as bool,
remembered: json['remembered'] as bool,
locked: json['locked'] as bool,
);
Map<String, dynamic> _$$_OathStateToJson(_$_OathState instance) =>
<String, dynamic>{
'device_id': instance.deviceId,
'has_key': instance.hasKey,
'remembered': instance.remembered,
'locked': instance.locked,
};

View File

@ -5,31 +5,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart';
import '../app/models.dart';
import '../app/state.dart';
import '../core/state.dart';
import 'models.dart';
final log = Logger('oath.state');
// This remembers the key for all devices for the duration of the process.
final oathLockKeyProvider =
StateNotifierProvider.family<_LockKeyNotifier, String?, List<String>>(
(ref, devicePath) => _LockKeyNotifier(null));
class _LockKeyNotifier extends StateNotifier<String?> {
_LockKeyNotifier(String? state) : super(state);
setKey(String key) {
state = key;
}
unsetKey() {
state = null;
}
}
final oathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, OathState?, List<String>>(
.family<OathStateNotifier, OathState?, DevicePath>(
(ref, devicePath) => throw UnimplementedError(),
);
@ -37,13 +21,14 @@ abstract class OathStateNotifier extends StateNotifier<OathState?> {
OathStateNotifier() : super(null);
Future<void> reset();
Future<bool> unlock(String password);
Future<bool> unlock(String password, {bool remember = false});
Future<bool> setPassword(String? current, String password);
Future<bool> unsetPassword(String current);
Future<void> forgetPassword();
}
final credentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
.family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
(ref, arg) => throw UnimplementedError(),
);

View File

@ -23,20 +23,13 @@ class OathScreen extends ConsumerWidget {
}
if (state.locked) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
return ListView(
children: [
const Text('Password required'),
TextField(
autofocus: true,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
onSubmitted: (value) async {
_UnlockForm(
onSubmit: (password, remember) async {
final result = await ref
.read(oathStateProvider(deviceData.node.path).notifier)
.unlock(value);
.unlock(password, remember: remember);
if (!result) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@ -48,7 +41,6 @@ class OathScreen extends ConsumerWidget {
},
),
],
),
);
} else {
final accounts = ref.watch(credentialListProvider(deviceData.node.path));
@ -68,3 +60,77 @@ class OathScreen extends ConsumerWidget {
}
}
}
class _UnlockForm extends StatefulWidget {
final Function(String, bool) onSubmit;
const _UnlockForm({Key? key, required this.onSubmit}) : super(key: key);
@override
State<StatefulWidget> createState() => _UnlockFormState();
}
class _UnlockFormState extends State<_UnlockForm> {
String _password = '';
bool _remember = false;
@override
Widget build(BuildContext context) {
return Column(
//mainAxisAlignment: MainAxisAlignment.center,
//crossAxisAlignment: CrossAxisAlignment.end,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 24.0),
child: Text(
'Unlock YubiKey',
style: Theme.of(context).textTheme.headline5,
),
),
const Text(
'Enter the password for your YubiKey. If you don\'t know your password, you\'ll need to reset the YubiKey.',
),
TextField(
autofocus: true,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
onChanged: (value) {
setState(() {
_password = value;
});
},
onSubmitted: (value) {
widget.onSubmit(value, _remember);
},
),
],
),
),
CheckboxListTile(
title: const Text('Remember password'),
controlAffinity: ListTileControlAffinity.leading,
value: _remember,
onChanged: (value) {
setState(() {
_remember = value ?? false;
});
},
),
Container(
padding: const EdgeInsets.all(16.0),
alignment: Alignment.centerRight,
child: ElevatedButton(
child: const Text('Unlock'),
onPressed: () {
widget.onSubmit(_password, _remember);
},
),
),
],
);
}
}

View File

@ -32,6 +32,7 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
final state = ref.watch(oathStateProvider(widget.device.path));
final hasKey = state?.hasKey ?? false;
final remembered = state?.remembered ?? false;
return AlertDialog(
title: const Text('Manage password'),
@ -41,8 +42,48 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
if (hasKey)
Column(
children: [
if (remembered)
// TODO: This is temporary, to be able to forget a password.
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Column(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: const [
Text(
'You password is remembered by the app.',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () async {
await ref
.read(oathStateProvider(widget.device.path)
.notifier)
.forgetPassword();
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Password forgotten'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Forget'),
),
],
),
],
),
),
const Text(
'Enter your current password to change it. If you don\'t know your password, you\'ll need to reset the YubiKey, thne create a new password.'),
'Enter your current password to change it. If you don\'t know your password, you\'ll need to reset the YubiKey, then create a new password.'),
Row(
children: [
Expanded(

@ -1 +1 @@
Subproject commit 765ccf63d9ccc972858d71730b9712514a9b0e0d
Subproject commit f90e4d6f59e8acc4399e9ff587f8c821456d069c