mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-23 18:22:39 +03:00
Merge PR #1154.
This commit is contained in:
commit
ad645a4b43
@ -12,6 +12,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from yubikit.core import InvalidPinError
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -123,6 +124,8 @@ class RpcNode:
|
|||||||
except ChildResetException as e:
|
except ChildResetException as e:
|
||||||
self._close_child()
|
self._close_child()
|
||||||
raise StateResetException(e.message, traversed)
|
raise StateResetException(e.message, traversed)
|
||||||
|
except InvalidPinError:
|
||||||
|
raise # Prevent catching this as a ValueError below
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise InvalidParametersException(e)
|
raise InvalidParametersException(e)
|
||||||
raise NoSuchActionException(action)
|
raise NoSuchActionException(action)
|
||||||
|
@ -21,13 +21,12 @@ from .base import (
|
|||||||
TimeoutException,
|
TimeoutException,
|
||||||
AuthRequiredException,
|
AuthRequiredException,
|
||||||
)
|
)
|
||||||
from yubikit.core import NotSupportedError, BadResponseError
|
from yubikit.core import NotSupportedError, BadResponseError, InvalidPinError
|
||||||
from yubikit.core.smartcard import ApduError, SW
|
from yubikit.core.smartcard import ApduError, SW
|
||||||
from yubikit.piv import (
|
from yubikit.piv import (
|
||||||
PivSession,
|
PivSession,
|
||||||
OBJECT_ID,
|
OBJECT_ID,
|
||||||
MANAGEMENT_KEY_TYPE,
|
MANAGEMENT_KEY_TYPE,
|
||||||
InvalidPinError,
|
|
||||||
SLOT,
|
SLOT,
|
||||||
require_version,
|
require_version,
|
||||||
KEY_TYPE,
|
KEY_TYPE,
|
||||||
@ -43,6 +42,7 @@ from ykman.piv import (
|
|||||||
generate_self_signed_certificate,
|
generate_self_signed_certificate,
|
||||||
generate_csr,
|
generate_csr,
|
||||||
generate_chuid,
|
generate_chuid,
|
||||||
|
parse_rfc4514_string,
|
||||||
)
|
)
|
||||||
from ykman.util import (
|
from ykman.util import (
|
||||||
parse_certificates,
|
parse_certificates,
|
||||||
@ -234,6 +234,34 @@ class PivNode(RpcNode):
|
|||||||
def slots(self):
|
def slots(self):
|
||||||
return SlotsNode(self.session)
|
return SlotsNode(self.session)
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def examine_file(self, params, event, signal):
|
||||||
|
data = bytes.fromhex(params.pop("data"))
|
||||||
|
password = params.pop("password", None)
|
||||||
|
try:
|
||||||
|
private_key, certs = _parse_file(data, password)
|
||||||
|
certificate = _choose_cert(certs)
|
||||||
|
|
||||||
|
return dict(
|
||||||
|
status=True,
|
||||||
|
password=password is not None,
|
||||||
|
key_type=KEY_TYPE.from_public_key(private_key.public_key())
|
||||||
|
if private_key
|
||||||
|
else None,
|
||||||
|
cert_info=_get_cert_info(certificate),
|
||||||
|
)
|
||||||
|
except InvalidPasswordError:
|
||||||
|
logger.debug("Invalid or missing password", exc_info=True)
|
||||||
|
return dict(status=False)
|
||||||
|
|
||||||
|
@action(closes_child=False)
|
||||||
|
def validate_rfc4514(self, params, event, signal):
|
||||||
|
try:
|
||||||
|
parse_rfc4514_string(params.pop("data"))
|
||||||
|
return dict(status=True)
|
||||||
|
except ValueError:
|
||||||
|
return dict(status=False)
|
||||||
|
|
||||||
|
|
||||||
def _slot_for(name):
|
def _slot_for(name):
|
||||||
return SLOT(int(name, base=16))
|
return SLOT(int(name, base=16))
|
||||||
@ -255,6 +283,29 @@ def _parse_file(data, password=None):
|
|||||||
return private_key, certs
|
return private_key, certs
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_cert(certs):
|
||||||
|
if certs:
|
||||||
|
if len(certs) > 1:
|
||||||
|
leafs = get_leaf_certificates(certs)
|
||||||
|
return leafs[0]
|
||||||
|
else:
|
||||||
|
return certs[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_cert_info(cert):
|
||||||
|
if cert is None:
|
||||||
|
return None
|
||||||
|
return dict(
|
||||||
|
subject=cert.subject.rfc4514_string(),
|
||||||
|
issuer=cert.issuer.rfc4514_string(),
|
||||||
|
serial=hex(cert.serial_number)[2:],
|
||||||
|
not_valid_before=cert.not_valid_before.isoformat(),
|
||||||
|
not_valid_after=cert.not_valid_after.isoformat(),
|
||||||
|
fingerprint=cert.fingerprint(hashes.SHA256()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SlotsNode(RpcNode):
|
class SlotsNode(RpcNode):
|
||||||
def __init__(self, session):
|
def __init__(self, session):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@ -290,16 +341,7 @@ class SlotsNode(RpcNode):
|
|||||||
slot=int(slot),
|
slot=int(slot),
|
||||||
name=slot.name,
|
name=slot.name,
|
||||||
has_key=metadata is not None if self._has_metadata else None,
|
has_key=metadata is not None if self._has_metadata else None,
|
||||||
cert_info=dict(
|
cert_info=_get_cert_info(cert),
|
||||||
subject=cert.subject.rfc4514_string(),
|
|
||||||
issuer=cert.issuer.rfc4514_string(),
|
|
||||||
serial=hex(cert.serial_number)[2:],
|
|
||||||
not_valid_before=cert.not_valid_before.isoformat(),
|
|
||||||
not_valid_after=cert.not_valid_after.isoformat(),
|
|
||||||
fingerprint=cert.fingerprint(hashes.SHA256()),
|
|
||||||
)
|
|
||||||
if cert
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
for slot, (metadata, cert) in self._slots.items()
|
for slot, (metadata, cert) in self._slots.items()
|
||||||
}
|
}
|
||||||
@ -311,22 +353,6 @@ class SlotsNode(RpcNode):
|
|||||||
return SlotNode(self.session, slot, metadata, certificate, self.refresh)
|
return SlotNode(self.session, slot, metadata, certificate, self.refresh)
|
||||||
return super().create_child(name)
|
return super().create_child(name)
|
||||||
|
|
||||||
@action
|
|
||||||
def examine_file(self, params, event, signal):
|
|
||||||
data = bytes.fromhex(params.pop("data"))
|
|
||||||
password = params.pop("password", None)
|
|
||||||
try:
|
|
||||||
private_key, certs = _parse_file(data, password)
|
|
||||||
return dict(
|
|
||||||
status=True,
|
|
||||||
password=password is not None,
|
|
||||||
private_key=bool(private_key),
|
|
||||||
certificates=len(certs),
|
|
||||||
)
|
|
||||||
except InvalidPasswordError:
|
|
||||||
logger.debug("Invalid or missing password", exc_info=True)
|
|
||||||
return dict(status=False)
|
|
||||||
|
|
||||||
|
|
||||||
class SlotNode(RpcNode):
|
class SlotNode(RpcNode):
|
||||||
def __init__(self, session, slot, metadata, certificate, refresh):
|
def __init__(self, session, slot, metadata, certificate, refresh):
|
||||||
@ -382,12 +408,8 @@ class SlotNode(RpcNode):
|
|||||||
except (ApduError, BadResponseError):
|
except (ApduError, BadResponseError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if certs:
|
certificate = _choose_cert(certs)
|
||||||
if len(certs) > 1:
|
if certificate:
|
||||||
leafs = get_leaf_certificates(certs)
|
|
||||||
certificate = leafs[0]
|
|
||||||
else:
|
|
||||||
certificate = certs[0]
|
|
||||||
self.session.put_certificate(self.slot, certificate)
|
self.session.put_certificate(self.slot, certificate)
|
||||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||||
self.certificate = certificate
|
self.certificate = certificate
|
||||||
@ -414,7 +436,9 @@ class SlotNode(RpcNode):
|
|||||||
pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT))
|
pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT))
|
||||||
touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT))
|
touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT))
|
||||||
subject = params.pop("subject")
|
subject = params.pop("subject")
|
||||||
generate_type = GENERATE_TYPE(params.pop("generate_type", GENERATE_TYPE.CERTIFICATE))
|
generate_type = GENERATE_TYPE(
|
||||||
|
params.pop("generate_type", GENERATE_TYPE.CERTIFICATE)
|
||||||
|
)
|
||||||
public_key = self.session.generate_key(
|
public_key = self.session.generate_key(
|
||||||
self.slot, key_type, pin_policy, touch_policy
|
self.slot, key_type, pin_policy, touch_policy
|
||||||
)
|
)
|
||||||
|
@ -380,9 +380,7 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PivExamineResult> examine(String data, {String? password}) async {
|
Future<PivExamineResult> examine(String data, {String? password}) async {
|
||||||
final result = await _session.command('examine_file', target: [
|
final result = await _session.command('examine_file', params: {
|
||||||
'slots',
|
|
||||||
], params: {
|
|
||||||
'data': data,
|
'data': data,
|
||||||
'password': password,
|
'password': password,
|
||||||
});
|
});
|
||||||
@ -394,6 +392,14 @@ class _DesktopPivSlotsNotifier extends PivSlotsNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> validateRfc4514(String value) async {
|
||||||
|
final result = await _session.command('validate_rfc4514', params: {
|
||||||
|
'data': value,
|
||||||
|
});
|
||||||
|
return result['status'];
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PivImportResult> import(SlotId slot, String data,
|
Future<PivImportResult> import(SlotId slot, String data,
|
||||||
{String? password,
|
{String? password,
|
||||||
|
@ -30,10 +30,12 @@
|
|||||||
"s_unlock": "Unlock",
|
"s_unlock": "Unlock",
|
||||||
"s_calculate": "Calculate",
|
"s_calculate": "Calculate",
|
||||||
"s_import": "Import",
|
"s_import": "Import",
|
||||||
|
"s_overwrite": "Overwrite",
|
||||||
"s_label": "Label",
|
"s_label": "Label",
|
||||||
"s_name": "Name",
|
"s_name": "Name",
|
||||||
"s_usb": "USB",
|
"s_usb": "USB",
|
||||||
"s_nfc": "NFC",
|
"s_nfc": "NFC",
|
||||||
|
"s_options": "Options",
|
||||||
"s_show_window": "Show window",
|
"s_show_window": "Show window",
|
||||||
"s_hide_window": "Hide window",
|
"s_hide_window": "Hide window",
|
||||||
"q_rename_target": "Rename {label}?",
|
"q_rename_target": "Rename {label}?",
|
||||||
@ -48,14 +50,9 @@
|
|||||||
"item": {}
|
"item": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"s_definition": "{item}:",
|
|
||||||
"@s_definition" : {
|
|
||||||
"placeholders": {
|
|
||||||
"item": {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"s_about": "About",
|
"s_about": "About",
|
||||||
|
"s_algorithm": "Algorithm",
|
||||||
"s_appearance": "Appearance",
|
"s_appearance": "Appearance",
|
||||||
"s_authenticator": "Authenticator",
|
"s_authenticator": "Authenticator",
|
||||||
"s_actions": "Actions",
|
"s_actions": "Actions",
|
||||||
@ -281,6 +278,8 @@
|
|||||||
"p_change_management_key_desc": "Change your management key. You can optionally choose to allow the PIN to be used instead of the management key.",
|
"p_change_management_key_desc": "Change your management key. You can optionally choose to allow the PIN to be used instead of the management key.",
|
||||||
"l_management_key_changed": "Management key changed",
|
"l_management_key_changed": "Management key changed",
|
||||||
"l_default_key_used": "Default management key used",
|
"l_default_key_used": "Default management key used",
|
||||||
|
"s_generate_random": "Generate random",
|
||||||
|
"s_use_default": "Use default",
|
||||||
"l_warning_default_key": "Warning: Default key used",
|
"l_warning_default_key": "Warning: Default key used",
|
||||||
"s_protect_key": "Protect with PIN",
|
"s_protect_key": "Protect with PIN",
|
||||||
"l_pin_protected_key": "PIN can be used instead",
|
"l_pin_protected_key": "PIN can be used instead",
|
||||||
@ -457,12 +456,26 @@
|
|||||||
},
|
},
|
||||||
"l_certificate_deleted": "Certificate deleted",
|
"l_certificate_deleted": "Certificate deleted",
|
||||||
"p_password_protected_file": "The selected file is password protected. Enter the password to proceed.",
|
"p_password_protected_file": "The selected file is password protected. Enter the password to proceed.",
|
||||||
"p_import_items_desc": "The following items will be imported into PIV slot {slot}.",
|
"p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.",
|
||||||
"@p_import_items_desc" : {
|
"@p_import_items_desc" : {
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
"slot": {}
|
"slot": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"p_subject_desc": "A distinguished name (DN) formatted in accordance to the RFC 4514 specification.",
|
||||||
|
"l_rfc4514_invalid": "Invalid RFC 4514 format",
|
||||||
|
"rfc4514_examples": "Examples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
|
||||||
|
"p_cert_options_desc": "Key algorithm to use, output format, and expiration date (certificate only).",
|
||||||
|
"s_overwrite_slot": "Overwrite slot",
|
||||||
|
"p_overwrite_slot_desc": "This will permanently overwrite existing content in slot {slot}.",
|
||||||
|
"@p_overwrite_slot_desc" : {
|
||||||
|
"placeholders": {
|
||||||
|
"slot": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"l_overwrite_cert": "The certificate will be overwritten",
|
||||||
|
"l_overwrite_key": "The private key will be overwritten",
|
||||||
|
"l_overwrite_key_maybe": "Any existing private key in the slot will be overwritten",
|
||||||
|
|
||||||
"@_piv_slots": {},
|
"@_piv_slots": {},
|
||||||
"s_slot_display_name": "{name} ({hexid})",
|
"s_slot_display_name": "{name} ({hexid})",
|
||||||
|
@ -266,8 +266,8 @@ class PivSlot with _$PivSlot {
|
|||||||
class PivExamineResult with _$PivExamineResult {
|
class PivExamineResult with _$PivExamineResult {
|
||||||
factory PivExamineResult.result({
|
factory PivExamineResult.result({
|
||||||
required bool password,
|
required bool password,
|
||||||
required bool privateKey,
|
required KeyType? keyType,
|
||||||
required int certificates,
|
required CertInfo? certInfo,
|
||||||
}) = _ExamineResult;
|
}) = _ExamineResult;
|
||||||
factory PivExamineResult.invalidPassword() = _InvalidPassword;
|
factory PivExamineResult.invalidPassword() = _InvalidPassword;
|
||||||
|
|
||||||
|
@ -1860,20 +1860,23 @@ PivExamineResult _$PivExamineResultFromJson(Map<String, dynamic> json) {
|
|||||||
mixin _$PivExamineResult {
|
mixin _$PivExamineResult {
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(bool password, bool privateKey, int certificates)
|
required TResult Function(
|
||||||
|
bool password, KeyType? keyType, CertInfo? certInfo)
|
||||||
result,
|
result,
|
||||||
required TResult Function() invalidPassword,
|
required TResult Function() invalidPassword,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(bool password, bool privateKey, int certificates)? result,
|
TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult? Function()? invalidPassword,
|
TResult? Function()? invalidPassword,
|
||||||
}) =>
|
}) =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(bool password, bool privateKey, int certificates)? result,
|
TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult Function()? invalidPassword,
|
TResult Function()? invalidPassword,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) =>
|
}) =>
|
||||||
@ -1924,7 +1927,9 @@ abstract class _$$_ExamineResultCopyWith<$Res> {
|
|||||||
_$_ExamineResult value, $Res Function(_$_ExamineResult) then) =
|
_$_ExamineResult value, $Res Function(_$_ExamineResult) then) =
|
||||||
__$$_ExamineResultCopyWithImpl<$Res>;
|
__$$_ExamineResultCopyWithImpl<$Res>;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({bool password, bool privateKey, int certificates});
|
$Res call({bool password, KeyType? keyType, CertInfo? certInfo});
|
||||||
|
|
||||||
|
$CertInfoCopyWith<$Res>? get certInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1939,24 +1944,36 @@ class __$$_ExamineResultCopyWithImpl<$Res>
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? password = null,
|
Object? password = null,
|
||||||
Object? privateKey = null,
|
Object? keyType = freezed,
|
||||||
Object? certificates = null,
|
Object? certInfo = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$_ExamineResult(
|
return _then(_$_ExamineResult(
|
||||||
password: null == password
|
password: null == password
|
||||||
? _value.password
|
? _value.password
|
||||||
: password // ignore: cast_nullable_to_non_nullable
|
: password // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as bool,
|
||||||
privateKey: null == privateKey
|
keyType: freezed == keyType
|
||||||
? _value.privateKey
|
? _value.keyType
|
||||||
: privateKey // ignore: cast_nullable_to_non_nullable
|
: keyType // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,
|
as KeyType?,
|
||||||
certificates: null == certificates
|
certInfo: freezed == certInfo
|
||||||
? _value.certificates
|
? _value.certInfo
|
||||||
: certificates // ignore: cast_nullable_to_non_nullable
|
: certInfo // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as CertInfo?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$CertInfoCopyWith<$Res>? get certInfo {
|
||||||
|
if (_value.certInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $CertInfoCopyWith<$Res>(_value.certInfo!, (value) {
|
||||||
|
return _then(_value.copyWith(certInfo: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -1964,8 +1981,8 @@ class __$$_ExamineResultCopyWithImpl<$Res>
|
|||||||
class _$_ExamineResult implements _ExamineResult {
|
class _$_ExamineResult implements _ExamineResult {
|
||||||
_$_ExamineResult(
|
_$_ExamineResult(
|
||||||
{required this.password,
|
{required this.password,
|
||||||
required this.privateKey,
|
required this.keyType,
|
||||||
required this.certificates,
|
required this.certInfo,
|
||||||
final String? $type})
|
final String? $type})
|
||||||
: $type = $type ?? 'result';
|
: $type = $type ?? 'result';
|
||||||
|
|
||||||
@ -1975,16 +1992,16 @@ class _$_ExamineResult implements _ExamineResult {
|
|||||||
@override
|
@override
|
||||||
final bool password;
|
final bool password;
|
||||||
@override
|
@override
|
||||||
final bool privateKey;
|
final KeyType? keyType;
|
||||||
@override
|
@override
|
||||||
final int certificates;
|
final CertInfo? certInfo;
|
||||||
|
|
||||||
@JsonKey(name: 'runtimeType')
|
@JsonKey(name: 'runtimeType')
|
||||||
final String $type;
|
final String $type;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PivExamineResult.result(password: $password, privateKey: $privateKey, certificates: $certificates)';
|
return 'PivExamineResult.result(password: $password, keyType: $keyType, certInfo: $certInfo)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1994,16 +2011,14 @@ class _$_ExamineResult implements _ExamineResult {
|
|||||||
other is _$_ExamineResult &&
|
other is _$_ExamineResult &&
|
||||||
(identical(other.password, password) ||
|
(identical(other.password, password) ||
|
||||||
other.password == password) &&
|
other.password == password) &&
|
||||||
(identical(other.privateKey, privateKey) ||
|
(identical(other.keyType, keyType) || other.keyType == keyType) &&
|
||||||
other.privateKey == privateKey) &&
|
(identical(other.certInfo, certInfo) ||
|
||||||
(identical(other.certificates, certificates) ||
|
other.certInfo == certInfo));
|
||||||
other.certificates == certificates));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => Object.hash(runtimeType, password, keyType, certInfo);
|
||||||
Object.hash(runtimeType, password, privateKey, certificates);
|
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@override
|
@override
|
||||||
@ -2014,31 +2029,34 @@ class _$_ExamineResult implements _ExamineResult {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(bool password, bool privateKey, int certificates)
|
required TResult Function(
|
||||||
|
bool password, KeyType? keyType, CertInfo? certInfo)
|
||||||
result,
|
result,
|
||||||
required TResult Function() invalidPassword,
|
required TResult Function() invalidPassword,
|
||||||
}) {
|
}) {
|
||||||
return result(password, privateKey, certificates);
|
return result(password, keyType, certInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(bool password, bool privateKey, int certificates)? result,
|
TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult? Function()? invalidPassword,
|
TResult? Function()? invalidPassword,
|
||||||
}) {
|
}) {
|
||||||
return result?.call(password, privateKey, certificates);
|
return result?.call(password, keyType, certInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(bool password, bool privateKey, int certificates)? result,
|
TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult Function()? invalidPassword,
|
TResult Function()? invalidPassword,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
return result(password, privateKey, certificates);
|
return result(password, keyType, certInfo);
|
||||||
}
|
}
|
||||||
return orElse();
|
return orElse();
|
||||||
}
|
}
|
||||||
@ -2085,15 +2103,15 @@ class _$_ExamineResult implements _ExamineResult {
|
|||||||
abstract class _ExamineResult implements PivExamineResult {
|
abstract class _ExamineResult implements PivExamineResult {
|
||||||
factory _ExamineResult(
|
factory _ExamineResult(
|
||||||
{required final bool password,
|
{required final bool password,
|
||||||
required final bool privateKey,
|
required final KeyType? keyType,
|
||||||
required final int certificates}) = _$_ExamineResult;
|
required final CertInfo? certInfo}) = _$_ExamineResult;
|
||||||
|
|
||||||
factory _ExamineResult.fromJson(Map<String, dynamic> json) =
|
factory _ExamineResult.fromJson(Map<String, dynamic> json) =
|
||||||
_$_ExamineResult.fromJson;
|
_$_ExamineResult.fromJson;
|
||||||
|
|
||||||
bool get password;
|
bool get password;
|
||||||
bool get privateKey;
|
KeyType? get keyType;
|
||||||
int get certificates;
|
CertInfo? get certInfo;
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
_$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith =>
|
_$$_ExamineResultCopyWith<_$_ExamineResult> get copyWith =>
|
||||||
throw _privateConstructorUsedError;
|
throw _privateConstructorUsedError;
|
||||||
@ -2145,7 +2163,8 @@ class _$_InvalidPassword implements _InvalidPassword {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult when<TResult extends Object?>({
|
TResult when<TResult extends Object?>({
|
||||||
required TResult Function(bool password, bool privateKey, int certificates)
|
required TResult Function(
|
||||||
|
bool password, KeyType? keyType, CertInfo? certInfo)
|
||||||
result,
|
result,
|
||||||
required TResult Function() invalidPassword,
|
required TResult Function() invalidPassword,
|
||||||
}) {
|
}) {
|
||||||
@ -2155,7 +2174,8 @@ class _$_InvalidPassword implements _InvalidPassword {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult? whenOrNull<TResult extends Object?>({
|
TResult? whenOrNull<TResult extends Object?>({
|
||||||
TResult? Function(bool password, bool privateKey, int certificates)? result,
|
TResult? Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult? Function()? invalidPassword,
|
TResult? Function()? invalidPassword,
|
||||||
}) {
|
}) {
|
||||||
return invalidPassword?.call();
|
return invalidPassword?.call();
|
||||||
@ -2164,7 +2184,8 @@ class _$_InvalidPassword implements _InvalidPassword {
|
|||||||
@override
|
@override
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
TResult maybeWhen<TResult extends Object?>({
|
TResult maybeWhen<TResult extends Object?>({
|
||||||
TResult Function(bool password, bool privateKey, int certificates)? result,
|
TResult Function(bool password, KeyType? keyType, CertInfo? certInfo)?
|
||||||
|
result,
|
||||||
TResult Function()? invalidPassword,
|
TResult Function()? invalidPassword,
|
||||||
required TResult orElse(),
|
required TResult orElse(),
|
||||||
}) {
|
}) {
|
||||||
|
@ -168,16 +168,18 @@ const _$SlotIdEnumMap = {
|
|||||||
_$_ExamineResult _$$_ExamineResultFromJson(Map<String, dynamic> json) =>
|
_$_ExamineResult _$$_ExamineResultFromJson(Map<String, dynamic> json) =>
|
||||||
_$_ExamineResult(
|
_$_ExamineResult(
|
||||||
password: json['password'] as bool,
|
password: json['password'] as bool,
|
||||||
privateKey: json['private_key'] as bool,
|
keyType: $enumDecodeNullable(_$KeyTypeEnumMap, json['key_type']),
|
||||||
certificates: json['certificates'] as int,
|
certInfo: json['cert_info'] == null
|
||||||
|
? null
|
||||||
|
: CertInfo.fromJson(json['cert_info'] as Map<String, dynamic>),
|
||||||
$type: json['runtimeType'] as String?,
|
$type: json['runtimeType'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$_ExamineResultToJson(_$_ExamineResult instance) =>
|
Map<String, dynamic> _$$_ExamineResultToJson(_$_ExamineResult instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'password': instance.password,
|
'password': instance.password,
|
||||||
'private_key': instance.privateKey,
|
'key_type': _$KeyTypeEnumMap[instance.keyType],
|
||||||
'certificates': instance.certificates,
|
'cert_info': instance.certInfo,
|
||||||
'runtimeType': instance.$type,
|
'runtimeType': instance.$type,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ final pivSlotsProvider = AsyncNotifierProvider.autoDispose
|
|||||||
abstract class PivSlotsNotifier
|
abstract class PivSlotsNotifier
|
||||||
extends AutoDisposeFamilyAsyncNotifier<List<PivSlot>, DevicePath> {
|
extends AutoDisposeFamilyAsyncNotifier<List<PivSlot>, DevicePath> {
|
||||||
Future<PivExamineResult> examine(String data, {String? password});
|
Future<PivExamineResult> examine(String data, {String? password});
|
||||||
|
Future<bool> validateRfc4514(String value);
|
||||||
Future<(SlotMetadata?, String?)> read(SlotId slot);
|
Future<(SlotMetadata?, String?)> read(SlotId slot);
|
||||||
Future<PivGenerateResult> generate(
|
Future<PivGenerateResult> generate(
|
||||||
SlotId slot,
|
SlotId slot,
|
||||||
|
@ -47,23 +47,23 @@ class ExportIntent extends Intent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _authenticate(
|
Future<bool> _authenticate(
|
||||||
WidgetRef ref, DevicePath devicePath, PivState pivState) async {
|
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||||
final withContext = ref.read(withContextProvider);
|
return await showBlurDialog(
|
||||||
return await withContext((context) async =>
|
|
||||||
await showBlurDialog(
|
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AuthenticationDialog(
|
builder: (context) => pivState.protectedKey
|
||||||
devicePath,
|
? PinDialog(devicePath)
|
||||||
pivState,
|
: AuthenticationDialog(
|
||||||
),
|
devicePath,
|
||||||
|
pivState,
|
||||||
|
),
|
||||||
) ??
|
) ??
|
||||||
false);
|
false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _authIfNeeded(
|
Future<bool> _authIfNeeded(
|
||||||
WidgetRef ref, DevicePath devicePath, PivState pivState) async {
|
BuildContext context, DevicePath devicePath, PivState pivState) async {
|
||||||
if (pivState.needsAuth) {
|
if (pivState.needsAuth) {
|
||||||
return await _authenticate(ref, devicePath, pivState);
|
return await _authenticate(context, devicePath, pivState);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -80,13 +80,13 @@ Widget registerPivActions(
|
|||||||
actions: {
|
actions: {
|
||||||
GenerateIntent:
|
GenerateIntent:
|
||||||
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
CallbackAction<GenerateIntent>(onInvoke: (intent) async {
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
if (!pivState.protectedKey &&
|
if (!pivState.protectedKey &&
|
||||||
!await _authIfNeeded(ref, devicePath, pivState)) {
|
!await withContext(
|
||||||
|
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
|
|
||||||
// TODO: Avoid asking for PIN if not needed?
|
// TODO: Avoid asking for PIN if not needed?
|
||||||
final verified = await withContext((context) async =>
|
final verified = await withContext((context) async =>
|
||||||
await showBlurDialog(
|
await showBlurDialog(
|
||||||
@ -130,12 +130,13 @@ Widget registerPivActions(
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
ImportIntent: CallbackAction<ImportIntent>(onInvoke: (intent) async {
|
||||||
if (!await _authIfNeeded(ref, devicePath, pivState)) {
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
if (!await withContext(
|
||||||
|
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
|
|
||||||
final picked = await withContext(
|
final picked = await withContext(
|
||||||
(context) async {
|
(context) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
@ -198,10 +199,12 @@ Widget registerPivActions(
|
|||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
|
||||||
if (!await _authIfNeeded(ref, devicePath, pivState)) {
|
final withContext = ref.read(withContextProvider);
|
||||||
|
if (!await withContext(
|
||||||
|
(context) => _authIfNeeded(context, devicePath, pivState))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
final bool? deleted = await withContext((context) async =>
|
final bool? deleted = await withContext((context) async =>
|
||||||
await showBlurDialog(
|
await showBlurDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
@ -37,12 +37,20 @@ class AuthenticationDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
||||||
String _managementKey = '';
|
bool _defaultKeyUsed = false;
|
||||||
bool _keyIsWrong = false;
|
bool _keyIsWrong = false;
|
||||||
|
final _keyController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_keyController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final hasMetadata = widget.pivState.metadata != null;
|
||||||
final keyLen = (widget.pivState.metadata?.managementKeyMetadata.keyType ??
|
final keyLen = (widget.pivState.metadata?.managementKeyMetadata.keyType ??
|
||||||
ManagementKeyType.tdes)
|
ManagementKeyType.tdes)
|
||||||
.keyLength *
|
.keyLength *
|
||||||
@ -52,13 +60,13 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: _managementKey.length == keyLen
|
onPressed: _keyController.text.length == keyLen
|
||||||
? () async {
|
? () async {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
try {
|
try {
|
||||||
final status = await ref
|
final status = await ref
|
||||||
.read(pivStateProvider(widget.devicePath).notifier)
|
.read(pivStateProvider(widget.devicePath).notifier)
|
||||||
.authenticate(_managementKey);
|
.authenticate(_keyController.text);
|
||||||
if (status) {
|
if (status) {
|
||||||
navigator.pop(true);
|
navigator.pop(true);
|
||||||
} else {
|
} else {
|
||||||
@ -88,24 +96,44 @@ class _AuthenticationDialogState extends ConsumerState<AuthenticationDialog> {
|
|||||||
TextField(
|
TextField(
|
||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
maxLength: keyLen,
|
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
|
controller: _keyController,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.allow(
|
FilteringTextInputFormatter.allow(
|
||||||
RegExp('[a-f0-9]', caseSensitive: false))
|
RegExp('[a-f0-9]', caseSensitive: false))
|
||||||
],
|
],
|
||||||
|
readOnly: _defaultKeyUsed,
|
||||||
|
maxLength: !_defaultKeyUsed ? keyLen : null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_management_key,
|
labelText: l10n.s_management_key,
|
||||||
prefixIcon: const Icon(Icons.key_outlined),
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
errorText: _keyIsWrong ? l10n.l_wrong_key : null,
|
errorText: _keyIsWrong ? l10n.l_wrong_key : null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
|
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
||||||
|
suffixIcon: hasMetadata
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
icon: Icon(_defaultKeyUsed
|
||||||
|
? Icons.auto_awesome
|
||||||
|
: Icons.auto_awesome_outlined),
|
||||||
|
tooltip: l10n.s_use_default,
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_defaultKeyUsed = !_defaultKeyUsed;
|
||||||
|
if (_defaultKeyUsed) {
|
||||||
|
_keyController.text = defaultManagementKey;
|
||||||
|
} else {
|
||||||
|
_keyController.clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_keyIsWrong = false;
|
_keyIsWrong = false;
|
||||||
_managementKey = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
99
lib/piv/views/cert_info_view.dart
Normal file
99
lib/piv/views/cert_info_view.dart
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:yubico_authenticator/app/message.dart';
|
||||||
|
import 'package:yubico_authenticator/app/state.dart';
|
||||||
|
import 'package:yubico_authenticator/piv/models.dart';
|
||||||
|
import 'package:yubico_authenticator/widgets/tooltip_if_truncated.dart';
|
||||||
|
|
||||||
|
class CertInfoTable extends ConsumerWidget {
|
||||||
|
final CertInfo certInfo;
|
||||||
|
|
||||||
|
const CertInfoTable(this.certInfo, {super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
// This is what ListTile uses for subtitle
|
||||||
|
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||||
|
color: textTheme.bodySmall!.color,
|
||||||
|
);
|
||||||
|
final dateFormat = DateFormat.yMMMEd();
|
||||||
|
final clipboard = ref.watch(clipboardProvider);
|
||||||
|
final withContext = ref.watch(withContextProvider);
|
||||||
|
|
||||||
|
Widget header(String title) => Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget body(String title, String value) => GestureDetector(
|
||||||
|
onDoubleTap: () async {
|
||||||
|
await clipboard.setText(value);
|
||||||
|
if (!clipboard.platformGivesFeedback()) {
|
||||||
|
await withContext((context) async {
|
||||||
|
showMessage(context, l10n.p_target_copied_clipboard(title));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: TooltipIfTruncated(
|
||||||
|
text: value,
|
||||||
|
style: subtitleStyle,
|
||||||
|
tooltip: value.replaceAllMapped(
|
||||||
|
RegExp(r',([A-Z]+)='), (match) => '\n${match[1]}='),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
header(l10n.s_subject),
|
||||||
|
header(l10n.s_issuer),
|
||||||
|
header(l10n.s_serial),
|
||||||
|
header(l10n.s_certificate_fingerprint),
|
||||||
|
header(l10n.s_valid_from),
|
||||||
|
header(l10n.s_valid_to),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Flexible(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
body(l10n.s_subject, certInfo.subject),
|
||||||
|
body(l10n.s_issuer, certInfo.issuer),
|
||||||
|
body(l10n.s_serial, certInfo.serial),
|
||||||
|
body(l10n.s_certificate_fingerprint, certInfo.fingerprint),
|
||||||
|
body(l10n.s_valid_from,
|
||||||
|
dateFormat.format(DateTime.parse(certInfo.notValidBefore))),
|
||||||
|
body(l10n.s_valid_to,
|
||||||
|
dateFormat.format(DateTime.parse(certInfo.notValidAfter))),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,7 @@ import '../../widgets/responsive_dialog.dart';
|
|||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
class GenerateKeyDialog extends ConsumerStatefulWidget {
|
class GenerateKeyDialog extends ConsumerStatefulWidget {
|
||||||
final DevicePath devicePath;
|
final DevicePath devicePath;
|
||||||
@ -42,6 +43,7 @@ class GenerateKeyDialog extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
||||||
String _subject = '';
|
String _subject = '';
|
||||||
|
bool _invalidSubject = true;
|
||||||
GenerateType _generateType = defaultGenerateType;
|
GenerateType _generateType = defaultGenerateType;
|
||||||
KeyType _keyType = defaultKeyType;
|
KeyType _keyType = defaultKeyType;
|
||||||
late DateTime _validFrom;
|
late DateTime _validFrom;
|
||||||
@ -64,6 +66,11 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
// This is what ListTile uses for subtitle
|
||||||
|
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||||
|
color: textTheme.bodySmall!.color,
|
||||||
|
);
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
allowCancel: !_generating,
|
allowCancel: !_generating,
|
||||||
@ -71,36 +78,56 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.saveButton,
|
key: keys.saveButton,
|
||||||
onPressed: _generating || _subject.isEmpty
|
onPressed: _generating || _invalidSubject
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
|
if (!await confirmOverwrite(
|
||||||
|
context,
|
||||||
|
widget.pivSlot,
|
||||||
|
writeKey: true,
|
||||||
|
writeCert: _generateType == GenerateType.certificate,
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_generating = true;
|
_generating = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
Function()? close;
|
final pivNotifier =
|
||||||
|
ref.read(pivSlotsProvider(widget.devicePath).notifier);
|
||||||
|
final withContext = ref.read(withContextProvider);
|
||||||
|
|
||||||
|
if (!await pivNotifier.validateRfc4514(_subject)) {
|
||||||
|
setState(() {
|
||||||
|
_generating = false;
|
||||||
|
});
|
||||||
|
_invalidSubject = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Function()? close;
|
||||||
final PivGenerateResult result;
|
final PivGenerateResult result;
|
||||||
try {
|
try {
|
||||||
close = showMessage(
|
close = await withContext<void Function()>(
|
||||||
context,
|
(context) async => showMessage(
|
||||||
l10n.l_generating_private_key,
|
context,
|
||||||
duration: const Duration(seconds: 30),
|
l10n.l_generating_private_key,
|
||||||
|
duration: const Duration(seconds: 30),
|
||||||
|
));
|
||||||
|
result = await pivNotifier.generate(
|
||||||
|
widget.pivSlot.slot,
|
||||||
|
_keyType,
|
||||||
|
parameters: switch (_generateType) {
|
||||||
|
GenerateType.certificate =>
|
||||||
|
PivGenerateParameters.certificate(
|
||||||
|
subject: _subject,
|
||||||
|
validFrom: _validFrom,
|
||||||
|
validTo: _validTo),
|
||||||
|
GenerateType.csr =>
|
||||||
|
PivGenerateParameters.csr(subject: _subject),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
result = await ref
|
|
||||||
.read(pivSlotsProvider(widget.devicePath).notifier)
|
|
||||||
.generate(
|
|
||||||
widget.pivSlot.slot,
|
|
||||||
_keyType,
|
|
||||||
parameters: switch (_generateType) {
|
|
||||||
GenerateType.certificate =>
|
|
||||||
PivGenerateParameters.certificate(
|
|
||||||
subject: _subject,
|
|
||||||
validFrom: _validFrom,
|
|
||||||
validTo: _validTo),
|
|
||||||
GenerateType.csr =>
|
|
||||||
PivGenerateParameters.csr(subject: _subject),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
close?.call();
|
close?.call();
|
||||||
}
|
}
|
||||||
@ -123,43 +150,45 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.p_generate_desc(widget.pivSlot.slot.getDisplayName(l10n))),
|
||||||
|
Text(
|
||||||
|
l10n.s_subject,
|
||||||
|
style: textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(l10n.p_subject_desc),
|
||||||
TextField(
|
TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
key: keys.subjectField,
|
key: keys.subjectField,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_subject,
|
labelText: l10n.s_subject,
|
||||||
),
|
errorText: _subject.isNotEmpty && _invalidSubject
|
||||||
|
? l10n.l_rfc4514_invalid
|
||||||
|
: null),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
enabled: !_generating,
|
enabled: !_generating,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (value.isEmpty) {
|
_invalidSubject = value.isEmpty;
|
||||||
_subject = '';
|
_subject = value;
|
||||||
} else {
|
|
||||||
_subject = value.contains('=') ? value : 'CN=$value';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
l10n.rfc4514_examples,
|
||||||
|
style: subtitleStyle,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
l10n.s_options,
|
||||||
|
style: textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
Text(l10n.p_cert_options_desc),
|
||||||
Wrap(
|
Wrap(
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
spacing: 4.0,
|
spacing: 4.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
ChoiceFilterChip<GenerateType>(
|
|
||||||
items: GenerateType.values,
|
|
||||||
value: _generateType,
|
|
||||||
selected: _generateType != defaultGenerateType,
|
|
||||||
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
|
|
||||||
onChanged: _generating
|
|
||||||
? null
|
|
||||||
: (value) {
|
|
||||||
setState(() {
|
|
||||||
_generateType = value;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ChoiceFilterChip<KeyType>(
|
ChoiceFilterChip<KeyType>(
|
||||||
items: KeyType.values,
|
items: KeyType.values,
|
||||||
value: _keyType,
|
value: _keyType,
|
||||||
@ -173,6 +202,19 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ChoiceFilterChip<GenerateType>(
|
||||||
|
items: GenerateType.values,
|
||||||
|
value: _generateType,
|
||||||
|
selected: _generateType != defaultGenerateType,
|
||||||
|
itemBuilder: (value) => Text(value.getDisplayName(l10n)),
|
||||||
|
onChanged: _generating
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
setState(() {
|
||||||
|
_generateType = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
if (_generateType == GenerateType.certificate)
|
if (_generateType == GenerateType.certificate)
|
||||||
FilterChip(
|
FilterChip(
|
||||||
label: Text(dateFormatter.format(_validTo)),
|
label: Text(dateFormatter.format(_validTo)),
|
||||||
|
@ -25,6 +25,8 @@ import '../../widgets/responsive_dialog.dart';
|
|||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import '../keys.dart' as keys;
|
import '../keys.dart' as keys;
|
||||||
|
import 'cert_info_view.dart';
|
||||||
|
import 'overwrite_confirm_dialog.dart';
|
||||||
|
|
||||||
class ImportFileDialog extends ConsumerStatefulWidget {
|
class ImportFileDialog extends ConsumerStatefulWidget {
|
||||||
final DevicePath devicePath;
|
final DevicePath devicePath;
|
||||||
@ -77,6 +79,11 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
// This is what ListTile uses for subtitle
|
||||||
|
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||||
|
color: textTheme.bodySmall!.color,
|
||||||
|
);
|
||||||
final state = _state;
|
final state = _state;
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
@ -141,26 +148,39 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
result: (_, privateKey, certificates) => ResponsiveDialog(
|
result: (_, keyType, certInfo) => ResponsiveDialog(
|
||||||
title: Text(l10n.l_import_file),
|
title: Text(l10n.l_import_file),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: () async {
|
onPressed: (keyType == null && certInfo == null)
|
||||||
final navigator = Navigator.of(context);
|
? null
|
||||||
try {
|
: () async {
|
||||||
await ref
|
final navigator = Navigator.of(context);
|
||||||
.read(pivSlotsProvider(widget.devicePath).notifier)
|
|
||||||
.import(widget.pivSlot.slot, _data,
|
if (!await confirmOverwrite(
|
||||||
password: _password.isNotEmpty ? _password : null);
|
context,
|
||||||
navigator.pop(true);
|
widget.pivSlot,
|
||||||
} catch (_) {
|
writeKey: keyType != null,
|
||||||
// TODO: More error cases
|
writeCert: certInfo != null,
|
||||||
setState(() {
|
)) {
|
||||||
_passwordIsWrong = true;
|
return;
|
||||||
});
|
}
|
||||||
}
|
|
||||||
},
|
try {
|
||||||
|
await ref
|
||||||
|
.read(pivSlotsProvider(widget.devicePath).notifier)
|
||||||
|
.import(widget.pivSlot.slot, _data,
|
||||||
|
password:
|
||||||
|
_password.isNotEmpty ? _password : null);
|
||||||
|
navigator.pop(true);
|
||||||
|
} catch (err) {
|
||||||
|
// TODO: More error cases
|
||||||
|
setState(() {
|
||||||
|
_passwordIsWrong = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
child: Text(l10n.s_import),
|
child: Text(l10n.s_import),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -171,8 +191,37 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
|
|||||||
children: [
|
children: [
|
||||||
Text(l10n.p_import_items_desc(
|
Text(l10n.p_import_items_desc(
|
||||||
widget.pivSlot.slot.getDisplayName(l10n))),
|
widget.pivSlot.slot.getDisplayName(l10n))),
|
||||||
if (privateKey) Text(l10n.l_bullet(l10n.s_private_key)),
|
if (keyType != null) ...[
|
||||||
if (certificates > 0) Text(l10n.l_bullet(l10n.s_certificate)),
|
Text(
|
||||||
|
l10n.s_private_key,
|
||||||
|
style: textTheme.bodyLarge,
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(l10n.s_algorithm),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
keyType.name.toUpperCase(),
|
||||||
|
style: subtitleStyle,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
if (certInfo != null) ...[
|
||||||
|
Text(
|
||||||
|
l10n.s_certificate,
|
||||||
|
style: textTheme.bodyLarge,
|
||||||
|
softWrap: true,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
height: 120, // Needed for layout, adapt if text sizes changes
|
||||||
|
child: CertInfoTable(certInfo),
|
||||||
|
),
|
||||||
|
]
|
||||||
]
|
]
|
||||||
.map((e) => Padding(
|
.map((e) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
@ -42,24 +42,26 @@ class ManageKeyDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
||||||
|
late bool _hasMetadata;
|
||||||
late bool _defaultKeyUsed;
|
late bool _defaultKeyUsed;
|
||||||
late bool _usesStoredKey;
|
late bool _usesStoredKey;
|
||||||
late bool _storeKey;
|
late bool _storeKey;
|
||||||
String _currentKeyOrPin = '';
|
|
||||||
bool _currentIsWrong = false;
|
bool _currentIsWrong = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
ManagementKeyType _keyType = ManagementKeyType.tdes;
|
ManagementKeyType _keyType = ManagementKeyType.tdes;
|
||||||
|
final _currentController = TextEditingController();
|
||||||
final _keyController = TextEditingController();
|
final _keyController = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
_hasMetadata = widget.pivState.metadata != null;
|
||||||
_defaultKeyUsed =
|
_defaultKeyUsed =
|
||||||
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
|
widget.pivState.metadata?.managementKeyMetadata.defaultValue ?? false;
|
||||||
_usesStoredKey = widget.pivState.protectedKey;
|
_usesStoredKey = widget.pivState.protectedKey;
|
||||||
if (!_usesStoredKey && _defaultKeyUsed) {
|
if (!_usesStoredKey && _defaultKeyUsed) {
|
||||||
_currentKeyOrPin = defaultManagementKey;
|
_currentController.text = defaultManagementKey;
|
||||||
}
|
}
|
||||||
_storeKey = _usesStoredKey;
|
_storeKey = _usesStoredKey;
|
||||||
}
|
}
|
||||||
@ -67,13 +69,14 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_keyController.dispose();
|
_keyController.dispose();
|
||||||
|
_currentController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
_submit() async {
|
_submit() async {
|
||||||
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
final notifier = ref.read(pivStateProvider(widget.path).notifier);
|
||||||
if (_usesStoredKey) {
|
if (_usesStoredKey) {
|
||||||
final status = (await notifier.verifyPin(_currentKeyOrPin)).when(
|
final status = (await notifier.verifyPin(_currentController.text)).when(
|
||||||
success: () => true,
|
success: () => true,
|
||||||
failure: (attemptsRemaining) {
|
failure: (attemptsRemaining) {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -87,7 +90,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!await notifier.authenticate(_currentKeyOrPin)) {
|
if (!await notifier.authenticate(_currentController.text)) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = true;
|
_currentIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -126,9 +129,10 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
ManagementKeyType.tdes;
|
ManagementKeyType.tdes;
|
||||||
final hexLength = _keyType.keyLength * 2;
|
final hexLength = _keyType.keyLength * 2;
|
||||||
final protected = widget.pivState.protectedKey;
|
final protected = widget.pivState.protectedKey;
|
||||||
|
final currentKeyOrPin = _currentController.text;
|
||||||
final currentLenOk = protected
|
final currentLenOk = protected
|
||||||
? _currentKeyOrPin.length >= 4
|
? currentKeyOrPin.length >= 4
|
||||||
: _currentKeyOrPin.length == currentType.keyLength * 2;
|
: currentKeyOrPin.length == currentType.keyLength * 2;
|
||||||
final newLenOk = _keyController.text.length == hexLength;
|
final newLenOk = _keyController.text.length == hexLength;
|
||||||
|
|
||||||
return ResponsiveDialog(
|
return ResponsiveDialog(
|
||||||
@ -153,6 +157,7 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.pinPukField,
|
key: keys.pinPukField,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
|
controller: _currentController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
@ -166,7 +171,6 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = false;
|
_currentIsWrong = false;
|
||||||
_currentKeyOrPin = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -175,16 +179,34 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
autofocus: !_defaultKeyUsed,
|
autofocus: !_defaultKeyUsed,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
initialValue: _defaultKeyUsed ? defaultManagementKey : null,
|
controller: _currentController,
|
||||||
readOnly: _defaultKeyUsed,
|
readOnly: _defaultKeyUsed,
|
||||||
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
maxLength: !_defaultKeyUsed ? currentType.keyLength * 2 : null,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_current_management_key,
|
labelText: l10n.s_current_management_key,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
errorText: _currentIsWrong ? l10n.l_wrong_key : null,
|
errorText: _currentIsWrong ? l10n.l_wrong_key : null,
|
||||||
errorMaxLines: 3,
|
errorMaxLines: 3,
|
||||||
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
helperText: _defaultKeyUsed ? l10n.l_default_key_used : null,
|
||||||
|
suffixIcon: _hasMetadata
|
||||||
|
? null
|
||||||
|
: 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
inputFormatters: <TextInputFormatter>[
|
inputFormatters: <TextInputFormatter>[
|
||||||
FilteringTextInputFormatter.allow(
|
FilteringTextInputFormatter.allow(
|
||||||
@ -194,7 +216,6 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentIsWrong = false;
|
_currentIsWrong = false;
|
||||||
_currentKeyOrPin = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@ -211,10 +232,11 @@ class _ManageKeyDialogState extends ConsumerState<ManageKeyDialog> {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_new_management_key,
|
labelText: l10n.s_new_management_key,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.key_outlined),
|
||||||
enabled: currentLenOk,
|
enabled: currentLenOk,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
|
tooltip: l10n.s_generate_random,
|
||||||
onPressed: currentLenOk
|
onPressed: currentLenOk
|
||||||
? () {
|
? () {
|
||||||
final random = Random.secure();
|
final random = Random.secure();
|
||||||
|
@ -114,7 +114,7 @@ class _ManagePinPukDialogState extends ConsumerState<ManagePinPukDialog> {
|
|||||||
: l10n.s_current_puk,
|
: l10n.s_current_puk,
|
||||||
prefixIcon: const Icon(Icons.password_outlined),
|
prefixIcon: const Icon(Icons.password_outlined),
|
||||||
errorText: _currentIsWrong
|
errorText: _currentIsWrong
|
||||||
? (widget.target == ManageTarget.puk
|
? (widget.target == ManageTarget.pin
|
||||||
? l10n.l_wrong_pin_attempts_remaining(
|
? l10n.l_wrong_pin_attempts_remaining(
|
||||||
_attemptsRemaining)
|
_attemptsRemaining)
|
||||||
: l10n.l_wrong_puk_attempts_remaining(
|
: l10n.l_wrong_puk_attempts_remaining(
|
||||||
|
83
lib/piv/views/overwrite_confirm_dialog.dart
Normal file
83
lib/piv/views/overwrite_confirm_dialog.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
|
|
||||||
|
import '../../app/message.dart';
|
||||||
|
import '../../widgets/responsive_dialog.dart';
|
||||||
|
import '../models.dart';
|
||||||
|
|
||||||
|
class _OverwriteConfirmDialog extends StatelessWidget {
|
||||||
|
final SlotId slot;
|
||||||
|
final bool certificate;
|
||||||
|
final bool? privateKey;
|
||||||
|
|
||||||
|
const _OverwriteConfirmDialog({
|
||||||
|
required this.certificate,
|
||||||
|
required this.privateKey,
|
||||||
|
required this.slot,
|
||||||
|
});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return ResponsiveDialog(
|
||||||
|
title: Text(l10n.s_overwrite_slot),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: Text(l10n.s_overwrite)),
|
||||||
|
],
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(l10n.p_overwrite_slot_desc(slot.getDisplayName(l10n))),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
if (certificate) Text(l10n.l_bullet(l10n.l_overwrite_cert)),
|
||||||
|
if (privateKey == true) Text(l10n.l_bullet(l10n.l_overwrite_key)),
|
||||||
|
if (privateKey == null)
|
||||||
|
Text(l10n.l_bullet(l10n.l_overwrite_key_maybe)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> confirmOverwrite(
|
||||||
|
BuildContext context,
|
||||||
|
PivSlot pivSlot, {
|
||||||
|
required bool writeKey,
|
||||||
|
required bool writeCert,
|
||||||
|
}) async {
|
||||||
|
final overwritesCert = writeCert && pivSlot.certInfo != null;
|
||||||
|
final overwritesKey = writeKey ? pivSlot.hasKey : false;
|
||||||
|
if (overwritesCert || overwritesKey != false) {
|
||||||
|
return await showBlurDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => _OverwriteConfirmDialog(
|
||||||
|
slot: pivSlot.slot,
|
||||||
|
certificate: overwritesCert,
|
||||||
|
privateKey: overwritesKey,
|
||||||
|
)) ??
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
@ -33,22 +33,30 @@ class PinDialog extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PinDialogState extends ConsumerState<PinDialog> {
|
class _PinDialogState extends ConsumerState<PinDialog> {
|
||||||
String _pin = '';
|
final _pinController = TextEditingController();
|
||||||
bool _pinIsWrong = false;
|
bool _pinIsWrong = false;
|
||||||
int _attemptsRemaining = -1;
|
int _attemptsRemaining = -1;
|
||||||
|
bool _isObscure = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_pinController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
final navigator = Navigator.of(context);
|
final navigator = Navigator.of(context);
|
||||||
try {
|
try {
|
||||||
final status = await ref
|
final status = await ref
|
||||||
.read(pivStateProvider(widget.devicePath).notifier)
|
.read(pivStateProvider(widget.devicePath).notifier)
|
||||||
.verifyPin(_pin);
|
.verifyPin(_pinController.text);
|
||||||
status.when(
|
status.when(
|
||||||
success: () {
|
success: () {
|
||||||
navigator.pop(true);
|
navigator.pop(true);
|
||||||
},
|
},
|
||||||
failure: (attemptsRemaining) {
|
failure: (attemptsRemaining) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_pinController.clear();
|
||||||
_attemptsRemaining = attemptsRemaining;
|
_attemptsRemaining = attemptsRemaining;
|
||||||
_pinIsWrong = true;
|
_pinIsWrong = true;
|
||||||
});
|
});
|
||||||
@ -67,7 +75,7 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
key: keys.unlockButton,
|
key: keys.unlockButton,
|
||||||
onPressed: _pin.length >= 4 ? _submit : null,
|
onPressed: _pinController.text.length >= 4 ? _submit : null,
|
||||||
child: Text(l10n.s_unlock),
|
child: Text(l10n.s_unlock),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -79,23 +87,35 @@ class _PinDialogState extends ConsumerState<PinDialog> {
|
|||||||
Text(l10n.p_pin_required_desc),
|
Text(l10n.p_pin_required_desc),
|
||||||
TextField(
|
TextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
obscureText: true,
|
obscureText: _isObscure,
|
||||||
maxLength: 8,
|
maxLength: 8,
|
||||||
autofillHints: const [AutofillHints.password],
|
autofillHints: const [AutofillHints.password],
|
||||||
key: keys.managementKeyField,
|
key: keys.managementKeyField,
|
||||||
|
controller: _pinController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
labelText: l10n.s_pin,
|
labelText: l10n.s_pin,
|
||||||
prefixIcon: const Icon(Icons.pin_outlined),
|
prefixIcon: const Icon(Icons.pin_outlined),
|
||||||
errorText: _pinIsWrong
|
errorText: _pinIsWrong
|
||||||
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
? l10n.l_wrong_pin_attempts_remaining(_attemptsRemaining)
|
||||||
: null,
|
: null,
|
||||||
errorMaxLines: 3),
|
errorMaxLines: 3,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
_isObscure ? Icons.visibility : Icons.visibility_off,
|
||||||
|
color: IconTheme.of(context).color,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isObscure = !_isObscure;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_pinIsWrong = false;
|
_pinIsWrong = false;
|
||||||
_pin = value;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
|
@ -105,7 +105,8 @@ class _CertificateListItem extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
title: slot.getDisplayName(l10n),
|
title: slot.getDisplayName(l10n),
|
||||||
subtitle: certInfo != null
|
subtitle: certInfo != null
|
||||||
? certInfo.subject
|
// Simplify subtitle by stripping "CN=", etc.
|
||||||
|
? certInfo.subject.replaceAll(RegExp(r'[A-Z]+='), ' ').trimLeft()
|
||||||
: pivSlot.hasKey == true
|
: pivSlot.hasKey == true
|
||||||
? l10n.l_key_no_certificate
|
? l10n.l_key_no_certificate
|
||||||
: l10n.l_no_certificate,
|
: l10n.l_no_certificate,
|
||||||
|
@ -17,16 +17,14 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
|
|
||||||
import '../../app/message.dart';
|
|
||||||
import '../../app/state.dart';
|
import '../../app/state.dart';
|
||||||
import '../../app/views/fs_dialog.dart';
|
import '../../app/views/fs_dialog.dart';
|
||||||
import '../../app/views/action_list.dart';
|
import '../../app/views/action_list.dart';
|
||||||
import '../../widgets/tooltip_if_truncated.dart';
|
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
import '../state.dart';
|
import '../state.dart';
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
|
import 'cert_info_view.dart';
|
||||||
|
|
||||||
class SlotDialog extends ConsumerWidget {
|
class SlotDialog extends ConsumerWidget {
|
||||||
final SlotId pivSlot;
|
final SlotId pivSlot;
|
||||||
@ -48,8 +46,6 @@ class SlotDialog extends ConsumerWidget {
|
|||||||
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
final subtitleStyle = textTheme.bodyMedium!.copyWith(
|
||||||
color: textTheme.bodySmall!.color,
|
color: textTheme.bodySmall!.color,
|
||||||
);
|
);
|
||||||
final clipboard = ref.watch(clipboardProvider);
|
|
||||||
final withContext = ref.read(withContextProvider);
|
|
||||||
|
|
||||||
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
|
final pivState = ref.watch(pivStateProvider(node.path)).valueOrNull;
|
||||||
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
|
final slotData = ref.watch(pivSlotsProvider(node.path).select((value) =>
|
||||||
@ -61,32 +57,6 @@ class SlotDialog extends ConsumerWidget {
|
|||||||
return const FsDialog(child: CircularProgressIndicator());
|
return const FsDialog(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
TableRow detailRow(String title, String value) {
|
|
||||||
return TableRow(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
l10n.s_definition(title),
|
|
||||||
textAlign: TextAlign.right,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8.0),
|
|
||||||
GestureDetector(
|
|
||||||
onDoubleTap: () async {
|
|
||||||
await clipboard.setText(value);
|
|
||||||
if (!clipboard.platformGivesFeedback()) {
|
|
||||||
await withContext((context) async {
|
|
||||||
showMessage(context, l10n.p_target_copied_clipboard(title));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: TooltipIfTruncated(
|
|
||||||
text: value,
|
|
||||||
style: subtitleStyle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final certInfo = slotData.certInfo;
|
final certInfo = slotData.certInfo;
|
||||||
return registerPivActions(
|
return registerPivActions(
|
||||||
node.path,
|
node.path,
|
||||||
@ -111,27 +81,7 @@ class SlotDialog extends ConsumerWidget {
|
|||||||
if (certInfo != null) ...[
|
if (certInfo != null) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Table(
|
child: CertInfoTable(certInfo),
|
||||||
defaultColumnWidth: const IntrinsicColumnWidth(),
|
|
||||||
columnWidths: const {2: FlexColumnWidth()},
|
|
||||||
children: [
|
|
||||||
detailRow(l10n.s_subject, certInfo.subject),
|
|
||||||
detailRow(l10n.s_issuer, certInfo.issuer),
|
|
||||||
detailRow(l10n.s_serial, certInfo.serial),
|
|
||||||
detailRow(l10n.s_certificate_fingerprint,
|
|
||||||
certInfo.fingerprint),
|
|
||||||
detailRow(
|
|
||||||
l10n.s_valid_from,
|
|
||||||
DateFormat.yMMMEd().format(
|
|
||||||
DateTime.parse(certInfo.notValidBefore)),
|
|
||||||
),
|
|
||||||
detailRow(
|
|
||||||
l10n.s_valid_to,
|
|
||||||
DateFormat.yMMMEd().format(
|
|
||||||
DateTime.parse(certInfo.notValidAfter)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -78,7 +78,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
|||||||
scrollable: true,
|
scrollable: true,
|
||||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
content: SizedBox(
|
content: SizedBox(
|
||||||
width: 380,
|
width: 550,
|
||||||
child: Container(key: _childKey, child: widget.child),
|
child: Container(key: _childKey, child: widget.child),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -107,7 +107,7 @@ class _ResponsiveDialogState extends State<ResponsiveDialog> {
|
|||||||
_hasLostFocus = true;
|
_hasLostFocus = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: constraints.maxWidth < 540
|
child: constraints.maxWidth < 400
|
||||||
? _buildFullscreen(context)
|
? _buildFullscreen(context)
|
||||||
: _buildDialog(context),
|
: _buildDialog(context),
|
||||||
);
|
);
|
||||||
|
@ -19,8 +19,9 @@ import 'package:flutter/material.dart';
|
|||||||
class TooltipIfTruncated extends StatelessWidget {
|
class TooltipIfTruncated extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final TextStyle style;
|
final TextStyle style;
|
||||||
|
final String? tooltip;
|
||||||
const TooltipIfTruncated(
|
const TooltipIfTruncated(
|
||||||
{super.key, required this.text, required this.style});
|
{super.key, required this.text, required this.style, this.tooltip});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -41,7 +42,7 @@ class TooltipIfTruncated extends StatelessWidget {
|
|||||||
return textPainter.didExceedMaxLines
|
return textPainter.didExceedMaxLines
|
||||||
? Tooltip(
|
? Tooltip(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
message: text,
|
message: tooltip ?? text,
|
||||||
child: textWidget,
|
child: textWidget,
|
||||||
)
|
)
|
||||||
: textWidget;
|
: textWidget;
|
||||||
|
Loading…
Reference in New Issue
Block a user