mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 02:01:36 +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 '../app/state.dart';
|
||||||
import 'state.dart';
|
import 'state.dart';
|
||||||
import 'views/add_account_page.dart';
|
import 'views/add_account_page.dart';
|
||||||
|
import 'views/password_dialog.dart';
|
||||||
|
|
||||||
List<MenuAction> buildOathMenuActions(
|
List<MenuAction> buildOathMenuActions(
|
||||||
BuildContext context, AutoDisposeProviderRef ref) {
|
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(
|
MenuAction(
|
||||||
text: 'Factory reset',
|
text: 'Factory reset',
|
||||||
icon: const Icon(Icons.delete_forever),
|
icon: const Icon(Icons.delete_forever),
|
||||||
|
@ -29,6 +29,10 @@ class _LockKeyNotifier extends StateNotifier<String?> {
|
|||||||
setKey(String key) {
|
setKey(String key) {
|
||||||
state = key;
|
state = key;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unsetKey() {
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final oathStateProvider = StateNotifierProvider.autoDispose
|
final oathStateProvider = StateNotifierProvider.autoDispose
|
||||||
@ -63,8 +67,12 @@ class OathStateNotifier extends StateNotifier<OathState?> {
|
|||||||
var oathState = OathState.fromJson(result['data']);
|
var oathState = OathState.fromJson(result['data']);
|
||||||
final key = _read(_lockKeyProvider(_session.devicePath));
|
final key = _read(_lockKeyProvider(_session.devicePath));
|
||||||
if (oathState.locked && key != null) {
|
if (oathState.locked && key != null) {
|
||||||
await _session.command('validate', params: {'key': key});
|
final result = await _session.command('validate', params: {'key': key});
|
||||||
|
if (result['unlocked']) {
|
||||||
oathState = oathState.copyWith(locked: false);
|
oathState = oathState.copyWith(locked: false);
|
||||||
|
} else {
|
||||||
|
_read(_lockKeyProvider(_session.devicePath).notifier).unsetKey();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
state = oathState;
|
state = oathState;
|
||||||
@ -75,12 +83,58 @@ class OathStateNotifier extends StateNotifier<OathState?> {
|
|||||||
var result =
|
var result =
|
||||||
await _session.command('derive', params: {'password': password});
|
await _session.command('derive', params: {'password': password});
|
||||||
var key = result['key'];
|
var key = result['key'];
|
||||||
await _session.command('validate', params: {'key': key});
|
final status = await _session.command('validate', params: {'key': key});
|
||||||
if (mounted) {
|
if (mounted && status['unlocked']) {
|
||||||
log.config('applet unlocked');
|
log.config('applet unlocked');
|
||||||
_read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
|
_read(_lockKeyProvider(_session.devicePath).notifier).setKey(key);
|
||||||
state = state?.copyWith(locked: false);
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,23 @@ class OathScreen extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text('YubiKey locked'),
|
const Text('Password required'),
|
||||||
TextField(
|
TextField(
|
||||||
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: true,
|
||||||
decoration: const InputDecoration(labelText: 'Password'),
|
decoration: const InputDecoration(labelText: 'Password'),
|
||||||
onSubmitted: (value) {
|
onSubmitted: (value) async {
|
||||||
ref.read(oathStateProvider(device.path).notifier).unlock(value);
|
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