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; _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 @freezed
class DeviceNode with _$DeviceNode { class DeviceNode with _$DeviceNode {
factory DeviceNode.usbYubiKey( factory DeviceNode.usbYubiKey(
List<String> path, String name, int pid, DeviceInfo info) = DevicePath path, String name, int pid, DeviceInfo info) = UsbYubiKeyNode;
UsbYubiKeyNode; factory DeviceNode.nfcReader(DevicePath path, String name) = NfcReaderNode;
factory DeviceNode.nfcReader(List<String> path, String name) = NfcReaderNode;
} }
@freezed @freezed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -55,7 +55,12 @@ class OathPair with _$OathPair {
@freezed @freezed
class OathState with _$OathState { 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) => factory OathState.fromJson(Map<String, dynamic> json) =>
_$OathStateFromJson(json); _$OathStateFromJson(json);

View File

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

View File

@ -48,14 +48,16 @@ Map<String, dynamic> _$$_OathCodeToJson(_$_OathCode instance) =>
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState( _$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
json['device_id'] as String, json['device_id'] as String,
json['has_key'] as bool, hasKey: json['has_key'] as bool,
json['locked'] as bool, remembered: json['remembered'] as bool,
locked: json['locked'] as bool,
); );
Map<String, dynamic> _$$_OathStateToJson(_$_OathState instance) => Map<String, dynamic> _$$_OathStateToJson(_$_OathState instance) =>
<String, dynamic>{ <String, dynamic>{
'device_id': instance.deviceId, 'device_id': instance.deviceId,
'has_key': instance.hasKey, 'has_key': instance.hasKey,
'remembered': instance.remembered,
'locked': instance.locked, '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:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../app/models.dart';
import '../app/state.dart'; import '../app/state.dart';
import '../core/state.dart'; import '../core/state.dart';
import 'models.dart'; import 'models.dart';
final log = Logger('oath.state'); 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 final oathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, OathState?, List<String>>( .family<OathStateNotifier, OathState?, DevicePath>(
(ref, devicePath) => throw UnimplementedError(), (ref, devicePath) => throw UnimplementedError(),
); );
@ -37,13 +21,14 @@ abstract class OathStateNotifier extends StateNotifier<OathState?> {
OathStateNotifier() : super(null); OathStateNotifier() : super(null);
Future<void> reset(); 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> setPassword(String? current, String password);
Future<bool> unsetPassword(String current); Future<bool> unsetPassword(String current);
Future<void> forgetPassword();
} }
final credentialListProvider = StateNotifierProvider.autoDispose final credentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>( .family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
(ref, arg) => throw UnimplementedError(), (ref, arg) => throw UnimplementedError(),
); );

View File

@ -23,20 +23,13 @@ class OathScreen extends ConsumerWidget {
} }
if (state.locked) { if (state.locked) {
return Padding( return ListView(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Text('Password required'), _UnlockForm(
TextField( onSubmit: (password, remember) async {
autofocus: true,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
onSubmitted: (value) async {
final result = await ref final result = await ref
.read(oathStateProvider(deviceData.node.path).notifier) .read(oathStateProvider(deviceData.node.path).notifier)
.unlock(value); .unlock(password, remember: remember);
if (!result) { if (!result) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
@ -48,7 +41,6 @@ class OathScreen extends ConsumerWidget {
}, },
), ),
], ],
),
); );
} else { } else {
final accounts = ref.watch(credentialListProvider(deviceData.node.path)); 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 state = ref.watch(oathStateProvider(widget.device.path));
final hasKey = state?.hasKey ?? false; final hasKey = state?.hasKey ?? false;
final remembered = state?.remembered ?? false;
return AlertDialog( return AlertDialog(
title: const Text('Manage password'), title: const Text('Manage password'),
@ -41,8 +42,48 @@ class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
if (hasKey) if (hasKey)
Column( Column(
children: [ 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( 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( Row(
children: [ children: [
Expanded( Expanded(

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