mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-22 17:51:29 +03:00
Add "Manage password" dialog.
This commit is contained in:
parent
b81d020d26
commit
65560e728d
@ -5,6 +5,7 @@ import '../app/models.dart';
|
||||
import '../app/state.dart';
|
||||
import 'state.dart';
|
||||
import 'views/add_account_page.dart';
|
||||
import 'views/password_dialog.dart';
|
||||
|
||||
List<MenuAction> buildOathMenuActions(
|
||||
BuildContext context, AutoDisposeProviderRef ref) {
|
||||
@ -25,6 +26,17 @@ List<MenuAction> buildOathMenuActions(
|
||||
);
|
||||
},
|
||||
),
|
||||
if (!state.locked)
|
||||
MenuAction(
|
||||
text: 'Manage password',
|
||||
icon: const Icon(Icons.password),
|
||||
action: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => ManagePasswordDialog(device),
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
text: 'Factory reset',
|
||||
icon: const Icon(Icons.delete_forever),
|
||||
|
@ -29,6 +29,10 @@ class _LockKeyNotifier extends StateNotifier<String?> {
|
||||
setKey(String key) {
|
||||
state = key;
|
||||
}
|
||||
|
||||
unsetKey() {
|
||||
state = null;
|
||||
}
|
||||
}
|
||||
|
||||
final oathStateProvider = StateNotifierProvider.autoDispose
|
||||
@ -63,8 +67,12 @@ class OathStateNotifier extends StateNotifier<OathState?> {
|
||||
var oathState = OathState.fromJson(result['data']);
|
||||
final key = _read(_lockKeyProvider(_session.devicePath));
|
||||
if (oathState.locked && key != null) {
|
||||
await _session.command('validate', params: {'key': key});
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
final result = await _session.command('validate', params: {'key': key});
|
||||
if (result['unlocked']) {
|
||||
oathState = oathState.copyWith(locked: false);
|
||||
} else {
|
||||
_read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
}
|
||||
}
|
||||
if (mounted) {
|
||||
state = oathState;
|
||||
@ -75,12 +83,58 @@ class OathStateNotifier extends StateNotifier<OathState?> {
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
var key = result['key'];
|
||||
await _session.command('validate', params: {'key': key});
|
||||
if (mounted) {
|
||||
final status = await _session.command('validate', params: {'key': key});
|
||||
if (mounted && status['unlocked']) {
|
||||
log.config('applet unlocked');
|
||||
_read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||
state = state?.copyWith(locked: false);
|
||||
}
|
||||
return status['unlocked'];
|
||||
}
|
||||
|
||||
Future<bool> _checkPassword(String password) async {
|
||||
log.info('Calling check password $password');
|
||||
var result =
|
||||
await _session.command('derive', params: {'password': password});
|
||||
log.info(
|
||||
'Check ${_read(_lockKeyProvider(_session.devicePath))} == ${result['key']}');
|
||||
return _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');
|
||||
_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');
|
||||
_read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||
if (mounted) {
|
||||
state = state?.copyWith(hasKey: false, locked: false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -28,12 +28,23 @@ class OathScreen extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('YubiKey locked'),
|
||||
const Text('Password required'),
|
||||
TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
onSubmitted: (value) {
|
||||
ref.read(oathStateProvider(device.path).notifier).unlock(value);
|
||||
onSubmitted: (value) async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(device.path).notifier)
|
||||
.unlock(value);
|
||||
if (!result) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Wrong password'),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
|
175
lib/oath/views/password_dialog.dart
Executable file
175
lib/oath/views/password_dialog.dart
Executable file
@ -0,0 +1,175 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
import '../../app/models.dart';
|
||||
import '../../app/state.dart';
|
||||
import '../state.dart';
|
||||
|
||||
final log = Logger('oath.views.password_dialog');
|
||||
|
||||
class ManagePasswordDialog extends ConsumerStatefulWidget {
|
||||
final DeviceNode device;
|
||||
const ManagePasswordDialog(this.device, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
ConsumerState<ConsumerStatefulWidget> createState() =>
|
||||
_ManagePasswordDialogState();
|
||||
}
|
||||
|
||||
class _ManagePasswordDialogState extends ConsumerState<ManagePasswordDialog> {
|
||||
String _currentPassword = '';
|
||||
String _newPassword = '';
|
||||
String _confirmPassword = '';
|
||||
bool _currentIsWrong = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// If current device changes, we need to pop back to the main Page.
|
||||
ref.listen<DeviceNode?>(currentDeviceProvider, (previous, next) {
|
||||
Navigator.of(context).pop();
|
||||
});
|
||||
|
||||
final state = ref.watch(oathStateProvider(widget.device.path));
|
||||
final hasKey = state?.hasKey ?? false;
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Manage password'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (hasKey)
|
||||
Column(
|
||||
children: [
|
||||
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.'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: true,
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Current',
|
||||
errorText:
|
||||
_currentIsWrong ? 'Wrong password' : null),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_currentPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
child: const Text('Remove'),
|
||||
onPressed: _currentPassword.isNotEmpty
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(
|
||||
widget.device.path)
|
||||
.notifier)
|
||||
.unsetPassword(_currentPassword);
|
||||
if (result) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Password removed'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Text(
|
||||
'Enter your new password. A password may contain letters, numbers and other characters.'),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
autofocus: !hasKey,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_newPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Confirm',
|
||||
),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_confirmPassword = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _newPassword.isNotEmpty && _newPassword == _confirmPassword
|
||||
? () async {
|
||||
final result = await ref
|
||||
.read(oathStateProvider(widget.device.path).notifier)
|
||||
.setPassword(_currentPassword, _newPassword);
|
||||
if (result) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Password set'),
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setState(() {
|
||||
_currentIsWrong = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: const Text('Save'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user