This commit is contained in:
Dain Nilsson 2022-01-28 17:05:39 +01:00
commit 7d8a09529e
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
15 changed files with 1636 additions and 1510 deletions

View File

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'core/state.dart';
import 'desktop/state.dart';
final log = Logger('about');
@ -13,7 +14,6 @@ class AboutPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final rpcState = ref.watch(rpcStateProvider);
return Scaffold(
appBar: AppBar(
title: const Text('About Yubico Authenticator'),
@ -25,54 +25,42 @@ class AboutPage extends ConsumerWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('ykman version: ${rpcState.version}'),
if (isDesktop)
Text('ykman version: ${ref.watch(rpcStateProvider).version}'),
Text('Dart version: ${Platform.version}'),
const SizedBox(height: 8.0),
Text('Log level: ${ref.watch(logLevelProvider)}'),
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
ref.read(logLevelProvider.notifier).setLevel(Level.INFO);
log.info('Log level changed to INFO');
},
child: const Text('Info'),
),
TextButton(
onPressed: () {
ref
.read(logLevelProvider.notifier)
.setLevel(Level.CONFIG);
log.config('Log level changed to CONFIG');
},
child: const Text('Config'),
),
TextButton(
onPressed: () {
ref.read(logLevelProvider.notifier).setLevel(Level.FINE);
log.fine('Log level changed to FINE');
},
child: const Text('Fine'),
),
],
children: [Level.INFO, Level.CONFIG, Level.FINE]
.map((level) => TextButton(
onPressed: () {
ref.read(logLevelProvider.notifier).state = level;
log.info(
'Log level changed to ${level.name.toUpperCase()}');
},
child: Text(level.name.toUpperCase()),
))
.toList(),
),
const Divider(),
TextButton(
onPressed: () async {
log.info('Running diagnostics...');
final response =
await ref.read(rpcProvider).command('diagnose', []);
log.info('Response', response['diagnostics']);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Diagnostics done. See log for results...'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Run diagnostics...'),
),
if (isDesktop)
TextButton(
onPressed: () async {
log.info('Running diagnostics...');
final response =
await ref.read(rpcProvider).command('diagnose', []);
log.info('Response', response['diagnostics']);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Diagnostics done. See log for results...'),
duration: Duration(seconds: 2),
),
);
},
child: const Text('Run diagnostics...'),
),
],
),
),

View File

@ -1,93 +1,18 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
import 'package:yubico_authenticator/management/models.dart';
import '../core/models.dart';
import '../core/state.dart';
import '../core/rpc.dart';
import '../oath/menu_actions.dart';
import 'models.dart';
const _usbPollDelay = Duration(milliseconds: 500);
const _nfcPollDelay = Duration(milliseconds: 2500);
const _nfcAttachPollDelay = Duration(seconds: 1);
const _nfcDetachPollDelay = Duration(seconds: 5);
final log = Logger('app.state');
final windowStateProvider =
StateNotifierProvider<WindowStateNotifier, WindowState>(
(ref) => WindowStateNotifier());
class WindowStateNotifier extends StateNotifier<WindowState>
with WindowListener {
Timer? _idleTimer;
WindowStateNotifier()
: super(WindowState(focused: true, visible: true, active: true)) {
_init();
}
void _init() async {
windowManager.addListener(this);
if (!await windowManager.isVisible() && mounted) {
state = WindowState(focused: false, visible: false, active: true);
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
}
}
@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}
@override
set state(WindowState value) {
log.config('Window state changed: $value');
super.state = value;
}
@override
void onWindowEvent(String eventName) {
if (mounted) {
switch (eventName) {
case 'blur':
state = state.copyWith(focused: false);
_idleTimer?.cancel();
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
break;
case 'focus':
state = state.copyWith(focused: true, active: true);
_idleTimer?.cancel();
break;
case 'minimize':
state = state.copyWith(visible: false, active: false);
_idleTimer?.cancel();
break;
case 'restore':
state = state.copyWith(visible: true, active: true);
break;
default:
log.fine('Window event ignored: $eventName');
}
}
}
}
// Default implementation is always focused, override with platform specific version.
final windowStateProvider = Provider<WindowState>(
(ref) => WindowState(focused: true, visible: true, active: true),
);
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(ref.watch(prefProvider)));
@ -125,140 +50,15 @@ class SearchNotifier extends StateNotifier<String> {
}
}
final _usbDevicesProvider =
StateNotifierProvider<UsbDeviceNotifier, List<UsbYubiKeyNode>>((ref) {
final notifier = UsbDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
// Override with platform implementation
final attachedDevicesProvider = Provider<List<DeviceNode>>(
(ref) => [],
);
class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
final RpcSession _rpc;
Timer? _pollTimer;
int _usbState = -1;
UsbDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollDevices();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['usb']);
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollDevices() async {
_pollTimer?.cancel();
try {
var scan = await _rpc.command('scan', ['usb']);
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
var usbResult = await _rpc.command('get', ['usb']);
log.info('USB state change', jsonEncode(usbResult));
_usbState = usbResult['data']['state'];
List<UsbYubiKeyNode> usbDevices = [];
for (String id in (usbResult['children'] as Map).keys) {
var path = ['usb', id];
var deviceResult = await _rpc.command('get', path);
var deviceData = deviceResult['data'];
usbDevices.add(DeviceNode.usbYubiKey(
path,
deviceData['name'],
deviceData['pid'],
DeviceInfo.fromJson(deviceData['info']),
) as UsbYubiKeyNode);
}
log.info('USB state updated');
if (mounted) {
state = usbDevices;
}
}
} on RpcError catch (e) {
log.severe('Error polling USB', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(_usbPollDelay, _pollDevices);
}
}
}
final _nfcDevicesProvider =
StateNotifierProvider<NfcDeviceNotifier, List<NfcReaderNode>>((ref) {
final notifier = NfcDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
final RpcSession _rpc;
Timer? _pollTimer;
String _nfcState = '';
NfcDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReaders();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['nfc']);
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReaders() async {
_pollTimer?.cancel();
try {
var children = await _rpc.command('scan', ['nfc']);
var newState = children.keys.join(':');
if (mounted && newState != _nfcState) {
log.info('NFC state change', jsonEncode(children));
_nfcState = newState;
state = children.entries
.map((e) =>
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
as NfcReaderNode)
.toList();
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
}
}
}
final attachedDevicesProvider = Provider<List<DeviceNode>>((ref) {
final usbDevices = ref.watch(_usbDevicesProvider).toList();
final nfcDevices = ref.watch(_nfcDevicesProvider).toList();
usbDevices.sort((a, b) => a.name.compareTo(b.name));
nfcDevices.sort((a, b) => a.name.compareTo(b.name));
return [...usbDevices, ...nfcDevices];
});
// Override with platform implementation
final currentDeviceDataProvider = Provider<YubiKeyData?>(
(ref) => null,
);
final currentDeviceProvider =
StateNotifierProvider<CurrentDeviceNotifier, DeviceNode?>((ref) {
@ -305,75 +105,6 @@ class CurrentDeviceNotifier extends StateNotifier<DeviceNode?> {
}
}
final currentDeviceDataProvider =
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
final notifier = CurrentDeviceDataNotifier(
ref.watch(rpcProvider),
ref.watch(currentDeviceProvider),
);
if (notifier._deviceNode is NfcReaderNode) {
// If this is an NFC reader, listen on WindowState.
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
}
return notifier;
});
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
final RpcSession _rpc;
final DeviceNode? _deviceNode;
Timer? _pollTimer;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
final dev = _deviceNode;
if (dev is UsbYubiKeyNode) {
state = YubiKeyData(dev, dev.name, dev.info);
}
}
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReader();
} else {
_pollTimer?.cancel();
// TODO: Should we clear the key here?
/*if (mounted) {
state = null;
}*/
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReader() async {
_pollTimer?.cancel();
final node = _deviceNode!;
try {
var result = await _rpc.command('get', node.path);
if (mounted) {
if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
} else {
state = null;
}
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
_pollReader);
}
}
}
final subPageProvider = StateNotifierProvider<SubPageNotifier, SubPage>(
(ref) => SubPageNotifier(SubPage.authenticator));

View File

@ -1,7 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'models.freezed.dart';
part 'models.g.dart';
@freezed
class Version with _$Version {
@ -19,19 +18,3 @@ class Version with _$Version {
return '$major.$minor.$patch';
}
}
@Freezed(unionKey: 'kind')
class RpcResponse with _$RpcResponse {
factory RpcResponse.success(Map<String, dynamic> body) = Success;
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
factory RpcResponse.error(
String status, String message, Map<String, dynamic> body) = RpcError;
factory RpcResponse.fromJson(Map<String, dynamic> json) =>
_$RpcResponseFromJson(json);
}
@freezed
class RpcState with _$RpcState {
const factory RpcState(String version) = _RpcState;
}

View File

@ -168,764 +168,3 @@ abstract class _Version extends Version {
_$VersionCopyWith<_Version> get copyWith =>
throw _privateConstructorUsedError;
}
RpcResponse _$RpcResponseFromJson(Map<String, dynamic> json) {
switch (json['kind']) {
case 'success':
return Success.fromJson(json);
case 'signal':
return Signal.fromJson(json);
case 'error':
return RpcError.fromJson(json);
default:
throw CheckedFromJsonException(
json, 'kind', 'RpcResponse', 'Invalid union type "${json['kind']}"!');
}
}
/// @nodoc
class _$RpcResponseTearOff {
const _$RpcResponseTearOff();
Success success(Map<String, dynamic> body) {
return Success(
body,
);
}
Signal signal(String status, Map<String, dynamic> body) {
return Signal(
status,
body,
);
}
RpcError error(String status, String message, Map<String, dynamic> body) {
return RpcError(
status,
message,
body,
);
}
RpcResponse fromJson(Map<String, Object?> json) {
return RpcResponse.fromJson(json);
}
}
/// @nodoc
const $RpcResponse = _$RpcResponseTearOff();
/// @nodoc
mixin _$RpcResponse {
Map<String, dynamic> get body => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RpcResponseCopyWith<RpcResponse> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcResponseCopyWith<$Res> {
factory $RpcResponseCopyWith(
RpcResponse value, $Res Function(RpcResponse) then) =
_$RpcResponseCopyWithImpl<$Res>;
$Res call({Map<String, dynamic> body});
}
/// @nodoc
class _$RpcResponseCopyWithImpl<$Res> implements $RpcResponseCopyWith<$Res> {
_$RpcResponseCopyWithImpl(this._value, this._then);
final RpcResponse _value;
// ignore: unused_field
final $Res Function(RpcResponse) _then;
@override
$Res call({
Object? body = freezed,
}) {
return _then(_value.copyWith(
body: body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
abstract class $SuccessCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $SuccessCopyWith(Success value, $Res Function(Success) then) =
_$SuccessCopyWithImpl<$Res>;
@override
$Res call({Map<String, dynamic> body});
}
/// @nodoc
class _$SuccessCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $SuccessCopyWith<$Res> {
_$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then)
: super(_value, (v) => _then(v as Success));
@override
Success get _value => super._value as Success;
@override
$Res call({
Object? body = freezed,
}) {
return _then(Success(
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$Success implements Success {
_$Success(this.body, {String? $type}) : $type = $type ?? 'success';
factory _$Success.fromJson(Map<String, dynamic> json) =>
_$$SuccessFromJson(json);
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.success(body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Success &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$SuccessCopyWith<Success> get copyWith =>
_$SuccessCopyWithImpl<Success>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return success(body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return success?.call(body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (success != null) {
return success(body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return success(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return success?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SuccessToJson(this);
}
}
abstract class Success implements RpcResponse {
factory Success(Map<String, dynamic> body) = _$Success;
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$SuccessCopyWith<Success> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SignalCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $SignalCopyWith(Signal value, $Res Function(Signal) then) =
_$SignalCopyWithImpl<$Res>;
@override
$Res call({String status, Map<String, dynamic> body});
}
/// @nodoc
class _$SignalCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $SignalCopyWith<$Res> {
_$SignalCopyWithImpl(Signal _value, $Res Function(Signal) _then)
: super(_value, (v) => _then(v as Signal));
@override
Signal get _value => super._value as Signal;
@override
$Res call({
Object? status = freezed,
Object? body = freezed,
}) {
return _then(Signal(
status == freezed
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$Signal implements Signal {
_$Signal(this.status, this.body, {String? $type}) : $type = $type ?? 'signal';
factory _$Signal.fromJson(Map<String, dynamic> json) =>
_$$SignalFromJson(json);
@override
final String status;
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.signal(status: $status, body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Signal &&
const DeepCollectionEquality().equals(other.status, status) &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(status),
const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$SignalCopyWith<Signal> get copyWith =>
_$SignalCopyWithImpl<Signal>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return signal(status, body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return signal?.call(status, body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (signal != null) {
return signal(status, body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return signal(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return signal?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (signal != null) {
return signal(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SignalToJson(this);
}
}
abstract class Signal implements RpcResponse {
factory Signal(String status, Map<String, dynamic> body) = _$Signal;
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
String get status;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$SignalCopyWith<Signal> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcErrorCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $RpcErrorCopyWith(RpcError value, $Res Function(RpcError) then) =
_$RpcErrorCopyWithImpl<$Res>;
@override
$Res call({String status, String message, Map<String, dynamic> body});
}
/// @nodoc
class _$RpcErrorCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $RpcErrorCopyWith<$Res> {
_$RpcErrorCopyWithImpl(RpcError _value, $Res Function(RpcError) _then)
: super(_value, (v) => _then(v as RpcError));
@override
RpcError get _value => super._value as RpcError;
@override
$Res call({
Object? status = freezed,
Object? message = freezed,
Object? body = freezed,
}) {
return _then(RpcError(
status == freezed
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
message == freezed
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RpcError implements RpcError {
_$RpcError(this.status, this.message, this.body, {String? $type})
: $type = $type ?? 'error';
factory _$RpcError.fromJson(Map<String, dynamic> json) =>
_$$RpcErrorFromJson(json);
@override
final String status;
@override
final String message;
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.error(status: $status, message: $message, body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is RpcError &&
const DeepCollectionEquality().equals(other.status, status) &&
const DeepCollectionEquality().equals(other.message, message) &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(status),
const DeepCollectionEquality().hash(message),
const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$RpcErrorCopyWith<RpcError> get copyWith =>
_$RpcErrorCopyWithImpl<RpcError>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return error(status, message, body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return error?.call(status, message, body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (error != null) {
return error(status, message, body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$RpcErrorToJson(this);
}
}
abstract class RpcError implements RpcResponse {
factory RpcError(String status, String message, Map<String, dynamic> body) =
_$RpcError;
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
String get status;
String get message;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$RpcErrorCopyWith<RpcError> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
class _$RpcStateTearOff {
const _$RpcStateTearOff();
_RpcState call(String version) {
return _RpcState(
version,
);
}
}
/// @nodoc
const $RpcState = _$RpcStateTearOff();
/// @nodoc
mixin _$RpcState {
String get version => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RpcStateCopyWith<RpcState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcStateCopyWith<$Res> {
factory $RpcStateCopyWith(RpcState value, $Res Function(RpcState) then) =
_$RpcStateCopyWithImpl<$Res>;
$Res call({String version});
}
/// @nodoc
class _$RpcStateCopyWithImpl<$Res> implements $RpcStateCopyWith<$Res> {
_$RpcStateCopyWithImpl(this._value, this._then);
final RpcState _value;
// ignore: unused_field
final $Res Function(RpcState) _then;
@override
$Res call({
Object? version = freezed,
}) {
return _then(_value.copyWith(
version: version == freezed
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
abstract class _$RpcStateCopyWith<$Res> implements $RpcStateCopyWith<$Res> {
factory _$RpcStateCopyWith(_RpcState value, $Res Function(_RpcState) then) =
__$RpcStateCopyWithImpl<$Res>;
@override
$Res call({String version});
}
/// @nodoc
class __$RpcStateCopyWithImpl<$Res> extends _$RpcStateCopyWithImpl<$Res>
implements _$RpcStateCopyWith<$Res> {
__$RpcStateCopyWithImpl(_RpcState _value, $Res Function(_RpcState) _then)
: super(_value, (v) => _then(v as _RpcState));
@override
_RpcState get _value => super._value as _RpcState;
@override
$Res call({
Object? version = freezed,
}) {
return _then(_RpcState(
version == freezed
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$_RpcState implements _RpcState {
const _$_RpcState(this.version);
@override
final String version;
@override
String toString() {
return 'RpcState(version: $version)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _RpcState &&
const DeepCollectionEquality().equals(other.version, version));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(version));
@JsonKey(ignore: true)
@override
_$RpcStateCopyWith<_RpcState> get copyWith =>
__$RpcStateCopyWithImpl<_RpcState>(this, _$identity);
}
abstract class _RpcState implements RpcState {
const factory _RpcState(String version) = _$_RpcState;
@override
String get version;
@override
@JsonKey(ignore: true)
_$RpcStateCopyWith<_RpcState> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -1,90 +1,14 @@
import 'dart:io';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'models.dart';
import 'rpc.dart';
// This must be initialized before use, in main.dart.
final prefProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
// This must be initialized before use, in main.dart.
final rpcProvider = Provider<RpcSession>((ref) {
throw UnimplementedError();
});
final logLevelProvider = StateProvider<Level>((ref) => Logger.root.level);
final rpcStateProvider = StateNotifierProvider<RpcStateNotifier, RpcState>(
(ref) => RpcStateNotifier(ref.watch(rpcProvider)));
class RpcStateNotifier extends StateNotifier<RpcState> {
final RpcSession rpc;
RpcStateNotifier(this.rpc) : super(const RpcState('unknown')) {
_init();
}
_init() async {
final response = await rpc.command('get', []);
if (mounted) {
state = state.copyWith(version: response['data']['version']);
}
}
}
final logLevelProvider = StateNotifierProvider<LogLevelNotifier, Level>(
(ref) => LogLevelNotifier(ref.watch(rpcProvider), Logger.root.level));
class LogLevelNotifier extends StateNotifier<Level> {
final RpcSession rpc;
LogLevelNotifier(this.rpc, Level state) : super(state);
setLevel(Level level) {
Logger.root.level = level;
rpc.setLogLevel(level);
state = level;
}
}
typedef ErrorHandler = Future<void> Function(RpcError e);
class RpcNodeSession {
final RpcSession _rpc;
final List<String> devicePath;
final List<String> subPath;
final Map<String, ErrorHandler> _errorHandlers = {};
RpcNodeSession(this._rpc, this.devicePath, this.subPath);
void setErrorHandler(String status, ErrorHandler handler) {
_errorHandlers[status] = handler;
}
void unserErrorHandler(String status) {
_errorHandlers.remove(status);
}
Future<Map<String, dynamic>> command(
String action, {
List<String> target = const [],
Map<dynamic, dynamic>? params,
Signaler? signal,
}) async {
try {
return await _rpc.command(
action,
devicePath + subPath + target,
params: params,
signal: signal,
);
} on RpcError catch (e) {
final handler = _errorHandlers[e.status];
if (handler != null) {
log.info('Attempting recovery on "${e.status}"');
await handler(e);
return command(action, target: target, params: params, signal: signal);
}
rethrow;
}
}
}
final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux;

228
lib/desktop/devices.dart Executable file
View File

@ -0,0 +1,228 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import '../app/models.dart';
import '../app/state.dart';
import '../management/models.dart';
import 'models.dart';
import 'rpc.dart';
import 'state.dart';
const _usbPollDelay = Duration(milliseconds: 500);
const _nfcPollDelay = Duration(milliseconds: 2500);
const _nfcAttachPollDelay = Duration(seconds: 1);
const _nfcDetachPollDelay = Duration(seconds: 5);
final log = Logger('desktop.devices');
final _usbDevicesProvider =
StateNotifierProvider<UsbDeviceNotifier, List<UsbYubiKeyNode>>((ref) {
final notifier = UsbDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
class UsbDeviceNotifier extends StateNotifier<List<UsbYubiKeyNode>> {
final RpcSession _rpc;
Timer? _pollTimer;
int _usbState = -1;
UsbDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollDevices();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['usb']);
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollDevices() async {
_pollTimer?.cancel();
try {
var scan = await _rpc.command('scan', ['usb']);
if (_usbState != scan['state'] || state.length != scan['pids'].length) {
var usbResult = await _rpc.command('get', ['usb']);
log.info('USB state change', jsonEncode(usbResult));
_usbState = usbResult['data']['state'];
List<UsbYubiKeyNode> usbDevices = [];
for (String id in (usbResult['children'] as Map).keys) {
var path = ['usb', id];
var deviceResult = await _rpc.command('get', path);
var deviceData = deviceResult['data'];
usbDevices.add(DeviceNode.usbYubiKey(
path,
deviceData['name'],
deviceData['pid'],
DeviceInfo.fromJson(deviceData['info']),
) as UsbYubiKeyNode);
}
log.info('USB state updated');
if (mounted) {
state = usbDevices;
}
}
} on RpcError catch (e) {
log.severe('Error polling USB', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(_usbPollDelay, _pollDevices);
}
}
}
final _nfcDevicesProvider =
StateNotifierProvider<NfcDeviceNotifier, List<NfcReaderNode>>((ref) {
final notifier = NfcDeviceNotifier(ref.watch(rpcProvider));
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
});
class NfcDeviceNotifier extends StateNotifier<List<NfcReaderNode>> {
final RpcSession _rpc;
Timer? _pollTimer;
String _nfcState = '';
NfcDeviceNotifier(this._rpc) : super([]);
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReaders();
} else {
_pollTimer?.cancel();
// Release any held device
_rpc.command('get', ['nfc']);
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReaders() async {
_pollTimer?.cancel();
try {
var children = await _rpc.command('scan', ['nfc']);
var newState = children.keys.join(':');
if (mounted && newState != _nfcState) {
log.info('NFC state change', jsonEncode(children));
_nfcState = newState;
state = children.entries
.map((e) =>
DeviceNode.nfcReader(['nfc', e.key], e.value['name'] as String)
as NfcReaderNode)
.toList();
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(_nfcPollDelay, _pollReaders);
}
}
}
final desktopDevicesProvider = Provider<List<DeviceNode>>((ref) {
final usbDevices = ref.watch(_usbDevicesProvider).toList();
final nfcDevices = ref.watch(_nfcDevicesProvider).toList();
usbDevices.sort((a, b) => a.name.compareTo(b.name));
nfcDevices.sort((a, b) => a.name.compareTo(b.name));
return [...usbDevices, ...nfcDevices];
});
final _desktopDeviceDataProvider =
StateNotifierProvider<CurrentDeviceDataNotifier, YubiKeyData?>((ref) {
final notifier = CurrentDeviceDataNotifier(
ref.watch(rpcProvider),
ref.watch(currentDeviceProvider),
);
if (notifier._deviceNode is NfcReaderNode) {
// If this is an NFC reader, listen on WindowState.
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
}
return notifier;
});
final desktopDeviceDataProvider = Provider<YubiKeyData?>(
(ref) => ref.watch(_desktopDeviceDataProvider),
);
class CurrentDeviceDataNotifier extends StateNotifier<YubiKeyData?> {
final RpcSession _rpc;
final DeviceNode? _deviceNode;
Timer? _pollTimer;
CurrentDeviceDataNotifier(this._rpc, this._deviceNode) : super(null) {
final dev = _deviceNode;
if (dev is UsbYubiKeyNode) {
state = YubiKeyData(dev, dev.name, dev.info);
}
}
void _notifyWindowState(WindowState windowState) {
if (windowState.active) {
_pollReader();
} else {
_pollTimer?.cancel();
// TODO: Should we clear the key here?
/*if (mounted) {
state = null;
}*/
}
}
@override
void dispose() {
_pollTimer?.cancel();
super.dispose();
}
void _pollReader() async {
_pollTimer?.cancel();
final node = _deviceNode!;
try {
var result = await _rpc.command('get', node.path);
if (mounted) {
if (result['data']['present']) {
state = YubiKeyData(node, result['data']['name'],
DeviceInfo.fromJson(result['data']['info']));
} else {
state = null;
}
}
} on RpcError catch (e) {
log.severe('Error polling NFC', jsonEncode(e));
}
if (mounted) {
_pollTimer = Timer(
state == null ? _nfcAttachPollDelay : _nfcDetachPollDelay,
_pollReader);
}
}
}

56
lib/desktop/init.dart Executable file
View File

@ -0,0 +1,56 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:window_manager/window_manager.dart';
import 'package:yubico_authenticator/desktop/devices.dart';
import 'package:yubico_authenticator/desktop/oath/state.dart';
import 'package:yubico_authenticator/desktop/state.dart';
import 'package:yubico_authenticator/oath/state.dart';
import '../app/state.dart';
import 'rpc.dart';
final log = Logger('desktop.init');
Future<List<Override>> initializeAndGetOverrides() async {
await windowManager.ensureInitialized();
// Linux doesn't currently support hiding the window at start currently.
// For now, this size should match linux/flutter/my_application.cc to avoid window flicker at startup.
unawaited(windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setSize(const Size(400, 720));
await windowManager.show();
}));
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
var exe = Platform.environment['_YKMAN_PATH'];
if (exe?.isEmpty ?? true) {
var relativePath = 'ykman/ykman';
if (Platform.isMacOS) {
relativePath = '../Resources/' + relativePath;
} else if (Platform.isWindows) {
relativePath += '.exe';
}
exe = Uri.file(Platform.resolvedExecutable)
.resolve(relativePath)
.toFilePath();
}
log.info('Starting subprocess: $exe');
var rpc = await RpcSession.launch(exe!);
log.info('ykman process started', exe);
rpc.setLogLevel(Logger.root.level);
return [
rpcProvider.overrideWithValue(rpc),
windowStateProvider.overrideWithProvider(desktopWindowStateProvider),
attachedDevicesProvider.overrideWithProvider(desktopDevicesProvider),
currentDeviceDataProvider.overrideWithProvider(desktopDeviceDataProvider),
oathStateProvider.overrideWithProvider(desktopOathState),
credentialListProvider
.overrideWithProvider(desktopOathCredentialListProvider),
];
}

20
lib/desktop/models.dart Executable file
View File

@ -0,0 +1,20 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'models.freezed.dart';
part 'models.g.dart';
@Freezed(unionKey: 'kind')
class RpcResponse with _$RpcResponse {
factory RpcResponse.success(Map<String, dynamic> body) = Success;
factory RpcResponse.signal(String status, Map<String, dynamic> body) = Signal;
factory RpcResponse.error(
String status, String message, Map<String, dynamic> body) = RpcError;
factory RpcResponse.fromJson(Map<String, dynamic> json) =>
_$RpcResponseFromJson(json);
}
@freezed
class RpcState with _$RpcState {
const factory RpcState(String version) = _RpcState;
}

776
lib/desktop/models.freezed.dart Executable file
View File

@ -0,0 +1,776 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'models.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
RpcResponse _$RpcResponseFromJson(Map<String, dynamic> json) {
switch (json['kind']) {
case 'success':
return Success.fromJson(json);
case 'signal':
return Signal.fromJson(json);
case 'error':
return RpcError.fromJson(json);
default:
throw CheckedFromJsonException(
json, 'kind', 'RpcResponse', 'Invalid union type "${json['kind']}"!');
}
}
/// @nodoc
class _$RpcResponseTearOff {
const _$RpcResponseTearOff();
Success success(Map<String, dynamic> body) {
return Success(
body,
);
}
Signal signal(String status, Map<String, dynamic> body) {
return Signal(
status,
body,
);
}
RpcError error(String status, String message, Map<String, dynamic> body) {
return RpcError(
status,
message,
body,
);
}
RpcResponse fromJson(Map<String, Object?> json) {
return RpcResponse.fromJson(json);
}
}
/// @nodoc
const $RpcResponse = _$RpcResponseTearOff();
/// @nodoc
mixin _$RpcResponse {
Map<String, dynamic> get body => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RpcResponseCopyWith<RpcResponse> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcResponseCopyWith<$Res> {
factory $RpcResponseCopyWith(
RpcResponse value, $Res Function(RpcResponse) then) =
_$RpcResponseCopyWithImpl<$Res>;
$Res call({Map<String, dynamic> body});
}
/// @nodoc
class _$RpcResponseCopyWithImpl<$Res> implements $RpcResponseCopyWith<$Res> {
_$RpcResponseCopyWithImpl(this._value, this._then);
final RpcResponse _value;
// ignore: unused_field
final $Res Function(RpcResponse) _then;
@override
$Res call({
Object? body = freezed,
}) {
return _then(_value.copyWith(
body: body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
abstract class $SuccessCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $SuccessCopyWith(Success value, $Res Function(Success) then) =
_$SuccessCopyWithImpl<$Res>;
@override
$Res call({Map<String, dynamic> body});
}
/// @nodoc
class _$SuccessCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $SuccessCopyWith<$Res> {
_$SuccessCopyWithImpl(Success _value, $Res Function(Success) _then)
: super(_value, (v) => _then(v as Success));
@override
Success get _value => super._value as Success;
@override
$Res call({
Object? body = freezed,
}) {
return _then(Success(
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$Success implements Success {
_$Success(this.body, {String? $type}) : $type = $type ?? 'success';
factory _$Success.fromJson(Map<String, dynamic> json) =>
_$$SuccessFromJson(json);
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.success(body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Success &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$SuccessCopyWith<Success> get copyWith =>
_$SuccessCopyWithImpl<Success>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return success(body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return success?.call(body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (success != null) {
return success(body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return success(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return success?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (success != null) {
return success(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SuccessToJson(this);
}
}
abstract class Success implements RpcResponse {
factory Success(Map<String, dynamic> body) = _$Success;
factory Success.fromJson(Map<String, dynamic> json) = _$Success.fromJson;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$SuccessCopyWith<Success> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SignalCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $SignalCopyWith(Signal value, $Res Function(Signal) then) =
_$SignalCopyWithImpl<$Res>;
@override
$Res call({String status, Map<String, dynamic> body});
}
/// @nodoc
class _$SignalCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $SignalCopyWith<$Res> {
_$SignalCopyWithImpl(Signal _value, $Res Function(Signal) _then)
: super(_value, (v) => _then(v as Signal));
@override
Signal get _value => super._value as Signal;
@override
$Res call({
Object? status = freezed,
Object? body = freezed,
}) {
return _then(Signal(
status == freezed
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$Signal implements Signal {
_$Signal(this.status, this.body, {String? $type}) : $type = $type ?? 'signal';
factory _$Signal.fromJson(Map<String, dynamic> json) =>
_$$SignalFromJson(json);
@override
final String status;
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.signal(status: $status, body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is Signal &&
const DeepCollectionEquality().equals(other.status, status) &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(status),
const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$SignalCopyWith<Signal> get copyWith =>
_$SignalCopyWithImpl<Signal>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return signal(status, body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return signal?.call(status, body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (signal != null) {
return signal(status, body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return signal(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return signal?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (signal != null) {
return signal(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$SignalToJson(this);
}
}
abstract class Signal implements RpcResponse {
factory Signal(String status, Map<String, dynamic> body) = _$Signal;
factory Signal.fromJson(Map<String, dynamic> json) = _$Signal.fromJson;
String get status;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$SignalCopyWith<Signal> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcErrorCopyWith<$Res> implements $RpcResponseCopyWith<$Res> {
factory $RpcErrorCopyWith(RpcError value, $Res Function(RpcError) then) =
_$RpcErrorCopyWithImpl<$Res>;
@override
$Res call({String status, String message, Map<String, dynamic> body});
}
/// @nodoc
class _$RpcErrorCopyWithImpl<$Res> extends _$RpcResponseCopyWithImpl<$Res>
implements $RpcErrorCopyWith<$Res> {
_$RpcErrorCopyWithImpl(RpcError _value, $Res Function(RpcError) _then)
: super(_value, (v) => _then(v as RpcError));
@override
RpcError get _value => super._value as RpcError;
@override
$Res call({
Object? status = freezed,
Object? message = freezed,
Object? body = freezed,
}) {
return _then(RpcError(
status == freezed
? _value.status
: status // ignore: cast_nullable_to_non_nullable
as String,
message == freezed
? _value.message
: message // ignore: cast_nullable_to_non_nullable
as String,
body == freezed
? _value.body
: body // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>,
));
}
}
/// @nodoc
@JsonSerializable()
class _$RpcError implements RpcError {
_$RpcError(this.status, this.message, this.body, {String? $type})
: $type = $type ?? 'error';
factory _$RpcError.fromJson(Map<String, dynamic> json) =>
_$$RpcErrorFromJson(json);
@override
final String status;
@override
final String message;
@override
final Map<String, dynamic> body;
@JsonKey(name: 'kind')
final String $type;
@override
String toString() {
return 'RpcResponse.error(status: $status, message: $message, body: $body)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is RpcError &&
const DeepCollectionEquality().equals(other.status, status) &&
const DeepCollectionEquality().equals(other.message, message) &&
const DeepCollectionEquality().equals(other.body, body));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(status),
const DeepCollectionEquality().hash(message),
const DeepCollectionEquality().hash(body));
@JsonKey(ignore: true)
@override
$RpcErrorCopyWith<RpcError> get copyWith =>
_$RpcErrorCopyWithImpl<RpcError>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(Map<String, dynamic> body) success,
required TResult Function(String status, Map<String, dynamic> body) signal,
required TResult Function(
String status, String message, Map<String, dynamic> body)
error,
}) {
return error(status, message, body);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
}) {
return error?.call(status, message, body);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(Map<String, dynamic> body)? success,
TResult Function(String status, Map<String, dynamic> body)? signal,
TResult Function(String status, String message, Map<String, dynamic> body)?
error,
required TResult orElse(),
}) {
if (error != null) {
return error(status, message, body);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Success value) success,
required TResult Function(Signal value) signal,
required TResult Function(RpcError value) error,
}) {
return error(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
}) {
return error?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Success value)? success,
TResult Function(Signal value)? signal,
TResult Function(RpcError value)? error,
required TResult orElse(),
}) {
if (error != null) {
return error(this);
}
return orElse();
}
@override
Map<String, dynamic> toJson() {
return _$$RpcErrorToJson(this);
}
}
abstract class RpcError implements RpcResponse {
factory RpcError(String status, String message, Map<String, dynamic> body) =
_$RpcError;
factory RpcError.fromJson(Map<String, dynamic> json) = _$RpcError.fromJson;
String get status;
String get message;
@override
Map<String, dynamic> get body;
@override
@JsonKey(ignore: true)
$RpcErrorCopyWith<RpcError> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
class _$RpcStateTearOff {
const _$RpcStateTearOff();
_RpcState call(String version) {
return _RpcState(
version,
);
}
}
/// @nodoc
const $RpcState = _$RpcStateTearOff();
/// @nodoc
mixin _$RpcState {
String get version => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$RpcStateCopyWith<RpcState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $RpcStateCopyWith<$Res> {
factory $RpcStateCopyWith(RpcState value, $Res Function(RpcState) then) =
_$RpcStateCopyWithImpl<$Res>;
$Res call({String version});
}
/// @nodoc
class _$RpcStateCopyWithImpl<$Res> implements $RpcStateCopyWith<$Res> {
_$RpcStateCopyWithImpl(this._value, this._then);
final RpcState _value;
// ignore: unused_field
final $Res Function(RpcState) _then;
@override
$Res call({
Object? version = freezed,
}) {
return _then(_value.copyWith(
version: version == freezed
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
abstract class _$RpcStateCopyWith<$Res> implements $RpcStateCopyWith<$Res> {
factory _$RpcStateCopyWith(_RpcState value, $Res Function(_RpcState) then) =
__$RpcStateCopyWithImpl<$Res>;
@override
$Res call({String version});
}
/// @nodoc
class __$RpcStateCopyWithImpl<$Res> extends _$RpcStateCopyWithImpl<$Res>
implements _$RpcStateCopyWith<$Res> {
__$RpcStateCopyWithImpl(_RpcState _value, $Res Function(_RpcState) _then)
: super(_value, (v) => _then(v as _RpcState));
@override
_RpcState get _value => super._value as _RpcState;
@override
$Res call({
Object? version = freezed,
}) {
return _then(_RpcState(
version == freezed
? _value.version
: version // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
class _$_RpcState implements _RpcState {
const _$_RpcState(this.version);
@override
final String version;
@override
String toString() {
return 'RpcState(version: $version)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _RpcState &&
const DeepCollectionEquality().equals(other.version, version));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(version));
@JsonKey(ignore: true)
@override
_$RpcStateCopyWith<_RpcState> get copyWith =>
__$RpcStateCopyWithImpl<_RpcState>(this, _$identity);
}
abstract class _RpcState implements RpcState {
const factory _RpcState(String version) = _$_RpcState;
@override
String get version;
@override
@JsonKey(ignore: true)
_$RpcStateCopyWith<_RpcState> get copyWith =>
throw _privateConstructorUsedError;
}

330
lib/desktop/oath/state.dart Executable file
View File

@ -0,0 +1,330 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../oath/models.dart';
import '../../oath/state.dart';
import '../rpc.dart';
import '../state.dart';
final log = Logger('desktop.oath.state');
final _sessionProvider =
Provider.autoDispose.family<RpcNodeSession, List<String>>(
(ref, devicePath) =>
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['ccid', 'oath']),
);
final desktopOathState = StateNotifierProvider.autoDispose
.family<OathStateNotifier, OathState?, List<String>>(
(ref, devicePath) {
final session = ref.watch(_sessionProvider(devicePath));
final notifier = _DesktopOathStateNotifier(session, ref);
session
..setErrorHandler('state-reset', (_) async {
ref.refresh(_sessionProvider(devicePath));
})
..setErrorHandler('auth-required', (_) async {
await notifier.refresh();
});
ref.onDispose(() {
session
..unserErrorHandler('state-reset')
..unserErrorHandler('auth-required');
});
return notifier..refresh();
},
);
class _DesktopOathStateNotifier extends OathStateNotifier {
final RpcNodeSession _session;
final Ref _ref;
_DesktopOathStateNotifier(this._session, this._ref) : super();
refresh() async {
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));
if (oathState.locked && key != null) {
final result = await _session.command('validate', params: {'key': key});
if (result['unlocked']) {
oathState = oathState.copyWith(locked: false);
} else {
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
}
}
if (mounted) {
state = oathState;
}
}
@override
Future<void> reset() async {
await _session.command('reset');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.refresh(_sessionProvider(_session.devicePath));
}
@override
Future<bool> unlock(String password) 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']) {
log.config('applet unlocked');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
state = state?.copyWith(locked: false);
}
return status['unlocked'];
}
Future<bool> _checkPassword(String password) async {
var result =
await _session.command('derive', params: {'password': password});
return _ref.read(oathLockKeyProvider(_session.devicePath)) == result['key'];
}
@override
Future<bool> setPassword(String? current, String password) async {
if (state?.hasKey ?? false) {
if (current != null) {
if (!await _checkPassword(current)) {
return false;
}
} else {
return false;
}
}
var result =
await _session.command('derive', params: {'password': password});
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);
if (mounted) {
state = state?.copyWith(hasKey: true);
}
return true;
}
@override
Future<bool> unsetPassword(String current) async {
if (state?.hasKey ?? false) {
if (!await _checkPassword(current)) {
return false;
}
}
await _session.command('unset_key');
_ref.read(oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
if (mounted) {
state = state?.copyWith(hasKey: false, locked: false);
}
return true;
}
}
final desktopOathCredentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
(ref, devicePath) {
var notifier = _DesktopCredentialListNotifier(
ref.watch(_sessionProvider(devicePath)),
ref.watch(oathStateProvider(devicePath).select((s) => s?.locked ?? true)),
);
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
},
);
extension on OathCredential {
bool get isSteam => issuer == 'Steam' && oathType == OathType.totp;
}
const String _steamCharTable = '23456789BCDFGHJKMNPQRTVWXY';
String _formatSteam(String response) {
final offset = int.parse(response.substring(response.length - 1), radix: 16);
var number =
int.parse(response.substring(offset * 2, offset * 2 + 8), radix: 16) &
0x7fffffff;
var value = '';
for (var i = 0; i < 5; i++) {
value += _steamCharTable[number % _steamCharTable.length];
number ~/= _steamCharTable.length;
}
return value;
}
class _DesktopCredentialListNotifier extends OathCredentialListNotifier {
final RpcNodeSession _session;
final bool _locked;
Timer? _timer;
_DesktopCredentialListNotifier(this._session, this._locked) : super();
void _notifyWindowState(WindowState windowState) {
if (_locked) return;
if (windowState.active) {
_scheduleRefresh();
} else {
_timer?.cancel();
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Future<OathCode> calculate(OathCredential credential,
{bool update = true}) async {
final OathCode code;
if (credential.isSteam) {
final timeStep = DateTime.now().millisecondsSinceEpoch ~/ 30000;
var result = await _session.command('calculate', target: [
'accounts',
credential.id
], params: {
'challenge': timeStep.toRadixString(16).padLeft(16, '0'),
});
code = OathCode(
_formatSteam(result['response']), timeStep * 30, (timeStep + 1) * 30);
} else {
var result =
await _session.command('code', target: ['accounts', credential.id]);
code = OathCode.fromJson(result);
}
log.config('Calculate', jsonEncode(code));
if (update && mounted) {
final creds = state!.toList();
final i = creds.indexWhere((e) => e.credential.id == credential.id);
state = creds..[i] = creds[i].copyWith(code: code);
}
return code;
}
@override
Future<OathCredential> addAccount(Uri otpauth,
{bool requireTouch = false, bool update = true}) async {
var result = await _session.command('put', target: [
'accounts'
], params: {
'uri': otpauth.toString(),
'require_touch': requireTouch,
});
final credential = OathCredential.fromJson(result);
if (update && mounted) {
state = state!.toList()..add(OathPair(credential, null));
if (!requireTouch && credential.oathType == OathType.totp) {
await calculate(credential);
}
}
return credential;
}
@override
Future<OathCredential> renameAccount(
OathCredential credential,
String? issuer,
String name,
) async {
final result = await _session.command('rename', target: [
'accounts',
credential.id,
], params: {
'issuer': issuer,
'name': name,
});
String credentialId = result['credential_id'];
final renamedCredential =
credential.copyWith(id: credentialId, issuer: issuer, name: name);
if (mounted) {
final newState = state!.toList();
final index = newState.indexWhere((e) => e.credential == credential);
final oldPair = newState.removeAt(index);
newState.add(OathPair(
renamedCredential,
oldPair.code,
));
state = newState;
}
return renamedCredential;
}
@override
Future<void> deleteAccount(OathCredential credential) async {
await _session.command('delete', target: ['accounts', credential.id]);
if (mounted) {
state = state!.toList()..removeWhere((e) => e.credential == credential);
}
}
refresh() async {
if (_locked) return;
log.config('refreshing credentials...');
var result = await _session.command('calculate_all', target: ['accounts']);
log.config('Entries', jsonEncode(result));
final pairs = [];
for (var e in result['entries']) {
final credential = OathCredential.fromJson(e['credential']);
final code = e['code'] == null
? null
: credential.isSteam // Steam codes require a re-calculate
? await calculate(credential, update: false)
: OathCode.fromJson(e['code']);
pairs.add(OathPair(credential, code));
}
if (mounted) {
final current = state?.toList() ?? [];
for (var pair in pairs) {
final i =
current.indexWhere((e) => e.credential.id == pair.credential.id);
if (i < 0) {
current.add(pair);
} else if (pair.code != null) {
current[i] = current[i].copyWith(code: pair.code);
}
}
state = current;
_scheduleRefresh();
}
}
_scheduleRefresh() {
_timer?.cancel();
if (_locked) return;
if (state == null) {
refresh();
} else if (mounted) {
final expirations = (state ?? [])
.where((pair) =>
pair.credential.oathType == OathType.totp &&
!pair.credential.touchRequired)
.map((e) => e.code)
.whereType<OathCode>()
.map((e) => e.validTo);
if (expirations.isEmpty) {
_timer = null;
} else {
final earliest = expirations.reduce(min) * 1000;
final now = DateTime.now().millisecondsSinceEpoch;
if (earliest < now) {
refresh();
} else {
_timer = Timer(Duration(milliseconds: earliest - now), refresh);
}
}
}
}
}

View File

@ -142,3 +142,46 @@ class RpcSession {
}
}
}
typedef ErrorHandler = Future<void> Function(RpcError e);
class RpcNodeSession {
final RpcSession _rpc;
final List<String> devicePath;
final List<String> subPath;
final Map<String, ErrorHandler> _errorHandlers = {};
RpcNodeSession(this._rpc, this.devicePath, this.subPath);
void setErrorHandler(String status, ErrorHandler handler) {
_errorHandlers[status] = handler;
}
void unserErrorHandler(String status) {
_errorHandlers.remove(status);
}
Future<Map<String, dynamic>> command(
String action, {
List<String> target = const [],
Map<dynamic, dynamic>? params,
Signaler? signal,
}) async {
try {
return await _rpc.command(
action,
devicePath + subPath + target,
params: params,
signal: signal,
);
} on RpcError catch (e) {
final handler = _errorHandlers[e.status];
if (handler != null) {
log.info('Attempting recovery on "${e.status}"');
await handler(e);
return command(action, target: target, params: params, signal: signal);
}
rethrow;
}
}
}

110
lib/desktop/state.dart Executable file
View File

@ -0,0 +1,110 @@
import 'dart:async';
import 'package:logging/logging.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart';
import '../core/state.dart';
import '../app/models.dart';
import 'models.dart';
import 'rpc.dart';
// This must be initialized before use in initialize.dart.
final rpcProvider = Provider<RpcSession>((ref) {
throw UnimplementedError();
});
final rpcStateProvider = StateNotifierProvider<_RpcStateNotifier, RpcState>(
(ref) {
final rpc = ref.watch(rpcProvider);
ref.listen<Level>(logLevelProvider, (_, level) {
rpc.setLogLevel(level);
}, fireImmediately: true);
return _RpcStateNotifier(rpc);
},
);
class _RpcStateNotifier extends StateNotifier<RpcState> {
final RpcSession rpc;
_RpcStateNotifier(this.rpc) : super(const RpcState('unknown')) {
_init();
}
_init() async {
final response = await rpc.command('get', []);
if (mounted) {
state = state.copyWith(version: response['data']['version']);
}
}
}
final _windowStateProvider =
StateNotifierProvider<_WindowStateNotifier, WindowState>(
(ref) => _WindowStateNotifier());
final desktopWindowStateProvider = Provider<WindowState>(
(ref) => ref.watch(_windowStateProvider),
);
class _WindowStateNotifier extends StateNotifier<WindowState>
with WindowListener {
Timer? _idleTimer;
_WindowStateNotifier()
: super(WindowState(focused: true, visible: true, active: true)) {
_init();
}
void _init() async {
windowManager.addListener(this);
if (!await windowManager.isVisible() && mounted) {
state = WindowState(focused: false, visible: false, active: true);
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
}
}
@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}
@override
set state(WindowState value) {
log.config('Window state changed: $value');
super.state = value;
}
@override
void onWindowEvent(String eventName) {
if (mounted) {
switch (eventName) {
case 'blur':
state = state.copyWith(focused: false);
_idleTimer?.cancel();
_idleTimer = Timer(const Duration(seconds: 5), () {
if (mounted) {
state = state.copyWith(active: false);
}
});
break;
case 'focus':
state = state.copyWith(focused: true, active: true);
_idleTimer?.cancel();
break;
case 'minimize':
state = state.copyWith(visible: false, active: false);
_idleTimer?.cancel();
break;
case 'restore':
state = state.copyWith(visible: true, active: true);
break;
default:
log.fine('Window event ignored: $eventName');
}
}
}
}

View File

@ -1,17 +1,14 @@
import 'dart:async';
import 'dart:io';
import 'dart:developer' as developer;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart';
import 'package:window_manager/window_manager.dart';
import 'app/app.dart';
import 'app/views/main_page.dart';
import 'core/rpc.dart';
import 'core/state.dart';
import 'desktop/init.dart' as desktop;
import 'error_page.dart';
@ -20,47 +17,23 @@ final log = Logger('main');
void main() async {
_initLogging(Level.INFO);
WidgetsFlutterBinding.ensureInitialized();
await windowManager.ensureInitialized();
// Either use the _YKMAN_EXE environment variable, or look relative to executable.
var exe = Platform.environment['_YKMAN_PATH'];
if (exe?.isEmpty ?? true) {
var relativePath = 'ykman/ykman';
if (Platform.isMacOS) {
relativePath = '../Resources/' + relativePath;
} else if (Platform.isWindows) {
relativePath += '.exe';
}
exe = Uri.file(Platform.resolvedExecutable)
.resolve(relativePath)
.toFilePath();
}
Widget page;
List<Override> overrides = [
prefProvider.overrideWithValue(await SharedPreferences.getInstance())
prefProvider.overrideWithValue(await SharedPreferences.getInstance()),
];
log.info('Starting subprocess: $exe');
Widget page;
try {
var rpc = await RpcSession.launch(exe!);
// Enable logging TODO: Make this configurable
log.info('ykman process started', exe);
rpc.setLogLevel(Logger.root.level);
overrides.add(rpcProvider.overrideWithValue(rpc));
// Platform specific initialization
if (isDesktop) {
log.config('Initializing desktop platform.');
overrides.addAll(await desktop.initializeAndGetOverrides());
}
page = const MainPage();
} catch (e) {
log.warning('ykman process failed: $e');
log.warning('Platform initialization failed: $e');
page = ErrorPage(error: e.toString());
}
// Linux doesn't currently support hiding the window at start currently.
// For now, this size should match linux/flutter/my_application.cc to avoid window flicker at startup.
unawaited(windowManager.waitUntilReadyToShow().then((_) async {
await windowManager.setSize(const Size(400, 720));
await windowManager.show();
}));
runApp(ProviderScope(
overrides: overrides,
child: YubicoAuthenticatorApp(page: page),

View File

@ -1,12 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/app/models.dart';
import '../app/state.dart';
import '../core/state.dart';
@ -14,12 +11,8 @@ import 'models.dart';
final log = Logger('oath.state');
final _sessionProvider = Provider.autoDispose
.family<RpcNodeSession, List<String>>((ref, devicePath) =>
RpcNodeSession(ref.watch(rpcProvider), devicePath, ['ccid', 'oath']));
// This remembers the key for all devices for the duration of the process.
final _lockKeyProvider =
final oathLockKeyProvider =
StateNotifierProvider.family<_LockKeyNotifier, String?, List<String>>(
(ref, devicePath) => _LockKeyNotifier(null));
@ -37,163 +30,26 @@ class _LockKeyNotifier extends StateNotifier<String?> {
final oathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, OathState?, List<String>>(
(ref, devicePath) {
final session = ref.watch(_sessionProvider(devicePath));
final notifier = OathStateNotifier(session, ref);
session
..setErrorHandler('state-reset', (_) async {
ref.refresh(_sessionProvider(devicePath));
})
..setErrorHandler('auth-required', (_) async {
await notifier.refresh();
});
ref.onDispose(() {
session
..unserErrorHandler('state-reset')
..unserErrorHandler('auth-required');
});
return notifier..refresh();
},
(ref, devicePath) => throw UnimplementedError(),
);
class OathStateNotifier extends StateNotifier<OathState?> {
final RpcNodeSession _session;
final Ref _ref;
OathStateNotifier(this._session, this._ref) : super(null);
abstract class OathStateNotifier extends StateNotifier<OathState?> {
OathStateNotifier() : super(null);
refresh() async {
var result = await _session.command('get');
log.config('application status', jsonEncode(result));
var oathState = OathState.fromJson(result['data']);
final key = _ref.read(_lockKeyProvider(_session.devicePath));
if (oathState.locked && key != null) {
final result = await _session.command('validate', params: {'key': key});
if (result['unlocked']) {
oathState = oathState.copyWith(locked: false);
} else {
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
}
}
if (mounted) {
state = oathState;
}
}
Future<void> reset() async {
await _session.command('reset');
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.refresh(_sessionProvider(_session.devicePath));
}
Future<bool> unlock(String password) 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']) {
log.config('applet unlocked');
_ref.read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
state = state?.copyWith(locked: false);
}
return status['unlocked'];
}
Future<bool> _checkPassword(String password) async {
var result =
await _session.command('derive', params: {'password': password});
return _ref.read(_lockKeyProvider(_session.devicePath)) == result['key'];
}
Future<bool> setPassword(String? current, String password) async {
if (state?.hasKey ?? false) {
if (current != null) {
if (!await _checkPassword(current)) {
return false;
}
} else {
return false;
}
}
var result =
await _session.command('derive', params: {'password': password});
var key = result['key'];
await _session.command('set_key', params: {'key': key});
log.config('OATH key set');
_ref.read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
if (mounted) {
state = state?.copyWith(hasKey: true);
}
return true;
}
Future<bool> unsetPassword(String current) async {
if (state?.hasKey ?? false) {
if (!await _checkPassword(current)) {
return false;
}
}
await _session.command('unset_key');
_ref.read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
if (mounted) {
state = state?.copyWith(hasKey: false, locked: false);
}
return true;
}
Future<void> reset();
Future<bool> unlock(String password);
Future<bool> setPassword(String? current, String password);
Future<bool> unsetPassword(String current);
}
final credentialListProvider = StateNotifierProvider.autoDispose
.family<CredentialListNotifier, List<OathPair>?, List<String>>(
(ref, devicePath) {
var notifier = CredentialListNotifier(
ref.watch(_sessionProvider(devicePath)),
ref.watch(oathStateProvider(devicePath).select((s) => s?.locked ?? true)),
);
ref.listen<WindowState>(windowStateProvider, (_, windowState) {
notifier._notifyWindowState(windowState);
}, fireImmediately: true);
return notifier;
},
.family<OathCredentialListNotifier, List<OathPair>?, List<String>>(
(ref, arg) => throw UnimplementedError(),
);
extension on OathCredential {
bool get isSteam => issuer == 'Steam' && oathType == OathType.totp;
}
const String _steamCharTable = '23456789BCDFGHJKMNPQRTVWXY';
String _formatSteam(String response) {
final offset = int.parse(response.substring(response.length - 1), radix: 16);
var number =
int.parse(response.substring(offset * 2, offset * 2 + 8), radix: 16) &
0x7fffffff;
var value = '';
for (var i = 0; i < 5; i++) {
value += _steamCharTable[number % _steamCharTable.length];
number ~/= _steamCharTable.length;
}
return value;
}
class CredentialListNotifier extends StateNotifier<List<OathPair>?> {
final RpcNodeSession _session;
final bool _locked;
Timer? _timer;
CredentialListNotifier(this._session, this._locked) : super(null);
void _notifyWindowState(WindowState windowState) {
if (_locked) return;
if (windowState.active) {
_scheduleRefresh();
} else {
_timer?.cancel();
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
abstract class OathCredentialListNotifier
extends StateNotifier<List<OathPair>?> {
OathCredentialListNotifier() : super(null);
@override
@protected
@ -201,142 +57,11 @@ class CredentialListNotifier extends StateNotifier<List<OathPair>?> {
super.state = value != null ? List.unmodifiable(value) : null;
}
Future<OathCode> calculate(OathCredential credential,
{bool update = true}) async {
final OathCode code;
if (credential.isSteam) {
final timeStep = DateTime.now().millisecondsSinceEpoch ~/ 30000;
var result = await _session.command('calculate', target: [
'accounts',
credential.id
], params: {
'challenge': timeStep.toRadixString(16).padLeft(16, '0'),
});
code = OathCode(
_formatSteam(result['response']), timeStep * 30, (timeStep + 1) * 30);
} else {
var result =
await _session.command('code', target: ['accounts', credential.id]);
code = OathCode.fromJson(result);
}
log.config('Calculate', jsonEncode(code));
if (update && mounted) {
final creds = state!.toList();
final i = creds.indexWhere((e) => e.credential.id == credential.id);
state = creds..[i] = creds[i].copyWith(code: code);
}
return code;
}
Future<OathCredential> addAccount(Uri otpauth,
{bool requireTouch = false, bool update = true}) async {
var result = await _session.command('put', target: [
'accounts'
], params: {
'uri': otpauth.toString(),
'require_touch': requireTouch,
});
final credential = OathCredential.fromJson(result);
if (update && mounted) {
state = state!.toList()..add(OathPair(credential, null));
if (!requireTouch && credential.oathType == OathType.totp) {
await calculate(credential);
}
}
return credential;
}
Future<void> renameAccount(
OathCredential credential,
String? issuer,
String name,
) async {
final result = await _session.command('rename', target: [
'accounts',
credential.id,
], params: {
'issuer': issuer,
'name': name,
});
String credentialId = result['credential_id'];
if (mounted) {
final newState = state!.toList();
final index = newState.indexWhere((e) => e.credential == credential);
final oldPair = newState.removeAt(index);
newState.add(OathPair(
credential.copyWith(id: credentialId, issuer: issuer, name: name),
oldPair.code,
));
state = newState;
}
}
Future<void> deleteAccount(OathCredential credential) async {
await _session.command('delete', target: ['accounts', credential.id]);
if (mounted) {
state = state!.toList()..removeWhere((e) => e.credential == credential);
}
}
refresh() async {
if (_locked) return;
log.config('refreshing credentials...');
var result = await _session.command('calculate_all', target: ['accounts']);
log.config('Entries', jsonEncode(result));
final pairs = [];
for (var e in result['entries']) {
final credential = OathCredential.fromJson(e['credential']);
final code = e['code'] == null
? null
: credential.isSteam // Steam codes require a re-calculate
? await calculate(credential, update: false)
: OathCode.fromJson(e['code']);
pairs.add(OathPair(credential, code));
}
if (mounted) {
final current = state?.toList() ?? [];
for (var pair in pairs) {
final i =
current.indexWhere((e) => e.credential.id == pair.credential.id);
if (i < 0) {
current.add(pair);
} else if (pair.code != null) {
current[i] = current[i].copyWith(code: pair.code);
}
}
state = current;
_scheduleRefresh();
}
}
_scheduleRefresh() {
_timer?.cancel();
if (_locked) return;
if (state == null) {
refresh();
} else if (mounted) {
final expirations = (state ?? [])
.where((pair) =>
pair.credential.oathType == OathType.totp &&
!pair.credential.touchRequired)
.map((e) => e.code)
.whereType<OathCode>()
.map((e) => e.validTo);
if (expirations.isEmpty) {
_timer = null;
} else {
final earliest = expirations.reduce(min) * 1000;
final now = DateTime.now().millisecondsSinceEpoch;
if (earliest < now) {
refresh();
} else {
_timer = Timer(Duration(milliseconds: earliest - now), refresh);
}
}
}
}
Future<OathCode> calculate(OathCredential credential);
Future<OathCredential> addAccount(Uri otpauth, {bool requireTouch = false});
Future<OathCredential> renameAccount(
OathCredential credential, String? issuer, String name);
Future<void> deleteAccount(OathCredential credential);
}
final favoritesProvider =