This commit is contained in:
Dain Nilsson 2022-11-28 15:59:35 +01:00
commit 71382634b5
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
19 changed files with 244 additions and 54 deletions

View File

@ -17,7 +17,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.7'
flutter-version: '3.3.9'
- run: |
flutter config
flutter --version

View File

@ -78,7 +78,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.7'
flutter-version: '3.3.9'
- run: flutter config --enable-linux-desktop
- run: flutter --version

View File

@ -9,6 +9,9 @@ jobs:
env:
PYVER: 3.11
env:
MACOSX_DEPLOYMENT_TARGET: "10.15"
steps:
- uses: actions/checkout@v3
@ -48,7 +51,7 @@ jobs:
with:
channel: 'stable'
architecture: 'x64'
flutter-version: '3.3.7'
flutter-version: '3.3.9'
- run: flutter config --enable-macos-desktop
- run: flutter --version
@ -78,12 +81,13 @@ jobs:
export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3)
mkdir deploy
mv yubioath-desktop.dmg deploy
mv build/macos/Build/Products/Release/"Yubico Authenticator.app" deploy
tar -czf deploy/yubioath-desktop-${REF}.app.tar.gz -C build/macos/Build/Products/Release "Yubico Authenticator.app"
mv create-dmg.sh deploy
mv resources/icons/dmg-background.png deploy
mv macos/helper.entitlements deploy
mv macos/helper-sandbox.entitlements deploy
mv macos/Runner/Release.entitlements deploy
mv macos/release-macos.sh deploy
- name: Upload artifact
uses: actions/upload-artifact@v3

View File

@ -49,7 +49,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.3.7'
flutter-version: '3.3.9'
- run: flutter config --enable-windows-desktop
- run: flutter --version

7
NEWS
View File

@ -1,3 +1,10 @@
* Version 6.0.2 (released 2022-11-28)
** Android: Fix USB connectivity issues after returning from sleep.
** MacOS: Fix helper subprocess compatibility on older MacOS versions.
** Better indicate if there is an error starting the helper subprocess.
** OATH: Better indicate error for too long issuer/name when scanning a QR code.
** OATH: Sorting of credential names is now case-insensitive.
* Version 6.0.1 (released 2022-11-17)
** Android: Fix issues of YubiKey NEO NFC connectivity on certain phones

View File

@ -444,9 +444,22 @@ class OathManager(
private suspend fun requestRefresh() =
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
useOathSessionUsb(usbYubiKeyDevice) { session ->
oathViewModel.updateCredentials(
calculateOathCodes(session).model(session.deviceId)
)
try {
oathViewModel.updateCredentials(
calculateOathCodes(session).model(session.deviceId)
)
} catch(apduException: ApduException) {
if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) {
Log.d(TAG, "Handled oath credential refresh on locked session.")
oathViewModel.setSessionState(session.model(keyManager.isRemembered(session.deviceId)))
} else {
Log.e(
TAG,
"Unexpected sw when refreshing oath credentials",
apduException.message
)
}
}
}
}

View File

@ -6,8 +6,8 @@ VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=(6, 0, 2, 0),
prodvers=(6, 0, 2, 0),
filevers=(6, 0, 3, 0),
prodvers=(6, 0, 3, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
@ -31,11 +31,11 @@ VSVersionInfo(
'040904b0',
[StringStruct('CompanyName', 'Yubico'),
StringStruct('FileDescription', 'Yubico Authenticator Helper'),
StringStruct('FileVersion', '6.0.2-dev.1'),
StringStruct('FileVersion', '6.0.3-dev.0'),
StringStruct('LegalCopyright', 'Copyright (c) 2022 Yubico AB'),
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
StringStruct('ProductName', 'Yubico Authenticator'),
StringStruct('ProductVersion', '6.0.2-dev.1')])
StringStruct('ProductVersion', '6.0.3-dev.0')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]

View File

@ -27,6 +27,21 @@ import 'models.dart';
final _log = Logger('app.state');
// When non-null, an unrecoverable error preventing the app from functioning has occurred.
final applicationError =
StateNotifierProvider<ApplicationErrorNotifier, String?>(
(ref) => ApplicationErrorNotifier(),
);
class ApplicationErrorNotifier extends StateNotifier<String?> {
ApplicationErrorNotifier() : super(null);
void setApplicationError(String? error) {
_log.debug('Set ApplicationError to $error');
state = error;
}
}
// Override this to alter the set of supported apps.
final supportedAppsProvider =
Provider<List<Application>>((ref) => Application.values);
@ -42,8 +57,7 @@ final supportedThemesProvider = StateProvider<List<ThemeMode>>(
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
(ref) => ThemeModeNotifier(
ref.watch(prefProvider),
ref.read(supportedThemesProvider)),
ref.watch(prefProvider), ref.read(supportedThemesProvider)),
);
class ThemeModeNotifier extends StateNotifier<ThemeMode> {

View File

@ -16,6 +16,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart';
import 'package:yubico_authenticator/cancellation_exception.dart';
import 'package:yubico_authenticator/core/state.dart';
@ -23,12 +25,16 @@ import '../../fido/views/fido_screen.dart';
import '../../oath/models.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/oath_screen.dart';
import '../../version.dart';
import '../logging.dart';
import '../message.dart';
import '../models.dart';
import '../state.dart';
import 'device_error_screen.dart';
import 'message_page.dart';
final _log = Logger('app.views.main_page');
class MainPage extends ConsumerWidget {
const MainPage({super.key});
@ -40,6 +46,35 @@ class MainPage extends ConsumerWidget {
next?.call(context);
},
);
final appError = ref.watch(applicationError);
if (appError != null) {
return MessagePage(
header: 'An unrecoverable error has occured',
message: appError,
actions: [
ActionChip(
avatar: const Icon(Icons.copy),
label: Text(AppLocalizations.of(context)!.general_copy_log),
onPressed: () async {
_log.info('Copying log to clipboard ($version)...');
final logs = await ref.read(logLevelProvider.notifier).getLogs();
var clipboard = ref.read(clipboardProvider);
await clipboard.setText(logs.join('\n'));
if (!clipboard.platformGivesFeedback()) {
await ref.read(withContextProvider)(
(context) async {
showMessage(context,
AppLocalizations.of(context)!.general_log_copied);
},
);
}
},
),
],
);
}
// If the current device changes, we need to pop any open dialogs.
ref.listen<AsyncValue<YubiKeyData>>(currentDeviceDataProvider, (_, __) {
Navigator.of(context).popUntil((route) {

View File

@ -102,7 +102,11 @@ Future<Widget> initialize(List<String> argv) async {
final rpc = RpcSession(exe!);
await rpc.initialize();
_log.info('Helper process started', exe);
rpc.setLogLevel(Logger.root.level);
// Set the initial logging level. As this is the first message to the RPC,
// it also serves to check that the Helper is functioning correctly.
// The future will be awaited further down.
final initRpcLogFuture = rpc.setLogLevel(Logger.root.level);
_initLicenses();
@ -131,7 +135,8 @@ Future<Widget> initialize(List<String> argv) async {
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
credentialProvider.overrideWithProvider(desktopCredentialProvider),
clipboardProvider.overrideWithProvider(desktopClipboardProvider),
supportedThemesProvider.overrideWithProvider(desktopSupportedThemesProvider)
supportedThemesProvider
.overrideWithProvider(desktopSupportedThemesProvider)
],
child: YubicoAuthenticatorApp(
page: Consumer(
@ -141,6 +146,17 @@ Future<Widget> initialize(List<String> argv) async {
rpc.setLogLevel(level);
});
// Ensure the initial log level was successfully set within 5s, or
// assume the Helper isn't functional.
initRpcLogFuture.timeout(const Duration(seconds: 5)).onError(
(error, stackTrace) {
_log.error('Helper is not responsive.');
ref
.read(applicationError.notifier)
.setApplicationError('Helper subprocess failed to start');
},
);
return const MainPage();
}),
),
@ -175,10 +191,12 @@ void _initLogging(List<String> argv) {
void _initLicenses() async {
LicenseRegistry.addLicense(() async* {
final python = await rootBundle.loadString('assets/licenses/raw/python.txt');
final python =
await rootBundle.loadString('assets/licenses/raw/python.txt');
yield LicenseEntryWithLineBreaks(['Python'], python);
final zxingcpp = await rootBundle.loadString('assets/licenses/raw/apache-2.0.txt');
final zxingcpp =
await rootBundle.loadString('assets/licenses/raw/apache-2.0.txt');
yield LicenseEntryWithLineBreaks(['zxing-cpp'], zxingcpp);
final helper = await rootBundle.loadStructuredData<List>(

View File

@ -121,7 +121,7 @@ class RpcSession {
);
}
} catch (e) {
_log.error(e.toString(), entry);
_log.error(entry);
}
}
@ -235,7 +235,7 @@ class RpcSession {
return request.completer.future;
}
void setLogLevel(Level level) async {
Future<void> setLogLevel(Level level) async {
final name = Levels.LEVELS
.firstWhere((e) => level.value <= e.value, orElse: () => Level.OFF)
.name

View File

@ -195,7 +195,8 @@ class FilteredCredentialsNotifier extends StateNotifier<List<OathPair>> {
.where((pair) => pair.credential.issuer != '_hidden')
.toList()
..sort((a, b) {
String searchKey(OathCredential c) => (c.issuer ?? '') + c.name;
String searchKey(OathCredential c) =>
((c.issuer ?? '') + c.name).toLowerCase();
return searchKey(a.credential).compareTo(searchKey(b.credential));
}),
);

View File

@ -261,23 +261,28 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
}
final period = int.tryParse(_periodController.text) ?? -1;
final issuerText = _issuerController.text.trim();
final nameText = _accountController.text.trim();
final remaining = getRemainingKeySpace(
oathType: _oathType,
period: period,
issuer: _issuerController.text.trim(),
name: _accountController.text.trim(),
issuer: issuerText,
name: nameText,
);
final issuerRemaining = remaining.first;
final nameRemaining = remaining.second;
final issuerMaxLength = max(issuerRemaining, 1);
final nameMaxLength = max(nameRemaining, 1);
final secret = _secretController.text.replaceAll(' ', '');
final secretLengthValid = secret.length * 5 % 8 < 5;
// is this credentials name/issuer pair different from all other?
final isUnique = _credentials
?.where((element) =>
element.name == _accountController.text.trim() &&
(element.issuer ?? '') == _issuerController.text.trim())
element.name == nameText &&
(element.issuer ?? '') == issuerText)
.isEmpty ??
true;
final issuerNoColon = !_issuerController.text.contains(':');
@ -285,7 +290,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final isLocked = oathState?.locked ?? false;
final isValid = !isLocked &&
_accountController.text.trim().isNotEmpty &&
nameText.isNotEmpty &&
secret.isNotEmpty &&
isUnique &&
issuerNoColon &&
@ -311,11 +316,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
void submit() async {
if (secretLengthValid) {
final issuer = _issuerController.text.trim();
final cred = CredentialData(
issuer: issuer.isEmpty ? null : issuer,
name: _accountController.text.trim(),
issuer: issuerText.isEmpty ? null : issuerText,
name: nameText,
secret: secret,
oathType: _oathType,
hashAlgorithm: _hashAlgorithm,
@ -389,12 +392,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
controller: _issuerController,
autofocus: widget.credentialData == null,
enabled: issuerRemaining > 0,
maxLength: max(issuerRemaining, 1),
maxLength: issuerMaxLength,
inputFormatters: [
limitBytesLength(issuerRemaining),
],
buildCounter:
buildByteCounterFor(_issuerController.text.trim()),
buildCounter: buildByteCounterFor(issuerText),
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText:
@ -402,10 +404,12 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
helperText:
'', // Prevents dialog resizing when disabled
prefixIcon: const Icon(Icons.business_outlined),
errorText: issuerNoColon
? null
: AppLocalizations.of(context)!
.oath_invalid_character_issuer,
errorText: (byteLength(issuerText) > issuerMaxLength)
? '' // needs empty string to render as error
: issuerNoColon
? null
: AppLocalizations.of(context)!
.oath_invalid_character_issuer,
),
textInputAction: TextInputAction.next,
onChanged: (value) {
@ -420,9 +424,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
TextField(
key: keys.nameField,
controller: _accountController,
maxLength: max(nameRemaining, 1),
buildCounter:
buildByteCounterFor(_accountController.text.trim()),
maxLength: nameMaxLength,
buildCounter: buildByteCounterFor(nameText),
inputFormatters: [limitBytesLength(nameRemaining)],
decoration: InputDecoration(
border: const OutlineInputBorder(),
@ -431,9 +434,12 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
AppLocalizations.of(context)!.oath_account_name,
helperText:
'', // Prevents dialog resizing when disabled
errorText: isUnique
? null
: AppLocalizations.of(context)!.oath_duplicate_name,
errorText: (byteLength(nameText) > nameMaxLength)
? '' // needs empty string to render as error
: isUnique
? null
: AppLocalizations.of(context)!
.oath_duplicate_name,
),
textInputAction: TextInputAction.next,
onChanged: (value) {

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.0.2-dev.1';
const int build = 60004;
const String version = '6.0.3-dev.0';
const int build = 60009;

View File

@ -28,10 +28,18 @@ int byteLength(String value) => utf8.encode(value).length;
/// used rather than number of characters. [currentValue] should always match
/// the input text value to measure.
InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
(context, {required currentLength, required isFocused, maxLength}) => Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: Theme.of(context).textTheme.caption,
);
(context, {required currentLength, required isFocused, maxLength}) {
final theme = Theme.of(context);
final caption = theme.textTheme.caption;
final style = (byteLength(currentValue) <= (maxLength ?? 0))
? caption
: caption?.copyWith(color: theme.errorColor);
return Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: style,
semanticsLabel: 'Character count',
);
};
/// Limits the input in length based on the byte length when encoded.
/// This is generally used together with [buildByteCounterFor].

View File

@ -407,7 +407,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@ -429,6 +429,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.yubioath;
PRODUCT_NAME = "Yubico Authenticator";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -488,7 +489,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@ -535,7 +536,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.11;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
@ -557,6 +558,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.yubioath;
PRODUCT_NAME = "Yubico Authenticator";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -579,6 +581,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 10.15;
PRODUCT_BUNDLE_IDENTIFIER = com.yubico.yubioath;
PRODUCT_NAME = "Yubico Authenticator";
PROVISIONING_PROFILE_SPECIFIER = "";

81
macos/release-macos.sh Normal file
View File

@ -0,0 +1,81 @@
#!/bin/sh
if [ -z "$1" ]
then
echo "No username given"
exit
fi
if [ -z "$2" ]
then
echo "No password given"
exit
fi
if ! command -v create-dmg &> /dev/null
then
echo "create-dmg could not be found"
exit
fi
echo "# Extract .app from .tar.gz"
tar -xzvf yubioath-desktop*.tar.gz
echo "# Sign the main binaries, with the entitlements"
codesign -f --timestamp --options runtime --entitlements helper.entitlements --sign 'Application' Yubico\ Authenticator.app/Contents/Resources/helper/authenticator-helper
codesign -f --timestamp --options runtime --entitlements helper.entitlements --sign 'Application' Yubico\ Authenticator.app/Contents/Resources/helper-arm64/authenticator-helper
echo "# Sign the dylib and so files, without entitlements"
cd Yubico\ Authenticator.app/
codesign -f --timestamp --options runtime --sign 'Application' $(find Contents/Resources/helper/ -name "*.dylib" -o -name "*.so")
codesign -f --timestamp --options runtime --sign 'Application' $(find Contents/Resources/helper-arm64/ -name "*.dylib" -o -name "*.so")
cd ..
echo "# Sign the Python binary (if it exists), without entitlements"
codesign -f --timestamp --options runtime --sign 'Application' Yubico\ Authenticator.app/Contents/Resources/helper-arm64/Python
codesign -f --timestamp --options runtime --sign 'Application' Yubico\ Authenticator.app/Contents/Resources/helper/Python
echo "# Sign the GUI"
codesign -f --timestamp --options runtime --sign 'Application' --entitlements Release.entitlements --deep "Yubico Authenticator.app"
echo "# Compress the .app to .zip and notarize"
ditto -c -k --sequesterRsrc --keepParent "Yubico Authenticator.app" "Yubico Authenticator.zip"
RES=$(xcrun altool -t osx -f "Yubico Authenticator.zip" --primary-bundle-id com.yubico.authenticator --notarize-app -u $1 -p $2)
echo ${RES}
ERRORS=${RES:0:9}
if [ "$ERRORS" != "No errors" ]; then
echo "Error uploading for notarization"
exit
fi
UUID=${RES#*=}
STATUS=$(xcrun altool --notarization-info $UUID -u $1 -p $2)
while true
do
if [[ "$STATUS" == *"in progress"* ]]; then
echo "Notarization still in progress. Sleep 30s."
sleep 30
echo "Retrieving status again."
STATUS=$(xcrun altool --notarization-info $UUID -u $1 -p $2)
else
echo "Status changed."
break
fi
done
echo "${STATUS}"
if [[ "$STATUS" == *"success"* ]]; then
echo "Notarization successfull. Staple the .app"
xcrun stapler staple -v "Yubico Authenticator.app"
echo "# Create dmg"
rm yubioath-desktop.dmg # Remove old .dmg
mkdir source_folder
mv "Yubico Authenticator.app" source_folder
sh create-dmg.sh
echo "# .dmg created. Everything should be ready for release!"
fi
echo "# End of script"

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.0.2-dev.1+60004
version: 6.0.3-dev.0+60009
environment:
sdk: ">=2.17.0 <3.0.0"

View File

@ -1,4 +1,4 @@
$version="6.0.2-dev.1"
$version="6.0.3-dev.0"
echo "Renaming the Actions folder and moving it"
mv yubioath-desktop-* release