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

343 lines
12 KiB
Dart
Raw Normal View History

2023-04-27 10:13:38 +03:00
/*
2023-11-10 17:24:53 +03:00
* Copyright (C) 2022-2023 Yubico.
2023-04-27 10:13:38 +03:00
*
* 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_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';
2023-12-13 21:35:17 +03:00
import '../../core/models.dart';
import '../../widgets/app_input_decoration.dart';
2023-11-10 17:24:53 +03:00
import '../../widgets/app_text_field.dart';
import '../../widgets/app_text_form_field.dart';
2023-04-27 10:13:38 +03:00
import '../../widgets/choice_filter_chip.dart';
import '../../widgets/responsive_dialog.dart';
2023-11-10 17:24:53 +03:00
import '../keys.dart' as keys;
2023-04-27 10:13:38 +03:00
import '../models.dart';
import '../state.dart';
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> {
2023-08-22 17:20:04 +03:00
late bool _hasMetadata;
2023-04-27 10:13:38 +03:00
late bool _defaultKeyUsed;
late bool _usesStoredKey;
late bool _storeKey;
bool _currentIsWrong = false;
2023-11-24 16:37:37 +03:00
bool _currentInvalidFormat = false;
bool _newInvalidFormat = false;
2023-04-27 10:13:38 +03:00
int _attemptsRemaining = -1;
late ManagementKeyType _keyType;
2023-08-22 17:20:04 +03:00
final _currentController = TextEditingController();
2023-06-13 15:33:23 +03:00
final _keyController = TextEditingController();
bool _isObscure = true;
2023-04-27 10:13:38 +03:00
@override
void initState() {
super.initState();
2023-08-22 17:20:04 +03:00
_hasMetadata = widget.pivState.metadata != null;
_keyType = widget.pivState.metadata?.managementKeyMetadata.keyType ??
defaultManagementKeyType;
2023-04-27 10:13:38 +03:00
_defaultKeyUsed =
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
_usesStoredKey = widget.pivState.protectedKey;
if (!_usesStoredKey && _defaultKeyUsed) {
2023-08-22 17:20:04 +03:00
_currentController.text = defaultManagementKey;
2023-04-27 10:13:38 +03:00
}
_storeKey = _usesStoredKey;
}
2023-06-13 15:33:23 +03:00
@override
void dispose() {
_keyController.dispose();
2023-08-22 17:20:04 +03:00
_currentController.dispose();
2023-06-13 15:33:23 +03:00
super.dispose();
}
2023-04-27 10:13:38 +03:00
_submit() async {
2023-11-24 16:37:37 +03:00
final currentInvalidFormat = Format.hex.isValid(_currentController.text);
final newInvalidFormat = Format.hex.isValid(_keyController.text);
if (!currentInvalidFormat || !newInvalidFormat) {
setState(() {
_currentInvalidFormat = !currentInvalidFormat;
_newInvalidFormat = !newInvalidFormat;
});
return;
}
2023-04-27 10:13:38 +03:00
final notifier = ref.read(pivStateProvider(widget.path).notifier);
if (_usesStoredKey) {
2023-08-22 17:20:04 +03:00
final status = (await notifier.verifyPin(_currentController.text)).when(
2023-04-27 10:13:38 +03:00
success: () => true,
failure: (attemptsRemaining) {
setState(() {
_attemptsRemaining = attemptsRemaining;
_currentIsWrong = true;
});
return false;
},
);
if (!status) {
return;
}
} else {
2023-08-22 17:20:04 +03:00
if (!await notifier.authenticate(_currentController.text)) {
2023-04-27 10:13:38 +03:00
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 ??
defaultManagementKeyType;
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;
2023-08-22 17:20:04 +03:00
final currentKeyOrPin = _currentController.text;
2023-06-13 15:33:23 +03:00
final currentLenOk = protected
2023-08-22 17:20:04 +03:00
? currentKeyOrPin.length >= 4
: currentKeyOrPin.length == currentType.keyLength * 2;
2023-06-16 18:27:10 +03:00
final newLenOk = _keyController.text.length == hexLength;
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(
2023-06-16 18:27:10 +03:00
onPressed: currentLenOk && newLenOk ? _submit : null,
2023-04-27 10:13:38 +03:00
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-11-10 17:24:53 +03:00
AppTextField(
2023-04-27 10:13:38 +03:00
autofocus: true,
obscureText: _isObscure,
2023-04-27 10:13:38 +03:00
autofillHints: const [AutofillHints.password],
2023-06-13 15:33:23 +03:00
key: keys.pinPukField,
maxLength: 8,
2023-08-22 17:20:04 +03:00
controller: _currentController,
2023-12-14 18:38:10 +03:00
decoration: AppInputDecoration(
2023-11-24 16:37:37 +03:00
border: const OutlineInputBorder(),
labelText: l10n.s_pin,
errorText: _currentIsWrong
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
: _currentInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.pin_outlined),
2023-12-14 18:38:10 +03:00
suffixIcon: IconButton(
icon: Icon(
_isObscure ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_isObscure = !_isObscure;
});
},
tooltip: _isObscure ? l10n.s_show_pin : l10n.s_hide_pin),
2023-11-24 16:37:37 +03:00
),
2023-04-27 10:13:38 +03:00
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
2023-11-24 16:37:37 +03:00
_currentInvalidFormat = false;
2023-04-27 10:13:38 +03:00
});
},
),
2023-06-13 15:33:23 +03:00
if (!protected)
2023-11-10 17:24:53 +03:00
AppTextFormField(
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],
2023-08-22 17:20:04 +03:00
controller: _currentController,
2023-04-27 10:13:38 +03:00
readOnly: _defaultKeyUsed,
2023-06-13 15:33:23 +03:00
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
decoration: AppInputDecoration(
2023-04-27 10:13:38 +03:00
border: const OutlineInputBorder(),
2023-06-05 16:56:13 +03:00
labelText: l10n.s_current_management_key,
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
2023-11-24 16:37:37 +03:00
errorText: _currentIsWrong
? l10n.l_wrong_key
: _currentInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
errorMaxLines: 3,
prefixIcon: const Icon(Icons.key_outlined),
2023-12-15 11:37:34 +03:00
suffixIcon: _hasMetadata
2023-08-22 17:20:04 +03:00
? null
2023-12-15 11:37:34 +03:00
: IconButton(
icon: Icon(_defaultKeyUsed
? Icons.auto_awesome
: Icons.auto_awesome_outlined),
tooltip: l10n.s_use_default,
onPressed: () {
setState(() {
_defaultKeyUsed = !_defaultKeyUsed;
if (_defaultKeyUsed) {
_currentController.text = defaultManagementKey;
} else {
_currentController.clear();
}
});
},
),
2023-04-27 10:13:38 +03:00
),
textInputAction: TextInputAction.next,
onChanged: (value) {
setState(() {
_currentIsWrong = false;
});
},
),
2023-11-10 17:24:53 +03:00
AppTextField(
2023-04-27 10:13:38 +03:00
key: keys.newPinPukField,
autofocus: _defaultKeyUsed,
autofillHints: const [AutofillHints.newPassword],
maxLength: hexLength,
2023-06-13 15:33:23 +03:00
controller: _keyController,
2023-12-14 18:38:10 +03:00
decoration: AppInputDecoration(
2023-04-27 10:13:38 +03:00
border: const OutlineInputBorder(),
2023-06-05 16:56:13 +03:00
labelText: l10n.s_new_management_key,
2023-11-24 16:37:37 +03:00
errorText: _newInvalidFormat
? l10n.l_invalid_format_allowed_chars(
Format.hex.allowedCharacters)
: null,
2023-06-13 15:33:23 +03:00
enabled: currentLenOk,
2023-11-24 16:37:37 +03:00
prefixIcon: const Icon(Icons.key_outlined),
2023-12-14 18:38:10 +03:00
suffixIcon: IconButton(
2023-12-15 16:34:10 +03:00
key: keys.managementKeyRefresh,
2023-12-14 18:38:10 +03:00
icon: const Icon(Icons.refresh),
tooltip: l10n.s_generate_random,
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;
_newInvalidFormat = false;
});
}
: null,
2023-06-13 15:33:23 +03:00
),
2023-04-27 10:13:38 +03:00
),
textInputAction: TextInputAction.next,
onChanged: (_) {
setState(() {
// Update length
});
},
2023-04-27 10:13:38 +03:00
onSubmitted: (_) {
2023-06-16 18:27:10 +03:00
if (currentLenOk && newLenOk) {
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 != currentType,
2023-04-27 10:13:38 +03:00
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
onChanged: (value) {
setState(() {
_keyType = value;
});
},
),
FilterChip(
2023-11-28 13:30:22 +03:00
key: keys.pinLockManagementKeyChip,
2023-12-20 17:09:31 +03:00
backgroundColor:
Theme.of(context).colorScheme.surfaceVariant,
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(),
),
),
);
}
}