Merge branch 'release/6.4.0' into merge/release/6.4.0

This commit is contained in:
Adam Velebil 2024-02-20 09:26:09 +01:00
commit 3d0af125fd
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
22 changed files with 261 additions and 155 deletions

View File

@ -1,2 +1,2 @@
FLUTTER=3.16.9
PYVER=3.12.1
PYVER=3.12.2

View File

@ -21,6 +21,7 @@ jobs:
- name: Install dependencies
run: |
echo "PYVER=3.12.1" >> $GITHUB_ENV # Remove once 3.12.2 is available from PPA
export PYVER_MINOR=${PYVER%.*}
echo "PYVER_MINOR: $PYVER_MINOR"
apt-get update

15
NEWS
View File

@ -1,3 +1,18 @@
* Version 6.4.0
** UI: Major UI overhaul, with improvements including:
*** Add new UI layouts for wider windows to better utilize screen space.
*** Add YubiKey personalization through custom naming and theme color.
*** Split FIDO/WebAuthn into multiple sections.
*** Move factory reset functionality into a single dialog, from the individual sections.
** Add support for Yubico OTP provisioning.
** PIV: Display more information about keys and certificates.
** PIV: Add output format for public key when generating keys.
** Desktop: Window hidden/shown state no longer saved when closing the app,
use --hidden to start the app in a hidden to systray state.
** Desktop: Fix FIDO reset over NFC.
** Windows: Add option to launch Windows Settings for FIDO management.
** Android: Increase read timeout for NFC, improving compatibility with older YubiKeys.
* Version 6.3.1 (released 2023-12-12)
** Add command line options: --hidden/--shown, --log-file FILE.
** Disable autocorrect in text fields.

View File

@ -124,10 +124,12 @@ class Ctap2Node(RpcNode):
removed = False
while not event.wait(0.5):
try:
with dev.open_connection(FidoConnection):
if removed:
sleep(1.0) # Wait for the device to settle
return dev.open_connection(FidoConnection)
conn = dev.open_connection(FidoConnection)
if removed:
conn.close()
sleep(1.0) # Wait for the device to settle
return dev.open_connection(FidoConnection)
conn.close()
except CardConnectionException:
pass # Expected, ignore
except NoCardException:

21
helper/poetry.lock generated
View File

@ -235,21 +235,21 @@ files = [
[[package]]
name = "jaraco-classes"
version = "3.3.0"
version = "3.3.1"
description = "Utility functions for Python class constructs"
optional = false
python-versions = ">=3.8"
files = [
{file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"},
{file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"},
{file = "jaraco.classes-3.3.1-py3-none-any.whl", hash = "sha256:86b534de565381f6b3c1c830d13f931d7be1a75f0081c57dff615578676e2206"},
{file = "jaraco.classes-3.3.1.tar.gz", hash = "sha256:cb28a5ebda8bc47d8c8015307d93163464f9f2b91ab4006e09ff0ce07e8bfb30"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "jeepney"
@ -575,11 +575,16 @@ description = "Smartcard module for Python."
optional = false
python-versions = "*"
files = [
{file = "pyscard-2.0.7-cp310-cp310-win32.whl", hash = "sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046"},
{file = "pyscard-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab"},
{file = "pyscard-2.0.7-cp311-cp311-win32.whl", hash = "sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646"},
{file = "pyscard-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed"},
{file = "pyscard-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a"},
{file = "pyscard-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b"},
{file = "pyscard-2.0.7-cp38-cp38-win32.whl", hash = "sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c"},
{file = "pyscard-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43"},
{file = "pyscard-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc"},
{file = "pyscard-2.0.7-cp39-cp39-win32.whl", hash = "sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2"},
{file = "pyscard-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9"},
{file = "pyscard-2.0.7.tar.gz", hash = "sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf"},
]
@ -688,13 +693,13 @@ files = [
[[package]]
name = "types-pillow"
version = "10.2.0.20240125"
version = "10.2.0.20240206"
description = "Typing stubs for Pillow"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-Pillow-10.2.0.20240125.tar.gz", hash = "sha256:c449b2c43b9fdbe0494a7b950e6b39a4e50516091213fec24ef3f33c1d017717"},
{file = "types_Pillow-10.2.0.20240125-py3-none-any.whl", hash = "sha256:322dbae32b4b7918da5e8a47c50ac0f24b0aa72a804a23857620f2722b03c858"},
{file = "types-Pillow-10.2.0.20240206.tar.gz", hash = "sha256:f0de5107ff8362ffdbbd53ec896202ac905e6ab22ae784b46bcdad160ea143b9"},
{file = "types_Pillow-10.2.0.20240206-py3-none-any.whl", hash = "sha256:abc339ae28af5916146a7729261480d68ac902cd4ff57e0bdd402eee7962644d"},
]
[[package]]

View File

@ -31,11 +31,11 @@ VSVersionInfo(
'040904b0',
[StringStruct('CompanyName', 'Yubico'),
StringStruct('FileDescription', 'Yubico Authenticator Helper'),
StringStruct('FileVersion', '6.4.0-dev.0'),
StringStruct('FileVersion', '6.4.0'),
StringStruct('LegalCopyright', 'Copyright (c) Yubico'),
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
StringStruct('ProductName', 'Yubico Authenticator'),
StringStruct('ProductVersion', '6.4.0-dev.0')])
StringStruct('ProductVersion', '6.4.0')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]

View File

@ -43,7 +43,7 @@ class ElevateFidoButtons extends ConsumerWidget {
duration: const Duration(seconds: 30));
try {
if (await ref.read(rpcProvider).requireValue.elevate()) {
ref.invalidate(rpcProvider);
ref.invalidate(rpcStateProvider);
} else {
await ref.read(withContextProvider)((context) async =>
showMessage(context, l10n.s_permission_denied));

View File

@ -15,6 +15,7 @@
*/
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -25,6 +26,7 @@ import '../../app/logging.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../../desktop/models.dart';
import '../../desktop/state.dart';
import '../../fido/models.dart';
import '../../fido/state.dart';
import '../../management/models.dart';
@ -36,6 +38,7 @@ import '../features.dart' as features;
import '../message.dart';
import '../models.dart';
import '../state.dart';
import 'elevate_fido_buttons.dart';
import 'keys.dart';
final _log = Logger('fido.views.reset_dialog');
@ -68,12 +71,19 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
StreamSubscription<InteractionEvent>? _subscription;
InteractionEvent? _interaction;
int _currentStep = -1;
final _totalSteps = 3;
late final int _totalSteps;
@override
void initState() {
super.initState();
final nfc = widget.data.node.transport == Transport.nfc;
_totalSteps = nfc ? 2 : 3;
}
String _getMessage() {
final l10n = AppLocalizations.of(context)!;
final nfc = widget.data.node.transport == Transport.nfc;
if (_currentStep == 3) {
if (_currentStep == _totalSteps) {
return l10n.l_fido_app_reset;
}
return switch (_interaction) {
@ -99,20 +109,26 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
.contains(widget.data.info.formFactor);
final globalReset = isBio && (supported & Capability.piv.value) != 0;
final l10n = AppLocalizations.of(context)!;
double progress = _currentStep == -1 ? 0.0 : _currentStep / (_totalSteps);
final needsElevation = Platform.isWindows &&
_application == Capability.fido2 &&
!ref.watch(rpcStateProvider.select((state) => state.isAdmin));
return ResponsiveDialog(
title: Text(l10n.s_factory_reset),
key: factoryResetCancel,
onCancel: switch (_application) {
Capability.fido2 => _currentStep < 3
Capability.fido2 => _currentStep < _totalSteps
? () {
_currentStep = -1;
_subscription?.cancel();
}
: null,
_ => null,
},
actions: [
if (_currentStep < 3)
if (_currentStep < _totalSteps)
TextButton(
onPressed: switch (_application) {
Capability.fido2 => _subscription == null
@ -222,7 +238,8 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
: null,
tooltip:
!showLabels ? c.getDisplayName(l10n) : null,
enabled: enabled & c.value != 0,
enabled:
enabled & c.value != 0 && (_currentStep == -1),
))
.toList(),
selected: _application != null ? {_application!} : {},
@ -247,16 +264,21 @@ class _ResetDialogState extends ConsumerState<ResetDialog> {
.bodyMedium
?.copyWith(fontWeight: FontWeight.w700),
),
Text(
switch (_application) {
Capability.oath => l10n.p_warning_disable_credentials,
Capability.piv => l10n.p_warning_piv_reset_desc,
Capability.fido2 => l10n.p_warning_disable_accounts,
_ => globalReset
? l10n.p_warning_global_reset_desc
: l10n.p_factory_reset_desc,
},
),
if (needsElevation) ...[
Text(l10n.p_elevated_permissions_required),
const ElevateFidoButtons(),
] else ...[
Text(
switch (_application) {
Capability.oath => l10n.p_warning_disable_credentials,
Capability.piv => l10n.p_warning_piv_reset_desc,
Capability.fido2 => l10n.p_warning_disable_accounts,
_ => globalReset
? l10n.p_warning_global_reset_desc
: l10n.p_factory_reset_desc,
},
),
],
if (_application == Capability.fido2 && _currentStep >= 0) ...[
Text('${l10n.s_status}: ${_getMessage()}'),
LinearProgressIndicator(value: progress)

View File

@ -479,6 +479,7 @@
"l_import_nothing": null,
"l_importing_file": null,
"s_file_imported": null,
"l_unsupported_key_type": null,
"l_delete_certificate": null,
"l_delete_certificate_desc": null,
"s_issuer": null,

View File

@ -479,6 +479,7 @@
"l_import_nothing": "Nothing to import",
"l_importing_file": "Importing file\u2026",
"s_file_imported": "File imported",
"l_unsupported_key_type": "Unsupported key type",
"l_delete_certificate": "Delete certificate",
"l_delete_certificate_desc": "Remove the certificate from your YubiKey",
"s_issuer": "Issuer",

View File

@ -479,6 +479,7 @@
"l_import_nothing": null,
"l_importing_file": "Importation d'un fichier\u2026",
"s_file_imported": "Fichier importé",
"l_unsupported_key_type": null,
"l_delete_certificate": "Supprimer un certificat",
"l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey",
"s_issuer": "Émetteur",

View File

@ -479,6 +479,7 @@
"l_import_nothing": null,
"l_importing_file": "ファイルのインポート中\u2026",
"s_file_imported": "ファイル をインポートしました",
"l_unsupported_key_type": null,
"l_delete_certificate": "証明書を削除",
"l_delete_certificate_desc": "YubiKeyか証明書の削除",
"s_issuer": "発行者",

View File

@ -479,6 +479,7 @@
"l_import_nothing": null,
"l_importing_file": "Importowanie pliku\u2026",
"s_file_imported": "Plik został zaimportowany",
"l_unsupported_key_type": null,
"l_delete_certificate": "Usuń certyfikat",
"l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey",
"s_issuer": "Wydawca",

View File

@ -83,7 +83,7 @@ class _ConfigureStaticDialogState extends ConsumerState<ConfigureStaticDialog> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final password = _passwordController.text.replaceAll(' ', '');
final password = _passwordController.text;
final passwordLengthValid =
password.isNotEmpty && password.length <= passwordMaxLength;
final passwordFormatValid =

View File

@ -77,7 +77,11 @@ enum PinPolicy {
@JsonValue(0x02)
once,
@JsonValue(0x03)
always;
always,
@JsonValue(0x04)
matchOnce,
@JsonValue(0x05)
matchAlways;
const PinPolicy();

View File

@ -84,6 +84,8 @@ const _$PinPolicyEnumMap = {
PinPolicy.never: 1,
PinPolicy.once: 2,
PinPolicy.always: 3,
PinPolicy.matchOnce: 4,
PinPolicy.matchAlways: 5,
};
_$PivStateMetadataImpl _$$PivStateMetadataImplFromJson(

View File

@ -30,6 +30,7 @@ import '../keys.dart' as keys;
import '../models.dart';
import '../state.dart';
import 'overwrite_confirm_dialog.dart';
import 'utils.dart';
class GenerateKeyDialog extends ConsumerStatefulWidget {
final DevicePath devicePath;
@ -65,19 +66,6 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
_validToMax = DateTime.utc(now.year + 10, now.month, now.day);
}
List<KeyType> _getSupportedKeyTypes(bool isFips) => [
if (!isFips) KeyType.rsa1024,
KeyType.rsa2048,
if (widget.pivState.version.isAtLeast(5, 7)) ...[
KeyType.rsa3072,
KeyType.rsa4096,
KeyType.ed25519,
if (!isFips) KeyType.x25519,
],
KeyType.eccp256,
KeyType.eccp384,
];
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
@ -202,7 +190,8 @@ class _GenerateKeyDialogState extends ConsumerState<GenerateKeyDialog> {
runSpacing: 8.0,
children: [
ChoiceFilterChip<KeyType>(
items: _getSupportedKeyTypes(isFips),
items:
getSupportedKeyTypes(widget.pivState.version, isFips),
value: _keyType,
selected: _keyType != defaultKeyType,
itemBuilder: (value) => Text(value.getDisplayName(l10n)),

View File

@ -31,6 +31,7 @@ import '../models.dart';
import '../state.dart';
import 'cert_info_view.dart';
import 'overwrite_confirm_dialog.dart';
import 'utils.dart';
class ImportFileDialog extends ConsumerStatefulWidget {
final DevicePath devicePath;
@ -86,10 +87,13 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final textTheme = Theme.of(context).textTheme;
final colorScheme = Theme.of(context).colorScheme;
// This is what ListTile uses for subtitle
final subtitleStyle = textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
color: colorScheme.onSurfaceVariant,
);
// This is what TextInput errors look like
final errorStyle = textTheme.labelLarge!.copyWith(color: colorScheme.error);
final state = _state;
if (state == null) {
return ResponsiveDialog(
@ -166,116 +170,142 @@ class _ImportFileDialogState extends ConsumerState<ImportFileDialog> {
),
),
),
result: (_, keyType, certInfo) => ResponsiveDialog(
title: Text(l10n.l_import_file),
actions: [
TextButton(
key: keys.unlockButton,
onPressed: (keyType == null && certInfo == null) || _importing
? null
: () async {
final withContext = ref.read(withContextProvider);
result: (_, keyType, certInfo) {
final isFips =
ref.watch(currentDeviceDataProvider).valueOrNull?.info.isFips ??
false;
final unsupportedKey = keyType != null &&
!getSupportedKeyTypes(widget.pivState.version, isFips)
.contains(keyType);
return ResponsiveDialog(
title: Text(l10n.l_import_file),
actions: [
TextButton(
key: keys.unlockButton,
onPressed: (keyType == null && certInfo == null) ||
_importing ||
unsupportedKey
? null
: () async {
final withContext = ref.read(withContextProvider);
if (!await confirmOverwrite(
context,
widget.pivSlot,
writeKey: keyType != null,
writeCert: certInfo != null,
)) {
return;
}
if (!await confirmOverwrite(
context,
widget.pivSlot,
writeKey: keyType != null,
writeCert: certInfo != null,
)) {
return;
}
setState(() {
_importing = true;
});
void Function()? close;
try {
close = await withContext<void Function()>(
(context) async => showMessage(
context,
l10n.l_importing_file,
duration: const Duration(seconds: 30),
));
await ref
.read(pivSlotsProvider(widget.devicePath).notifier)
.import(widget.pivSlot.slot, _data,
password:
_password.isNotEmpty ? _password : null);
await withContext(
(context) async {
Navigator.of(context).pop(true);
showMessage(context, l10n.s_file_imported);
},
);
} catch (err) {
// TODO: More error cases
setState(() {
_passwordIsWrong = true;
_importing = false;
_importing = true;
});
} finally {
close?.call();
}
},
child: Text(l10n.s_import),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_import_items_desc(
widget.pivSlot.slot.getDisplayName(l10n))),
if (keyType == null && certInfo == null) ...[
Text(
l10n.l_import_nothing,
style: subtitleStyle,
softWrap: true,
textAlign: TextAlign.center,
),
],
if (keyType != null) ...[
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,
void Function()? close;
try {
close = await withContext<void Function()>(
(context) async => showMessage(
context,
l10n.l_importing_file,
duration: const Duration(seconds: 30),
));
await ref
.read(pivSlotsProvider(widget.devicePath).notifier)
.import(widget.pivSlot.slot, _data,
password:
_password.isNotEmpty ? _password : null);
await withContext(
(context) async {
Navigator.of(context).pop(true);
showMessage(context, l10n.s_file_imported);
},
);
} catch (err) {
// TODO: More error cases
setState(() {
_passwordIsWrong = true;
_importing = false;
});
} finally {
close?.call();
}
},
child: Text(l10n.s_import),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_import_items_desc(
widget.pivSlot.slot.getDisplayName(l10n))),
if (keyType == null && certInfo == null) ...[
Row(
children: [
Icon(Icons.error, color: colorScheme.error),
const SizedBox(width: 8),
Text(
l10n.l_import_nothing,
style: errorStyle,
),
],
),
],
if (keyType != null) ...[
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 (unsupportedKey)
Row(
children: [
Icon(Icons.error, color: colorScheme.error),
const SizedBox(width: 8),
Text(
l10n.l_unsupported_key_type,
style: errorStyle,
),
],
),
],
)
],
if (certInfo != null) ...[
Text(
l10n.s_certificate,
style: textTheme.bodyLarge,
softWrap: true,
textAlign: TextAlign.center,
),
SizedBox(
height: 140, // Needed for layout, adapt if text sizes changes
child: CertInfoTable(certInfo, null),
),
],
if (certInfo != null) ...[
Text(
l10n.s_certificate,
style: textTheme.bodyLarge,
softWrap: true,
textAlign: TextAlign.center,
),
SizedBox(
height:
140, // Needed for layout, adapt if text sizes changes
child: CertInfoTable(certInfo, null),
),
]
]
]
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
.map((e) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: e,
))
.toList(),
),
),
),
),
);
},
);
}
}

31
lib/piv/views/utils.dart Normal file
View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2024 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 '../../core/models.dart';
import '../models.dart';
List<KeyType> getSupportedKeyTypes(Version version, bool isFips) => [
if (!isFips) KeyType.rsa1024,
KeyType.rsa2048,
if (version.isAtLeast(5, 7)) ...[
KeyType.rsa3072,
KeyType.rsa4096,
KeyType.ed25519,
if (!isFips) KeyType.x25519,
],
KeyType.eccp256,
KeyType.eccp384,
];

View File

@ -1,5 +1,5 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
// This file is generated by running ./set-version.py <version> <build>
const String version = '6.4.0-dev.0';
const int build = 60400;
const String version = '6.4.0';
const int build = 60401;

View File

@ -18,7 +18,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# This field is updated by running ./set-version.py <version>
# DO NOT MANUALLY EDIT THIS!
version: 6.4.0-dev.0+60400
version: 6.4.0+60401
environment:
sdk: '>=3.0.0 <4.0.0'

View File

@ -1,4 +1,4 @@
$version="6.4.0-dev.0"
$version="6.4.0"
echo "Clean-up of old files"
rm *.msi