mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-12-29 05:03:05 +03:00
Merge PR #134.
This commit is contained in:
commit
9e28512dcb
@ -3,6 +3,7 @@ package com.yubico.authenticator.oath
|
|||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.builtins.ByteArraySerializer
|
||||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
@ -15,10 +16,19 @@ fun Model.Credential.isInteractive(): Boolean {
|
|||||||
|
|
||||||
class Model {
|
class Model {
|
||||||
|
|
||||||
|
@Serializable(with = VersionSerializer::class)
|
||||||
|
data class Version(
|
||||||
|
val major: Byte,
|
||||||
|
val minor: Byte,
|
||||||
|
val micro: Byte
|
||||||
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Session(
|
data class Session(
|
||||||
@SerialName("device_id")
|
@SerialName("device_id")
|
||||||
val deviceId: String = "",
|
val deviceId: String = "",
|
||||||
|
@SerialName("version")
|
||||||
|
val version: Version = Version(0, 0, 0),
|
||||||
@SerialName("has_key")
|
@SerialName("has_key")
|
||||||
val isAccessKeySet: Boolean = false,
|
val isAccessKeySet: Boolean = false,
|
||||||
@SerialName("remembered")
|
@SerialName("remembered")
|
||||||
@ -94,7 +104,26 @@ class Model {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var _credentials = mutableMapOf<Credential, Code?>(); private set
|
object VersionSerializer : KSerializer<Version> {
|
||||||
|
override val descriptor: SerialDescriptor = ByteArraySerializer().descriptor
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Version) {
|
||||||
|
encoder.encodeSerializableValue(
|
||||||
|
ByteArraySerializer(),
|
||||||
|
byteArrayOf(value.major, value.minor, value.micro)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Version {
|
||||||
|
val byteArray = decoder.decodeSerializableValue(ByteArraySerializer())
|
||||||
|
val major = if (byteArray.isNotEmpty()) byteArray[0] else 0
|
||||||
|
val minor = if (byteArray.size > 1) byteArray[1] else 0
|
||||||
|
val micro = if (byteArray.size > 2) byteArray[2] else 0
|
||||||
|
return Version(major, minor, micro)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _credentials = mutableMapOf<Credential, Code?>()
|
||||||
|
|
||||||
var session = Session()
|
var session = Session()
|
||||||
val credentials: List<CredentialWithCode>
|
val credentials: List<CredentialWithCode>
|
||||||
|
@ -438,6 +438,11 @@ class OathManager(
|
|||||||
|
|
||||||
_model.session = Model.Session(
|
_model.session = Model.Session(
|
||||||
oathSession.deviceId,
|
oathSession.deviceId,
|
||||||
|
Model.Version(
|
||||||
|
oathSession.version.major,
|
||||||
|
oathSession.version.minor,
|
||||||
|
oathSession.version.micro
|
||||||
|
),
|
||||||
oathSession.isAccessKeySet,
|
oathSession.isAccessKeySet,
|
||||||
isRemembered,
|
isRemembered,
|
||||||
oathSession.isLocked
|
oathSession.isLocked
|
||||||
|
@ -91,6 +91,7 @@ class OathNode(RpcNode):
|
|||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.session = OathSession(connection)
|
self.session = OathSession(connection)
|
||||||
|
self._key_verifier = None
|
||||||
|
|
||||||
if self.session.locked:
|
if self.session.locked:
|
||||||
key = self._get_access_key(self.session.device_id)
|
key = self._get_access_key(self.session.device_id)
|
||||||
@ -162,12 +163,15 @@ class OathNode(RpcNode):
|
|||||||
return decode_bytes(params.pop("key"))
|
return decode_bytes(params.pop("key"))
|
||||||
raise ValueError("One of 'key' and 'password' must be provided.")
|
raise ValueError("One of 'key' and 'password' must be provided.")
|
||||||
|
|
||||||
def _do_validate(self, key):
|
def _set_key_verifier(self, key):
|
||||||
self.session.validate(key)
|
|
||||||
salt = os.urandom(32)
|
salt = os.urandom(32)
|
||||||
digest = hmac.new(salt, key, "sha256").digest()
|
digest = hmac.new(salt, key, "sha256").digest()
|
||||||
self._key_verifier = (salt, digest)
|
self._key_verifier = (salt, digest)
|
||||||
|
|
||||||
|
def _do_validate(self, key):
|
||||||
|
self.session.validate(key)
|
||||||
|
self._set_key_verifier(key)
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def validate(self, params, event, signal):
|
def validate(self, params, event, signal):
|
||||||
remember = params.pop("remember", False)
|
remember = params.pop("remember", False)
|
||||||
@ -181,7 +185,7 @@ class OathNode(RpcNode):
|
|||||||
valid = False
|
valid = False
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
elif hasattr(self, "_key_verifier"):
|
elif self._key_verifier:
|
||||||
salt, digest = self._key_verifier
|
salt, digest = self._key_verifier
|
||||||
verify = hmac.new(salt, key, "sha256").digest()
|
verify = hmac.new(salt, key, "sha256").digest()
|
||||||
valid = hmac.compare_digest(digest, verify)
|
valid = hmac.compare_digest(digest, verify)
|
||||||
@ -198,18 +202,21 @@ class OathNode(RpcNode):
|
|||||||
remember = params.pop("remember", False)
|
remember = params.pop("remember", False)
|
||||||
key = self._get_key(params)
|
key = self._get_key(params)
|
||||||
self.session.set_key(key)
|
self.session.set_key(key)
|
||||||
|
self._set_key_verifier(key)
|
||||||
remember &= self._remember_key(key if remember else None)
|
remember &= self._remember_key(key if remember else None)
|
||||||
return dict(remembered=remember)
|
return dict(remembered=remember)
|
||||||
|
|
||||||
@action(condition=lambda self: self.session.has_key)
|
@action(condition=lambda self: self.session.has_key)
|
||||||
def unset_key(self, params, event, signal):
|
def unset_key(self, params, event, signal):
|
||||||
self.session.unset_key()
|
self.session.unset_key()
|
||||||
|
self._key_verifier = None
|
||||||
self._remember_key(None)
|
self._remember_key(None)
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def reset(self, params, event, signal):
|
def reset(self, params, event, signal):
|
||||||
self.session.reset()
|
self.session.reset()
|
||||||
|
self._key_verifier = None
|
||||||
self._remember_key(None)
|
self._remember_key(None)
|
||||||
return dict()
|
return dict()
|
||||||
|
|
||||||
|
@ -31,11 +31,11 @@ VSVersionInfo(
|
|||||||
'040904b0',
|
'040904b0',
|
||||||
[StringStruct('CompanyName', 'Yubico'),
|
[StringStruct('CompanyName', 'Yubico'),
|
||||||
StringStruct('FileDescription', 'Yubico Authenticator Helper'),
|
StringStruct('FileDescription', 'Yubico Authenticator Helper'),
|
||||||
StringStruct('FileVersion', '4.1.0.0'),
|
StringStruct('FileVersion', '6.0.0-alpha.3'),
|
||||||
StringStruct('LegalCopyright', 'Copyright (c) 2022 Yubico AB'),
|
StringStruct('LegalCopyright', 'Copyright (c) 2022 Yubico AB'),
|
||||||
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
|
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
|
||||||
StringStruct('ProductName', 'Yubico Authenticator'),
|
StringStruct('ProductName', 'Yubico Authenticator'),
|
||||||
StringStruct('ProductVersion', '4.1.0.0')])
|
StringStruct('ProductVersion', '6.0.0-alpha.3')])
|
||||||
]),
|
]),
|
||||||
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||||
]
|
]
|
||||||
|
@ -36,7 +36,9 @@ class AboutPage extends ConsumerWidget {
|
|||||||
const LoggingPanel(),
|
const LoggingPanel(),
|
||||||
if (isDesktop) ...[
|
if (isDesktop) ...[
|
||||||
const Divider(),
|
const Divider(),
|
||||||
OutlinedButton(
|
OutlinedButton.icon(
|
||||||
|
icon: const Icon(Icons.healing),
|
||||||
|
label: const Text('Run diagnostics...'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_log.info('Running diagnostics...');
|
_log.info('Running diagnostics...');
|
||||||
final response =
|
final response =
|
||||||
@ -51,7 +53,6 @@ class AboutPage extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: const Text('Run diagnostics...'),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@ -85,8 +86,9 @@ class LoggingPanel extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
OutlinedButton(
|
OutlinedButton.icon(
|
||||||
child: const Text('Copy log'),
|
icon: const Icon(Icons.copy),
|
||||||
|
label: const Text('Copy log'),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
_log.info('Copying log to clipboard ($version)...');
|
_log.info('Copying log to clipboard ($version)...');
|
||||||
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
final logs = await ref.read(logLevelProvider.notifier).getLogs();
|
||||||
|
@ -4,6 +4,13 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
String _pad(int value, int zeroes) => value.toString().padLeft(zeroes, '0');
|
||||||
|
|
||||||
|
extension DateTimeFormat on DateTime {
|
||||||
|
String get logFormat =>
|
||||||
|
'${_pad(hour, 2)}:${_pad(minute, 2)}:${_pad(second, 2)}.${_pad(millisecond, 3)}';
|
||||||
|
}
|
||||||
|
|
||||||
class Levels {
|
class Levels {
|
||||||
/// Key for tracing information ([value] = 500).
|
/// Key for tracing information ([value] = 500).
|
||||||
static const Level TRAFFIC = Level('TRAFFIC', 500);
|
static const Level TRAFFIC = Level('TRAFFIC', 500);
|
||||||
@ -45,7 +52,8 @@ class LogLevelNotifier extends StateNotifier<Level> {
|
|||||||
final List<String> _buffer = [];
|
final List<String> _buffer = [];
|
||||||
LogLevelNotifier() : super(Logger.root.level) {
|
LogLevelNotifier() : super(Logger.root.level) {
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
_buffer.add('[${record.loggerName}] ${record.level}: ${record.message}');
|
_buffer.add(
|
||||||
|
'${record.time.logFormat} [${record.loggerName}] ${record.level}: ${record.message}');
|
||||||
if (record.error != null) {
|
if (record.error != null) {
|
||||||
_buffer.add('${record.error}');
|
_buffer.add('${record.error}');
|
||||||
}
|
}
|
||||||
|
0
lib/app/models.freezed.dart
Normal file → Executable file
0
lib/app/models.freezed.dart
Normal file → Executable file
@ -105,8 +105,14 @@ enum UsbPid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class Version with _$Version {
|
class Version with _$Version implements Comparable<Version> {
|
||||||
const Version._();
|
const Version._();
|
||||||
|
@Assert('major >= 0')
|
||||||
|
@Assert('major < 256')
|
||||||
|
@Assert('minor >= 0')
|
||||||
|
@Assert('minor < 256')
|
||||||
|
@Assert('patch >= 0')
|
||||||
|
@Assert('patch < 256')
|
||||||
const factory Version(int major, int minor, int patch) = _Version;
|
const factory Version(int major, int minor, int patch) = _Version;
|
||||||
|
|
||||||
factory Version.fromJson(List<dynamic> values) {
|
factory Version.fromJson(List<dynamic> values) {
|
||||||
@ -119,6 +125,16 @@ class Version with _$Version {
|
|||||||
String toString() {
|
String toString() {
|
||||||
return '$major.$minor.$patch';
|
return '$major.$minor.$patch';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAtLeast(int major, [int minor = 0, int patch = 0]) =>
|
||||||
|
compareTo(Version(major, minor, patch)) >= 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(Version other) {
|
||||||
|
final a = major << 16 | minor << 8 | patch;
|
||||||
|
final b = other.major << 16 | other.minor << 8 | other.patch;
|
||||||
|
return a - b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
|
9
lib/core/models.freezed.dart
Normal file → Executable file
9
lib/core/models.freezed.dart
Normal file → Executable file
@ -106,7 +106,14 @@ class __$$_VersionCopyWithImpl<$Res> extends _$VersionCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
|
|
||||||
class _$_Version extends _Version {
|
class _$_Version extends _Version {
|
||||||
const _$_Version(this.major, this.minor, this.patch) : super._();
|
const _$_Version(this.major, this.minor, this.patch)
|
||||||
|
: assert(major >= 0),
|
||||||
|
assert(major < 256),
|
||||||
|
assert(minor >= 0),
|
||||||
|
assert(minor < 256),
|
||||||
|
assert(patch >= 0),
|
||||||
|
assert(patch < 256),
|
||||||
|
super._();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final int major;
|
final int major;
|
||||||
|
0
lib/core/models.g.dart
Normal file → Executable file
0
lib/core/models.g.dart
Normal file → Executable file
@ -77,7 +77,7 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
final arm64exe = Uri.file(exe)
|
final arm64exe = Uri.file(exe)
|
||||||
.resolve('../helper-arm64/authenticator-helper')
|
.resolve('../helper-arm64/authenticator-helper')
|
||||||
.toFilePath();
|
.toFilePath();
|
||||||
if (await Directory(arm64exe).exists()) {
|
if (await File(arm64exe).exists()) {
|
||||||
exe = arm64exe;
|
exe = arm64exe;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,13 +114,25 @@ Future<Widget> initialize(List<String> argv) async {
|
|||||||
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
|
||||||
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
credentialProvider.overrideWithProvider(desktopCredentialProvider),
|
||||||
],
|
],
|
||||||
child: const YubicoAuthenticatorApp(page: MainPage()),
|
child: YubicoAuthenticatorApp(
|
||||||
|
page: Consumer(
|
||||||
|
builder: ((_, ref, child) {
|
||||||
|
// keep RPC log level in sync with app
|
||||||
|
ref.listen<Level>(logLevelProvider, (_, level) {
|
||||||
|
rpc.setLogLevel(level);
|
||||||
|
});
|
||||||
|
|
||||||
|
return const MainPage();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initLogging(List<String> argv) {
|
void _initLogging(List<String> argv) {
|
||||||
Logger.root.onRecord.listen((record) {
|
Logger.root.onRecord.listen((record) {
|
||||||
stderr.writeln('[${record.loggerName}] ${record.level}: ${record.message}');
|
stderr.writeln(
|
||||||
|
'${record.time.logFormat} [${record.loggerName}] ${record.level}: ${record.message}');
|
||||||
if (record.error != null) {
|
if (record.error != null) {
|
||||||
stderr.writeln(record.error);
|
stderr.writeln(record.error);
|
||||||
}
|
}
|
||||||
|
0
lib/desktop/models.freezed.dart
Normal file → Executable file
0
lib/desktop/models.freezed.dart
Normal file → Executable file
0
lib/desktop/models.g.dart
Normal file → Executable file
0
lib/desktop/models.g.dart
Normal file → Executable file
0
lib/fido/models.freezed.dart
Normal file → Executable file
0
lib/fido/models.freezed.dart
Normal file → Executable file
0
lib/fido/models.g.dart
Normal file → Executable file
0
lib/fido/models.g.dart
Normal file → Executable file
@ -24,107 +24,104 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
List<Widget> children = [
|
List<Widget> children = [];
|
||||||
if (state.credMgmt)
|
if (state.credMgmt) {
|
||||||
...ref.watch(credentialProvider(node.path)).maybeWhen(
|
final data = ref.watch(credentialProvider(node.path)).asData;
|
||||||
data: (creds) => creds.isNotEmpty
|
if (data == null) {
|
||||||
? [
|
return _buildLoadingPage();
|
||||||
const ListTile(title: Text('Credentials')),
|
}
|
||||||
...creds.map((cred) => ListTile(
|
final creds = data.value;
|
||||||
leading: CircleAvatar(
|
if (creds.isNotEmpty) {
|
||||||
foregroundColor:
|
children.add(const ListTile(title: Text('Credentials')));
|
||||||
Theme.of(context).colorScheme.onPrimary,
|
children.addAll(
|
||||||
backgroundColor:
|
creds.map(
|
||||||
Theme.of(context).colorScheme.primary,
|
(cred) => ListTile(
|
||||||
child: const Icon(Icons.person),
|
leading: CircleAvatar(
|
||||||
),
|
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||||
title: Text(
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||||
cred.userName,
|
child: const Icon(Icons.person),
|
||||||
softWrap: false,
|
),
|
||||||
overflow: TextOverflow.fade,
|
title: Text(
|
||||||
),
|
cred.userName,
|
||||||
subtitle: Text(
|
softWrap: false,
|
||||||
cred.rpId,
|
overflow: TextOverflow.fade,
|
||||||
softWrap: false,
|
),
|
||||||
overflow: TextOverflow.fade,
|
subtitle: Text(
|
||||||
),
|
cred.rpId,
|
||||||
trailing: Row(
|
softWrap: false,
|
||||||
mainAxisSize: MainAxisSize.min,
|
overflow: TextOverflow.fade,
|
||||||
children: [
|
),
|
||||||
IconButton(
|
trailing: Row(
|
||||||
onPressed: () {
|
mainAxisSize: MainAxisSize.min,
|
||||||
showDialog(
|
children: [
|
||||||
context: context,
|
IconButton(
|
||||||
builder: (context) =>
|
onPressed: () {
|
||||||
DeleteCredentialDialog(
|
showDialog(
|
||||||
node.path, cred),
|
context: context,
|
||||||
);
|
builder: (context) =>
|
||||||
},
|
DeleteCredentialDialog(node.path, cred),
|
||||||
icon: const Icon(Icons.delete_outline)),
|
);
|
||||||
],
|
},
|
||||||
),
|
icon: const Icon(Icons.delete_outline)),
|
||||||
)),
|
],
|
||||||
]
|
),
|
||||||
: [],
|
|
||||||
orElse: () => [],
|
|
||||||
),
|
),
|
||||||
if (state.bioEnroll != null)
|
),
|
||||||
...ref.watch(fingerprintProvider(node.path)).maybeWhen(
|
);
|
||||||
data: (fingerprints) => fingerprints.isNotEmpty
|
}
|
||||||
? [
|
}
|
||||||
const ListTile(title: Text('Fingerprints')),
|
|
||||||
...fingerprints.map((fp) => ListTile(
|
if (state.bioEnroll != null) {
|
||||||
leading: CircleAvatar(
|
final data = ref.watch(fingerprintProvider(node.path)).asData;
|
||||||
foregroundColor:
|
if (data == null) {
|
||||||
Theme.of(context).colorScheme.onSecondary,
|
return _buildLoadingPage();
|
||||||
backgroundColor:
|
}
|
||||||
Theme.of(context).colorScheme.secondary,
|
final fingerprints = data.value;
|
||||||
child: const Icon(Icons.fingerprint),
|
if (fingerprints.isNotEmpty) {
|
||||||
),
|
children.add(const ListTile(title: Text('Fingerprints')));
|
||||||
title: Text(
|
children.addAll(fingerprints.map((fp) => ListTile(
|
||||||
fp.label,
|
leading: CircleAvatar(
|
||||||
softWrap: false,
|
foregroundColor: Theme.of(context).colorScheme.onSecondary,
|
||||||
overflow: TextOverflow.fade,
|
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||||
),
|
child: const Icon(Icons.fingerprint),
|
||||||
trailing: Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: Text(
|
||||||
children: [
|
fp.label,
|
||||||
IconButton(
|
softWrap: false,
|
||||||
onPressed: () {
|
overflow: TextOverflow.fade,
|
||||||
showDialog(
|
),
|
||||||
context: context,
|
trailing: Row(
|
||||||
builder: (context) =>
|
mainAxisSize: MainAxisSize.min,
|
||||||
RenameFingerprintDialog(
|
children: [
|
||||||
node.path, fp),
|
IconButton(
|
||||||
);
|
onPressed: () {
|
||||||
},
|
showDialog(
|
||||||
icon: const Icon(Icons.edit_outlined)),
|
context: context,
|
||||||
IconButton(
|
builder: (context) =>
|
||||||
onPressed: () {
|
RenameFingerprintDialog(node.path, fp),
|
||||||
showDialog(
|
);
|
||||||
context: context,
|
},
|
||||||
builder: (context) =>
|
icon: const Icon(Icons.edit_outlined)),
|
||||||
DeleteFingerprintDialog(
|
IconButton(
|
||||||
node.path, fp),
|
onPressed: () {
|
||||||
);
|
showDialog(
|
||||||
},
|
context: context,
|
||||||
icon: const Icon(Icons.delete_outline)),
|
builder: (context) =>
|
||||||
],
|
DeleteFingerprintDialog(node.path, fp),
|
||||||
),
|
);
|
||||||
))
|
},
|
||||||
]
|
icon: const Icon(Icons.delete_outline)),
|
||||||
: [],
|
],
|
||||||
orElse: () => [],
|
),
|
||||||
),
|
)));
|
||||||
];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (children.isNotEmpty) {
|
if (children.isNotEmpty) {
|
||||||
return AppPage(
|
return AppPage(
|
||||||
title: const Text('WebAuthn'),
|
title: const Text('WebAuthn'),
|
||||||
actions: _buildActions(context),
|
actions: _buildActions(context),
|
||||||
child: Column(
|
child: Column(children: children),
|
||||||
children: children,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -147,6 +144,12 @@ class FidoUnlockedPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildLoadingPage() => AppPage(
|
||||||
|
title: const Text('WebAuthn'),
|
||||||
|
centered: true,
|
||||||
|
child: const CircularProgressIndicator(),
|
||||||
|
);
|
||||||
|
|
||||||
List<Widget> _buildActions(BuildContext context,
|
List<Widget> _buildActions(BuildContext context,
|
||||||
{bool fingerprintPrimary = false}) =>
|
{bool fingerprintPrimary = false}) =>
|
||||||
[
|
[
|
||||||
|
0
lib/management/models.freezed.dart
Normal file → Executable file
0
lib/management/models.freezed.dart
Normal file → Executable file
0
lib/management/models.g.dart
Normal file → Executable file
0
lib/management/models.g.dart
Normal file → Executable file
@ -1,5 +1,7 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
import '../core/models.dart';
|
||||||
|
|
||||||
part 'models.freezed.dart';
|
part 'models.freezed.dart';
|
||||||
part 'models.g.dart';
|
part 'models.g.dart';
|
||||||
|
|
||||||
@ -61,7 +63,8 @@ class OathPair with _$OathPair {
|
|||||||
@freezed
|
@freezed
|
||||||
class OathState with _$OathState {
|
class OathState with _$OathState {
|
||||||
factory OathState(
|
factory OathState(
|
||||||
String deviceId, {
|
String deviceId,
|
||||||
|
Version version, {
|
||||||
required bool hasKey,
|
required bool hasKey,
|
||||||
required bool remembered,
|
required bool remembered,
|
||||||
required bool locked,
|
required bool locked,
|
||||||
|
37
lib/oath/models.freezed.dart
Normal file → Executable file
37
lib/oath/models.freezed.dart
Normal file → Executable file
@ -615,6 +615,7 @@ OathState _$OathStateFromJson(Map<String, dynamic> json) {
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$OathState {
|
mixin _$OathState {
|
||||||
String get deviceId => throw _privateConstructorUsedError;
|
String get deviceId => throw _privateConstructorUsedError;
|
||||||
|
Version get version => throw _privateConstructorUsedError;
|
||||||
bool get hasKey => throw _privateConstructorUsedError;
|
bool get hasKey => throw _privateConstructorUsedError;
|
||||||
bool get remembered => throw _privateConstructorUsedError;
|
bool get remembered => throw _privateConstructorUsedError;
|
||||||
bool get locked => throw _privateConstructorUsedError;
|
bool get locked => throw _privateConstructorUsedError;
|
||||||
@ -632,10 +633,13 @@ abstract class $OathStateCopyWith<$Res> {
|
|||||||
_$OathStateCopyWithImpl<$Res>;
|
_$OathStateCopyWithImpl<$Res>;
|
||||||
$Res call(
|
$Res call(
|
||||||
{String deviceId,
|
{String deviceId,
|
||||||
|
Version version,
|
||||||
bool hasKey,
|
bool hasKey,
|
||||||
bool remembered,
|
bool remembered,
|
||||||
bool locked,
|
bool locked,
|
||||||
KeystoreState keystore});
|
KeystoreState keystore});
|
||||||
|
|
||||||
|
$VersionCopyWith<$Res> get version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -649,6 +653,7 @@ class _$OathStateCopyWithImpl<$Res> implements $OathStateCopyWith<$Res> {
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? deviceId = freezed,
|
Object? deviceId = freezed,
|
||||||
|
Object? version = freezed,
|
||||||
Object? hasKey = freezed,
|
Object? hasKey = freezed,
|
||||||
Object? remembered = freezed,
|
Object? remembered = freezed,
|
||||||
Object? locked = freezed,
|
Object? locked = freezed,
|
||||||
@ -659,6 +664,10 @@ class _$OathStateCopyWithImpl<$Res> implements $OathStateCopyWith<$Res> {
|
|||||||
? _value.deviceId
|
? _value.deviceId
|
||||||
: deviceId // ignore: cast_nullable_to_non_nullable
|
: deviceId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
version: version == freezed
|
||||||
|
? _value.version
|
||||||
|
: version // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Version,
|
||||||
hasKey: hasKey == freezed
|
hasKey: hasKey == freezed
|
||||||
? _value.hasKey
|
? _value.hasKey
|
||||||
: hasKey // ignore: cast_nullable_to_non_nullable
|
: hasKey // ignore: cast_nullable_to_non_nullable
|
||||||
@ -677,6 +686,13 @@ class _$OathStateCopyWithImpl<$Res> implements $OathStateCopyWith<$Res> {
|
|||||||
as KeystoreState,
|
as KeystoreState,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$VersionCopyWith<$Res> get version {
|
||||||
|
return $VersionCopyWith<$Res>(_value.version, (value) {
|
||||||
|
return _then(_value.copyWith(version: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -687,10 +703,14 @@ abstract class _$$_OathStateCopyWith<$Res> implements $OathStateCopyWith<$Res> {
|
|||||||
@override
|
@override
|
||||||
$Res call(
|
$Res call(
|
||||||
{String deviceId,
|
{String deviceId,
|
||||||
|
Version version,
|
||||||
bool hasKey,
|
bool hasKey,
|
||||||
bool remembered,
|
bool remembered,
|
||||||
bool locked,
|
bool locked,
|
||||||
KeystoreState keystore});
|
KeystoreState keystore});
|
||||||
|
|
||||||
|
@override
|
||||||
|
$VersionCopyWith<$Res> get version;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@ -706,6 +726,7 @@ class __$$_OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
|
|||||||
@override
|
@override
|
||||||
$Res call({
|
$Res call({
|
||||||
Object? deviceId = freezed,
|
Object? deviceId = freezed,
|
||||||
|
Object? version = freezed,
|
||||||
Object? hasKey = freezed,
|
Object? hasKey = freezed,
|
||||||
Object? remembered = freezed,
|
Object? remembered = freezed,
|
||||||
Object? locked = freezed,
|
Object? locked = freezed,
|
||||||
@ -716,6 +737,10 @@ class __$$_OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
|
|||||||
? _value.deviceId
|
? _value.deviceId
|
||||||
: deviceId // ignore: cast_nullable_to_non_nullable
|
: deviceId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
version == freezed
|
||||||
|
? _value.version
|
||||||
|
: version // ignore: cast_nullable_to_non_nullable
|
||||||
|
as Version,
|
||||||
hasKey: hasKey == freezed
|
hasKey: hasKey == freezed
|
||||||
? _value.hasKey
|
? _value.hasKey
|
||||||
: hasKey // ignore: cast_nullable_to_non_nullable
|
: hasKey // ignore: cast_nullable_to_non_nullable
|
||||||
@ -739,7 +764,7 @@ class __$$_OathStateCopyWithImpl<$Res> extends _$OathStateCopyWithImpl<$Res>
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class _$_OathState implements _OathState {
|
class _$_OathState implements _OathState {
|
||||||
_$_OathState(this.deviceId,
|
_$_OathState(this.deviceId, this.version,
|
||||||
{required this.hasKey,
|
{required this.hasKey,
|
||||||
required this.remembered,
|
required this.remembered,
|
||||||
required this.locked,
|
required this.locked,
|
||||||
@ -751,6 +776,8 @@ class _$_OathState implements _OathState {
|
|||||||
@override
|
@override
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
@override
|
@override
|
||||||
|
final Version version;
|
||||||
|
@override
|
||||||
final bool hasKey;
|
final bool hasKey;
|
||||||
@override
|
@override
|
||||||
final bool remembered;
|
final bool remembered;
|
||||||
@ -761,7 +788,7 @@ class _$_OathState implements _OathState {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'OathState(deviceId: $deviceId, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore)';
|
return 'OathState(deviceId: $deviceId, version: $version, hasKey: $hasKey, remembered: $remembered, locked: $locked, keystore: $keystore)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -770,6 +797,7 @@ class _$_OathState implements _OathState {
|
|||||||
(other.runtimeType == runtimeType &&
|
(other.runtimeType == runtimeType &&
|
||||||
other is _$_OathState &&
|
other is _$_OathState &&
|
||||||
const DeepCollectionEquality().equals(other.deviceId, deviceId) &&
|
const DeepCollectionEquality().equals(other.deviceId, deviceId) &&
|
||||||
|
const DeepCollectionEquality().equals(other.version, version) &&
|
||||||
const DeepCollectionEquality().equals(other.hasKey, hasKey) &&
|
const DeepCollectionEquality().equals(other.hasKey, hasKey) &&
|
||||||
const DeepCollectionEquality()
|
const DeepCollectionEquality()
|
||||||
.equals(other.remembered, remembered) &&
|
.equals(other.remembered, remembered) &&
|
||||||
@ -782,6 +810,7 @@ class _$_OathState implements _OathState {
|
|||||||
int get hashCode => Object.hash(
|
int get hashCode => Object.hash(
|
||||||
runtimeType,
|
runtimeType,
|
||||||
const DeepCollectionEquality().hash(deviceId),
|
const DeepCollectionEquality().hash(deviceId),
|
||||||
|
const DeepCollectionEquality().hash(version),
|
||||||
const DeepCollectionEquality().hash(hasKey),
|
const DeepCollectionEquality().hash(hasKey),
|
||||||
const DeepCollectionEquality().hash(remembered),
|
const DeepCollectionEquality().hash(remembered),
|
||||||
const DeepCollectionEquality().hash(locked),
|
const DeepCollectionEquality().hash(locked),
|
||||||
@ -799,7 +828,7 @@ class _$_OathState implements _OathState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
abstract class _OathState implements OathState {
|
abstract class _OathState implements OathState {
|
||||||
factory _OathState(final String deviceId,
|
factory _OathState(final String deviceId, final Version version,
|
||||||
{required final bool hasKey,
|
{required final bool hasKey,
|
||||||
required final bool remembered,
|
required final bool remembered,
|
||||||
required final bool locked,
|
required final bool locked,
|
||||||
@ -811,6 +840,8 @@ abstract class _OathState implements OathState {
|
|||||||
@override
|
@override
|
||||||
String get deviceId => throw _privateConstructorUsedError;
|
String get deviceId => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
|
Version get version => throw _privateConstructorUsedError;
|
||||||
|
@override
|
||||||
bool get hasKey => throw _privateConstructorUsedError;
|
bool get hasKey => throw _privateConstructorUsedError;
|
||||||
@override
|
@override
|
||||||
bool get remembered => throw _privateConstructorUsedError;
|
bool get remembered => throw _privateConstructorUsedError;
|
||||||
|
2
lib/oath/models.g.dart
Normal file → Executable file
2
lib/oath/models.g.dart
Normal file → Executable file
@ -61,6 +61,7 @@ Map<String, dynamic> _$$_OathPairToJson(_$_OathPair instance) =>
|
|||||||
|
|
||||||
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
|
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
|
||||||
json['device_id'] as String,
|
json['device_id'] as String,
|
||||||
|
Version.fromJson(json['version'] as List<dynamic>),
|
||||||
hasKey: json['has_key'] as bool,
|
hasKey: json['has_key'] as bool,
|
||||||
remembered: json['remembered'] as bool,
|
remembered: json['remembered'] as bool,
|
||||||
locked: json['locked'] as bool,
|
locked: json['locked'] as bool,
|
||||||
@ -70,6 +71,7 @@ _$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
|
|||||||
Map<String, dynamic> _$$_OathStateToJson(_$_OathState instance) =>
|
Map<String, dynamic> _$$_OathStateToJson(_$_OathState instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'device_id': instance.deviceId,
|
'device_id': instance.deviceId,
|
||||||
|
'version': instance.version,
|
||||||
'has_key': instance.hasKey,
|
'has_key': instance.hasKey,
|
||||||
'remembered': instance.remembered,
|
'remembered': instance.remembered,
|
||||||
'locked': instance.locked,
|
'locked': instance.locked,
|
||||||
|
@ -185,8 +185,7 @@ mixin AccountMixin {
|
|||||||
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
ref.read(favoritesProvider.notifier).toggleFavorite(credential.id);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (deviceData.info.version.major >= 5 &&
|
if (deviceData.info.version.isAtLeast(5, 3))
|
||||||
deviceData.info.version.minor >= 3)
|
|
||||||
MenuAction(
|
MenuAction(
|
||||||
icon: const Icon(Icons.edit_outlined),
|
icon: const Icon(Icons.edit_outlined),
|
||||||
text: 'Rename account',
|
text: 'Rename account',
|
||||||
|
@ -25,8 +25,9 @@ enum _QrScanState { none, scanning, success, failed }
|
|||||||
|
|
||||||
class OathAddAccountPage extends ConsumerStatefulWidget {
|
class OathAddAccountPage extends ConsumerStatefulWidget {
|
||||||
final DevicePath devicePath;
|
final DevicePath devicePath;
|
||||||
|
final OathState state;
|
||||||
final bool openQrScanner;
|
final bool openQrScanner;
|
||||||
const OathAddAccountPage(this.devicePath,
|
const OathAddAccountPage(this.devicePath, this.state,
|
||||||
{super.key, required this.openQrScanner});
|
{super.key, required this.openQrScanner});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -287,15 +288,16 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
spacing: 4.0,
|
spacing: 4.0,
|
||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
FilterChip(
|
if (widget.state.version.isAtLeast(4, 2))
|
||||||
label: const Text('Require touch'),
|
FilterChip(
|
||||||
selected: _touch,
|
label: const Text('Require touch'),
|
||||||
onSelected: (value) {
|
selected: _touch,
|
||||||
setState(() {
|
onSelected: (value) {
|
||||||
_touch = value;
|
setState(() {
|
||||||
});
|
_touch = value;
|
||||||
},
|
});
|
||||||
),
|
},
|
||||||
|
),
|
||||||
Chip(
|
Chip(
|
||||||
backgroundColor: ChipTheme.of(context).selectedColor,
|
backgroundColor: ChipTheme.of(context).selectedColor,
|
||||||
label: DropdownButtonHideUnderline(
|
label: DropdownButtonHideUnderline(
|
||||||
@ -327,6 +329,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
|
|||||||
isDense: true,
|
isDense: true,
|
||||||
underline: null,
|
underline: null,
|
||||||
items: HashAlgorithm.values
|
items: HashAlgorithm.values
|
||||||
|
.where((alg) =>
|
||||||
|
alg != HashAlgorithm.sha512 ||
|
||||||
|
widget.state.version.isAtLeast(4, 3, 1))
|
||||||
.map((e) => DropdownMenuItem(
|
.map((e) => DropdownMenuItem(
|
||||||
value: e,
|
value: e,
|
||||||
child: Text(e.name.toUpperCase()),
|
child: Text(e.name.toUpperCase()),
|
||||||
|
@ -158,6 +158,7 @@ class _UnlockedView extends ConsumerWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => OathAddAccountPage(
|
builder: (context) => OathAddAccountPage(
|
||||||
devicePath,
|
devicePath,
|
||||||
|
oathState,
|
||||||
openQrScanner: Platform.isAndroid,
|
openQrScanner: Platform.isAndroid,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -11,6 +11,12 @@ lib_version_pattern = rf"const\s+String\s+version\s+=\s+'({version_pattern})';"
|
|||||||
lib_build_pattern = rf"const\s+int\s+build\s+=\s+(\d+);"
|
lib_build_pattern = rf"const\s+int\s+build\s+=\s+(\d+);"
|
||||||
|
|
||||||
|
|
||||||
|
def sub1(pattern, repl, string):
|
||||||
|
buf, n = re.subn(pattern, repl, string)
|
||||||
|
if n != 1:
|
||||||
|
raise ValueError(f"Did not find string matching {pattern} to replace")
|
||||||
|
return buf
|
||||||
|
|
||||||
def update_file(fname, func):
|
def update_file(fname, func):
|
||||||
with open(fname) as f:
|
with open(fname) as f:
|
||||||
orig = f.read()
|
orig = f.read()
|
||||||
@ -42,13 +48,13 @@ def read_lib_version():
|
|||||||
|
|
||||||
|
|
||||||
def update_lib(buf):
|
def update_lib(buf):
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
lib_version_pattern,
|
lib_version_pattern,
|
||||||
f"const String version = '{version}';",
|
f"const String version = '{version}';",
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
|
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
lib_build_pattern,
|
lib_build_pattern,
|
||||||
f"const int build = {build};",
|
f"const int build = {build};",
|
||||||
buf,
|
buf,
|
||||||
@ -78,7 +84,7 @@ short_version = re.search("(\d+\.\d+\.\d+)", version).group()
|
|||||||
|
|
||||||
# pubspec.yaml
|
# pubspec.yaml
|
||||||
def update_pubspec(buf):
|
def update_pubspec(buf):
|
||||||
return re.sub(
|
return sub1(
|
||||||
r'version:\s+\d+\.\d+\.\d+\+\d+',
|
r'version:\s+\d+\.\d+\.\d+\+\d+',
|
||||||
f'version: {short_version}+{build}',
|
f'version: {short_version}+{build}',
|
||||||
buf,
|
buf,
|
||||||
@ -86,14 +92,14 @@ def update_pubspec(buf):
|
|||||||
|
|
||||||
# Windows Runner.rc
|
# Windows Runner.rc
|
||||||
def update_runner_rc(buf):
|
def update_runner_rc(buf):
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
rf'#define VERSION_AS_STRING "{version_pattern}"',
|
rf'#define VERSION_AS_STRING "{version_pattern}"',
|
||||||
f'#define VERSION_AS_STRING "{version}"',
|
f'#define VERSION_AS_STRING "{version}"',
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
|
|
||||||
version_as_number = short_version.replace(".", ",")
|
version_as_number = short_version.replace(".", ",")
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
r"#define VERSION_AS_NUMBER \d+,\d+,\d+",
|
r"#define VERSION_AS_NUMBER \d+,\d+,\d+",
|
||||||
f"#define VERSION_AS_NUMBER {version_as_number}",
|
f"#define VERSION_AS_NUMBER {version_as_number}",
|
||||||
buf,
|
buf,
|
||||||
@ -103,22 +109,22 @@ def update_runner_rc(buf):
|
|||||||
# Helper version_info
|
# Helper version_info
|
||||||
def update_helper_version(buf):
|
def update_helper_version(buf):
|
||||||
version_tuple = repr(tuple(int(d) for d in short_version.split(".")) + (0,))
|
version_tuple = repr(tuple(int(d) for d in short_version.split(".")) + (0,))
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
rf'filevers=\(\d+, \d+, \d+, \d+\)',
|
rf'filevers=\(\d+, \d+, \d+, \d+\)',
|
||||||
f'filevers={version_tuple}',
|
f'filevers={version_tuple}',
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
rf'prodvers=\(\d+, \d+, \d+, \d+\)',
|
rf'prodvers=\(\d+, \d+, \d+, \d+\)',
|
||||||
f'prodvers={version_tuple}',
|
f'prodvers={version_tuple}',
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
rf"'FileVersion', '{version_pattern}'",
|
rf"'FileVersion', '{version_pattern}'",
|
||||||
f"'FileVersion', '{version}'",
|
f"'FileVersion', '{version}'",
|
||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
buf = re.sub(
|
buf = sub1(
|
||||||
rf"'ProductVersion', '{version_pattern}'",
|
rf"'ProductVersion', '{version_pattern}'",
|
||||||
f"'ProductVersion', '{version}'",
|
f"'ProductVersion', '{version}'",
|
||||||
buf,
|
buf,
|
||||||
|
Loading…
Reference in New Issue
Block a user