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

382 lines
14 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';
2024-03-08 11:30:47 +03:00
import 'package:material_symbols_icons/symbols.dart';
2023-04-27 10:13:38 +03:00
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
2023-12-13 21:35:17 +03:00
import '../../core/models.dart';
2024-07-14 20:51:14 +03:00
import '../../management/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';
2024-03-11 18:24:38 +03:00
import '../../widgets/utf8_utils.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;
2024-03-06 16:09:12 +03:00
late bool _defaultPinUsed;
2023-04-27 10:13:38 +03:00
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();
final _currentFocus = FocusNode();
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;
2024-03-06 16:09:12 +03:00
_defaultPinUsed =
widget.pivState.metadata?.pinMetadata.defaultValue ?? false;
2023-04-27 10:13:38 +03:00
_usesStoredKey = widget.pivState.protectedKey;
if (!_usesStoredKey && _defaultKeyUsed) {
2023-08-22 17:20:04 +03:00
_currentController.text = defaultManagementKey;
2024-03-06 16:09:12 +03:00
} else if (_usesStoredKey && _defaultPinUsed) {
_currentController.text = defaultPin;
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();
_currentFocus.dispose();
2023-06-13 15:33:23 +03:00
super.dispose();
}
2023-04-27 10:13:38 +03:00
_submit() async {
2024-03-11 17:26:03 +03:00
final currentValidFormat =
_usesStoredKey || Format.hex.isValid(_currentController.text);
final newValidFormat = Format.hex.isValid(_keyController.text);
if (!currentValidFormat || !newValidFormat) {
2023-11-24 16:37:37 +03:00
setState(() {
2024-03-11 17:26:03 +03:00
_currentInvalidFormat = !currentValidFormat;
_newInvalidFormat = !newValidFormat;
2023-11-24 16:37:37 +03:00
});
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,
2024-03-26 16:07:23 +03:00
failure: (reason) {
reason.maybeWhen(
invalidPin: (attemptsRemaining) {
_currentController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentController.text.length);
_currentFocus.requestFocus();
setState(() {
_attemptsRemaining = attemptsRemaining;
_currentIsWrong = true;
});
},
orElse: () {},
);
2023-04-27 10:13:38 +03:00
return false;
},
);
if (!status) {
return;
}
} else {
2023-08-22 17:20:04 +03:00
if (!await notifier.authenticate(_currentController.text)) {
_currentController.selection = TextSelection(
baseOffset: 0, extentOffset: _currentController.text.length);
_currentFocus.requestFocus();
2023-04-27 10:13:38 +03:00
setState(() {
_currentIsWrong = true;
});
return;
}
}
if (_storeKey && !_usesStoredKey) {
2024-03-06 16:09:12 +03:00
if (_defaultPinUsed) {
await notifier.verifyPin(defaultPin);
} else {
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-04-27 10:13:38 +03:00
}
}
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-08-22 17:20:04 +03:00
final currentKeyOrPin = _currentController.text;
2024-03-06 16:09:12 +03:00
final currentLenOk = _usesStoredKey
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;
2024-07-14 20:51:14 +03:00
final (fipsCapable, fipsApproved) = ref
.watch(currentDeviceDataProvider)
.valueOrNull
?.info
.getFipsStatus(Capability.piv) ??
(false, false);
final fipsUnready = fipsCapable && !fipsApproved;
final managementKeyTypes = ManagementKeyType.values.toList();
if (fipsCapable) {
managementKeyTypes.remove(ManagementKeyType.tdes);
}
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:
!_currentIsWrong && 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),
2024-03-06 16:09:12 +03:00
if (_usesStoredKey)
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,
2024-03-11 18:24:38 +03:00
inputFormatters: [limitBytesLength(8)],
buildCounter: buildByteCounterFor(_currentController.text),
2023-08-22 17:20:04 +03:00
controller: _currentController,
focusNode: _currentFocus,
2024-03-06 16:09:12 +03:00
readOnly: _defaultPinUsed,
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,
2024-03-06 16:09:12 +03:00
helperText: _defaultPinUsed ? l10n.l_default_pin_used : null,
2023-11-24 16:37:37 +03:00
errorText: _currentIsWrong
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
2024-03-11 17:26:03 +03:00
: null,
2023-11-24 16:37:37 +03:00
errorMaxLines: 3,
2024-03-08 11:30:47 +03:00
prefixIcon: const Icon(Symbols.pin),
2023-12-14 18:38:10 +03:00
suffixIcon: IconButton(
2024-03-08 11:30:47 +03:00
icon: Icon(_isObscure
? Symbols.visibility
: Symbols.visibility_off),
2023-12-14 18:38:10 +03:00
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
});
},
).init(),
2024-03-06 16:09:12 +03:00
if (!_usesStoredKey)
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,
focusNode: _currentFocus,
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,
2024-03-08 11:30:47 +03:00
prefixIcon: const Icon(Symbols.key),
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(
2024-03-08 11:30:47 +03:00
icon: Icon(Symbols.auto_awesome,
fill: _defaultKeyUsed ? 1.0 : 0.0),
2023-12-15 11:37:34 +03:00
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;
});
},
).init(),
2023-11-10 17:24:53 +03:00
AppTextField(
2024-09-10 11:30:29 +03:00
key: keys.newManagementKeyField,
2023-04-27 10:13:38 +03:00
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,
2024-03-08 11:30:47 +03:00
prefixIcon: const Icon(Symbols.key),
2023-12-14 18:38:10 +03:00
suffixIcon: IconButton(
2023-12-15 16:34:10 +03:00
key: keys.managementKeyRefresh,
2024-03-08 11:30:47 +03:00
icon: const Icon(Symbols.refresh),
2023-12-14 18:38:10 +03:00
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();
}
},
).init(),
2023-04-27 10:13:38 +03:00
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>(
2024-07-14 20:51:14 +03:00
items: managementKeyTypes,
2023-04-27 10:13:38 +03:00
value: _keyType,
selected: _keyType != currentType,
2023-04-27 10:13:38 +03:00
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
onChanged: (value) {
setState(() {
_keyType = value;
});
},
),
2024-07-14 20:51:14 +03:00
if (!fipsUnready)
FilterChip(
key: keys.pinLockManagementKeyChip,
label: Text(l10n.s_protect_key),
selected: _storeKey,
onSelected: (value) {
setState(() {
_storeKey = value;
});
},
),
2023-04-27 10:13:38 +03:00
]),
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
),
),
);
}
}