yubioath-flutter/lib/piv/views/manage_key_dialog.dart

278 lines
9.4 KiB
Dart
Raw Normal View History

2023-04-27 10:13:38 +03:00
/*
* Copyright (C) 2022 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
2023-06-13 15:33:23 +03:00
import 'dart:math';
2023-04-27 10:13:38 +03:00
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/responsive_dialog.dart';
import '../models.dart';
import '../state.dart';
import '../keys.dart' as keys;
import 'pin_dialog.dart';
class ManageKeyDialog extends ConsumerStatefulWidget {
final DevicePath path;
final PivState pivState;
const ManageKeyDialog(this.path, this.pivState, {super.key});
@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_ManageKeyDialogState();
}
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
late bool _defaultKeyUsed;
late bool _usesStoredKey;
late bool _storeKey;
String _currentKeyOrPin = '';
bool _currentIsWrong = false;
int _attemptsRemaining = -1;
ManagementKeyType _keyType = ManagementKeyType.tdes;
2023-06-13 15:33:23 +03:00
final _keyController = TextEditingController();
2023-04-27 10:13:38 +03:00
@override
void initState() {
super.initState();
_defaultKeyUsed =
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
_usesStoredKey = widget.pivState.protectedKey;
if (!_usesStoredKey && _defaultKeyUsed) {
_currentKeyOrPin = defaultManagementKey;
}
_storeKey = _usesStoredKey;
}
2023-06-13 15:33:23 +03:00
@override
void dispose() {
_keyController.dispose();
super.dispose();
}
2023-04-27 10:13:38 +03:00
_submit() async {
final notifier = ref.read(pivStateProvider(widget.path).notifier);
if (_usesStoredKey) {
final status = (await notifier.verifyPin(_currentKeyOrPin)).when(
success: () => true,
failure: (attemptsRemaining) {
setState(() {
_attemptsRemaining = attemptsRemaining;
_currentIsWrong = true;
});
return false;
},
);
if (!status) {
return;
}
} else {
if (!await notifier.authenticate(_currentKeyOrPin)) {
setState(() {
_currentIsWrong = true;
});
return;
}
}
if (_storeKey && !_usesStoredKey) {
final withContext = ref.read(withContextProvider);
final verified = await withContext((context) async =>
await showBlurDialog(
context: context,
builder: (context) => PinDialog(widget.path))) ??
false;
if (!verified) {
return;
}
}
2023-06-13 15:33:23 +03:00
await notifier.setManagementKey(_keyController.text,
2023-04-27 10:13:38 +03:00
managementKeyType: _keyType, storeKey: _storeKey);
if (!mounted) return;
2023-06-05 16:56:13 +03:00
final l10n = AppLocalizations.of(context)!;
showMessage(context, l10n.l_management_key_changed);
2023-04-27 10:13:38 +03:00
Navigator.of(context).pop();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
2023-06-13 15:33:23 +03:00
final currentType =
widget.pivState.metadata?.managementKeyMetadata.keyType ??
ManagementKeyType.tdes;
2023-04-27 10:13:38 +03:00
final hexLength = _keyType.keyLength * 2;
2023-06-13 15:33:23 +03:00
final protected = widget.pivState.protectedKey;
final currentLenOk = protected
? _currentKeyOrPin.length >= 4
: _currentKeyOrPin.length == currentType.keyLength * 2;
2023-04-27 10:13:38 +03:00
return ResponsiveDialog(
2023-06-05 16:56:13 +03:00
title: Text(l10n.l_change_management_key),
2023-04-27 10:13:38 +03:00
actions: [
TextButton(
onPressed: _submit,
key: keys.saveButton,
child: Text(l10n.s_save),
)
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2023-06-05 16:56:13 +03:00
Text(l10n.p_change_management_key_desc),
2023-06-13 15:33:23 +03:00
if (protected)
2023-04-27 10:13:38 +03:00
TextField(
autofocus: true,
obscureText: true,
autofillHints: const [AutofillHints.password],
2023-06-13 15:33:23 +03:00
key: keys.pinPukField,
maxLength: 8,
2023-04-27 10:13:38 +03:00
decoration: InputDecoration(
border: const OutlineInputBorder(),
2023-06-05 16:56:13 +03:00
labelText: l10n.s_pin,
2023-04-27 10:13:38 +03:00
prefixIcon: const Icon(Icons.pin_outlined),
errorText: _currentIsWrong
2023-06-05 16:56:13 +03:00
? l10n
.l_wrong_pin_attempts_remaining(_attemptsRemaining)
2023-04-27 10:13:38 +03:00
: null,
errorMaxLines: 3),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentKeyOrPin = value;
});
},
),
2023-06-13 15:33:23 +03:00
if (!protected)
2023-04-27 10:13:38 +03:00
TextFormField(
2023-06-13 15:33:23 +03:00
key: keys.managementKeyField,
2023-04-27 10:13:38 +03:00
autofocus: !_defaultKeyUsed,
autofillHints: const [AutofillHints.password],
initialValue: _defaultKeyUsed ? defaultManagementKey : null,
readOnly: _defaultKeyUsed,
2023-06-13 15:33:23 +03:00
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
2023-04-27 10:13:38 +03:00
decoration: InputDecoration(
border: const OutlineInputBorder(),
2023-06-05 16:56:13 +03:00
labelText: l10n.s_current_management_key,
2023-04-27 10:13:38 +03:00
prefixIcon: const Icon(Icons.password_outlined),
2023-06-05 16:56:13 +03:00
errorText: _currentIsWrong ? l10n.l_wrong_key : null,
2023-04-27 10:13:38 +03:00
errorMaxLines: 3,
2023-06-05 16:56:13 +03:00
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
2023-04-27 10:13:38 +03:00
),
2023-06-13 15:33:23 +03:00
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp('[a-f0-9]', caseSensitive: false))
],
2023-04-27 10:13:38 +03:00
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
_currentKeyOrPin = value;
});
},
),
TextField(
key: keys.newPinPukField,
autofocus: _defaultKeyUsed,
autofillHints: const [AutofillHints.newPassword],
maxLength: hexLength,
2023-06-13 15:33:23 +03:00
controller: _keyController,
2023-04-27 10:13:38 +03:00
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(
RegExp('[a-f0-9]', caseSensitive: false))
],
decoration: InputDecoration(
border: const OutlineInputBorder(),
2023-06-05 16:56:13 +03:00
labelText: l10n.s_new_management_key,
2023-04-27 10:13:38 +03:00
prefixIcon: const Icon(Icons.password_outlined),
2023-06-13 15:33:23 +03:00
enabled: currentLenOk,
suffixIcon: IconButton(
icon: const Icon(Icons.refresh),
onPressed: currentLenOk
? () {
final random = Random.secure();
final key = List.generate(
_keyType.keyLength,
(_) => random
.nextInt(256)
.toRadixString(16)
.padLeft(2, '0')).join();
setState(() {
_keyController.text = key;
});
}
: null,
),
2023-04-27 10:13:38 +03:00
),
textInputAction: TextInputAction.next,
onSubmitted: (_) {
2023-06-13 15:33:23 +03:00
if (_keyController.text.length == hexLength) {
2023-04-27 10:13:38 +03:00
_submit();
}
},
),
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.0,
runSpacing: 8.0,
children: [
2023-06-13 15:33:23 +03:00
if (widget.pivState.metadata != null)
2023-04-27 10:13:38 +03:00
ChoiceFilterChip<ManagementKeyType>(
items: ManagementKeyType.values,
value: _keyType,
selected: _keyType != defaultManagementKeyType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
onChanged: (value) {
setState(() {
_keyType = value;
});
},
),
FilterChip(
2023-06-05 16:56:13 +03:00
label: Text(l10n.s_protect_key),
2023-04-27 10:13:38 +03:00
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
]),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
),
),
);
}
}