Merge branch 'main' into adamve/nfc_activity_widget

This commit is contained in:
Adam Velebil 2023-09-26 08:40:45 +02:00
commit 3a0972e6c8
No known key found for this signature in database
GPG Key ID: C9B1E4A3CBBD2E10
144 changed files with 13622 additions and 2918 deletions

View File

@ -13,19 +13,23 @@ jobs:
distribution: 'zulu'
java-version: '11'
- uses: actions/checkout@v3
with:
path: 'app'
- name: Read variables from repo
run: cat .github/workflows/env >> $GITHUB_ENV
working-directory: ./app
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.10.1'
flutter-version: ${{ env.FLUTTER }}
- run: |
flutter config
flutter --version
- uses: actions/checkout@v3
with:
path: 'app'
- name: Check app versions
run: |
python set-version.py

View File

@ -6,12 +6,13 @@ jobs:
strings:
runs-on: ubuntu-latest
env:
FLUTTER: '3.10.1'
steps:
- uses: actions/checkout@v3
- name: Read variables from repo
run: cat .github/workflows/env >> $GITHUB_ENV
- name: Ensure main locale is correct
run: python check_strings.py lib/l10n/app_en.arb

View File

@ -39,19 +39,23 @@ jobs:
distribution: 'zulu'
java-version: '11'
- uses: actions/checkout@v3
with:
path: 'app'
- name: Read variables from repo
run: cat .github/workflows/env >> $GITHUB_ENV
working-directory: ./app
- name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.10.1'
flutter-version: ${{ env.FLUTTER }}
- run: |
flutter config
flutter --version
- uses: actions/checkout@v3
with:
path: 'app'
- name: Run flutter tests
run: |
flutter test

2
.github/workflows/env vendored Normal file
View File

@ -0,0 +1,2 @@
FLUTTER=3.13.4
PYVER=3.11.5

View File

@ -6,21 +6,25 @@ jobs:
build:
runs-on: ubuntu-latest
env:
PYVER: '3.11.3'
FLUTTER: '3.10.1'
container:
image: ubuntu:18.04
image: ubuntu:20.04
env:
DEBIAN_FRONTEND: noninteractive
steps:
- uses: actions/checkout@v3
with:
sparse-checkout: .github/workflows/env
- name: Read variables from repo
run: cat .github/workflows/env >> $GITHUB_ENV
- name: Install dependencies
run: |
export PYVER_MINOR=${PYVER%.*}
echo "PYVER_MINOR: $PYVER_MINOR"
apt-get update
apt-get install -qq software-properties-common libnotify-dev libayatana-appindicator3-dev patchelf
apt-get install -qq curl software-properties-common libnotify-dev libayatana-appindicator3-dev patchelf
add-apt-repository -y ppa:git-core/ppa
add-apt-repository -y ppa:deadsnakes/ppa
apt-get install -qq git python$PYVER_MINOR-dev python$PYVER_MINOR-venv
@ -61,7 +65,9 @@ jobs:
apt-get install -qq swig libpcsclite-dev build-essential cmake
python -m ensurepip --user
python -m pip install -U pip pipx
pipx ensurepath
# pipx ensurepath
echo "export PATH=$PATH:$HOME/.local/bin" >> ~/.bashrc
. ~/.bashrc # Needed to ensure poetry on PATH
pipx install poetry
- name: Build the Helper

View File

@ -6,13 +6,13 @@ jobs:
build:
runs-on: macos-latest
env:
PYVER: '3.11.3'
MACOSX_DEPLOYMENT_TARGET: "10.15"
steps:
- uses: actions/checkout@v3
- name: Read variables from repo
run: cat .github/workflows/env >> $GITHUB_ENV
- name: Check app versions
run: |
python3 set-version.py
@ -49,7 +49,7 @@ jobs:
with:
channel: 'stable'
architecture: 'x64'
flutter-version: '3.10.1'
flutter-version: ${{ env.FLUTTER }}
- run: flutter config --enable-macos-desktop
- run: flutter --version

View File

@ -6,12 +6,14 @@ jobs:
build:
runs-on: windows-latest
env:
PYVER: '3.11.3'
steps:
- uses: actions/checkout@v3
- name: Read variables from repo
shell: bash
run: cat .github/workflows/env >> $GITHUB_ENV
- name: Check app versions
run: |
python set-version.py
@ -45,7 +47,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version: '3.10.1'
flutter-version: ${{ env.FLUTTER }}
- run: flutter config --enable-windows-desktop
- run: flutter --version

8
NEWS
View File

@ -1,3 +1,11 @@
* Version 6.3.0 (released 2023-09-04)
** Add support for importing accounts through QR codes from Google Authenticator.
** Add community translations for French, Japanese, German and Polish languages.
** Improve user interface with new Material UI widgets.
** Bug fixes and improvements based on user feedback.
** Desktop: Add support for PIV.
** Android: Update Android 14 compatibility.
* Version 6.2.0 (released 2023-04-19)
** Add support for custom account icons.
** Desktop: Add systray icon for quick access to pinned accounts.

View File

@ -93,15 +93,15 @@ dependencies {
api "com.yubico.yubikit:oath:$project.yubiKitVersion"
api "com.yubico.yubikit:support:$project.yubiKitVersion"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
// Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1'
implementation "androidx.core:core-ktx:1.10.1"
implementation 'androidx.fragment:fragment-ktx:1.5.7'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.fragment:fragment-ktx:1.6.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'com.google.android.material:material:1.9.0'

View File

@ -50,6 +50,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
<data android:scheme="otpauth-migration" />
</intent-filter>
</activity>

View File

@ -24,8 +24,14 @@
</encoder>
</appender>
<!-- Write TRACE (and higher-level) messages to logcat -->
<appender name="buffer" class="com.yubico.authenticator.logging.BufferAppender">
<encoder>
<pattern>%d{HH:mm:ss:SSS} [%thread] %-5level %logger{36} - %X{app} %msg</pattern>
</encoder>
</appender>
<root level="TRACE">
<appender-ref ref="logcat" />
<appender-ref ref="buffer" />
</root>
</configuration>

View File

@ -24,10 +24,16 @@ import kotlinx.serialization.json.Json
typealias OnDialogCancelled = suspend () -> Unit
enum class Icon(val value: String) {
NFC("nfc"),
SUCCESS("success"),
ERROR("error");
enum class DialogIcon(val value: Int) {
Nfc(0),
Success(1),
Failure(2);
}
enum class DialogTitle(val value: Int) {
TapKey(0),
OperationSuccessful(1),
OperationFailed(2)
}
class DialogManager(messenger: BinaryMessenger, private val coroutineScope: CoroutineScope) {
@ -45,16 +51,21 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
}
}
fun showDialog(icon: Icon, title: String, description: String, cancelled: OnDialogCancelled?) {
fun showDialog(
dialogIcon: DialogIcon,
dialogTitle: DialogTitle,
dialogDescriptionId: Int,
cancelled: OnDialogCancelled?
) {
onCancelled = cancelled
coroutineScope.launch {
channel.invoke(
"show",
Json.encodeToString(
mapOf(
"title" to title,
"description" to description,
"icon" to icon.value
"title" to dialogTitle.value,
"description" to dialogDescriptionId,
"icon" to dialogIcon.value
)
)
)
@ -62,17 +73,17 @@ class DialogManager(messenger: BinaryMessenger, private val coroutineScope: Coro
}
suspend fun updateDialogState(
icon: Icon? = null,
title: String? = null,
description: String? = null
dialogIcon: DialogIcon? = null,
dialogTitle: DialogTitle,
dialogDescriptionId: Int? = null,
) {
channel.invoke(
"state",
Json.encodeToString(
mapOf(
"title" to title,
"description" to description,
"icon" to icon?.value
"title" to dialogTitle.value,
"description" to dialogDescriptionId,
"icon" to dialogIcon?.value
)
)
)

View File

@ -182,18 +182,17 @@ class MainActivity : FlutterFragmentActivity() {
@SuppressLint("WrongConstant")
override fun onStart() {
super.onStart()
val receiverFlags = ContextCompat.RECEIVER_NOT_EXPORTED
ContextCompat.registerReceiver(
this,
qrScannerCameraClosedBR,
QRScannerCameraClosedBR.intentFilter,
receiverFlags
ContextCompat.RECEIVER_NOT_EXPORTED
)
ContextCompat.registerReceiver(
this,
nfcAdapterStateChangeBR,
NfcAdapterStateChangedBR.intentFilter,
receiverFlags
ContextCompat.RECEIVER_EXPORTED
)
}
@ -222,7 +221,10 @@ class MainActivity : FlutterFragmentActivity() {
// Handle opening through otpauth:// link
val intentData = intent.data
if (intentData != null && intentData.scheme == "otpauth") {
if (intentData != null &&
(intentData.scheme == "otpauth" ||
intentData.scheme == "otpauth-migration")
) {
intent.data = null
appLinkMethodChannel.handleUri(intentData)
}
@ -366,11 +368,15 @@ class MainActivity : FlutterFragmentActivity() {
* this receiver restarts the YubiKit NFC discovery when the QR Scanner camera is closed.
*/
class QRScannerCameraClosedBR : BroadcastReceiver() {
private val logger = LoggerFactory.getLogger(QRScannerCameraClosedBR::class.java)
companion object {
val intentFilter = IntentFilter("com.yubico.authenticator.QRScannerView.CameraClosed")
}
override fun onReceive(context: Context?, intent: Intent?) {
logger.debug("Restarting nfc discovery after camera was closed.")
(context as? MainActivity)?.startNfcDiscovery()
}
}

View File

@ -0,0 +1,54 @@
/*
* 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.
*/
package com.yubico.authenticator.logging
import ch.qos.logback.classic.encoder.PatternLayoutEncoder
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.UnsynchronizedAppenderBase
class BufferAppender : UnsynchronizedAppenderBase<ILoggingEvent>() {
private var encoder: PatternLayoutEncoder? = null
private val buffer = arrayListOf<String>()
public override fun append(event: ILoggingEvent) {
if (!isStarted) {
return
}
if (buffer.size > MAX_BUFFER_SIZE) {
buffer.removeAt(0)
}
buffer.add(encoder!!.layout.doLayout(event))
}
fun getEncoder(): PatternLayoutEncoder? = encoder
fun setEncoder(encoder: PatternLayoutEncoder?) {
this.encoder = encoder
}
fun getLogBuffer(): ArrayList<String> {
return buffer
}
companion object {
private const val MAX_BUFFER_SIZE = 1000
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,11 +18,18 @@ package com.yubico.authenticator.logging
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodChannel
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class FlutterLog(messenger: BinaryMessenger) {
private var channel = MethodChannel(messenger, "android.log.redirect")
private val bufferAppender =
(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) as ch.qos.logback.classic.Logger)
.getAppender("buffer") as BufferAppender
init {
channel.setMethodCallHandler { call, result ->
when (call.method) {
@ -53,7 +60,7 @@ class FlutterLog(messenger: BinaryMessenger) {
result.success(null)
}
"getLogs" -> {
result.success(Log.getBuffer())
result.success(bufferAppender.getLogBuffer())
}
else -> {
result.notImplemented()

View File

@ -24,7 +24,7 @@ import org.slf4j.LoggerFactory
object Log {
private val logger = LoggerFactory.getLogger("com.yubico.authenticator")
private val logger = LoggerFactory.getLogger("com.yubico.authenticator.Log")
enum class LogLevel {
TRAFFIC,
@ -34,13 +34,6 @@ object Log {
ERROR
}
private const val MAX_BUFFER_SIZE = 1000
private val buffer = arrayListOf<String>()
fun getBuffer() : List<String> {
return buffer
}
private var level = if (BuildConfig.DEBUG) {
LogLevel.DEBUG
} else {
@ -56,17 +49,10 @@ object Log {
return
}
if (buffer.size > MAX_BUFFER_SIZE) {
buffer.removeAt(0)
}
val logMessage = (if (error == null)
"[$loggerName] ${level.name}: $message"
"$message [$loggerName]"
else
"[$loggerName] ${level.name}: $message (err: $error)"
).also {
buffer.add(it)
}
"$message [$loggerName] (err: $error)")
when (level) {
LogLevel.TRAFFIC -> logger.trace(logMessage)

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* 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.
@ -14,8 +14,22 @@
* limitations under the License.
*/
import 'dart:ui';
package com.yubico.authenticator.oath
const double scannerAreaRadius = 40.0;
const val dialogDescriptionOathIndex = 100
double getScannerAreaWidth(Size size) => size.width - scannerAreaRadius;
enum class OathActionDescription(private val value: Int) {
Reset(0),
Unlock(1),
SetPassword(2),
UnsetPassword(3),
AddAccount(4),
RenameAccount(5),
DeleteAccount(6),
CalculateCode(7),
ActionFailure(8),
AddMultipleAccounts(9);
val id: Int
get() = value + dialogDescriptionOathIndex
}

View File

@ -63,6 +63,7 @@ import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.*
import kotlinx.serialization.encodeToString
import org.slf4j.LoggerFactory
import java.io.IOException
import java.net.URI
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
@ -197,6 +198,7 @@ class OathManager(
// OATH methods callable from Flutter:
oathChannel.setHandler(coroutineScope) { method, args ->
@Suppress("UNCHECKED_CAST")
when (method) {
"reset" -> reset()
"unlock" -> unlock(
@ -229,6 +231,11 @@ class OathManager(
args["requireTouch"] as Boolean
)
"addAccountsToAny" -> addAccountsToAny(
args["uris"] as List<String>,
args["requireTouch"] as List<Boolean>
)
else -> throw NotImplementedError()
}
}
@ -367,7 +374,7 @@ class OathManager(
val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri))
addToAny = true
return useOathSessionNfc("Add account") { session ->
return useOathSessionNfc(OathActionDescription.AddAccount) { session ->
// We need to check for duplicates here since we haven't yet read the credentials
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
throw Exception("A credential with this ID already exists!")
@ -390,8 +397,46 @@ class OathManager(
}
}
private suspend fun addAccountsToAny(
uris: List<String>,
requireTouch: List<Boolean>,
): String {
logger.trace("Adding following accounts: {}", uris)
addToAny = true
return useOathSessionNfc(OathActionDescription.AddMultipleAccounts) { session ->
var successCount = 0
for (index in uris.indices) {
val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uris[index]))
if (session.credentials.any { it.id.contentEquals(credentialData.id) }) {
logger.info("A credential with this ID already exists, skipping")
continue
}
val credential = session.putCredential(credentialData, requireTouch[index])
val code =
if (credentialData.oathType == YubiKitOathType.TOTP && !requireTouch[index]) {
// recalculate the code
calculateCode(session, credential)
} else null
oathViewModel.addCredential(
Credential(credential, session.deviceId),
Code.from(code)
)
logger.trace("Added cred {}", credential)
successCount++
}
jsonSerializer.encodeToString(mapOf("succeeded" to successCount))
}
}
private suspend fun reset(): String =
useOathSession("Reset YubiKey") {
useOathSession(OathActionDescription.Reset) {
// note, it is ok to reset locked session
it.reset()
keyManager.removeKey(it.deviceId)
@ -403,7 +448,7 @@ class OathManager(
}
private suspend fun unlock(password: String, remember: Boolean): String =
useOathSession("Unlocking") {
useOathSession(OathActionDescription.Unlock) {
val accessKey = it.deriveAccessKey(password.toCharArray())
keyManager.addKey(it.deviceId, accessKey, remember)
@ -425,7 +470,7 @@ class OathManager(
currentPassword: String?,
newPassword: String,
): String =
useOathSession("Set password", unlock = false) { session ->
useOathSession(OathActionDescription.SetPassword, unlock = false) { session ->
if (session.isAccessKeySet) {
if (currentPassword == null) {
throw Exception("Must provide current password to be able to change it")
@ -444,7 +489,7 @@ class OathManager(
}
private suspend fun unsetPassword(currentPassword: String): String =
useOathSession("Unset password", unlock = false) { session ->
useOathSession(OathActionDescription.UnsetPassword, unlock = false) { session ->
if (session.isAccessKeySet) {
// test current password sent by the user
if (session.unlock(currentPassword.toCharArray())) {
@ -476,7 +521,7 @@ class OathManager(
uri: String,
requireTouch: Boolean,
): String =
useOathSession("Add account") { session ->
useOathSession(OathActionDescription.AddAccount) { session ->
val credentialData: CredentialData =
CredentialData.parseUri(URI.create(uri))
@ -497,7 +542,7 @@ class OathManager(
}
private suspend fun renameAccount(uri: String, name: String, issuer: String?): String =
useOathSession("Rename") { session ->
useOathSession(OathActionDescription.RenameAccount) { session ->
val credential = getOathCredential(session, uri)
val renamedCredential =
Credential(session.renameCredential(credential, name, issuer), session.deviceId)
@ -510,39 +555,57 @@ class OathManager(
}
private suspend fun deleteAccount(credentialId: String): String =
useOathSession("Delete account") { session ->
useOathSession(OathActionDescription.DeleteAccount) { session ->
val credential = getOathCredential(session, credentialId)
session.deleteCredential(credential)
oathViewModel.removeCredential(Credential(credential, session.deviceId))
NULL
}
private suspend fun requestRefresh() =
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
useOathSessionUsb(usbYubiKeyDevice) { session ->
try {
oathViewModel.updateCredentials(calculateOathCodes(session))
} catch (apduException: ApduException) {
if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) {
logger.debug("Handled oath credential refresh on locked session.")
oathViewModel.setSessionState(
Session(
session,
keyManager.isRemembered(session.deviceId)
)
)
} else {
logger.error(
"Unexpected sw when refreshing oath credentials",
apduException
)
}
}
}
private suspend fun requestRefresh() {
val clearCodes = {
val currentCredentials = oathViewModel.credentials.value
oathViewModel.updateCredentials(currentCredentials?.associate {
it.credential to null
} ?: emptyMap())
}
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
try {
useOathSessionUsb(usbYubiKeyDevice) { session ->
try {
oathViewModel.updateCredentials(calculateOathCodes(session))
} catch (apduException: ApduException) {
if (apduException.sw == SW.SECURITY_CONDITION_NOT_SATISFIED) {
logger.debug("Handled oath credential refresh on locked session.")
oathViewModel.setSessionState(
Session(
session,
keyManager.isRemembered(session.deviceId)
)
)
} else {
logger.error(
"Unexpected sw when refreshing oath credentials",
apduException
)
}
}
}
} catch (ioException: IOException) {
logger.error("IOException when accessing USB device: ", ioException)
clearCodes()
} catch (illegalStateException: IllegalStateException) {
logger.error("IllegalStateException when accessing USB device: ", illegalStateException)
clearCodes()
}
}
}
private suspend fun calculate(credentialId: String): String =
useOathSession("Calculate") { session ->
useOathSession(OathActionDescription.CalculateCode) { session ->
val credential = getOathCredential(session, credentialId)
val code = Code.from(calculateCode(session, credential))
@ -655,7 +718,7 @@ class OathManager(
}
private suspend fun <T> useOathSession(
title: String,
oathActionDescription: OathActionDescription,
unlock: Boolean = true,
action: (YubiKitOathSession) -> T
): T {
@ -664,7 +727,7 @@ class OathManager(
unlockOnConnect.set(unlock)
return appViewModel.connectedYubiKey.value?.let {
useOathSessionUsb(it, action)
} ?: useOathSessionNfc(title, action)
} ?: useOathSessionNfc(oathActionDescription, action)
}
private suspend fun <T> useOathSessionUsb(
@ -675,7 +738,7 @@ class OathManager(
}
private suspend fun <T> useOathSessionNfc(
title: String,
oathActionDescription: OathActionDescription,
block: (YubiKitOathSession) -> T
): T {
try {
@ -685,15 +748,15 @@ class OathManager(
block.invoke(it.value)
})
}
dialogManager.showDialog(Icon.NFC, "Tap your key", title) {
logger.debug("Cancelled Dialog {}", title)
dialogManager.showDialog(DialogIcon.Nfc, DialogTitle.TapKey, oathActionDescription.id) {
logger.debug("Cancelled Dialog {}", oathActionDescription.name)
pendingAction?.invoke(Result.failure(CancellationException()))
pendingAction = null
}
}
dialogManager.updateDialogState(
icon = Icon.SUCCESS,
title = "Success"
dialogIcon = DialogIcon.Success,
dialogTitle = DialogTitle.OperationSuccessful
)
// TODO: This delays the closing of the dialog, but also the return value
delay(500)
@ -702,9 +765,9 @@ class OathManager(
throw cancelled
} catch (error: Throwable) {
dialogManager.updateDialogState(
icon = Icon.ERROR,
title = "Failure",
description = "Action failed - try again"
dialogIcon = DialogIcon.Failure,
dialogTitle = DialogTitle.OperationFailed,
dialogDescriptionId = OathActionDescription.ActionFailure.id
)
// TODO: This delays the closing of the dialog, but also the return value
delay(1500)
@ -721,4 +784,4 @@ class OathManager(
} ?: throw Exception("Failed to find account")
}
}

View File

@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.8.10'
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
@ -26,7 +26,7 @@ allprojects {
yubiKitVersion = "2.3.0"
junitVersion = "4.13.2"
mockitoVersion = "5.3.1"
mockitoVersion = "5.5.0"
}
}

View File

@ -2,7 +2,7 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing'
version '1.0'
buildscript {
ext.kotlin_version = '1.8.10'
ext.kotlin_version = '1.9.0'
repositories {
google()
mavenCentral()
@ -49,7 +49,7 @@ android {
}
dependencies {
def camerax_version = "1.2.2"
def camerax_version = "1.2.3"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

View File

@ -26,6 +26,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.util.Size
import android.view.View
import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
@ -81,8 +82,6 @@ internal class QRScannerView(
private val stateChangeObserver = StateChangeObserver(context)
private val uiThreadHandler = Handler(Looper.getMainLooper())
private var marginPct: Double? = null
companion object {
const val TAG = "QRScannerView"
@ -93,9 +92,6 @@ internal class QRScannerView(
Manifest.permission.CAMERA,
).toTypedArray()
// view related
private const val QR_SCANNER_ASPECT_RATIO = AspectRatio.RATIO_4_3
// communication channel
private const val CHANNEL_NAME =
"com.yubico.authenticator.flutter_plugins.qr_scanner_channel"
@ -128,10 +124,20 @@ internal class QRScannerView(
private var imageAnalysis: ImageAnalysis? = null
private var preview: Preview? = null
private var barcodeAnalyzer : BarcodeAnalyzer = BarcodeAnalyzer(marginPct) { analyzeResult ->
if (analyzeResult.isSuccess) {
analyzeResult.getOrNull()?.let { result ->
reportCodeFound(result)
private val barcodeAnalyzer = with(creationParams) {
var marginPct : Double? = null
if (this?.get("margin") is Number) {
val marginValue = this["margin"] as Number
if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) {
marginPct = marginValue.toDouble()
}
}
BarcodeAnalyzer(marginPct) { analyzeResult ->
if (analyzeResult.isSuccess) {
analyzeResult.getOrNull()?.let { result ->
reportCodeFound(result)
}
}
}
}
@ -155,19 +161,11 @@ internal class QRScannerView(
private val methodChannel: MethodChannel = MethodChannel(binaryMessenger, CHANNEL_NAME)
private var permissionsGranted = false
private val screenSize = with(context.resources.displayMetrics) {
Size(widthPixels, heightPixels)
}
init {
// read margin parameter
// only use it if it has reasonable value
if (creationParams?.get("margin") is Number) {
val marginValue = creationParams["margin"] as Number
if (marginValue.toDouble() > 0.0 && marginValue.toDouble() < 45) {
marginPct = marginValue.toDouble()
}
}
Log.v(TAG, "marginPct: $marginPct")
if (context is Activity) {
permissionsGranted = allPermissionsGranted(context)
@ -259,14 +257,14 @@ internal class QRScannerView(
imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO)
.setTargetResolution(Size(768,1024))
.build()
.also {
it.setAnalyzer(cameraExecutor, barcodeAnalyzer)
}
preview = Preview.Builder()
.setTargetAspectRatio(QR_SCANNER_ASPECT_RATIO)
.setTargetResolution(screenSize)
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
@ -369,7 +367,7 @@ internal class QRScannerView(
val fullSize = BinaryBitmap(HybridBinarizer(luminanceSource))
val bitmapToProcess = if (marginPct != null) {
val bitmapToProcess = if (marginPct != null && fullSize.isCropSupported) {
val shorterDim = min(imageProxy.width, imageProxy.height)
val cropMargin = marginPct * 0.01 * shorterDim
val cropWH = shorterDim - 2.0 * cropMargin
@ -395,6 +393,10 @@ internal class QRScannerView(
}
val result: com.google.zxing.Result = multiFormatReader.decode(bitmapToProcess)
if (analysisPaused) {
return
}
analysisPaused = true // pause
Log.v(TAG, "Analysis result: ${result.text}")
listener.invoke(Result.success(result.text))

View File

@ -7,7 +7,7 @@
set -e
case "$(uname)" in
Darwin*)
Darwin*)
OS="macos";;
Linux*)
OS="linux";;
@ -20,6 +20,50 @@ OUTPUT="build/$OS"
cd helper
poetry install
# Create a universal binary on MacOS
if [ "$OS" = "macos" ]; then
PYTHON=`poetry run python -c "import sys; print(sys.executable)"`
echo "Using Python: $PYTHON"
if [ $(lipo -archs $PYTHON | grep -c 'x86_64 arm64') -ne 0 ]; then
echo "Fixing single-arch dependencies..."
export MACOSX_DEPLOYMENT_TARGET="10.15"
export CFLAGS="-arch x86_64 -arch arm64"
export ARCHFLAGS="-arch x86_64 -arch arm64"
HELPER="../$OUTPUT/helper"
rm -rf $HELPER
mkdir -p $HELPER
# Needed to build zxing-cpp properly
export CMAKE_OSX_ARCHITECTURES="arm64;x86_64"
# Export exact versions
poetry export --without-hashes > $HELPER/requirements.txt
grep cryptography $HELPER/requirements.txt > $HELPER/cryptography.txt
grep cffi $HELPER/requirements.txt > $HELPER/cffi.txt
grep pillow $HELPER/requirements.txt > $HELPER/pillow.txt
grep zxing-cpp $HELPER/requirements.txt > $HELPER/zxing-cpp.txt
# Remove non-universal packages
poetry run pip uninstall -y cryptography cffi pillow zxing-cpp
# Build cffi from source to get universal build
poetry run pip install --upgrade -r $HELPER/cffi.txt --no-binary cffi
# Build zxing-cpp from source to get universal build
poetry run pip install --upgrade -r $HELPER/zxing-cpp.txt --no-binary zxing-cpp
# Explicitly install pre-build universal build of cryptography
poetry run pip download -r $HELPER/cryptography.txt --platform macosx_10_12_universal2 --only-binary :all: --no-deps --dest $HELPER
poetry run pip install -r $HELPER/cryptography.txt --no-cache-dir --no-index --find-links $HELPER
# Combine wheels of pillow to get universal build
poetry run pip download -r $HELPER/pillow.txt --platform macosx_10_10_x86_64 --only-binary :all: --no-deps --dest $HELPER
poetry run pip download -r $HELPER/pillow.txt --platform macosx_11_0_arm64 --only-binary :all: --no-deps --dest $HELPER
poetry run pip install delocate
poetry run delocate-fuse $HELPER/Pillow*.whl
WHL=$(ls $HELPER/Pillow*x86_64.whl)
UNIVERSAL_WHL=${WHL//x86_64/universal2}
mv $WHL $UNIVERSAL_WHL
poetry run pip install --upgrade $UNIVERSAL_WHL
fi
fi
rm -rf ../$OUTPUT/helper
poetry run pyinstaller authenticator-helper.spec --distpath ../$OUTPUT
cd ..

View File

@ -93,7 +93,7 @@ if len(sys.argv) != 2:
target = sys.argv[1]
with open(target) as f:
with open(target, encoding='utf-8') as f:
values = json.load(f, object_pairs_hook=check_duplicate_keys)
strings = {k: v for k, v in values.items() if not k.startswith("@")}

View File

@ -28,12 +28,30 @@ on `yubikey-manager >=5, <6`. It will likely not work with versions outside
this range!
=== Building the Yubico Authenticator Helper
Requirements: Python >= 3.8 and Poetry.
The GUI requires a compiled version of Helper to run, which is built from the
sources in helper/ in this repository. This needs to be build prior to running
`flutter build` or `flutter run`, by running `build-helper.sh` (or
`build-helper.bat` on Windows).
sources in helper/ in this repository. Requirements for all platforms are
Python >= 3.8 and Poetry. This needs to be built prior to running
`flutter build` or `flutter run`.
==== Windows
Make sure the http://www.swig.org/[swig] executable is in your PATH.
==== macOS
$ brew install swig
==== Linux (Debian-based distributions)
$ sudo apt install swig libu2f-udev pcscd libpcsclite-dev
==== Linux (RPM-based distributons)
# Tested on Fedora 34
$ sudo dnf install pcsc-lite-devel python3-devel swig
When prerequisites are installed you build the helper by running `build-helper.sh`
(or `build-helper.bat` on Windows).
NOTE: You will need to re-run the build script if changes have been made to
Helper's code, or if `flutter clean` has been run.

View File

@ -1,4 +1,6 @@
# -*- mode: python ; coding: utf-8 -*-
import sys
import subprocess
block_cipher = None
@ -21,6 +23,13 @@ a = Analysis(
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
target_arch = None
# MacOS: If the running Python process is "universal", build a univeral2 binary.
if sys.platform == "darwin":
r = subprocess.run(['lipo', '-archs', sys.executable], capture_output=True).stdout
if b"x86_64" in r and b"arm64" in r:
target_arch = "universal2"
exe = EXE(
pyz,
a.scripts,
@ -36,7 +45,7 @@ exe = EXE(
manifest="authenticator-helper.exe.manifest",
version="version_info.txt",
disable_windowed_traceback=False,
target_arch=None,
target_arch=target_arch,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from yubikit.core import InvalidPinError
from functools import partial
import logging
@ -123,6 +124,8 @@ class RpcNode:
except ChildResetException as e:
self._close_child()
raise StateResetException(e.message, traversed)
except InvalidPinError:
raise # Prevent catching this as a ValueError below
except ValueError as e:
raise InvalidParametersException(e)
raise NoSuchActionException(action)

View File

@ -24,6 +24,7 @@ from .oath import OathNode
from .fido import Ctap2Node
from .yubiotp import YubiOtpNode
from .management import ManagementNode
from .piv import PivNode
from .qr import scan_qr
from ykman import __version__ as ykman_version
from ykman.base import PID
@ -391,6 +392,13 @@ class ConnectionNode(RpcNode):
def oath(self):
return OathNode(self._connection)
@child(
condition=lambda self: isinstance(self._connection, SmartCardConnection)
and CAPABILITY.PIV in self.capabilities
)
def piv(self):
return PivNode(self._connection)
@child(
condition=lambda self: isinstance(self._connection, FidoConnection)
and CAPABILITY.FIDO2 in self.capabilities

481
helper/helper/piv.py Normal file
View File

@ -0,0 +1,481 @@
# 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.
from .base import (
RpcNode,
action,
child,
RpcException,
ChildResetException,
TimeoutException,
AuthRequiredException,
)
from yubikit.core import NotSupportedError, BadResponseError, InvalidPinError
from yubikit.core.smartcard import ApduError, SW
from yubikit.piv import (
PivSession,
OBJECT_ID,
MANAGEMENT_KEY_TYPE,
SLOT,
require_version,
KEY_TYPE,
PIN_POLICY,
TOUCH_POLICY,
)
from ykman.piv import (
get_pivman_data,
get_pivman_protected_data,
derive_management_key,
pivman_set_mgm_key,
pivman_change_pin,
generate_self_signed_certificate,
generate_csr,
generate_chuid,
parse_rfc4514_string,
)
from ykman.util import (
parse_certificates,
parse_private_key,
get_leaf_certificates,
InvalidPasswordError,
)
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from cryptography.hazmat.primitives import hashes
from dataclasses import asdict
from enum import Enum, unique
from threading import Timer
from time import time
import datetime
import logging
logger = logging.getLogger(__name__)
_date_format = "%Y-%m-%d"
class InvalidPinException(RpcException):
def __init__(self, cause):
super().__init__(
"invalid-pin",
"Wrong PIN",
dict(attempts_remaining=cause.attempts_remaining),
)
@unique
class GENERATE_TYPE(str, Enum):
CSR = "csr"
CERTIFICATE = "certificate"
class PivNode(RpcNode):
def __init__(self, connection):
super().__init__()
self.session = PivSession(connection)
self._pivman_data = get_pivman_data(self.session)
self._authenticated = False
def __call__(self, *args, **kwargs):
try:
return super().__call__(*args, **kwargs)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
raise AuthRequiredException()
# TODO: This should probably be in a baseclass of all "AppNodes".
raise ChildResetException(f"SW: {e.sw:x}")
except InvalidPinError as e:
raise InvalidPinException(cause=e)
def _get_object(self, object_id):
try:
return self.session.get_object(object_id)
except ApduError as e:
if e.sw == SW.FILE_NOT_FOUND:
return None
raise
except BadResponseError:
logger.warning(f"Couldn't read data object {object_id}", exc_info=True)
return None
def get_data(self):
try:
pin_md = self.session.get_pin_metadata()
puk_md = self.session.get_puk_metadata()
mgm_md = self.session.get_management_key_metadata()
pin_attempts = pin_md.attempts_remaining
metadata = dict(
pin_metadata=asdict(pin_md),
puk_metadata=asdict(puk_md),
management_key_metadata=asdict(mgm_md),
)
except NotSupportedError:
pin_attempts = self.session.get_pin_attempts()
metadata = None
return dict(
version=self.session.version,
authenticated=self._authenticated,
derived_key=self._pivman_data.has_derived_key,
stored_key=self._pivman_data.has_stored_key,
chuid=self._get_object(OBJECT_ID.CHUID),
ccc=self._get_object(OBJECT_ID.CAPABILITY),
pin_attempts=pin_attempts,
metadata=metadata,
)
def _authenticate(self, key, signal):
try:
metadata = self.session.get_management_key_metadata()
key_type = metadata.key_type
if metadata.touch_policy != TOUCH_POLICY.NEVER:
signal("touch")
timer = None
except NotSupportedError:
key_type = MANAGEMENT_KEY_TYPE.TDES
timer = Timer(0.5, lambda: signal("touch"))
timer.start()
try:
# TODO: Check if this is needed, maybe SW is enough
start = time()
self.session.authenticate(key_type, key)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED and time() - start > 5:
raise TimeoutException()
raise
finally:
if timer:
timer.cancel()
self._authenticated = True
@action
def verify_pin(self, params, event, signal):
pin = params.pop("pin")
self.session.verify_pin(pin)
key = None
if self._pivman_data.has_derived_key:
key = derive_management_key(pin, self._pivman_data.salt)
elif self._pivman_data.has_stored_key:
pivman_prot = get_pivman_protected_data(self.session)
key = pivman_prot.key
if key:
try:
self._authenticate(key, signal)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
pass # Authenticate failed, bad derived key?
# Ensure verify was the last thing we did
self.session.verify_pin(pin)
return dict(status=True, authenticated=self._authenticated)
@action
def authenticate(self, params, event, signal):
key = bytes.fromhex(params.pop("key"))
try:
self._authenticate(key, signal)
return dict(status=True)
except ApduError as e:
if e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED:
return dict(status=False)
raise
@action(condition=lambda self: self._authenticated)
def set_key(self, params, event, signal):
key_type = MANAGEMENT_KEY_TYPE(params.pop("key_type", MANAGEMENT_KEY_TYPE.TDES))
key = bytes.fromhex(params.pop("key"))
store_key = params.pop("store_key", False)
pivman_set_mgm_key(self.session, key, key_type, False, store_key)
self._pivman_data = get_pivman_data(self.session)
return dict()
@action
def change_pin(self, params, event, signal):
old_pin = params.pop("pin")
new_pin = params.pop("new_pin")
pivman_change_pin(self.session, old_pin, new_pin)
return dict()
@action
def change_puk(self, params, event, signal):
old_puk = params.pop("puk")
new_puk = params.pop("new_puk")
self.session.change_puk(old_puk, new_puk)
return dict()
@action
def unblock_pin(self, params, event, signal):
puk = params.pop("puk")
new_pin = params.pop("new_pin")
self.session.unblock_pin(puk, new_pin)
return dict()
@action
def reset(self, params, event, signal):
self.session.reset()
self._authenticated = False
self._pivman_data = get_pivman_data(self.session)
return dict()
@child
def slots(self):
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):
return SLOT(int(name, base=16))
def _parse_file(data, password=None):
if password:
password = password.encode()
try:
certs = parse_certificates(data, password)
except (ValueError, TypeError):
certs = []
try:
private_key = parse_private_key(data, password)
except (ValueError, TypeError):
private_key = None
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):
def __init__(self, session):
super().__init__()
self.session = session
try:
require_version(session.version, (5, 3, 0))
self._has_metadata = True
except NotSupportedError:
self._has_metadata = False
self.refresh()
def refresh(self):
self._slots = {}
for slot in set(SLOT) - {SLOT.ATTESTATION}:
metadata = None
if self._has_metadata:
try:
metadata = self.session.get_slot_metadata(slot)
except (ApduError, BadResponseError):
pass
try:
certificate = self.session.get_certificate(slot)
except (ApduError, BadResponseError):
# TODO: Differentiate between none and malformed
certificate = None
self._slots[slot] = (metadata, certificate)
if self._child and _slot_for(self._child_name) not in self._slots:
self._close_child()
def list_children(self):
return {
f"{int(slot):02x}": dict(
slot=int(slot),
name=slot.name,
has_key=metadata is not None if self._has_metadata else None,
cert_info=_get_cert_info(cert),
)
for slot, (metadata, cert) in self._slots.items()
}
def create_child(self, name):
slot = _slot_for(name)
if slot in self._slots:
metadata, certificate = self._slots[slot]
return SlotNode(self.session, slot, metadata, certificate, self.refresh)
return super().create_child(name)
class SlotNode(RpcNode):
def __init__(self, session, slot, metadata, certificate, refresh):
super().__init__()
self.session = session
self.slot = slot
self.metadata = metadata
self.certificate = certificate
self._refresh = refresh
def get_data(self):
return dict(
id=f"{int(self.slot):02x}",
name=self.slot.name,
metadata=asdict(self.metadata) if self.metadata else None,
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if self.certificate
else None,
)
@action(condition=lambda self: self.certificate)
def delete(self, params, event, signal):
self.session.delete_certificate(self.slot)
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
self._refresh()
self.certificate = None
return dict()
@action
def import_file(self, params, event, signal):
data = bytes.fromhex(params.pop("data"))
password = params.pop("password", None)
try:
private_key, certs = _parse_file(data, password)
except InvalidPasswordError:
logger.debug("Invalid or missing password", exc_info=True)
raise ValueError("Wrong/Missing password")
# Exception?
if not certs and not private_key:
raise ValueError("Failed to parse")
metadata = None
if private_key:
pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT))
touch_policy = TOUCH_POLICY(
params.pop("touch_policy", TOUCH_POLICY.DEFAULT)
)
self.session.put_key(self.slot, private_key, pin_policy, touch_policy)
try:
metadata = self.session.get_slot_metadata(self.slot)
except (ApduError, BadResponseError, NotSupportedError):
pass
certificate = _choose_cert(certs)
if certificate:
self.session.put_certificate(self.slot, certificate)
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
self.certificate = certificate
self._refresh()
return dict(
metadata=asdict(metadata) if metadata else None,
public_key=private_key.public_key()
.public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
.decode()
if private_key
else None,
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
if certs
else None,
)
@action
def generate(self, params, event, signal):
key_type = KEY_TYPE(params.pop("key_type"))
pin_policy = PIN_POLICY(params.pop("pin_policy", PIN_POLICY.DEFAULT))
touch_policy = TOUCH_POLICY(params.pop("touch_policy", TOUCH_POLICY.DEFAULT))
subject = params.pop("subject")
generate_type = GENERATE_TYPE(
params.pop("generate_type", GENERATE_TYPE.CERTIFICATE)
)
public_key = self.session.generate_key(
self.slot, key_type, pin_policy, touch_policy
)
if pin_policy != PIN_POLICY.NEVER:
# TODO: Check if verified?
pin = params.pop("pin")
self.session.verify_pin(pin)
if touch_policy in (TOUCH_POLICY.ALWAYS, TOUCH_POLICY.CACHED):
signal("touch")
if generate_type == GENERATE_TYPE.CSR:
result = generate_csr(self.session, self.slot, public_key, subject)
elif generate_type == GENERATE_TYPE.CERTIFICATE:
now = datetime.datetime.utcnow()
then = now + datetime.timedelta(days=365)
valid_from = params.pop("valid_from", now.strftime(_date_format))
valid_to = params.pop("valid_to", then.strftime(_date_format))
result = generate_self_signed_certificate(
self.session,
self.slot,
public_key,
subject,
datetime.datetime.strptime(valid_from, _date_format),
datetime.datetime.strptime(valid_to, _date_format),
)
self.session.put_certificate(self.slot, result)
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
else:
raise ValueError("Unsupported GENERATE_TYPE")
self._refresh()
return dict(
public_key=public_key.public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
).decode(),
result=result.public_bytes(encoding=Encoding.PEM).decode(),
)

View File

@ -22,7 +22,6 @@ import subprocess # nosec
import tempfile
from mss.exception import ScreenShotError
from PIL import Image
import numpy.core.multiarray # noqa
def _capture_screen():

462
helper/poetry.lock generated
View File

@ -1,10 +1,9 @@
# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
[[package]]
name = "altgraph"
version = "0.17.3"
description = "Python graph (network) package"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -16,7 +15,6 @@ files = [
name = "cffi"
version = "1.15.1"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
files = [
@ -91,14 +89,13 @@ pycparser = "*"
[[package]]
name = "click"
version = "8.1.3"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
@ -108,7 +105,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
@ -118,31 +114,34 @@ files = [
[[package]]
name = "cryptography"
version = "40.0.2"
version = "41.0.4"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_universal2.whl", hash = "sha256:8f79b5ff5ad9d3218afb1e7e20ea74da5f76943ee5edb7f76e56ec5161ec782b"},
{file = "cryptography-40.0.2-cp36-abi3-macosx_10_12_x86_64.whl", hash = "sha256:05dc219433b14046c476f6f09d7636b92a1c3e5808b9a6536adf4932b3b2c440"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4df2af28d7bedc84fe45bd49bc35d710aede676e2a4cb7fc6d103a2adc8afe4d"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dcca15d3a19a66e63662dc8d30f8036b07be851a8680eda92d079868f106288"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a04386fb7bc85fab9cd51b6308633a3c271e3d0d3eae917eebab2fac6219b6d2"},
{file = "cryptography-40.0.2-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:adc0d980fd2760c9e5de537c28935cc32b9353baaf28e0814df417619c6c8c3b"},
{file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:d5a1bd0e9e2031465761dfa920c16b0065ad77321d8a8c1f5ee331021fda65e9"},
{file = "cryptography-40.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a95f4802d49faa6a674242e25bfeea6fc2acd915b5e5e29ac90a32b1139cae1c"},
{file = "cryptography-40.0.2-cp36-abi3-win32.whl", hash = "sha256:aecbb1592b0188e030cb01f82d12556cf72e218280f621deed7d806afd2113f9"},
{file = "cryptography-40.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:b12794f01d4cacfbd3177b9042198f3af1c856eedd0a98f10f141385c809a14b"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:142bae539ef28a1c76794cca7f49729e7c54423f615cfd9b0b1fa90ebe53244b"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:956ba8701b4ffe91ba59665ed170a2ebbdc6fc0e40de5f6059195d9f2b33ca0e"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f01c9863da784558165f5d4d916093737a75203a5c5286fde60e503e4276c7a"},
{file = "cryptography-40.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:3daf9b114213f8ba460b829a02896789751626a2a4e7a43a28ee77c04b5e4958"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48f388d0d153350f378c7f7b41497a54ff1513c816bcbbcafe5b829e59b9ce5b"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c0764e72b36a3dc065c155e5b22f93df465da9c39af65516fe04ed3c68c92636"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:cbaba590180cba88cb99a5f76f90808a624f18b169b90a4abb40c1fd8c19420e"},
{file = "cryptography-40.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7a38250f433cd41df7fcb763caa3ee9362777fdb4dc642b9a349721d2bf47404"},
{file = "cryptography-40.0.2.tar.gz", hash = "sha256:c33c0d32b8594fa647d2e01dbccc303478e16fdd7cf98652d5b3ed11aa5e5c99"},
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:80907d3faa55dc5434a16579952ac6da800935cd98d14dbd62f6f042c7f5e839"},
{file = "cryptography-41.0.4-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:35c00f637cd0b9d5b6c6bd11b6c3359194a8eba9c46d4e875a3660e3b400005f"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cecfefa17042941f94ab54f769c8ce0fe14beff2694e9ac684176a2535bf9714"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e40211b4923ba5a6dc9769eab704bdb3fbb58d56c5b336d30996c24fcf12aadb"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:23a25c09dfd0d9f28da2352503b23e086f8e78096b9fd585d1d14eca01613e13"},
{file = "cryptography-41.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2ed09183922d66c4ec5fdaa59b4d14e105c084dd0febd27452de8f6f74704143"},
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:5a0f09cefded00e648a127048119f77bc2b2ec61e736660b5789e638f43cc397"},
{file = "cryptography-41.0.4-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:9eeb77214afae972a00dee47382d2591abe77bdae166bda672fb1e24702a3860"},
{file = "cryptography-41.0.4-cp37-abi3-win32.whl", hash = "sha256:3b224890962a2d7b57cf5eeb16ccaafba6083f7b811829f00476309bce2fe0fd"},
{file = "cryptography-41.0.4-cp37-abi3-win_amd64.whl", hash = "sha256:c880eba5175f4307129784eca96f4e70b88e57aa3f680aeba3bab0e980b0f37d"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:004b6ccc95943f6a9ad3142cfabcc769d7ee38a3f60fb0dddbfb431f818c3a67"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:86defa8d248c3fa029da68ce61fe735432b047e32179883bdb1e79ed9bb8195e"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:37480760ae08065437e6573d14be973112c9e6dcaf5f11d00147ee74f37a3829"},
{file = "cryptography-41.0.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b5f4dfe950ff0479f1f00eda09c18798d4f49b98f4e2006d644b3301682ebdca"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7e53db173370dea832190870e975a1e09c86a879b613948f09eb49324218c14d"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5b72205a360f3b6176485a333256b9bcd48700fc755fef51c8e7e67c4b63e3ac"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:93530900d14c37a46ce3d6c9e6fd35dbe5f5601bf6b3a5c325c7bffc030344d9"},
{file = "cryptography-41.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:efc8ad4e6fc4f1752ebfb58aefece8b4e3c4cae940b0994d43649bdfce8d0d4f"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3391bd8e6de35f6f1140e50aaeb3e2b3d6a9012536ca23ab0d9c35ec18c8a91"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0d9409894f495d465fe6fda92cb70e8323e9648af912d5b9141d616df40a87b8"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8ac4f9ead4bbd0bc8ab2d318f97d85147167a488be0e08814a37eb2f439d5cf6"},
{file = "cryptography-41.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:047c4603aeb4bbd8db2756e38f5b8bd7e94318c047cfe4efeb5d715e08b49311"},
{file = "cryptography-41.0.4.tar.gz", hash = "sha256:7febc3094125fc126a7f6fb1f420d0da639f3f32cb15c8ff0dc3997c4549f51a"},
]
[package.dependencies]
@ -151,23 +150,22 @@ cffi = ">=1.12"
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"]
pep8test = ["black", "check-manifest", "mypy", "ruff"]
sdist = ["setuptools-rust (>=0.11.4)"]
nox = ["nox"]
pep8test = ["black", "check-sdist", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-shard (>=0.1.2)", "pytest-subtests", "pytest-xdist"]
test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
tox = ["tox"]
[[package]]
name = "exceptiongroup"
version = "1.1.1"
version = "1.1.3"
description = "Backport of PEP 654 (exception groups)"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
{file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
]
[package.extras]
@ -175,32 +173,30 @@ test = ["pytest (>=6)"]
[[package]]
name = "fido2"
version = "1.1.1"
version = "1.1.2"
description = "FIDO2/WebAuthn library for implementing clients and servers."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "fido2-1.1.1-py3-none-any.whl", hash = "sha256:54017b69522b1581e4222443a0b3fff5eb2626f8e773a4a7b955f3e55fb3b4fc"},
{file = "fido2-1.1.1.tar.gz", hash = "sha256:5dc495ca8c59c1c337383b4b8c314d46b92d5c6fc650e71984c6d7f954079fc3"},
{file = "fido2-1.1.2-py3-none-any.whl", hash = "sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8"},
{file = "fido2-1.1.2.tar.gz", hash = "sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573"},
]
[package.dependencies]
cryptography = ">=2.6,<35 || >35,<43"
cryptography = ">=2.6,<35 || >35,<44"
[package.extras]
pcsc = ["pyscard (>=1.9,<3)"]
[[package]]
name = "importlib-metadata"
version = "6.4.1"
version = "6.8.0"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-6.4.1-py3-none-any.whl", hash = "sha256:63ace321e24167d12fbb176b6015f4dbe06868c54a2af4f15849586afb9027fd"},
{file = "importlib_metadata-6.4.1.tar.gz", hash = "sha256:eb1a7933041f0f85c94cd130258df3fb0dec060ad8c1c9318892ef4192c47ce1"},
{file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
{file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
]
[package.dependencies]
@ -209,32 +205,30 @@ zipp = ">=0.5"
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]]
name = "importlib-resources"
version = "5.12.0"
version = "6.0.1"
description = "Read resources from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"},
{file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"},
{file = "importlib_resources-6.0.1-py3-none-any.whl", hash = "sha256:134832a506243891221b88b4ae1213327eea96ceb4e407a00d790bb0626f45cf"},
{file = "importlib_resources-6.0.1.tar.gz", hash = "sha256:4359457e42708462b9626a04657c6208ad799ceb41e5c58c57ffa0e6a098a5d4"},
]
[package.dependencies]
zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
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"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -244,28 +238,26 @@ files = [
[[package]]
name = "jaraco-classes"
version = "3.2.3"
version = "3.3.0"
description = "Utility functions for Python class constructs"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"},
{file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"},
{file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"},
{file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"},
]
[package.dependencies]
more-itertools = "*"
[package.extras]
docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
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"]
[[package]]
name = "jeepney"
version = "0.8.0"
description = "Low-level, pure Python DBus protocol wrapper."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -281,7 +273,6 @@ trio = ["async_generator", "trio"]
name = "keyring"
version = "23.13.1"
description = "Store and access your passwords safely."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
@ -306,7 +297,6 @@ testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-chec
name = "macholib"
version = "1.16.2"
description = "Mach-O header analysis and editing"
category = "dev"
optional = false
python-versions = "*"
files = [
@ -319,71 +309,30 @@ altgraph = ">=0.17"
[[package]]
name = "more-itertools"
version = "9.1.0"
version = "10.1.0"
description = "More routines for operating on iterables, beyond itertools"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"},
{file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"},
{file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"},
{file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"},
]
[[package]]
name = "mss"
version = "8.0.3"
version = "9.0.1"
description = "An ultra fast cross-platform multiple screenshots module in pure python using ctypes."
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "mss-8.0.3-py3-none-any.whl", hash = "sha256:87c1eda213dab83431013ca98ee7217e536439f28446b979bb38d8f7af5c7d34"},
{file = "mss-8.0.3.tar.gz", hash = "sha256:07dc0602e325434e867621f257a8ec6ea14bdffd00bfa554a69bef554af7f524"},
]
[[package]]
name = "numpy"
version = "1.24.2"
description = "Fundamental package for array computing in Python"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
{file = "numpy-1.24.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d"},
{file = "numpy-1.24.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5"},
{file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253"},
{file = "numpy-1.24.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978"},
{file = "numpy-1.24.2-cp310-cp310-win32.whl", hash = "sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9"},
{file = "numpy-1.24.2-cp310-cp310-win_amd64.whl", hash = "sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0"},
{file = "numpy-1.24.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a"},
{file = "numpy-1.24.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0"},
{file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281"},
{file = "numpy-1.24.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910"},
{file = "numpy-1.24.2-cp311-cp311-win32.whl", hash = "sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95"},
{file = "numpy-1.24.2-cp311-cp311-win_amd64.whl", hash = "sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04"},
{file = "numpy-1.24.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2"},
{file = "numpy-1.24.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5"},
{file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a"},
{file = "numpy-1.24.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96"},
{file = "numpy-1.24.2-cp38-cp38-win32.whl", hash = "sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d"},
{file = "numpy-1.24.2-cp38-cp38-win_amd64.whl", hash = "sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756"},
{file = "numpy-1.24.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a"},
{file = "numpy-1.24.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f"},
{file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb"},
{file = "numpy-1.24.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780"},
{file = "numpy-1.24.2-cp39-cp39-win32.whl", hash = "sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468"},
{file = "numpy-1.24.2-cp39-cp39-win_amd64.whl", hash = "sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5"},
{file = "numpy-1.24.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d"},
{file = "numpy-1.24.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"},
{file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
{file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
{file = "mss-9.0.1-py3-none-any.whl", hash = "sha256:7ee44db7ab14cbea6a3eb63813c57d677a109ca5979d3b76046e4bddd3ca1a0b"},
{file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"},
]
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -395,7 +344,6 @@ files = [
name = "pefile"
version = "2023.2.7"
description = "Python PE parsing module"
category = "dev"
optional = false
python-versions = ">=3.6.0"
files = [
@ -405,78 +353,65 @@ files = [
[[package]]
name = "pillow"
version = "9.5.0"
version = "10.0.1"
description = "Python Imaging Library (Fork)"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "Pillow-9.5.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16"},
{file = "Pillow-9.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa"},
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38"},
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062"},
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e"},
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5"},
{file = "Pillow-9.5.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d"},
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903"},
{file = "Pillow-9.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a"},
{file = "Pillow-9.5.0-cp310-cp310-win32.whl", hash = "sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44"},
{file = "Pillow-9.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb"},
{file = "Pillow-9.5.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32"},
{file = "Pillow-9.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c"},
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3"},
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a"},
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1"},
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99"},
{file = "Pillow-9.5.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625"},
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579"},
{file = "Pillow-9.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296"},
{file = "Pillow-9.5.0-cp311-cp311-win32.whl", hash = "sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec"},
{file = "Pillow-9.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4"},
{file = "Pillow-9.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089"},
{file = "Pillow-9.5.0-cp312-cp312-win32.whl", hash = "sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb"},
{file = "Pillow-9.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b"},
{file = "Pillow-9.5.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906"},
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf"},
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78"},
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270"},
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392"},
{file = "Pillow-9.5.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47"},
{file = "Pillow-9.5.0-cp37-cp37m-win32.whl", hash = "sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7"},
{file = "Pillow-9.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6"},
{file = "Pillow-9.5.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597"},
{file = "Pillow-9.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c"},
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a"},
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082"},
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf"},
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf"},
{file = "Pillow-9.5.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51"},
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96"},
{file = "Pillow-9.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f"},
{file = "Pillow-9.5.0-cp38-cp38-win32.whl", hash = "sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc"},
{file = "Pillow-9.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569"},
{file = "Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66"},
{file = "Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e"},
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115"},
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3"},
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef"},
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705"},
{file = "Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1"},
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a"},
{file = "Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865"},
{file = "Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964"},
{file = "Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d"},
{file = "Pillow-9.5.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5"},
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140"},
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba"},
{file = "Pillow-9.5.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829"},
{file = "Pillow-9.5.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd"},
{file = "Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572"},
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe"},
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1"},
{file = "Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7"},
{file = "Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799"},
{file = "Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1"},
{file = "Pillow-10.0.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a"},
{file = "Pillow-10.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d"},
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d"},
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19"},
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f"},
{file = "Pillow-10.0.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff"},
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf"},
{file = "Pillow-10.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd"},
{file = "Pillow-10.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0"},
{file = "Pillow-10.0.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"},
{file = "Pillow-10.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1"},
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21"},
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54"},
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205"},
{file = "Pillow-10.0.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2"},
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b"},
{file = "Pillow-10.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1"},
{file = "Pillow-10.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088"},
{file = "Pillow-10.0.1-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b"},
{file = "Pillow-10.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed"},
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635"},
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad"},
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a"},
{file = "Pillow-10.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91"},
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4"},
{file = "Pillow-10.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08"},
{file = "Pillow-10.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08"},
{file = "Pillow-10.0.1-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a"},
{file = "Pillow-10.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68"},
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500"},
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21"},
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d"},
{file = "Pillow-10.0.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7"},
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a"},
{file = "Pillow-10.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7"},
{file = "Pillow-10.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3"},
{file = "Pillow-10.0.1-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849"},
{file = "Pillow-10.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1"},
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37"},
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876"},
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f"},
{file = "Pillow-10.0.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145"},
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2"},
{file = "Pillow-10.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf"},
{file = "Pillow-10.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971"},
{file = "Pillow-10.0.1-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db"},
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e"},
{file = "Pillow-10.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4"},
{file = "Pillow-10.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f"},
{file = "Pillow-10.0.1-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf"},
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317"},
{file = "Pillow-10.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d"},
{file = "Pillow-10.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d"},
{file = "Pillow-10.0.1.tar.gz", hash = "sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d"},
]
[package.extras]
@ -485,14 +420,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "pluggy"
version = "1.0.0"
version = "1.3.0"
description = "plugin and hook calling mechanisms for python"
category = "dev"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.8"
files = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
]
[package.extras]
@ -503,7 +437,6 @@ testing = ["pytest", "pytest-benchmark"]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
@ -513,24 +446,23 @@ files = [
[[package]]
name = "pyinstaller"
version = "5.10.1"
version = "5.13.2"
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
category = "dev"
optional = false
python-versions = "<3.12,>=3.7"
python-versions = "<3.13,>=3.7"
files = [
{file = "pyinstaller-5.10.1-py3-none-macosx_10_13_universal2.whl", hash = "sha256:247b99c52dc3cf69eba905da30dbca0a8ea309e1058cab44658ac838d9b8f2f0"},
{file = "pyinstaller-5.10.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:2d16641a495593d174504263b038a6d3d46b3b15a381ccb216cf6cce67723512"},
{file = "pyinstaller-5.10.1-py3-none-manylinux2014_i686.whl", hash = "sha256:df97aaf1103a1c485aa3c9947792a86675e370f5ce9b436b4a84e34a4180c8d2"},
{file = "pyinstaller-5.10.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:333b4ffda38d9c0a561c38429dd9848d37aa78f3b8ea8a6f2b2e69a60d523c02"},
{file = "pyinstaller-5.10.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:6afc7aa4885ffd3e6121a8cf2138830099f874c18cb5869bed8c1a42db82d060"},
{file = "pyinstaller-5.10.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:85e39e36d03355423636907a26a9bfa06fdc93cb1086441b19d2d0ca448479fa"},
{file = "pyinstaller-5.10.1-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:7a1db833bb0302b66ae3ae337fbd5487699658ce869ca4d538b5359b8179e83a"},
{file = "pyinstaller-5.10.1-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:bb7de35cd209a0a0358aec761a273ae951d2161c03728f15d9a640d06a88e472"},
{file = "pyinstaller-5.10.1-py3-none-win32.whl", hash = "sha256:9e9a38f41f8280c8e29b294716992852281b41fbe64ba330ebab671efe27b26d"},
{file = "pyinstaller-5.10.1-py3-none-win_amd64.whl", hash = "sha256:915a502802c751bafd92d568ac57468ec6cdf252b8308aa9a167bbc2c565ad2d"},
{file = "pyinstaller-5.10.1-py3-none-win_arm64.whl", hash = "sha256:f677fbc151db1eb00ada94e86ed128e7b359cbd6bf3f6ea815afdde687692d46"},
{file = "pyinstaller-5.10.1.tar.gz", hash = "sha256:6ecc464bf56919bf2d6bff275f38d85ff08ae747b8ead3a0c26cf85573b3c723"},
{file = "pyinstaller-5.13.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:16cbd66b59a37f4ee59373a003608d15df180a0d9eb1a29ff3bfbfae64b23d0f"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8f6dd0e797ae7efdd79226f78f35eb6a4981db16c13325e962a83395c0ec7420"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_i686.whl", hash = "sha256:65133ed89467edb2862036b35d7c5ebd381670412e1e4361215e289c786dd4e6"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:7d51734423685ab2a4324ab2981d9781b203dcae42839161a9ee98bfeaabdade"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:2c2fe9c52cb4577a3ac39626b84cf16cf30c2792f785502661286184f162ae0d"},
{file = "pyinstaller-5.13.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c63ef6133eefe36c4b2f4daf4cfea3d6412ece2ca218f77aaf967e52a95ac9b8"},
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:aadafb6f213549a5906829bb252e586e2cf72a7fbdb5731810695e6516f0ab30"},
{file = "pyinstaller-5.13.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:b2e1c7f5cceb5e9800927ddd51acf9cc78fbaa9e79e822c48b0ee52d9ce3c892"},
{file = "pyinstaller-5.13.2-py3-none-win32.whl", hash = "sha256:421cd24f26144f19b66d3868b49ed673176765f92fa9f7914cd2158d25b6d17e"},
{file = "pyinstaller-5.13.2-py3-none-win_amd64.whl", hash = "sha256:ddcc2b36052a70052479a9e5da1af067b4496f43686ca3cdda99f8367d0627e4"},
{file = "pyinstaller-5.13.2-py3-none-win_arm64.whl", hash = "sha256:27cd64e7cc6b74c5b1066cbf47d75f940b71356166031deb9778a2579bb874c6"},
{file = "pyinstaller-5.13.2.tar.gz", hash = "sha256:c8e5d3489c3a7cc5f8401c2d1f48a70e588f9967e391c3b06ddac1f685f8d5d2"},
]
[package.dependencies]
@ -538,7 +470,7 @@ altgraph = "*"
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
pyinstaller-hooks-contrib = ">=2021.4"
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
setuptools = ">=42.0.0"
[package.extras]
@ -547,29 +479,32 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2023.2"
version = "2023.8"
description = "Community maintained hooks for PyInstaller"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyinstaller-hooks-contrib-2023.2.tar.gz", hash = "sha256:7fb856a81fd06a717188a3175caa77e902035cc067b00b583c6409c62497b23f"},
{file = "pyinstaller_hooks_contrib-2023.2-py2.py3-none-any.whl", hash = "sha256:e02c5f0ee3d4f5814588c2128caf5036c058ba764aaf24d957bb5311ad8690ad"},
{file = "pyinstaller-hooks-contrib-2023.8.tar.gz", hash = "sha256:318ccc316fb2b8c0bbdff2456b444bf1ce0e94cb3948a0f4dd48f6fc33d41c01"},
{file = "pyinstaller_hooks_contrib-2023.8-py2.py3-none-any.whl", hash = "sha256:d091a52fbeed71cde0359aa9ad66288521a8441cfba163d9446606c5136c72a8"},
]
[[package]]
name = "pyscard"
version = "2.0.7"
description = "Smartcard module for Python."
category = "main"
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"},
]
@ -580,14 +515,13 @@ pyro = ["Pyro"]
[[package]]
name = "pytest"
version = "7.3.1"
version = "7.4.2"
description = "pytest: simple powerful testing with Python"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-7.3.1-py3-none-any.whl", hash = "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362"},
{file = "pytest-7.3.1.tar.gz", hash = "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3"},
{file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
{file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
]
[package.dependencies]
@ -599,13 +533,12 @@ pluggy = ">=0.12,<2.0"
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
category = "main"
optional = false
python-versions = "*"
files = [
@ -627,21 +560,19 @@ files = [
[[package]]
name = "pywin32-ctypes"
version = "0.2.0"
description = ""
category = "main"
version = "0.2.2"
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
optional = false
python-versions = "*"
python-versions = ">=3.6"
files = [
{file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
{file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
{file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"},
{file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"},
]
[[package]]
name = "secretstorage"
version = "3.3.3"
description = "Python bindings to FreeDesktop.org Secret Service API"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
@ -655,26 +586,24 @@ jeepney = ">=0.6"
[[package]]
name = "setuptools"
version = "67.6.1"
version = "68.2.2"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "setuptools-67.6.1-py3-none-any.whl", hash = "sha256:e728ca814a823bf7bf60162daf9db95b93d532948c4c0bea762ce62f60189078"},
{file = "setuptools-67.6.1.tar.gz", hash = "sha256:257de92a9d50a60b8e22abfcbb771571fde0dbf3ec234463212027a4eeecbe9a"},
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
@ -684,19 +613,18 @@ files = [
[[package]]
name = "yubikey-manager"
version = "5.1.0"
version = "5.2.0"
description = "Tool for managing your YubiKey configuration."
category = "main"
optional = false
python-versions = ">=3.7,<4.0"
files = [
{file = "yubikey_manager-5.1.0-py3-none-any.whl", hash = "sha256:72ac412319ee9c9db13173a68326de11478f1e8b3ed13b25bb3d33157b3f958e"},
{file = "yubikey_manager-5.1.0.tar.gz", hash = "sha256:d33efc9f82e511fd4d7c9397f6c40b37c7260221ca06fac93daeb4a46b1eb173"},
{file = "yubikey_manager-5.2.0-py3-none-any.whl", hash = "sha256:6e0c82605f92012363ae3d69673eec6c7876e2e366aa049cff66cc6734049165"},
{file = "yubikey_manager-5.2.0.tar.gz", hash = "sha256:45e0f09e3cee2375b6f930dd5d89c1d3a7ca5d5cccb599b16a12f8f7d989fd36"},
]
[package.dependencies]
click = ">=8.0,<9.0"
cryptography = ">=3.0,<43"
cryptography = ">=3.0,<44"
fido2 = ">=1.0,<2.0"
keyring = ">=23.4,<24.0"
pyscard = ">=2.0,<3.0"
@ -704,50 +632,50 @@ pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
[[package]]
name = "zipp"
version = "3.15.0"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"},
{file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"},
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[[package]]
name = "zxing-cpp"
version = "2.0.0"
version = "2.1.0"
description = "Python bindings for the zxing-cpp barcode library"
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "zxing-cpp-2.0.0.tar.gz", hash = "sha256:1b67b221aae15aad9b5609d99c38d57875bc0a4fef864142d7ca37e9ee7880b0"},
{file = "zxing_cpp-2.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54282d0e5c573754049113a0cdbf14cc1c6b986432a367d8a788112afa92a1d5"},
{file = "zxing_cpp-2.0.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76caafb8fc1e12c2e5ec33ce4f340a0e15e9a2aabfbfeaec170e8a2b405b8a77"},
{file = "zxing_cpp-2.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95dd06dc559f53c1ca0eb59dbaebd802ebc839937baaf2f8d2b3def3e814c07f"},
{file = "zxing_cpp-2.0.0-cp310-cp310-win32.whl", hash = "sha256:ea54fd242f93eea7bf039a68287e5e57fdf77d78e3bd5b4cbb2d289bb3380d63"},
{file = "zxing_cpp-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:8da9c912cca5829eedb2800ce3eaa1b1e52742f536aa9e798be69bf09639f399"},
{file = "zxing_cpp-2.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f70eefa5dc1fd9238087c024ef22f3d99ba79cb932a2c5bc5b0f1e152037722e"},
{file = "zxing_cpp-2.0.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97919f07c62edf1c8e0722fd64893057ce636b7067cf47bd593e98cc7e404d74"},
{file = "zxing_cpp-2.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd89065f620d6b78281308c6abfb760d95760a1c9b88eb7ac612b52b331bd41"},
{file = "zxing_cpp-2.0.0-cp311-cp311-win32.whl", hash = "sha256:631a0c783ad233c85295e0cf4cd7740f1fe2853124c61b1ef6bcf7eb5d2fa5e6"},
{file = "zxing_cpp-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:9f0c2c03f5df470ef71a7590be5042161e7590da767d4260a6d0d61a3fa80b88"},
{file = "zxing_cpp-2.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ce391f21763f00d5be3431e16d075e263e4b9205c2cf55d708625cb234b1f15"},
{file = "zxing_cpp-2.0.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0eefdfad91e15e3f5b7ed16d83806a36f96ca482f4b042baa6297784a58b0b3"},
{file = "zxing_cpp-2.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d665c45029346c70ae3df5dbc36f6335ffe4f275e98dc43772fa32a65844196"},
{file = "zxing_cpp-2.0.0-cp39-cp39-win32.whl", hash = "sha256:214a6a0e49b92fda8d2761c74f5bfd24a677b9bf1d0ef0e083412486af97faa9"},
{file = "zxing_cpp-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:a788551ddf3a6ba1152ff9a0b81d57018a3cc586544087c39d881428745faf1f"},
{file = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ced97aac933484556fcaa40fb402459b7af801dace4d87f4ac515661d267b211"},
]
[package.dependencies]
numpy = "*"
[package.source]
type = "file"
url = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl"
[[package]]
name = "zxing-cpp"
version = "2.1.0"
description = "Python bindings for the zxing-cpp barcode library"
optional = false
python-versions = ">=3.6"
files = []
develop = false
[package.source]
type = "git"
url = "https://github.com/zxing-cpp/zxing-cpp.git"
reference = "18a722a"
resolved_reference = "18a722a443855063a00b27d03d33794fa573a61f"
subdirectory = "wrappers/python"
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "f0fc2e7d5ef423dc8b247ab6b968a63c331e78bd74bd72020b634f6823a74e3d"
content-hash = "cc024d1775a438623aad52e1c9bf693808fbbf6b0a2c5001bde7964dbd87bba7"

View File

@ -10,14 +10,17 @@ packages = [
[tool.poetry.dependencies]
python = "^3.8"
yubikey-manager = "5.1.0"
mss = "^8.0.3"
zxing-cpp = "^2.0.0"
Pillow = "^9.5.0"
yubikey-manager = "5.2.0"
mss = "^9.0.1"
zxing-cpp = [
{git = "https://github.com/zxing-cpp/zxing-cpp.git", rev="18a722a", subdirectory = "wrappers/python", markers = "sys_platform != 'win32'"},
{path = "zxing_cpp-2.1.0-cp311-cp311-win_amd64.whl", markers = "sys_platform == 'win32'"}
]
Pillow = "^10.0.0"
[tool.poetry.dev-dependencies]
pyinstaller = {version = "^5.10.1", python = "<3.12"}
pytest = "^7.3.1"
pyinstaller = {version = "^5.13.0", python = "<3.12"}
pytest = "^7.4.0"
[build-system]
requires = ["poetry-core>=1.0.0"]

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, 3, 0, 0),
prodvers=(6, 3, 0, 0),
filevers=(6, 3, 1, 0),
prodvers=(6, 3, 1, 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.3.0-dev.0'),
StringStruct('FileVersion', '6.3.1-dev.0'),
StringStruct('LegalCopyright', 'Copyright (c) Yubico'),
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
StringStruct('ProductName', 'Yubico Authenticator'),
StringStruct('ProductVersion', '6.3.0-dev.0')])
StringStruct('ProductVersion', '6.3.1-dev.0')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]

Binary file not shown.

View File

@ -15,4 +15,6 @@
*/
/// list of YubiKey serial numbers which are approved to be used with integration tests
var approvedYubiKeys = <String>[];
var approvedYubiKeys = <String>[
'',
];

View File

@ -14,14 +14,14 @@
* limitations under the License.
*/
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/management/views/keys.dart'
as management_keys;
import 'test_util.dart';
import 'utils/test_util.dart';
Key _getCapabilityWidgetKey(bool isUsb, String name) =>
Key('management.keys.capability.${isUsb ? 'usb' : 'nfc'}.$name');
@ -37,7 +37,8 @@ void main() {
group('Management UI tests', () {
appTest('Drawer items exist', (WidgetTester tester) async {
await tester.openDrawer();
expect(find.byKey(app_keys.managementAppDrawer), findsOneWidget);
expect(find.byKey(app_keys.managementAppDrawer).hitTestable(),
findsOneWidget);
});
});

View File

@ -19,8 +19,8 @@ import 'package:integration_test/integration_test.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/oath/keys.dart' as keys;
import 'oath_test_util.dart';
import 'test_util.dart';
import 'utils/oath_test_util.dart';
import 'utils/test_util.dart';
void main() {
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();

View File

@ -30,9 +30,10 @@ Future<void> startUp(WidgetTester tester,
// only wait for yubikey connection when needed
// needs_yubikey defaults to true
if (startUpParams['needs_yubikey'] != false) {
await tester.openDrawer();
// wait for a YubiKey connection
await tester.waitForFinder(find.descendant(
of: tester.findDeviceButton(),
of: find.byKey(app_keys.deviceInfoListTile),
matching: find.byWidgetPredicate((widget) =>
widget is DeviceAvatar && widget.key != app_keys.noDeviceAvatar)));
}

View File

@ -17,12 +17,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/oath/keys.dart' as keys;
import 'package:yubico_authenticator/oath/views/account_list.dart';
import 'package:yubico_authenticator/oath/views/account_view.dart';
import 'android/util.dart';
import 'test_util.dart';
import '../utils/test_util.dart';
class Account {
final String? issuer;
@ -235,8 +236,12 @@ extension OathFunctions on WidgetTester {
/// now the account dialog is shown
/// TODO verify it shows correct issuer and name
/// close the account dialog by tapping out of it
await tapAt(const Offset(10, 10));
/// close the account dialog by tapping the close button
var closeButton = find.byKey(app_keys.closeButton).hitTestable();
// Wait for toast to clear
await waitForFinder(closeButton);
await tap(closeButton);
await longWait();
/// verify accounts in the list

View File

@ -17,18 +17,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:yubico_authenticator/app/views/device_button.dart';
import 'package:yubico_authenticator/app/views/keys.dart' as app_keys;
import 'package:yubico_authenticator/app/views/keys.dart';
import 'package:yubico_authenticator/core/state.dart';
import 'package:yubico_authenticator/management/views/keys.dart';
import 'android/util.dart' as android_test_util;
import 'approved_yubikeys.dart';
import '../_approved_yubikeys.dart';
import 'desktop/util.dart' as desktop_test_util;
const shortWaitMs = 10;
const longWaitMs = 50;
const shortWaitMs = 500;
const longWaitMs = 500;
/// information about YubiKey as seen by the app
String? yubiKeyName;
@ -65,16 +64,6 @@ extension AppWidgetTester on WidgetTester {
return f;
}
Finder findDeviceButton() {
return find.byType(DeviceButton).hitTestable();
}
/// Taps the device button
Future<void> tapDeviceButton() async {
await tap(findDeviceButton());
await pump(const Duration(milliseconds: 500));
}
Finder findActionIconButton() {
return find.byKey(actionsIconButtonKey).hitTestable();
}
@ -119,7 +108,7 @@ extension AppWidgetTester on WidgetTester {
await openDrawer();
}
await tap(find.byKey(managementAppDrawer));
await tap(find.byKey(managementAppDrawer).hitTestable());
await pump(const Duration(milliseconds: 500));
expect(find.byKey(screenKey), findsOneWidget);
@ -153,17 +142,22 @@ extension AppWidgetTester on WidgetTester {
return;
}
await tapDeviceButton();
await openDrawer();
var deviceInfo = find.byKey(app_keys.deviceInfoListTile);
if (deviceInfo.evaluate().isNotEmpty) {
ListTile lt = deviceInfo.evaluate().single.widget as ListTile;
ListTile lt = find
.descendant(of: deviceInfo, matching: find.byType(ListTile))
.evaluate()
.single
.widget as ListTile;
//ListTile lt = deviceInfo.evaluate().single.widget as ListTile;
yubiKeyName = (lt.title as Text).data;
var subtitle = (lt.subtitle as Text?)?.data;
if (subtitle != null) {
RegExpMatch? match = RegExp(r'S/N: (\d.*) F/W: (\d\.\d\.\d)')
.firstMatch(subtitle);
RegExpMatch? match =
RegExp(r'S/N: (\d.*) F/W: (\d\.\d\.\d)').firstMatch(subtitle);
if (match != null) {
yubiKeySerialNumber = match.group(1);
yubiKeyFirmware = match.group(2);
@ -177,7 +171,7 @@ extension AppWidgetTester on WidgetTester {
}
// close the opened menu
await tapTopLeftCorner();
await closeDrawer();
testLog(false,
'Connected YubiKey: $yubiKeySerialNumber/$yubiKeyFirmware - $yubiKeyName');

View File

@ -23,22 +23,19 @@ import '../../app/models.dart';
import '../../app/state.dart';
import '../../management/state.dart';
final androidManagementState = StateNotifierProvider.autoDispose
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
(ref, devicePath) {
// Make sure to rebuild if currentDevice changes (as on reboot)
ref.watch(currentDeviceProvider);
final notifier = _AndroidManagementStateNotifier(ref);
return notifier..refresh();
},
final androidManagementState = AsyncNotifierProvider.autoDispose
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
_AndroidManagementStateNotifier.new,
);
class _AndroidManagementStateNotifier extends ManagementStateNotifier {
final Ref _ref;
@override
FutureOr<DeviceInfo> build(DevicePath devicePath) {
// Make sure to rebuild if currentDevice changes (as on reboot)
ref.watch(currentDeviceProvider);
_AndroidManagementStateNotifier(this._ref) : super();
void refresh() async {}
return Completer<DeviceInfo>().future;
}
@override
Future<void> setMode(
@ -55,6 +52,6 @@ class _AndroidManagementStateNotifier extends ManagementStateNotifier {
state = const AsyncValue.loading();
}
_ref.read(attachedDevicesProvider.notifier).refresh();
ref.read(attachedDevicesProvider.notifier).refresh();
}
}

View File

@ -18,10 +18,9 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../oath/models.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/utils.dart';
const _appLinkMethodsChannel = MethodChannel('app.link.methods');
@ -30,24 +29,11 @@ void setupOtpAuthLinkHandler(BuildContext context) {
final args = jsonDecode(call.arguments);
switch (call.method) {
case 'handleOtpAuthLink':
{
var url = args['link'];
var otpauth = CredentialData.fromUri(Uri.parse(url));
Navigator.popUntil(context, ModalRoute.withName('/'));
await showBlurDialog(
context: context,
routeSettings: const RouteSettings(name: 'oath_add_account'),
builder: (_) {
return OathAddAccountPage(
null,
null,
credentials: null,
credentialData: otpauth,
);
},
);
break;
}
Navigator.popUntil(context, ModalRoute.withName('/'));
final l10n = AppLocalizations.of(context)!;
final uri = args['link'];
await handleUri(context, null, uri, null, null, l10n);
break;
default:
throw PlatformException(
code: 'NotImplemented',

View File

@ -22,6 +22,7 @@ import 'package:flutter/services.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/exception/cancellation_exception.dart';
import '../../app/logging.dart';
import '../../app/models.dart';
@ -36,33 +37,31 @@ final _log = Logger('android.oath.state');
const _methods = MethodChannel('android.oath.methods');
final androidOathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
(ref, devicePath) => _AndroidOathStateNotifier());
final androidOathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
_AndroidOathStateNotifier.new);
class _AndroidOathStateNotifier extends OathStateNotifier {
final _events = const EventChannel('android.oath.sessionState');
late StreamSubscription _sub;
_AndroidOathStateNotifier() : super() {
@override
FutureOr<OathState> build(DevicePath arg) {
_sub = _events.receiveBroadcastStream().listen((event) {
final json = jsonDecode(event);
if (mounted) {
if (json == null) {
state = const AsyncValue.loading();
} else {
final oathState = OathState.fromJson(json);
state = AsyncValue.data(oathState);
}
if (json == null) {
state = const AsyncValue.loading();
} else {
final oathState = OathState.fromJson(json);
state = AsyncValue.data(oathState);
}
}, onError: (err, stackTrace) {
state = AsyncValue.error(err, stackTrace);
});
}
@override
void dispose() {
_sub.cancel();
super.dispose();
ref.onDispose(_sub.cancel);
return Completer<OathState>().future;
}
@override
@ -141,6 +140,35 @@ final addCredentialToAnyProvider =
}
});
final addCredentialsToAnyProvider = Provider(
(ref) => (List<String> credentialUris, List<bool> touchRequired) async {
try {
_log.debug(
'Calling android with ${credentialUris.length} credentials to be added');
String resultString = await _methods.invokeMethod(
'addAccountsToAny',
{
'uris': credentialUris,
'requireTouch': touchRequired,
},
);
_log.debug('Call result: $resultString');
var result = jsonDecode(resultString);
return result['succeeded'] == credentialUris.length;
} on PlatformException catch (pe) {
var decodedException = pe.decode();
if (decodedException is CancellationException) {
_log.debug('User cancelled adding multiple accounts');
} else {
_log.error('Failed to add multiple accounts.', pe);
}
throw decodedException;
}
});
final androidCredentialListProvider = StateNotifierProvider.autoDispose
.family<OathCredentialListNotifier, List<OathPair>?, DevicePath>(
(ref, devicePath) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -14,114 +14,142 @@
* limitations under the License.
*/
import 'dart:math';
import 'package:flutter/material.dart';
import 'qr_scanner_scan_status.dart';
import 'qr_scanner_util.dart';
/// Return the rounded rect which represents the scanner area for the background
/// overlay and the stroke
RRect _getScannerAreaRRect(Size size) {
double scannerAreaWidth = getScannerAreaWidth(size);
var scannerAreaRect = Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: scannerAreaWidth,
height: scannerAreaWidth);
class QRScannerOverlay extends StatelessWidget {
final ScanStatus status;
final Size screenSize;
final GlobalKey overlayWidgetKey;
return RRect.fromRectAndRadius(
scannerAreaRect, const Radius.circular(scannerAreaRadius));
const QRScannerOverlay(
{super.key,
required this.status,
required this.screenSize,
required this.overlayWidgetKey});
RRect getOverlayRRect(Size size) {
final renderBox =
overlayWidgetKey.currentContext?.findRenderObject() as RenderBox;
final renderObjectSize = renderBox.size;
final renderObjectOffset = renderBox.globalToLocal(Offset.zero);
final double shorterEdge =
min(renderObjectSize.width, renderObjectSize.height);
var top = (size.height - shorterEdge) / 2 - 32;
if (top + renderObjectOffset.dy < 0) {
top = -renderObjectOffset.dy;
}
return RRect.fromRectAndRadius(
Rect.fromLTWH(
(size.width - shorterEdge) / 2, top, shorterEdge, shorterEdge),
const Radius.circular(10));
}
@override
Widget build(BuildContext context) {
overlayRectProvider(Size size) {
return getOverlayRRect(size);
}
return Stack(fit: StackFit.expand, children: [
/// clip scanner area "hole" into a darkened background
ClipPath(
clipper: _OverlayClipper(overlayRectProvider),
child: const Opacity(
opacity: 0.6,
child: ColoredBox(
color: Colors.black,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [Spacer()],
),
),
),
),
/// draw a stroke around the scanner area
CustomPaint(
painter: _OverlayPainter(status, overlayRectProvider),
),
]);
}
}
// CustomPainter which strokes the scannerArea
class _ScannerAreaStrokePainter extends CustomPainter {
final Color _strokeColor;
/// Paints a colored stroke and status icon.
/// The stroke area is acquired through passed in rectangle provider.
/// The color is computed from the scan status.
class _OverlayPainter extends CustomPainter {
final ScanStatus _status;
final Function(Size) _rectProvider;
_ScannerAreaStrokePainter(this._strokeColor) : super();
_OverlayPainter(this._status, this._rectProvider) : super();
@override
void paint(Canvas canvas, Size size) {
final color = _status == ScanStatus.error
? Colors.red.shade400
: Colors.green.shade400;
Paint paint = Paint()
..color = _strokeColor
..color = color
..style = PaintingStyle.stroke
..strokeWidth = 3.0;
Path path = Path()..addRRect(_getScannerAreaRRect(size));
final RRect overlayRRect = _rectProvider(size);
Path path = Path()..addRRect(overlayRRect);
canvas.drawPath(path, paint);
if (_status == ScanStatus.success) {
const icon = Icons.check_circle;
final iconSize =
overlayRRect.width < 150 ? overlayRRect.width - 5.0 : 150.0;
TextPainter iconPainter = TextPainter(
textDirection: TextDirection.rtl,
textAlign: TextAlign.center,
);
iconPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: iconSize,
fontFamily: icon.fontFamily,
color: color.withAlpha(240),
));
iconPainter.layout();
iconPainter.paint(
canvas,
overlayRRect.center.translate(-iconSize / 2, -iconSize / 2),
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// clips the scanner area rounded rect of specific Size
class _ScannerAreaClipper extends CustomClipper<Path> {
/// Clips a hole into the background.
/// The clipped area is acquired through passed in rectangle provider.
class _OverlayClipper extends CustomClipper<Path> {
final Function(Size) _rectProvider;
_OverlayClipper(this._rectProvider);
@override
Path getClip(Size size) {
return Path()
..addRect(Rect.fromLTWH(0, 0, size.width, size.height))
..addRRect(_getScannerAreaRRect(size))
..addRRect(_rectProvider(size))
..fillType = PathFillType.evenOdd;
}
@override
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => true;
}
class QRScannerOverlay extends StatelessWidget {
final ScanStatus status;
final Size screenSize;
const QRScannerOverlay({
super.key,
required this.status,
required this.screenSize,
});
@override
Widget build(BuildContext context) {
var size = screenSize;
return Stack(children: [
/// clip scanner area "hole" into a darkened background
ClipPath(
clipper: _ScannerAreaClipper(),
child: const Opacity(
opacity: 0.6,
child: ColoredBox(
color: Colors.black,
child: Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [Spacer()],
)))),
/// draw a stroke around the scanner area
Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
CustomPaint(
painter: _ScannerAreaStrokePainter(status == ScanStatus.error
? Colors.red.shade400
: Colors.green.shade400),
),
],
),
/// extra icon when successful scan occurred
if (status == ScanStatus.success)
Positioned.fromRect(
rect: Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width,
height: size.height),
child: Icon(
Icons.check_circle,
size: 200,
color: Colors.green.shade400,
)),
]);
}
bool shouldReclip(covariant CustomClipper<Path> oldClipper) => false;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'qr_scanner_scan_status.dart';
import 'qr_scanner_util.dart';
class QRScannerPermissionsUI extends StatelessWidget {
final ScanStatus status;
@ -34,71 +33,62 @@ class QRScannerPermissionsUI extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final scannerAreaWidth = getScannerAreaWidth(screenSize);
return Stack(children: [
/// instruction text under the scanner area
Positioned.fromRect(
rect: Rect.fromCenter(
center: Offset(screenSize.width / 2,
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
width: screenSize.width,
height: screenSize.height),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36),
child: Text(
return SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
l10n.p_need_camera_permission,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
)),
/// button for manual entry
Positioned.fromRect(
rect: Rect.fromCenter(
center: Offset(screenSize.width / 2, screenSize.height),
width: screenSize.width,
height: screenSize.height),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
l10n.q_have_account_info,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
},
child: Text(
l10n.s_enter_manually,
Column(
children: [
Text(
l10n.q_have_account_info,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
)),
],
),
Column(
children: [
Text(
l10n.q_want_to_scan,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
},
child: Text(
l10n.s_enter_manually,
style: const TextStyle(color: Colors.white),
)),
],
),
OutlinedButton(
onPressed: () {
onPermissionRequest();
},
child: Text(
l10n.s_review_permissions,
Column(
children: [
Text(
l10n.q_want_to_scan,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
)),
],
)
]),
),
OutlinedButton(
onPressed: () {
onPermissionRequest();
},
child: Text(
l10n.s_review_permissions,
style: const TextStyle(color: Colors.white),
)),
],
)
])
],
),
),
]);
);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,66 +19,76 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../keys.dart' as keys;
import 'qr_scanner_scan_status.dart';
import 'qr_scanner_util.dart';
class QRScannerUI extends StatelessWidget {
final ScanStatus status;
final Size screenSize;
final GlobalKey overlayWidgetKey;
const QRScannerUI({
super.key,
required this.status,
required this.screenSize,
});
const QRScannerUI(
{super.key,
required this.status,
required this.screenSize,
required this.overlayWidgetKey});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final scannerAreaWidth = getScannerAreaWidth(screenSize);
return Stack(children: [
/// instruction text under the scanner area
Positioned.fromRect(
rect: Rect.fromCenter(
center: Offset(screenSize.width / 2,
screenSize.height + scannerAreaWidth / 2.0 + 8.0),
width: screenSize.width,
height: screenSize.height),
child: Text(
status != ScanStatus.error
? l10n.l_point_camera_scan
: l10n.l_invalid_qr,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
/// button for manual entry
Positioned.fromRect(
rect: Rect.fromCenter(
center: Offset(screenSize.width / 2,
screenSize.height + scannerAreaWidth / 2.0 + 80.0),
width: screenSize.width,
height: screenSize.height),
child: Column(
children: [
Text(
l10n.q_no_qr,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
},
key: keys.manualEntryButton,
return Stack(
fit: StackFit.expand,
children: [
SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.max,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
left: 16, right: 16, top: 0, bottom: 0),
child: SizedBox(
// other widgets can find the RenderObject of this
// widget by its key value and query its size and offset.
key: overlayWidgetKey,
),
),
),
Padding(
padding: const EdgeInsets.only(top: 0.0),
child: Text(
l10n.s_enter_manually,
status != ScanStatus.error
? l10n.l_point_camera_scan
: l10n.l_invalid_qr,
style: const TextStyle(color: Colors.white),
)),
],
),
),
]);
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
Column(
children: [
Text(
l10n.q_no_qr,
textScaleFactor: 0.7,
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
},
key: keys.manualEntryButton,
child: Text(
l10n.s_enter_manually,
style: const TextStyle(color: Colors.white),
)),
],
),
const SizedBox(height: 8)
],
),
)
],
);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Yubico.
* Copyright (C) 2022-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -38,15 +38,11 @@ GlobalKey<QRScannerZxingViewState> _zxingViewKey = GlobalKey();
class _QrScannerViewState extends State<QrScannerView> {
String? _scannedString;
// will be used later
// ignore: unused_field
CredentialData? _credentialData;
ScanStatus _status = ScanStatus.scanning;
bool _previewInitialized = false;
bool _permissionsGranted = false;
void setError() {
_credentialData = null;
_scannedString = null;
_status = ScanStatus.error;
@ -59,7 +55,6 @@ class _QrScannerViewState extends State<QrScannerView> {
void resetError() {
setState(() {
_credentialData = null;
_scannedString = null;
_status = ScanStatus.scanning;
@ -67,17 +62,17 @@ class _QrScannerViewState extends State<QrScannerView> {
});
}
void handleResult(String barCode) {
void handleResult(String qrCodeData) {
if (_status != ScanStatus.scanning) {
// on success and error ignore reported codes
return;
}
setState(() {
if (barCode.isNotEmpty) {
if (qrCodeData.isNotEmpty) {
try {
var parsedCredential = CredentialData.fromUri(Uri.parse(barCode));
_credentialData = parsedCredential;
_scannedString = barCode;
CredentialData.fromUri(Uri.parse(
qrCodeData)); // throws ArgumentError if validation fails
_scannedString = qrCodeData;
_status = ScanStatus.success;
final navigator = Navigator.of(context);
@ -108,6 +103,7 @@ class _QrScannerViewState extends State<QrScannerView> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final screenSize = MediaQuery.of(context).size;
final overlayWidgetKey = GlobalKey();
return Scaffold(
resizeToAvoidBottomInset: false,
extendBodyBehindAppBar: true,
@ -143,7 +139,7 @@ class _QrScannerViewState extends State<QrScannerView> {
visible: _permissionsGranted,
child: QRScannerZxingView(
key: _zxingViewKey,
marginPct: 50,
marginPct: 10,
onDetect: (scannedData) => handleResult(scannedData),
onViewInitialized: (bool permissionsGranted) {
Future.delayed(const Duration(milliseconds: 50), () {
@ -158,12 +154,14 @@ class _QrScannerViewState extends State<QrScannerView> {
child: QRScannerOverlay(
status: _status,
screenSize: screenSize,
overlayWidgetKey: overlayWidgetKey,
)),
Visibility(
visible: _permissionsGranted,
child: QRScannerUI(
status: _status,
screenSize: screenSize,
overlayWidgetKey: overlayWidgetKey,
)),
Visibility(
visible: _previewInitialized && !_permissionsGranted,

View File

@ -19,6 +19,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'views/nfc/nfc_activity_widget.dart';
@ -28,6 +29,55 @@ import '../app/views/user_interaction.dart';
const _channel = MethodChannel('com.yubico.authenticator.channel.dialog');
// _DDesc contains id of title resource for the dialog
enum _DTitle {
tapKey,
operationSuccessful,
operationFailed,
invalid;
static _DTitle fromId(int? id) =>
const {
0: _DTitle.tapKey,
1: _DTitle.operationSuccessful,
2: _DTitle.operationFailed
}[id] ??
_DTitle.invalid;
}
// _DDesc contains action description in the dialog
enum _DDesc {
// oath descriptions
oathResetApplet,
oathUnlockSession,
oathSetPassword,
oathUnsetPassword,
oathAddAccount,
oathRenameAccount,
oathDeleteAccount,
oathCalculateCode,
oathActionFailure,
oathAddMultipleAccounts,
invalid;
static const int dialogDescriptionOathIndex = 100;
static _DDesc fromId(int? id) =>
const {
dialogDescriptionOathIndex + 0: _DDesc.oathResetApplet,
dialogDescriptionOathIndex + 1: _DDesc.oathUnlockSession,
dialogDescriptionOathIndex + 2: _DDesc.oathSetPassword,
dialogDescriptionOathIndex + 3: _DDesc.oathUnsetPassword,
dialogDescriptionOathIndex + 4: _DDesc.oathAddAccount,
dialogDescriptionOathIndex + 5: _DDesc.oathRenameAccount,
dialogDescriptionOathIndex + 6: _DDesc.oathDeleteAccount,
dialogDescriptionOathIndex + 7: _DDesc.oathCalculateCode,
dialogDescriptionOathIndex + 8: _DDesc.oathActionFailure,
dialogDescriptionOathIndex + 9: _DDesc.oathAddMultipleAccounts
}[id] ??
_DDesc.invalid;
}
final androidDialogProvider = Provider<_DialogProvider>(
(ref) {
return _DialogProvider(ref.watch(withContextProvider));
@ -67,24 +117,51 @@ class _DialogProvider {
_controller = null;
}
String _getTitle(BuildContext context, int? titleId) {
final l10n = AppLocalizations.of(context)!;
return switch (_DTitle.fromId(titleId)) {
_DTitle.tapKey => l10n.s_nfc_dialog_tap_key,
_DTitle.operationSuccessful => l10n.s_nfc_dialog_operation_success,
_DTitle.operationFailed => l10n.s_nfc_dialog_operation_failed,
_ => ''
};
}
String _getDialogDescription(BuildContext context, int? descriptionId) {
final l10n = AppLocalizations.of(context)!;
return switch (_DDesc.fromId(descriptionId)) {
_DDesc.oathResetApplet => l10n.s_nfc_dialog_oath_reset,
_DDesc.oathUnlockSession => l10n.s_nfc_dialog_oath_unlock,
_DDesc.oathSetPassword => l10n.s_nfc_dialog_oath_set_password,
_DDesc.oathUnsetPassword => l10n.s_nfc_dialog_oath_unset_password,
_DDesc.oathAddAccount => l10n.s_nfc_dialog_oath_add_account,
_DDesc.oathRenameAccount => l10n.s_nfc_dialog_oath_rename_account,
_DDesc.oathDeleteAccount => l10n.s_nfc_dialog_oath_delete_account,
_DDesc.oathCalculateCode => l10n.s_nfc_dialog_oath_calculate_code,
_DDesc.oathActionFailure => l10n.s_nfc_dialog_oath_failure,
_DDesc.oathAddMultipleAccounts => l10n.s_nfc_dialog_oath_add_multiple_accounts,
_ => ''
};
}
Future<void> _updateDialogState(
String? title, String? description, String? iconName) async {
int? title, int? description, int? iconName) async {
await _withContext((context) async {
_controller?.updateContent(
title: title,
description: description,
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: _icon,
);
});
}
Future<void> _showDialog(
String title, String description, String? iconName) async {
int title, int description, int? iconName) async {
_controller = await _withContext((context) async {
return promptUserInteraction(
context,
title: title,
description: description,
title: _getTitle(context, title),
description: _getDialogDescription(context, description),
icon: _icon,
onCancel: () {
_channel.invokeMethod('cancel');

View File

@ -20,7 +20,6 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import '../widgets/toast.dart';
import 'models.dart';
void Function() showMessage(
BuildContext context,
@ -29,44 +28,16 @@ void Function() showMessage(
}) =>
showToast(context, message, duration: duration);
Future<void> showBottomMenu(
BuildContext context, List<MenuAction> actions) async {
await showBlurDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Options'),
contentPadding: const EdgeInsets.only(bottom: 24, top: 4),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions
.map((a) => ListTile(
leading: a.icon,
title: Text(a.text),
contentPadding:
const EdgeInsets.symmetric(horizontal: 24),
enabled: a.intent != null,
onTap: a.intent == null
? null
: () {
Navigator.pop(context);
Actions.invoke(context, a.intent!);
},
))
.toList(),
),
);
});
}
Future<T?> showBlurDialog<T>({
required BuildContext context,
required Widget Function(BuildContext) builder,
RouteSettings? routeSettings,
}) =>
showGeneralDialog(
Color barrierColor = const Color(0x80000000),
}) async =>
await showGeneralDialog<T>(
context: context,
barrierDismissible: true,
barrierColor: barrierColor,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
pageBuilder: (ctx, anim1, anim2) => builder(ctx),
transitionDuration: const Duration(milliseconds: 150),

View File

@ -53,6 +53,7 @@ enum Application {
String getDisplayName(AppLocalizations l10n) => switch (this) {
Application.oath => l10n.s_authenticator,
Application.fido => l10n.s_webauthn,
Application.piv => l10n.s_piv,
_ => name.substring(0, 1).toUpperCase() + name.substring(1),
};
@ -115,14 +116,20 @@ class DeviceNode with _$DeviceNode {
map(usbYubiKey: (_) => Transport.usb, nfcReader: (_) => Transport.nfc);
}
enum ActionStyle { normal, primary, error }
@freezed
class MenuAction with _$MenuAction {
factory MenuAction({
required String text,
class ActionItem with _$ActionItem {
factory ActionItem({
required Widget icon,
String? trailing,
required String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
}) = _MenuAction;
ActionStyle? actionStyle,
Key? key,
}) = _ActionItem;
}
@freezed

View File

@ -624,30 +624,42 @@ abstract class NfcReaderNode extends DeviceNode {
}
/// @nodoc
mixin _$MenuAction {
String get text => throw _privateConstructorUsedError;
mixin _$ActionItem {
Widget get icon => throw _privateConstructorUsedError;
String? get trailing => throw _privateConstructorUsedError;
String get title => throw _privateConstructorUsedError;
String? get subtitle => throw _privateConstructorUsedError;
String? get shortcut => throw _privateConstructorUsedError;
Widget? get trailing => throw _privateConstructorUsedError;
Intent? get intent => throw _privateConstructorUsedError;
ActionStyle? get actionStyle => throw _privateConstructorUsedError;
Key? get key => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MenuActionCopyWith<MenuAction> get copyWith =>
$ActionItemCopyWith<ActionItem> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MenuActionCopyWith<$Res> {
factory $MenuActionCopyWith(
MenuAction value, $Res Function(MenuAction) then) =
_$MenuActionCopyWithImpl<$Res, MenuAction>;
abstract class $ActionItemCopyWith<$Res> {
factory $ActionItemCopyWith(
ActionItem value, $Res Function(ActionItem) then) =
_$ActionItemCopyWithImpl<$Res, ActionItem>;
@useResult
$Res call({String text, Widget icon, String? trailing, Intent? intent});
$Res call(
{Widget icon,
String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
ActionStyle? actionStyle,
Key? key});
}
/// @nodoc
class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction>
implements $MenuActionCopyWith<$Res> {
_$MenuActionCopyWithImpl(this._value, this._then);
class _$ActionItemCopyWithImpl<$Res, $Val extends ActionItem>
implements $ActionItemCopyWith<$Res> {
_$ActionItemCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
@ -657,140 +669,223 @@ class _$MenuActionCopyWithImpl<$Res, $Val extends MenuAction>
@pragma('vm:prefer-inline')
@override
$Res call({
Object? text = null,
Object? icon = null,
Object? title = null,
Object? subtitle = freezed,
Object? shortcut = freezed,
Object? trailing = freezed,
Object? intent = freezed,
Object? actionStyle = freezed,
Object? key = freezed,
}) {
return _then(_value.copyWith(
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
icon: null == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as Widget,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
subtitle: freezed == subtitle
? _value.subtitle
: subtitle // ignore: cast_nullable_to_non_nullable
as String?,
shortcut: freezed == shortcut
? _value.shortcut
: shortcut // ignore: cast_nullable_to_non_nullable
as String?,
trailing: freezed == trailing
? _value.trailing
: trailing // ignore: cast_nullable_to_non_nullable
as String?,
as Widget?,
intent: freezed == intent
? _value.intent
: intent // ignore: cast_nullable_to_non_nullable
as Intent?,
actionStyle: freezed == actionStyle
? _value.actionStyle
: actionStyle // ignore: cast_nullable_to_non_nullable
as ActionStyle?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as Key?,
) as $Val);
}
}
/// @nodoc
abstract class _$$_MenuActionCopyWith<$Res>
implements $MenuActionCopyWith<$Res> {
factory _$$_MenuActionCopyWith(
_$_MenuAction value, $Res Function(_$_MenuAction) then) =
__$$_MenuActionCopyWithImpl<$Res>;
abstract class _$$_ActionItemCopyWith<$Res>
implements $ActionItemCopyWith<$Res> {
factory _$$_ActionItemCopyWith(
_$_ActionItem value, $Res Function(_$_ActionItem) then) =
__$$_ActionItemCopyWithImpl<$Res>;
@override
@useResult
$Res call({String text, Widget icon, String? trailing, Intent? intent});
$Res call(
{Widget icon,
String title,
String? subtitle,
String? shortcut,
Widget? trailing,
Intent? intent,
ActionStyle? actionStyle,
Key? key});
}
/// @nodoc
class __$$_MenuActionCopyWithImpl<$Res>
extends _$MenuActionCopyWithImpl<$Res, _$_MenuAction>
implements _$$_MenuActionCopyWith<$Res> {
__$$_MenuActionCopyWithImpl(
_$_MenuAction _value, $Res Function(_$_MenuAction) _then)
class __$$_ActionItemCopyWithImpl<$Res>
extends _$ActionItemCopyWithImpl<$Res, _$_ActionItem>
implements _$$_ActionItemCopyWith<$Res> {
__$$_ActionItemCopyWithImpl(
_$_ActionItem _value, $Res Function(_$_ActionItem) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? text = null,
Object? icon = null,
Object? title = null,
Object? subtitle = freezed,
Object? shortcut = freezed,
Object? trailing = freezed,
Object? intent = freezed,
Object? actionStyle = freezed,
Object? key = freezed,
}) {
return _then(_$_MenuAction(
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
return _then(_$_ActionItem(
icon: null == icon
? _value.icon
: icon // ignore: cast_nullable_to_non_nullable
as Widget,
title: null == title
? _value.title
: title // ignore: cast_nullable_to_non_nullable
as String,
subtitle: freezed == subtitle
? _value.subtitle
: subtitle // ignore: cast_nullable_to_non_nullable
as String?,
shortcut: freezed == shortcut
? _value.shortcut
: shortcut // ignore: cast_nullable_to_non_nullable
as String?,
trailing: freezed == trailing
? _value.trailing
: trailing // ignore: cast_nullable_to_non_nullable
as String?,
as Widget?,
intent: freezed == intent
? _value.intent
: intent // ignore: cast_nullable_to_non_nullable
as Intent?,
actionStyle: freezed == actionStyle
? _value.actionStyle
: actionStyle // ignore: cast_nullable_to_non_nullable
as ActionStyle?,
key: freezed == key
? _value.key
: key // ignore: cast_nullable_to_non_nullable
as Key?,
));
}
}
/// @nodoc
class _$_MenuAction implements _MenuAction {
_$_MenuAction(
{required this.text, required this.icon, this.trailing, this.intent});
class _$_ActionItem implements _ActionItem {
_$_ActionItem(
{required this.icon,
required this.title,
this.subtitle,
this.shortcut,
this.trailing,
this.intent,
this.actionStyle,
this.key});
@override
final String text;
@override
final Widget icon;
@override
final String? trailing;
final String title;
@override
final String? subtitle;
@override
final String? shortcut;
@override
final Widget? trailing;
@override
final Intent? intent;
@override
final ActionStyle? actionStyle;
@override
final Key? key;
@override
String toString() {
return 'MenuAction(text: $text, icon: $icon, trailing: $trailing, intent: $intent)';
return 'ActionItem(icon: $icon, title: $title, subtitle: $subtitle, shortcut: $shortcut, trailing: $trailing, intent: $intent, actionStyle: $actionStyle, key: $key)';
}
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$_MenuAction &&
(identical(other.text, text) || other.text == text) &&
other is _$_ActionItem &&
(identical(other.icon, icon) || other.icon == icon) &&
(identical(other.title, title) || other.title == title) &&
(identical(other.subtitle, subtitle) ||
other.subtitle == subtitle) &&
(identical(other.shortcut, shortcut) ||
other.shortcut == shortcut) &&
(identical(other.trailing, trailing) ||
other.trailing == trailing) &&
(identical(other.intent, intent) || other.intent == intent));
(identical(other.intent, intent) || other.intent == intent) &&
(identical(other.actionStyle, actionStyle) ||
other.actionStyle == actionStyle) &&
(identical(other.key, key) || other.key == key));
}
@override
int get hashCode => Object.hash(runtimeType, text, icon, trailing, intent);
int get hashCode => Object.hash(runtimeType, icon, title, subtitle, shortcut,
trailing, intent, actionStyle, key);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
__$$_MenuActionCopyWithImpl<_$_MenuAction>(this, _$identity);
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
__$$_ActionItemCopyWithImpl<_$_ActionItem>(this, _$identity);
}
abstract class _MenuAction implements MenuAction {
factory _MenuAction(
{required final String text,
required final Widget icon,
final String? trailing,
final Intent? intent}) = _$_MenuAction;
abstract class _ActionItem implements ActionItem {
factory _ActionItem(
{required final Widget icon,
required final String title,
final String? subtitle,
final String? shortcut,
final Widget? trailing,
final Intent? intent,
final ActionStyle? actionStyle,
final Key? key}) = _$_ActionItem;
@override
String get text;
@override
Widget get icon;
@override
String? get trailing;
String get title;
@override
String? get subtitle;
@override
String? get shortcut;
@override
Widget? get trailing;
@override
Intent? get intent;
@override
ActionStyle? get actionStyle;
@override
Key? get key;
@override
@JsonKey(ignore: true)
_$$_MenuActionCopyWith<_$_MenuAction> get copyWith =>
_$$_ActionItemCopyWith<_$_ActionItem> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -28,6 +28,7 @@ import '../oath/keys.dart';
import 'message.dart';
import 'models.dart';
import 'state.dart';
import 'views/keys.dart';
import 'views/settings_page.dart';
class OpenIntent extends Intent {
@ -100,7 +101,10 @@ Widget registerGlobalShortcuts(
}),
NextDeviceIntent: CallbackAction<NextDeviceIntent>(onInvoke: (_) {
ref.read(withContextProvider)((context) async {
if (!Navigator.of(context).canPop()) {
// Only allow switching keys if no other views are open,
// with the exception of the drawer.
if (!Navigator.of(context).canPop() ||
scaffoldGlobalKey.currentState?.isDrawerOpen == true) {
final attached = ref
.read(attachedDevicesProvider)
.whereType<UsbYubiKeyNode>()
@ -145,18 +149,24 @@ Widget registerGlobalShortcuts(
child: Shortcuts(
shortcuts: {
ctrlOrCmd(LogicalKeyboardKey.keyC): const CopyIntent(),
ctrlOrCmd(LogicalKeyboardKey.keyW): const HideIntent(),
const SingleActivator(LogicalKeyboardKey.copy): const CopyIntent(),
ctrlOrCmd(LogicalKeyboardKey.keyF): const SearchIntent(),
if (isDesktop) ...{
const SingleActivator(LogicalKeyboardKey.tab, control: true):
const NextDeviceIntent(),
},
if (Platform.isMacOS) ...{
const SingleActivator(LogicalKeyboardKey.keyW, meta: true):
const HideIntent(),
const SingleActivator(LogicalKeyboardKey.keyQ, meta: true):
const CloseIntent(),
const SingleActivator(LogicalKeyboardKey.comma, meta: true):
const SettingsIntent(),
},
if (Platform.isWindows) ...{
const SingleActivator(LogicalKeyboardKey.keyW, control: true):
const HideIntent(),
},
if (Platform.isLinux) ...{
const SingleActivator(LogicalKeyboardKey.keyQ, control: true):
const CloseIntent(),

View File

@ -15,6 +15,7 @@
*/
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
@ -64,14 +65,32 @@ class CommunityTranslationsNotifier extends StateNotifier<bool> {
}
}
final supportedLocalesProvider = Provider<List<Locale>>((ref) =>
ref.watch(communityTranslationsProvider)
? AppLocalizations.supportedLocales
: officialLocales);
final supportedLocalesProvider = Provider<List<Locale>>((ref) {
final locales = [...officialLocales];
final localeStr = Platform.environment['_YA_LOCALE'];
if (localeStr != null) {
// Force locale
final locale = Locale(localeStr, '');
locales.add(locale);
}
return ref.watch(communityTranslationsProvider)
? AppLocalizations.supportedLocales
: locales;
});
final currentLocaleProvider = Provider<Locale>(
(ref) => basicLocaleListResolution(
PlatformDispatcher.instance.locales, ref.watch(supportedLocalesProvider)),
(ref) {
final localeStr = Platform.environment['_YA_LOCALE'];
if (localeStr != null) {
// Force locale
final locale = Locale(localeStr, '');
return basicLocaleListResolution(
[locale], AppLocalizations.supportedLocales);
}
// Choose from supported
return basicLocaleListResolution(PlatformDispatcher.instance.locales,
ref.watch(supportedLocalesProvider));
},
);
final l10nProvider = Provider<AppLocalizations>(

View File

@ -0,0 +1,109 @@
/*
* 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 '../../widgets/list_title.dart';
import '../models.dart';
class ActionListItem extends StatelessWidget {
final Widget icon;
final String title;
final String? subtitle;
final Widget? trailing;
final void Function(BuildContext context)? onTap;
final ActionStyle actionStyle;
const ActionListItem({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.trailing,
this.onTap,
this.actionStyle = ActionStyle.normal,
});
@override
Widget build(BuildContext context) {
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
final (foreground, background) = switch (actionStyle) {
ActionStyle.normal => (theme.onSecondary, theme.secondary),
ActionStyle.primary => (theme.onPrimary, theme.primary),
ActionStyle.error => (theme.onError, theme.error),
};
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
title: Text(title),
subtitle: subtitle != null ? Text(subtitle!) : null,
leading: Opacity(
opacity: onTap != null ? 1.0 : 0.4,
child: CircleAvatar(
foregroundColor: foreground,
backgroundColor: background,
child: icon,
),
),
trailing: trailing,
onTap: onTap != null ? () => onTap?.call(context) : null,
enabled: onTap != null,
);
}
}
class ActionListSection extends StatelessWidget {
final String title;
final List<ActionListItem> children;
const ActionListSection(this.title, {super.key, required this.children});
factory ActionListSection.fromMenuActions(BuildContext context, String title,
{Key? key, required List<ActionItem> actions}) {
return ActionListSection(
key: key,
title,
children: actions.map((action) {
final intent = action.intent;
return ActionListItem(
key: action.key,
actionStyle: action.actionStyle ?? ActionStyle.normal,
icon: action.icon,
title: action.title,
subtitle: action.subtitle,
onTap: intent != null
? (context) => Actions.invoke(context, intent)
: null,
trailing: action.trailing,
);
}).toList(),
);
}
@override
Widget build(BuildContext context) => SizedBox(
width: 360,
child: Column(children: [
ListTitle(
title,
textStyle: Theme.of(context).textTheme.bodyLarge,
),
...children,
]),
);
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (C) 2022-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 'dart:async';
import 'package:flutter/material.dart';
import '../models.dart';
Future showPopupMenu(BuildContext context, Offset globalPosition,
List<ActionItem> actions) =>
showMenu(
context: context,
position: RelativeRect.fromLTRB(
globalPosition.dx,
globalPosition.dy,
globalPosition.dx,
0,
),
items: actions.map((e) => _buildMenuItem(context, e)).toList(),
);
PopupMenuItem _buildMenuItem(BuildContext context, ActionItem actionItem) {
final intent = actionItem.intent;
final enabled = intent != null;
final shortcut = actionItem.shortcut;
return PopupMenuItem(
enabled: enabled,
onTap: enabled
? () {
// Wait for popup menu to close before running action.
Timer.run(() {
Actions.invoke(context, intent);
});
}
: null,
child: ListTile(
key: actionItem.key,
enabled: enabled,
dense: true,
contentPadding: EdgeInsets.zero,
minLeadingWidth: 0,
title: Text(actionItem.title),
leading: actionItem.icon,
trailing: shortcut != null
? Opacity(
opacity: 0.5,
child: Text(shortcut, textScaleFactor: 0.7),
)
: null,
),
);
}

View File

@ -0,0 +1,137 @@
/*
* 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/services.dart';
import '../../core/state.dart';
import '../models.dart';
import '../shortcuts.dart';
import 'action_popup_menu.dart';
class AppListItem extends StatefulWidget {
final Widget? leading;
final String title;
final String? subtitle;
final Widget? trailing;
final List<ActionItem> Function(BuildContext context)? buildPopupActions;
final Intent? activationIntent;
const AppListItem({
super.key,
this.leading,
required this.title,
this.subtitle,
this.trailing,
this.buildPopupActions,
this.activationIntent,
});
@override
State<StatefulWidget> createState() => _AppListItemState();
}
class _AppListItemState extends State<AppListItem> {
final FocusNode _focusNode = FocusNode();
int _lastTap = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final subtitle = widget.subtitle;
final buildPopupActions = widget.buildPopupActions;
final activationIntent = widget.activationIntent;
final trailing = widget.trailing;
return Shortcuts(
shortcuts: {
LogicalKeySet(LogicalKeyboardKey.enter): const OpenIntent(),
LogicalKeySet(LogicalKeyboardKey.space): const OpenIntent(),
},
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(30),
onSecondaryTapDown: buildPopupActions == null
? null
: (details) {
showPopupMenu(
context,
details.globalPosition,
buildPopupActions(context),
);
},
onTap: () {
if (isDesktop) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastTap < 500) {
setState(() {
_lastTap = 0;
});
Actions.invoke(context, activationIntent ?? const OpenIntent());
} else {
_focusNode.requestFocus();
setState(() {
_lastTap = now;
});
}
} else {
Actions.invoke<OpenIntent>(context, const OpenIntent());
}
},
onLongPress: activationIntent == null
? null
: () {
Actions.invoke(context, activationIntent);
},
child: Stack(
alignment: AlignmentDirectional.center,
children: [
const SizedBox(height: 64),
ListTile(
leading: widget.leading,
title: Text(
widget.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: trailing == null
? null
: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: trailing,
),
),
],
),
),
);
}
}

View File

@ -16,18 +16,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:yubico_authenticator/core/state.dart';
import '../../widgets/delayed_visibility.dart';
import '../message.dart';
import 'device_button.dart';
import 'keys.dart';
import 'main_drawer.dart';
import 'navigation.dart';
// We use global keys here to maintain the NavigatorContent between AppPages.
final _navKey = GlobalKey();
final _navExpandedKey = GlobalKey();
class AppPage extends StatelessWidget {
final Widget? title;
final Widget child;
final List<Widget> actions;
final Widget Function(BuildContext context)? keyActionsBuilder;
final bool keyActionsBadge;
final bool centered;
final bool delayedContent;
final Widget Function(BuildContext context)? actionButtonBuilder;
@ -40,31 +45,49 @@ class AppPage extends StatelessWidget {
this.keyActionsBuilder,
this.actionButtonBuilder,
this.delayedContent = false,
this.keyActionsBadge = false,
});
@override
Widget build(BuildContext context) => LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 540) {
// Single column layout
return _buildScaffold(context, true);
final bool singleColumn;
final bool hasRail;
if (isAndroid) {
final isPortrait = constraints.maxWidth < constraints.maxHeight;
singleColumn = isPortrait || constraints.maxWidth < 600;
hasRail = constraints.maxWidth > 600;
} else {
// Two-column layout
singleColumn = constraints.maxWidth < 600;
hasRail = constraints.maxWidth > 400;
}
if (singleColumn) {
// Single column layout, maybe with rail
return _buildScaffold(context, true, hasRail);
} else {
// Fully expanded layout
return Scaffold(
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 280,
child: DrawerTheme(
data: DrawerTheme.of(context).copyWith(
// Don't color the drawer differently
surfaceTintColor: Colors.transparent,
child: SingleChildScrollView(
child: Column(
children: [
_buildLogo(context),
NavigationContent(
key: _navExpandedKey,
shouldPop: false,
extended: true,
),
],
),
child: const MainPageDrawer(shouldPop: false),
),
),
Expanded(
child: _buildScaffold(context, false),
child: _buildScaffold(context, false, false),
),
],
),
@ -73,7 +96,51 @@ class AppPage extends StatelessWidget {
},
);
Widget _buildScrollView() {
Widget _buildLogo(BuildContext context) {
final color =
Theme.of(context).brightness == Brightness.dark ? 'white' : 'green';
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 12),
child: Image.asset(
'assets/graphics/yubico-$color.png',
alignment: Alignment.centerLeft,
height: 28,
filterQuality: FilterQuality.medium,
),
);
}
Widget _buildDrawer(BuildContext context) {
return Drawer(
child: SafeArea(
child: SingleChildScrollView(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.only(left: 16),
child: DrawerButton(
onPressed: () {
Navigator.of(context).pop();
},
),
),
_buildLogo(context),
const SizedBox(width: 48),
],
),
Material(
type: MaterialType.transparency,
child: NavigationContent(key: _navExpandedKey, extended: true)),
],
),
),
));
}
Widget _buildMainContent() {
final content = Column(
children: [
child,
@ -81,8 +148,7 @@ class AppPage extends StatelessWidget {
Align(
alignment: centered ? Alignment.center : Alignment.centerLeft,
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 18),
child: Wrap(
spacing: 4,
runSpacing: 4,
@ -93,6 +159,7 @@ class AppPage extends StatelessWidget {
],
);
return SingleChildScrollView(
primary: false,
child: SafeArea(
child: Center(
child: SizedBox(
@ -110,7 +177,27 @@ class AppPage extends StatelessWidget {
);
}
Scaffold _buildScaffold(BuildContext context, bool hasDrawer) {
Scaffold _buildScaffold(BuildContext context, bool hasDrawer, bool hasRail) {
var body =
centered ? Center(child: _buildMainContent()) : _buildMainContent();
if (hasRail) {
body = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 72,
child: SingleChildScrollView(
child: NavigationContent(
key: _navKey,
shouldPop: false,
extended: false,
),
),
),
Expanded(child: body),
],
);
}
return Scaffold(
key: scaffoldGlobalKey,
appBar: AppBar(
@ -118,6 +205,20 @@ class AppPage extends StatelessWidget {
titleSpacing: hasDrawer ? 2 : 8,
centerTitle: true,
titleTextStyle: Theme.of(context).textTheme.titleLarge,
leadingWidth: hasRail ? 84 : null,
leading: hasRail
? const Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: DrawerButton(),
)),
SizedBox(width: 12),
],
)
: null,
actions: [
if (actionButtonBuilder == null && keyActionsBuilder != null)
Padding(
@ -125,22 +226,31 @@ class AppPage extends StatelessWidget {
child: IconButton(
key: actionsIconButtonKey,
onPressed: () {
showBlurDialog(context: context, builder: keyActionsBuilder!);
showBlurDialog(
context: context,
barrierColor: Colors.transparent,
builder: keyActionsBuilder!,
);
},
icon: const Icon(Icons.tune),
icon: keyActionsBadge
? const Badge(
child: Icon(Icons.tune),
)
: const Icon(Icons.tune),
iconSize: 24,
tooltip: AppLocalizations.of(context)!.s_configure_yk,
padding: const EdgeInsets.all(12),
),
),
Padding(
padding: const EdgeInsets.only(right: 12),
child: actionButtonBuilder?.call(context) ?? const DeviceButton(),
),
if (actionButtonBuilder != null)
Padding(
padding: const EdgeInsets.only(right: 12),
child: actionButtonBuilder!.call(context),
),
],
),
drawer: hasDrawer ? const MainPageDrawer() : null,
body: centered ? Center(child: _buildScrollView()) : _buildScrollView(),
drawer: hasDrawer ? _buildDrawer(context) : null,
body: body,
);
}
}

View File

@ -1,62 +0,0 @@
/*
* Copyright (C) 2022 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/state.dart';
import '../message.dart';
import 'device_avatar.dart';
import 'device_picker_dialog.dart';
class _CircledDeviceAvatar extends ConsumerWidget {
final double radius;
const _CircledDeviceAvatar(this.radius);
@override
Widget build(BuildContext context, WidgetRef ref) => CircleAvatar(
radius: radius,
backgroundColor: Theme.of(context).colorScheme.primary,
child: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: DeviceAvatar.currentDevice(ref, radius: radius - 1),
),
);
}
class DeviceButton extends ConsumerWidget {
final double radius;
const DeviceButton({super.key, this.radius = 16});
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
tooltip: isAndroid
? AppLocalizations.of(context)!.s_yk_information
: AppLocalizations.of(context)!.s_select_yk,
icon: _CircledDeviceAvatar(radius),
onPressed: () async {
await showBlurDialog(
context: context,
builder: (context) => const DevicePickerDialog(),
routeSettings: const RouteSettings(name: 'device_picker'),
);
},
);
}
}

View File

@ -0,0 +1,428 @@
/*
* Copyright (C) 2022-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_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../android/state.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../models.dart';
import '../state.dart';
import 'device_avatar.dart';
import 'keys.dart' as keys;
final _hiddenDevicesProvider =
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
(ref) => _HiddenDevicesNotifier(ref.watch(prefProvider)));
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
static const String _key = 'DEVICE_PICKER_HIDDEN';
final SharedPreferences _prefs;
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
void showAll() {
state = [];
_prefs.setStringList(_key, state);
}
void hideDevice(DevicePath devicePath) {
state = [...state, devicePath.key];
_prefs.setStringList(_key, state);
}
}
List<(Widget, bool)> buildDeviceList(
BuildContext context, WidgetRef ref, bool extended) {
final l10n = AppLocalizations.of(context)!;
final hidden = ref.watch(_hiddenDevicesProvider);
final devices = ref
.watch(attachedDevicesProvider)
.where((e) => !hidden.contains(e.path.key))
.toList();
final currentNode = ref.watch(currentDeviceProvider);
final showUsb = isDesktop && devices.whereType<UsbYubiKeyNode>().isEmpty;
return [
if (showUsb)
(
_DeviceRow(
leading: const DeviceAvatar(child: Icon(Icons.usb)),
title: l10n.s_usb,
subtitle: l10n.l_no_yk_present,
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
},
selected: currentNode == null,
extended: extended,
),
currentNode == null
),
...devices.map(
(e) => e.path == currentNode?.path
? (
_buildCurrentDeviceRow(
context,
ref,
e,
ref.watch(currentDeviceDataProvider),
extended,
),
true
)
: (
e.map(
usbYubiKey: (node) => _buildDeviceRow(
context,
ref,
node,
node.info,
extended,
),
nfcReader: (node) => _NfcDeviceRow(node, extended: extended),
),
false
),
),
];
}
class DevicePickerContent extends ConsumerWidget {
final bool extended;
const DevicePickerContent({super.key, this.extended = true});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hidden = ref.watch(_hiddenDevicesProvider);
final devices = ref
.watch(attachedDevicesProvider)
.where((e) => !hidden.contains(e.path.key))
.toList();
final currentNode = ref.watch(currentDeviceProvider);
final showUsb = isDesktop && devices.whereType<UsbYubiKeyNode>().isEmpty;
Widget? androidNoKeyWidget;
if (isAndroid && devices.isEmpty) {
var hasNfcSupport = ref.watch(androidNfcSupportProvider);
var isNfcEnabled = ref.watch(androidNfcStateProvider);
final subtitle = hasNfcSupport && isNfcEnabled
? l10n.l_insert_or_tap_yk
: l10n.l_insert_yk;
androidNoKeyWidget = _DeviceRow(
leading: const DeviceAvatar(child: Icon(Icons.usb)),
title: l10n.l_no_yk_present,
subtitle: subtitle,
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
},
selected: currentNode == null,
extended: extended,
);
}
List<Widget> children = [
if (showUsb)
_DeviceRow(
leading: const DeviceAvatar(child: Icon(Icons.usb)),
title: l10n.s_usb,
subtitle: l10n.l_no_yk_present,
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
},
selected: currentNode == null,
extended: extended,
),
if (androidNoKeyWidget != null)
androidNoKeyWidget,
...devices.map(
(e) => e.path == currentNode?.path
? _buildCurrentDeviceRow(
context,
ref,
e,
ref.watch(currentDeviceDataProvider),
extended,
)
: e.map(
usbYubiKey: (node) => _buildDeviceRow(
context,
ref,
node,
node.info,
extended,
),
nfcReader: (node) => _NfcDeviceRow(node, extended: extended),
),
),
];
return GestureDetector(
onSecondaryTapDown: hidden.isEmpty
? null
: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: Text(l10n.s_show_hidden_devices),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: Column(
children: children,
),
);
}
}
String _getDeviceInfoString(BuildContext context, DeviceInfo info) {
final l10n = AppLocalizations.of(context)!;
final serial = info.serial;
return [
if (serial != null) l10n.s_sn_serial(serial),
if (info.version.isAtLeast(1))
l10n.s_fw_version(info.version)
else
l10n.s_unknown_type,
].join(' ');
}
List<String> _getDeviceStrings(
BuildContext context, DeviceNode node, AsyncValue<YubiKeyData> data) {
final l10n = AppLocalizations.of(context)!;
final messages = data.whenOrNull(
data: (data) => [data.name, _getDeviceInfoString(context, data.info)],
error: (error, _) => switch (error) {
'device-inaccessible' => [node.name, l10n.s_yk_inaccessible],
'unknown-device' => [l10n.s_unknown_device],
_ => null,
},
) ??
[l10n.l_no_yk_present];
// Add the NFC reader name, unless it's already included (as device name, like on Android)
if (node is NfcReaderNode && !messages.contains(node.name)) {
messages.add(node.name);
}
return messages;
}
class _DeviceRow extends StatelessWidget {
final Widget leading;
final String title;
final String subtitle;
final bool extended;
final bool selected;
final void Function() onTap;
const _DeviceRow({
super.key,
required this.leading,
required this.title,
required this.subtitle,
required this.extended,
required this.selected,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final tooltip = '$title\n$subtitle';
if (extended) {
final colorScheme = Theme.of(context).colorScheme;
return Tooltip(
message: tooltip,
child: ListTile(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)),
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
horizontalTitleGap: 8,
leading: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: leading,
),
title: Text(title, overflow: TextOverflow.fade, softWrap: false),
subtitle:
Text(subtitle, overflow: TextOverflow.fade, softWrap: false),
dense: true,
tileColor: selected ? colorScheme.primary : null,
textColor: selected ? colorScheme.onPrimary : null,
onTap: onTap,
),
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6.5),
child: selected
? IconButton.filled(
tooltip: tooltip,
icon: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: leading,
),
padding: const EdgeInsets.symmetric(horizontal: 12),
onPressed: onTap,
)
: IconButton(
tooltip: tooltip,
icon: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: leading,
),
padding: const EdgeInsets.symmetric(horizontal: 8),
onPressed: onTap,
),
);
}
}
}
_DeviceRow _buildDeviceRow(
BuildContext context,
WidgetRef ref,
DeviceNode node,
DeviceInfo? info,
bool extended,
) {
final l10n = AppLocalizations.of(context)!;
final subtitle = node.when(
usbYubiKey: (_, __, ___, info) => info == null
? l10n.s_yk_inaccessible
: _getDeviceInfoString(context, info),
nfcReader: (_, __) => l10n.s_select_to_scan,
);
return _DeviceRow(
key: ValueKey(node.path.key),
leading: IconTheme(
// Force the standard icon theme
data: IconTheme.of(context),
child: DeviceAvatar.deviceNode(node),
),
title: node.name,
subtitle: subtitle,
extended: extended,
selected: false,
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
},
);
}
_DeviceRow _buildCurrentDeviceRow(
BuildContext context,
WidgetRef ref,
DeviceNode node,
AsyncValue<YubiKeyData> data,
bool extended,
) {
final messages = _getDeviceStrings(context, node, data);
if (messages.length > 2) {
// Don't show readername
messages.removeLast();
}
final title = messages.removeAt(0);
final subtitle = messages.join('\n');
return _DeviceRow(
key: keys.deviceInfoListTile,
leading: data.maybeWhen(
data: (data) =>
DeviceAvatar.yubiKeyData(data, radius: extended ? null : 16),
orElse: () => DeviceAvatar.deviceNode(node, radius: extended ? null : 16),
),
title: title,
subtitle: subtitle,
extended: extended,
selected: true,
onTap: () {},
);
}
class _NfcDeviceRow extends ConsumerWidget {
final DeviceNode node;
final bool extended;
const _NfcDeviceRow(this.node, {required this.extended});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hidden = ref.watch(_hiddenDevicesProvider);
return GestureDetector(
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
enabled: hidden.isNotEmpty,
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: Text(l10n.s_show_hidden_devices),
dense: true,
contentPadding: EdgeInsets.zero,
enabled: hidden.isNotEmpty,
),
),
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path);
},
child: ListTile(
title: Text(l10n.s_hide_device),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: _buildDeviceRow(context, ref, node, null, extended),
);
}
}

View File

@ -1,362 +0,0 @@
/*
* Copyright (C) 2022 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_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/state.dart';
import '../../management/models.dart';
import '../models.dart';
import '../state.dart';
import 'device_avatar.dart';
import 'keys.dart';
final _hiddenDevicesProvider =
StateNotifierProvider<_HiddenDevicesNotifier, List<String>>(
(ref) => _HiddenDevicesNotifier(ref.watch(prefProvider)));
class _HiddenDevicesNotifier extends StateNotifier<List<String>> {
static const String _key = 'DEVICE_PICKER_HIDDEN';
final SharedPreferences _prefs;
_HiddenDevicesNotifier(this._prefs) : super(_prefs.getStringList(_key) ?? []);
void showAll() {
state = [];
_prefs.setStringList(_key, state);
}
void hideDevice(DevicePath devicePath) {
state = [...state, devicePath.key];
_prefs.setStringList(_key, state);
}
}
class DevicePickerDialog extends StatefulWidget {
const DevicePickerDialog({super.key});
@override
State<StatefulWidget> createState() => _DevicePickerDialogState();
}
class _DevicePickerDialogState extends State<DevicePickerDialog> {
late FocusScopeNode _focus;
@override
void initState() {
super.initState();
_focus = FocusScopeNode();
}
@override
void dispose() {
_focus.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// This keeps the focus in the dialog, even if the underlying page
// changes as it does when a new device is selected.
return FocusScope(
node: _focus,
autofocus: true,
onFocusChange: (focused) {
if (!focused) {
_focus.requestFocus();
}
},
child: const _DevicePickerContent(),
);
}
}
class _DevicePickerContent extends ConsumerWidget {
const _DevicePickerContent();
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hidden = ref.watch(_hiddenDevicesProvider);
final devices = ref
.watch(attachedDevicesProvider)
.where((e) => !hidden.contains(e.path.key))
.toList();
final currentNode = ref.watch(currentDeviceProvider);
final Widget hero;
final bool showUsb;
if (currentNode != null) {
showUsb = isDesktop && devices.whereType<UsbYubiKeyNode>().isEmpty;
devices.removeWhere((e) => e.path == currentNode.path);
hero = _CurrentDeviceRow(
currentNode,
ref.watch(currentDeviceDataProvider),
);
} else {
hero = Column(
children: [
_HeroAvatar(
child: DeviceAvatar(
radius: 64,
child: Icon(isAndroid ? Icons.no_cell : Icons.usb),
),
),
ListTile(
title: Center(child: Text(l10n.l_no_yk_present)),
subtitle: Center(
child: Text(isAndroid ? l10n.l_insert_or_tap_yk : l10n.s_usb)),
),
],
);
showUsb = false;
}
List<Widget> others = [
if (showUsb)
ListTile(
leading: const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar(child: Icon(Icons.usb)),
),
title: Text(l10n.s_usb),
subtitle: Text(l10n.l_no_yk_present),
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(null);
},
),
...devices.map(
(e) => e.map(
usbYubiKey: (node) => _DeviceRow(node, info: node.info),
nfcReader: (node) => _NfcDeviceRow(node),
),
),
];
return GestureDetector(
onSecondaryTapDown: hidden.isEmpty
? null
: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: Text(l10n.s_show_hidden_devices),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: SimpleDialog(
children: [
hero,
if (others.isNotEmpty)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 24),
child: Divider(),
),
...others,
],
),
);
}
}
String _getDeviceInfoString(BuildContext context, DeviceInfo info) {
final l10n = AppLocalizations.of(context)!;
final serial = info.serial;
return [
if (serial != null) l10n.s_sn_serial(serial),
if (info.version.isAtLeast(1))
l10n.s_fw_version(info.version)
else
l10n.s_unknown_type,
].join(' ');
}
List<String> _getDeviceStrings(
BuildContext context, DeviceNode node, AsyncValue<YubiKeyData> data) {
final l10n = AppLocalizations.of(context)!;
final messages = data.whenOrNull(
data: (data) => [data.name, _getDeviceInfoString(context, data.info)],
error: (error, _) => switch (error) {
'device-inaccessible' => [node.name, l10n.s_yk_inaccessible],
'unknown-device' => [l10n.s_unknown_device],
_ => null,
},
) ??
[l10n.l_no_yk_present];
// Add the NFC reader name, unless it's already included (as device name, like on Android)
if (node is NfcReaderNode && !messages.contains(node.name)) {
messages.add(node.name);
}
return messages;
}
class _HeroAvatar extends StatelessWidget {
final Widget child;
const _HeroAvatar({required this.child});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
theme.colorScheme.inverseSurface.withOpacity(0.6),
theme.colorScheme.inverseSurface.withOpacity(0.25),
(DialogTheme.of(context).backgroundColor ??
theme.dialogBackgroundColor)
.withOpacity(0),
],
),
),
padding: const EdgeInsets.all(12),
child: Theme(
// Give the avatar a transparent background
data: theme.copyWith(
colorScheme:
theme.colorScheme.copyWith(surfaceVariant: Colors.transparent)),
child: child,
),
);
}
}
class _CurrentDeviceRow extends StatelessWidget {
final DeviceNode node;
final AsyncValue<YubiKeyData> data;
const _CurrentDeviceRow(this.node, this.data);
@override
Widget build(BuildContext context) {
final hero = data.maybeWhen(
data: (data) => DeviceAvatar.yubiKeyData(data, radius: 64),
orElse: () => DeviceAvatar.deviceNode(node, radius: 64),
);
final messages = _getDeviceStrings(context, node, data);
return Column(
children: [
_HeroAvatar(child: hero),
ListTile(
key: deviceInfoListTile,
title: Text(messages.removeAt(0), textAlign: TextAlign.center),
isThreeLine: messages.length > 1,
subtitle: Text(messages.join('\n'), textAlign: TextAlign.center),
)
],
);
}
}
class _DeviceRow extends ConsumerWidget {
final DeviceNode node;
final DeviceInfo? info;
const _DeviceRow(this.node, {this.info});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return ListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: DeviceAvatar.deviceNode(node),
),
title: Text(node.name),
subtitle: Text(
node.when(
usbYubiKey: (_, __, ___, info) => info == null
? l10n.s_yk_inaccessible
: _getDeviceInfoString(context, info),
nfcReader: (_, __) => l10n.s_select_to_scan,
),
),
onTap: () {
ref.read(currentDeviceProvider.notifier).setCurrentDevice(node);
},
);
}
}
class _NfcDeviceRow extends ConsumerWidget {
final DeviceNode node;
const _NfcDeviceRow(this.node);
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final hidden = ref.watch(_hiddenDevicesProvider);
return GestureDetector(
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: [
PopupMenuItem(
enabled: hidden.isNotEmpty,
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).showAll();
},
child: ListTile(
title: Text(l10n.s_show_hidden_devices),
dense: true,
contentPadding: EdgeInsets.zero,
enabled: hidden.isNotEmpty,
),
),
PopupMenuItem(
onTap: () {
ref.read(_hiddenDevicesProvider.notifier).hideDevice(node.path);
},
child: ListTile(
title: Text(l10n.s_hide_device),
dense: true,
contentPadding: EdgeInsets.zero,
),
),
],
);
},
child: _DeviceRow(node),
);
}
}

View File

@ -0,0 +1,54 @@
/*
* 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 'keys.dart' as keys;
class FsDialog extends StatelessWidget {
final Widget child;
const FsDialog({required this.child, super.key});
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return Dialog.fullscreen(
backgroundColor:
Theme.of(context).colorScheme.background.withOpacity(0.7),
child: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: SingleChildScrollView(child: child),
),
Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: TextButton.icon(
key: keys.closeButton,
icon: const Icon(Icons.close),
label: Text(l10n.s_close),
onPressed: () {
Navigator.of(context).pop();
},
),
)
],
),
),
);
}
}

View File

@ -30,3 +30,6 @@ const managementAppDrawer = Key('$_prefix.drawer.management');
// settings page
const themeModeSetting = Key('$_prefix.settings.theme_mode');
Key themeModeOption(ThemeMode mode) => Key('$_prefix.theme_mode.${mode.name}');
// misc buttons
const closeButton = Key('$_prefix.close_button');

View File

@ -1,147 +0,0 @@
/*
* Copyright (C) 2022 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 '../../management/views/management_screen.dart';
import '../message.dart';
import '../models.dart';
import '../shortcuts.dart';
import '../state.dart';
import 'keys.dart';
extension on Application {
IconData get _icon => switch (this) {
Application.oath => Icons.supervisor_account_outlined,
Application.fido => Icons.security_outlined,
Application.otp => Icons.password_outlined,
Application.piv => Icons.approval_outlined,
Application.management => Icons.construction_outlined,
Application.openpgp => Icons.key_outlined,
Application.hsmauth => Icons.key_outlined,
};
IconData get _filledIcon => switch (this) {
Application.oath => Icons.supervisor_account,
Application.fido => Icons.security,
Application.otp => Icons.password,
Application.piv => Icons.approval,
Application.management => Icons.construction,
Application.openpgp => Icons.key,
Application.hsmauth => Icons.key,
};
}
class MainPageDrawer extends ConsumerWidget {
final bool shouldPop;
const MainPageDrawer({this.shouldPop = true, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final supportedApps = ref.watch(supportedAppsProvider);
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
final color =
Theme.of(context).brightness == Brightness.dark ? 'white' : 'green';
final availableApps = data != null
? supportedApps
.where(
(app) => app.getAvailability(data) != Availability.unsupported)
.toList()
: <Application>[];
final hasManagement = availableApps.remove(Application.management);
return NavigationDrawer(
selectedIndex: availableApps.indexOf(ref.watch(currentAppProvider)),
onDestinationSelected: (index) {
if (shouldPop) Navigator.of(context).pop();
if (index < availableApps.length) {
// Switch to selected app
final app = availableApps[index];
ref.read(currentAppProvider.notifier).setCurrentApp(app);
} else {
// Handle action
index -= availableApps.length;
if (!hasManagement) {
index++;
}
switch (index) {
case 0:
showBlurDialog(
context: context,
// data must be non-null when index == 0
builder: (context) => ManagementScreen(data!),
);
break;
case 1:
Actions.maybeInvoke(context, const SettingsIntent());
break;
case 2:
Actions.maybeInvoke(context, const AboutIntent());
break;
}
}
},
children: [
Padding(
padding: const EdgeInsets.only(top: 19.0, left: 30.0, bottom: 12.0),
child: Image.asset(
'assets/graphics/yubico-$color.png',
alignment: Alignment.centerLeft,
height: 28,
filterQuality: FilterQuality.medium,
),
),
const Divider(indent: 16.0, endIndent: 28.0),
if (data != null) ...[
// Normal YubiKey Applications
...availableApps.map((app) => NavigationDrawerDestination(
label: Text(app.getDisplayName(l10n)),
icon: Icon(app._icon),
selectedIcon: Icon(app._filledIcon),
)),
// Management app
if (hasManagement) ...[
NavigationDrawerDestination(
key: managementAppDrawer,
label: Text(
l10n.s_toggle_applications,
),
icon: Icon(Application.management._icon),
selectedIcon: Icon(Application.management._filledIcon),
),
],
const Divider(indent: 16.0, endIndent: 28.0),
],
// Non-YubiKey pages
NavigationDrawerDestination(
label: Text(l10n.s_settings),
icon: const Icon(Icons.settings_outlined),
),
NavigationDrawerDestination(
label: Text(l10n.s_help_and_about),
icon: const Icon(Icons.help_outline),
),
],
);
}
}

View File

@ -17,16 +17,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../android/app_methods.dart';
import '../../android/state.dart';
import '../../android/views/nfc/main_page_nfc_activity_widget.dart';
import '../../exception/cancellation_exception.dart';
import '../../core/state.dart';
import '../../fido/views/fido_screen.dart';
import '../../oath/models.dart';
import '../../oath/views/add_account_page.dart';
import '../../oath/views/oath_screen.dart';
import '../../oath/views/utils.dart';
import '../../piv/views/piv_screen.dart';
import '../../widgets/custom_icons.dart';
import '../message.dart';
import '../models.dart';
@ -100,30 +100,30 @@ class MainPage extends ConsumerWidget {
icon: const Icon(Icons.person_add_alt_1),
tooltip: l10n.s_add_account,
onPressed: () async {
CredentialData? otpauth;
final withContext = ref.read(withContextProvider);
final scanner = ref.read(qrScannerProvider);
if (scanner != null) {
try {
final url = await scanner.scanQr();
if (url != null) {
otpauth = CredentialData.fromUri(Uri.parse(url));
final qrData = await scanner.scanQr();
if (qrData != null) {
await withContext((context) =>
handleUri(context, null, qrData, null, null, l10n));
return;
}
} on CancellationException catch (_) {
// ignored - user cancelled
return;
}
}
await ref.read(withContextProvider)((context) => showBlurDialog(
await withContext((context) => showBlurDialog(
context: context,
routeSettings:
const RouteSettings(name: 'oath_add_account'),
builder: (context) {
return OathAddAccountPage(
return const OathAddAccountPage(
null,
null,
credentials: null,
credentialData: otpauth,
);
},
));
@ -162,6 +162,7 @@ class MainPage extends ConsumerWidget {
return switch (app) {
Application.oath => OathScreen(data.node.path),
Application.fido => FidoScreen(data),
Application.piv => PivScreen(data.node.path),
_ => MessagePage(
header: l10n.s_app_not_supported,
message: l10n.l_app_not_supported_desc,

View File

@ -27,6 +27,7 @@ class MessagePage extends StatelessWidget {
final bool delayedContent;
final Widget Function(BuildContext context)? keyActionsBuilder;
final Widget Function(BuildContext context)? actionButtonBuilder;
final bool keyActionsBadge;
const MessagePage({
super.key,
@ -38,6 +39,7 @@ class MessagePage extends StatelessWidget {
this.keyActionsBuilder,
this.actionButtonBuilder,
this.delayedContent = false,
this.keyActionsBadge = false,
});
@override
@ -46,6 +48,7 @@ class MessagePage extends StatelessWidget {
centered: true,
actions: actions,
keyActionsBuilder: keyActionsBuilder,
keyActionsBadge: keyActionsBadge,
actionButtonBuilder: actionButtonBuilder,
delayedContent: delayedContent,
child: Padding(

View File

@ -0,0 +1,213 @@
/*
* 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../management/views/management_screen.dart';
import '../message.dart';
import '../models.dart';
import '../shortcuts.dart';
import '../state.dart';
import 'device_picker.dart';
import 'keys.dart';
class NavigationItem extends StatelessWidget {
final Widget leading;
final String title;
final bool collapsed;
final bool selected;
final void Function() onTap;
const NavigationItem({
super.key,
required this.leading,
required this.title,
this.collapsed = false,
this.selected = false,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
if (collapsed) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: selected
? Theme(
data: theme.copyWith(
colorScheme: colorScheme.copyWith(
primary: colorScheme.secondaryContainer,
onPrimary: colorScheme.onSecondaryContainer)),
child: IconButton.filled(
icon: leading,
tooltip: title,
padding: const EdgeInsets.symmetric(horizontal: 16),
onPressed: onTap,
),
)
: IconButton(
icon: leading,
tooltip: title,
padding: const EdgeInsets.symmetric(horizontal: 16),
onPressed: onTap,
),
);
} else {
return ListTile(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(48)),
leading: leading,
title: Text(title),
minVerticalPadding: 16,
onTap: onTap,
tileColor: selected ? colorScheme.secondaryContainer : null,
textColor: selected ? colorScheme.onSecondaryContainer : null,
iconColor: selected ? colorScheme.onSecondaryContainer : null,
);
}
}
}
extension on Application {
IconData get _icon => switch (this) {
Application.oath => Icons.supervisor_account_outlined,
Application.fido => Icons.security_outlined,
Application.otp => Icons.password_outlined,
Application.piv => Icons.approval_outlined,
Application.management => Icons.construction_outlined,
Application.openpgp => Icons.key_outlined,
Application.hsmauth => Icons.key_outlined,
};
IconData get _filledIcon => switch (this) {
Application.oath => Icons.supervisor_account,
Application.fido => Icons.security,
Application.otp => Icons.password,
Application.piv => Icons.approval,
Application.management => Icons.construction,
Application.openpgp => Icons.key,
Application.hsmauth => Icons.key,
};
}
class NavigationContent extends ConsumerWidget {
final bool shouldPop;
final bool extended;
const NavigationContent(
{super.key, this.shouldPop = true, this.extended = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final supportedApps = ref.watch(supportedAppsProvider);
final data = ref.watch(currentDeviceDataProvider).valueOrNull;
final availableApps = data != null
? supportedApps
.where(
(app) => app.getAvailability(data) != Availability.unsupported)
.toList()
: <Application>[];
final hasManagement = availableApps.remove(Application.management);
final currentApp = ref.watch(currentAppProvider);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
AnimatedSize(
duration: const Duration(milliseconds: 150),
child: DevicePickerContent(extended: extended),
),
const SizedBox(height: 32),
AnimatedSize(
duration: const Duration(milliseconds: 150),
child: Column(
children: [
if (data != null) ...[
// Normal YubiKey Applications
...availableApps.map((app) => NavigationItem(
title: app.getDisplayName(l10n),
leading: app == currentApp
? Icon(app._filledIcon)
: Icon(app._icon),
collapsed: !extended,
selected: app == currentApp,
onTap: () {
ref
.read(currentAppProvider.notifier)
.setCurrentApp(app);
if (shouldPop) {
Navigator.of(context).pop();
}
},
)),
// Management app
if (hasManagement) ...[
NavigationItem(
key: managementAppDrawer,
leading: Icon(Application.management._icon),
title: l10n.s_toggle_applications,
collapsed: !extended,
onTap: () {
showBlurDialog(
context: context,
// data must be non-null when index == 0
builder: (context) => ManagementScreen(data),
);
},
),
],
const SizedBox(height: 32),
],
],
),
),
// Non-YubiKey pages
NavigationItem(
leading: const Icon(Icons.settings_outlined),
title: l10n.s_settings,
collapsed: !extended,
onTap: () {
if (shouldPop) {
Navigator.of(context).pop();
}
Actions.maybeInvoke(context, const SettingsIntent());
},
),
NavigationItem(
leading: const Icon(Icons.help_outline),
title: l10n.s_help_and_about,
collapsed: !extended,
onTap: () {
if (shouldPop) {
Navigator.of(context).pop();
}
Actions.maybeInvoke(context, const AboutIntent());
},
),
],
),
);
}
}

View File

@ -16,6 +16,7 @@
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:intl/intl.dart';
import '../management/models.dart';
@ -152,3 +153,5 @@ class Version with _$Version implements Comparable<Version> {
return a - b;
}
}
final DateFormat dateFormatter = DateFormat('yyyy-MM-dd');

View File

@ -18,6 +18,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../app/models.dart';
bool get isDesktop {
return const [
TargetPlatform.windows,
@ -36,21 +38,16 @@ final prefProvider = Provider<SharedPreferences>((ref) {
});
abstract class ApplicationStateNotifier<T>
extends StateNotifier<AsyncValue<T>> {
ApplicationStateNotifier() : super(const AsyncValue.loading());
extends AutoDisposeFamilyAsyncNotifier<T, DevicePath> {
ApplicationStateNotifier() : super();
@protected
Future<void> updateState(Future<T> Function() guarded) async {
final result = await AsyncValue.guard(guarded);
if (mounted) {
state = result;
}
state = await AsyncValue.guard(guarded);
}
@protected
void setData(T value) {
if (mounted) {
state = AsyncValue.data(value);
}
state = AsyncValue.data(value);
}
}

View File

@ -23,6 +23,7 @@ import 'package:logging/logging.dart';
import 'package:yubico_authenticator/app/logging.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../fido/models.dart';
import '../../fido/state.dart';
import '../models.dart';
@ -45,47 +46,70 @@ final _sessionProvider =
},
);
final desktopFidoState = StateNotifierProvider.autoDispose
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
(ref, devicePath) {
final session = ref.watch(_sessionProvider(devicePath));
final desktopFidoState = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
_DesktopFidoStateNotifier.new);
class _DesktopFidoStateNotifier extends FidoStateNotifier {
late RpcNodeSession _session;
late StateController<String?> _pinController;
FutureOr<FidoState> _build(DevicePath devicePath) async {
var result = await _session.command('get');
FidoState fidoState = FidoState.fromJson(result['data']);
if (fidoState.hasPin && !fidoState.unlocked) {
final pin = ref.read(_pinProvider(devicePath));
if (pin != null) {
await unlock(pin);
result = await _session.command('get');
fidoState = FidoState.fromJson(result['data']);
}
}
_log.debug('application status', jsonEncode(fidoState));
return fidoState;
}
@override
FutureOr<FidoState> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
if (Platform.isWindows) {
// Make sure to rebuild if isAdmin changes
ref.watch(rpcStateProvider.select((state) => state.isAdmin));
}
final notifier = _DesktopFidoStateNotifier(
session,
ref.watch(_pinProvider(devicePath).notifier),
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
session.setErrorHandler('state-reset', (_) async {
_pinController = ref.watch(_pinProvider(devicePath).notifier);
_session.setErrorHandler('state-reset', (_) async {
ref.invalidate(_sessionProvider(devicePath));
});
session.setErrorHandler('auth-required', (_) async {
_session.setErrorHandler('auth-required', (_) async {
final pin = ref.read(_pinProvider(devicePath));
if (pin != null) {
await notifier.unlock(pin);
await unlock(pin);
}
});
ref.onDispose(() {
session.unsetErrorHandler('auth-required');
_session.unsetErrorHandler('auth-required');
});
ref.onDispose(() {
session.unsetErrorHandler('state-reset');
_session.unsetErrorHandler('state-reset');
});
return notifier..refresh();
},
);
class _DesktopFidoStateNotifier extends FidoStateNotifier {
final RpcNodeSession _session;
final StateController<String?> _pinController;
_DesktopFidoStateNotifier(this._session, this._pinController) : super();
Future<void> refresh() => updateState(() async {
final result = await _session.command('get');
_log.debug('application status', jsonEncode(result));
return FidoState.fromJson(result['data']);
});
return _build(devicePath);
}
@override
Stream<InteractionEvent> reset() {
@ -105,8 +129,8 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
controller.onListen = () async {
try {
await _session.command('reset', signal: signaler);
await refresh();
await controller.sink.close();
ref.invalidateSelf();
} catch (e) {
controller.sink.addError(e);
}
@ -151,22 +175,38 @@ class _DesktopFidoStateNotifier extends FidoStateNotifier {
}
}
final desktopFingerprintProvider = StateNotifierProvider.autoDispose.family<
FidoFingerprintsNotifier, AsyncValue<List<Fingerprint>>, DevicePath>(
(ref, devicePath) => _DesktopFidoFingerprintsNotifier(
ref.watch(_sessionProvider(devicePath)),
));
final desktopFingerprintProvider = AsyncNotifierProvider.autoDispose
.family<FidoFingerprintsNotifier, List<Fingerprint>, DevicePath>(
_DesktopFidoFingerprintsNotifier.new);
class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
final RpcNodeSession _session;
late RpcNodeSession _session;
_DesktopFidoFingerprintsNotifier(this._session) {
_refresh();
@override
FutureOr<List<Fingerprint>> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
ref.watch(fidoStateProvider(devicePath));
// Refresh on active
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
return _build(devicePath);
}
Future<void> _refresh() async {
FutureOr<List<Fingerprint>> _build(DevicePath devicePath) async {
final result = await _session.command('fingerprints');
setItems((result['children'] as Map<String, dynamic>)
return List.unmodifiable((result['children'] as Map<String, dynamic>)
.entries
.map((e) => Fingerprint(e.key, e.value['name']))
.toList());
@ -176,7 +216,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
Future<void> deleteFingerprint(Fingerprint fingerprint) async {
await _session
.command('delete', target: ['fingerprints', fingerprint.templateId]);
await _refresh();
ref.invalidate(fidoStateProvider(_session.devicePath));
}
@override
@ -184,15 +224,11 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
final controller = StreamController<FingerprintEvent>();
final signaler = Signaler();
signaler.signals.listen((signal) {
switch (signal.status) {
case 'capture':
controller.sink
.add(FingerprintEvent.capture(signal.body['remaining']));
break;
case 'capture-error':
controller.sink.add(FingerprintEvent.error(signal.body['code']));
break;
}
controller.sink.add(switch (signal.status) {
'capture' => FingerprintEvent.capture(signal.body['remaining']),
'capture-error' => FingerprintEvent.error(signal.body['code']),
final other => throw UnimplementedError(other),
});
});
controller.onCancel = () {
@ -210,7 +246,7 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
);
controller.sink
.add(FingerprintEvent.complete(Fingerprint.fromJson(result)));
await _refresh();
ref.invalidate(fidoStateProvider(_session.devicePath));
await controller.sink.close();
} catch (e) {
controller.sink.addError(e);
@ -227,25 +263,41 @@ class _DesktopFidoFingerprintsNotifier extends FidoFingerprintsNotifier {
target: ['fingerprints', fingerprint.templateId],
params: {'name': name});
final renamed = fingerprint.copyWith(name: name);
await _refresh();
ref.invalidate(fidoStateProvider(_session.devicePath));
return renamed;
}
}
final desktopCredentialProvider = StateNotifierProvider.autoDispose.family<
FidoCredentialsNotifier, AsyncValue<List<FidoCredential>>, DevicePath>(
(ref, devicePath) => _DesktopFidoCredentialsNotifier(
ref.watch(_sessionProvider(devicePath)),
));
final desktopCredentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
_DesktopFidoCredentialsNotifier.new);
class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
final RpcNodeSession _session;
late RpcNodeSession _session;
_DesktopFidoCredentialsNotifier(this._session) {
_refresh();
@override
FutureOr<List<FidoCredential>> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
ref.watch(fidoStateProvider(devicePath));
// Refresh on active
ref.listen<WindowState>(
windowStateProvider,
(prev, next) async {
if (prev?.active == false && next.active) {
// Refresh state on active
final newState = await _build(devicePath);
if (state.valueOrNull != newState) {
state = AsyncValue.data(newState);
}
}
},
);
return _build(devicePath);
}
Future<void> _refresh() async {
FutureOr<List<FidoCredential>> _build(DevicePath devicePath) async {
final List<FidoCredential> creds = [];
final rps = await _session.command('credentials');
for (final rpId in (rps['children'] as Map<String, dynamic>).keys) {
@ -258,7 +310,7 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
userName: e.value['user_name']));
}
}
setItems(creds);
return List.unmodifiable(creds);
}
@override
@ -268,6 +320,6 @@ class _DesktopFidoCredentialsNotifier extends FidoCredentialsNotifier {
credential.rpId,
credential.credentialId,
]);
await _refresh();
ref.invalidate(fidoStateProvider(_session.devicePath));
}
}

View File

@ -41,11 +41,13 @@ import '../core/state.dart';
import '../fido/state.dart';
import '../management/state.dart';
import '../oath/state.dart';
import '../piv/state.dart';
import '../version.dart';
import 'devices.dart';
import 'fido/state.dart';
import 'management/state.dart';
import 'oath/state.dart';
import 'piv/state.dart';
import 'qr_scanner.dart';
import 'rpc.dart';
import 'state.dart';
@ -177,6 +179,7 @@ Future<Widget> initialize(List<String> argv) async {
supportedAppsProvider.overrideWithValue([
Application.oath,
Application.fido,
Application.piv,
Application.management,
]),
prefProvider.overrideWithValue(prefs),
@ -184,6 +187,12 @@ Future<Widget> initialize(List<String> argv) async {
windowStateProvider.overrideWith(
(ref) => ref.watch(desktopWindowStateProvider),
),
clipboardProvider.overrideWith(
(ref) => ref.watch(desktopClipboardProvider),
),
supportedThemesProvider.overrideWith(
(ref) => ref.watch(desktopSupportedThemesProvider),
),
attachedDevicesProvider.overrideWith(
() => DesktopDevicesNotifier(),
),
@ -206,12 +215,9 @@ Future<Widget> initialize(List<String> argv) async {
fidoStateProvider.overrideWithProvider(desktopFidoState),
fingerprintProvider.overrideWithProvider(desktopFingerprintProvider),
credentialProvider.overrideWithProvider(desktopCredentialProvider),
clipboardProvider.overrideWith(
(ref) => ref.watch(desktopClipboardProvider),
),
supportedThemesProvider.overrideWith(
(ref) => ref.watch(desktopSupportedThemesProvider),
)
// PIV
pivStateProvider.overrideWithProvider(desktopPivState),
pivSlotsProvider.overrideWithProvider(desktopPivSlots),
],
child: YubicoAuthenticatorApp(
page: Consumer(

View File

@ -36,53 +36,51 @@ final _sessionProvider =
RpcNodeSession(ref.watch(rpcProvider).requireValue, devicePath, []),
);
final desktopManagementState = StateNotifierProvider.autoDispose
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
(ref, devicePath) {
final desktopManagementState = AsyncNotifierProvider.autoDispose
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
_DesktopManagementStateNotifier.new);
class _DesktopManagementStateNotifier extends ManagementStateNotifier {
late RpcNodeSession _session;
List<String> _subpath = [];
_DesktopManagementStateNotifier() : super();
@override
FutureOr<DeviceInfo> build(DevicePath devicePath) async {
// Make sure to rebuild if currentDevice changes (as on reboot)
ref.watch(currentDeviceProvider);
final session = ref.watch(_sessionProvider(devicePath));
final notifier = _DesktopManagementStateNotifier(ref, session);
session.setErrorHandler('state-reset', (_) async {
_session = ref.watch(_sessionProvider(devicePath));
_session.setErrorHandler('state-reset', (_) async {
ref.invalidate(_sessionProvider(devicePath));
});
ref.onDispose(() {
session.unsetErrorHandler('state-reset');
_session.unsetErrorHandler('state-reset');
});
return notifier..refresh();
},
);
class _DesktopManagementStateNotifier extends ManagementStateNotifier {
final Ref _ref;
final RpcNodeSession _session;
List<String> _subpath = [];
_DesktopManagementStateNotifier(this._ref, this._session) : super();
Future<void> refresh() => updateState(() async {
final result = await _session.command('get');
final info = DeviceInfo.fromJson(result['data']['info']);
final interfaces = (result['children'] as Map).keys.toSet();
for (final iface in [
// This is the preferred order
UsbInterface.ccid,
UsbInterface.otp,
UsbInterface.fido,
]) {
if (interfaces.contains(iface.name)) {
final path = [iface.name, 'management'];
try {
await _session.command('get', target: path);
_subpath = path;
_log.debug('Using transport $iface for management');
return info;
} catch (e) {
_log.warning('Failed connecting to management via $iface');
}
}
final result = await _session.command('get');
final info = DeviceInfo.fromJson(result['data']['info']);
final interfaces = (result['children'] as Map).keys.toSet();
for (final iface in [
// This is the preferred order
UsbInterface.ccid,
UsbInterface.otp,
UsbInterface.fido,
]) {
if (interfaces.contains(iface.name)) {
final path = [iface.name, 'management'];
try {
await _session.command('get', target: path);
_subpath = path;
_log.debug('Using transport $iface for management');
return info;
} catch (e) {
_log.warning('Failed connecting to management via $iface');
}
throw 'Failed connection over all interfaces';
});
}
}
throw 'Failed connection over all interfaces';
}
@override
Future<void> setMode(
@ -94,7 +92,7 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
'challenge_response_timeout': challengeResponseTimeout,
'auto_eject_timeout': autoEjectTimeout,
});
_ref.read(attachedDevicesProvider.notifier).refresh();
ref.read(attachedDevicesProvider.notifier).refresh();
}
@override
@ -111,6 +109,6 @@ class _DesktopManagementStateNotifier extends ManagementStateNotifier {
'new_lock_code': newLockCode,
'reboot': reboot,
});
_ref.read(attachedDevicesProvider.notifier).refresh();
ref.read(attachedDevicesProvider.notifier).refresh();
}
}

View File

@ -57,56 +57,51 @@ class _LockKeyNotifier extends StateNotifier<String?> {
}
}
final desktopOathState = StateNotifierProvider.autoDispose
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
(ref, devicePath) {
final session = ref.watch(_sessionProvider(devicePath));
final notifier = _DesktopOathStateNotifier(session, ref);
session
final desktopOathState = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
_DesktopOathStateNotifier.new);
class _DesktopOathStateNotifier extends OathStateNotifier {
late RpcNodeSession _session;
@override
FutureOr<OathState> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
_session
..setErrorHandler('state-reset', (_) async {
ref.invalidate(_sessionProvider(devicePath));
})
..setErrorHandler('auth-required', (_) async {
await notifier.refresh();
});
ref.onDispose(() {
session
..unsetErrorHandler('state-reset')
..unsetErrorHandler('auth-required');
});
return notifier..refresh();
},
);
class _DesktopOathStateNotifier extends OathStateNotifier {
final RpcNodeSession _session;
final Ref _ref;
_DesktopOathStateNotifier(this._session, this._ref) : super();
refresh() => updateState(() async {
final result = await _session.command('get');
_log.debug('application status', jsonEncode(result));
var oathState = OathState.fromJson(result['data']);
final key = _ref.read(_oathLockKeyProvider(_session.devicePath));
if (oathState.locked && key != null) {
..setErrorHandler('auth-required', (e) async {
final key = ref.read(_oathLockKeyProvider(_session.devicePath));
if (key != null) {
final result =
await _session.command('validate', params: {'key': key});
if (result['valid']) {
oathState = oathState.copyWith(locked: false);
ref.invalidateSelf();
return;
} else {
_ref
ref
.read(_oathLockKeyProvider(_session.devicePath).notifier)
.unsetKey();
}
}
return oathState;
throw e;
});
ref.onDispose(() {
_session
..unsetErrorHandler('state-reset')
..unsetErrorHandler('auth-required');
});
final result = await _session.command('get');
_log.debug('application status', jsonEncode(result));
return OathState.fromJson(result['data']);
}
@override
Future<void> reset() async {
await _session.command('reset');
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
_ref.invalidate(_sessionProvider(_session.devicePath));
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
ref.invalidate(_sessionProvider(_session.devicePath));
}
@override
@ -120,7 +115,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
final bool remembered = validate['remembered'];
if (valid) {
_log.debug('applet unlocked');
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
setData(state.value!.copyWith(
locked: false,
remembered: remembered,
@ -158,7 +153,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
await _session.command('derive', params: {'password': password});
var key = derive['key'];
await _session.command('set_key', params: {'key': key});
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).setKey(key);
}
_log.debug('OATH key set');
@ -177,7 +172,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
}
}
await _session.command('unset_key');
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
setData(oathState.copyWith(hasKey: false, locked: false));
return true;
}
@ -185,7 +180,7 @@ class _DesktopOathStateNotifier extends OathStateNotifier {
@override
Future<void> forgetPassword() async {
await _session.command('forget');
_ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
ref.read(_oathLockKeyProvider(_session.devicePath).notifier).unsetKey();
setData(state.value!.copyWith(remembered: false));
}
}

435
lib/desktop/piv/state.dart Normal file
View File

@ -0,0 +1,435 @@
/*
* 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 'dart:async';
import 'dart:convert';
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 '../../app/logging.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../app/views/user_interaction.dart';
import '../../core/models.dart';
import '../../piv/models.dart';
import '../../piv/state.dart';
import '../models.dart';
import '../rpc.dart';
import '../state.dart';
final _log = Logger('desktop.piv.state');
final _managementKeyProvider =
StateProvider.autoDispose.family<String?, DevicePath>(
(ref, _) => null,
);
final _pinProvider = StateProvider.autoDispose.family<String?, DevicePath>(
(ref, _) => null,
);
final _sessionProvider =
Provider.autoDispose.family<RpcNodeSession, DevicePath>(
(ref, devicePath) {
// Make sure the managementKey and PIN are held for the duration of the session.
ref.watch(_managementKeyProvider(devicePath));
ref.watch(_pinProvider(devicePath));
return RpcNodeSession(
ref.watch(rpcProvider).requireValue, devicePath, ['ccid', 'piv']);
},
);
final desktopPivState = AsyncNotifierProvider.autoDispose
.family<PivStateNotifier, PivState, DevicePath>(
_DesktopPivStateNotifier.new);
class _DesktopPivStateNotifier extends PivStateNotifier {
late RpcNodeSession _session;
late DevicePath _devicePath;
@override
FutureOr<PivState> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
_session
..setErrorHandler('state-reset', (_) async {
ref.invalidate(_sessionProvider(devicePath));
})
..setErrorHandler('auth-required', (e) async {
final String? mgmtKey;
if (state.valueOrNull?.metadata?.managementKeyMetadata.defaultValue ==
true) {
mgmtKey = defaultManagementKey;
} else {
mgmtKey = ref.read(_managementKeyProvider(devicePath));
}
if (mgmtKey != null) {
if (await authenticate(mgmtKey)) {
ref.invalidateSelf();
} else {
ref.read(_managementKeyProvider(devicePath).notifier).state = null;
ref.invalidateSelf();
throw e;
}
} else {
ref.invalidateSelf();
throw e;
}
});
ref.onDispose(() {
_session
..unsetErrorHandler('state-reset')
..unsetErrorHandler('auth-required');
});
_devicePath = devicePath;
final result = await _session.command('get');
_log.debug('application status', jsonEncode(result));
final pivState = PivState.fromJson(result['data']);
return pivState;
}
@override
Future<void> reset() async {
await _session.command('reset');
ref.read(_managementKeyProvider(_devicePath).notifier).state = null;
ref.invalidate(_sessionProvider(_session.devicePath));
}
@override
Future<bool> authenticate(String managementKey) async {
final withContext = ref.watch(withContextProvider);
final signaler = Signaler();
UserInteractionController? controller;
try {
signaler.signals.listen((signal) async {
if (signal.status == 'touch') {
controller = await withContext(
(context) async {
final l10n = AppLocalizations.of(context)!;
return promptUserInteraction(
context,
icon: const Icon(Icons.touch_app),
title: l10n.s_touch_required,
description: l10n.l_touch_button_now,
);
},
);
}
});
final result = await _session.command(
'authenticate',
params: {'key': managementKey},
signal: signaler,
);
if (result['status']) {
ref.read(_managementKeyProvider(_devicePath).notifier).state =
managementKey;
final oldState = state.valueOrNull;
if (oldState != null) {
state = AsyncData(oldState.copyWith(authenticated: true));
}
return true;
} else {
return false;
}
} finally {
controller?.close();
}
}
@override
Future<PinVerificationStatus> verifyPin(String pin) async {
final pivState = state.valueOrNull;
final signaler = Signaler();
UserInteractionController? controller;
try {
if (pivState?.protectedKey == true) {
// Might require touch as this will also authenticate
final withContext = ref.watch(withContextProvider);
signaler.signals.listen((signal) async {
if (signal.status == 'touch') {
controller = await withContext(
(context) async {
final l10n = AppLocalizations.of(context)!;
return promptUserInteraction(
context,
icon: const Icon(Icons.touch_app),
title: l10n.s_touch_required,
description: l10n.l_touch_button_now,
);
},
);
}
});
}
await _session.command(
'verify_pin',
params: {'pin': pin},
signal: signaler,
);
ref.read(_pinProvider(_devicePath).notifier).state = pin;
return const PinVerificationStatus.success();
} on RpcError catch (e) {
if (e.status == 'invalid-pin') {
return PinVerificationStatus.failure(e.body['attempts_remaining']);
}
rethrow;
} finally {
controller?.close();
ref.invalidateSelf();
}
}
@override
Future<PinVerificationStatus> changePin(String pin, String newPin) async {
try {
await _session.command(
'change_pin',
params: {'pin': pin, 'new_pin': newPin},
);
ref.read(_pinProvider(_devicePath).notifier).state = null;
return const PinVerificationStatus.success();
} on RpcError catch (e) {
if (e.status == 'invalid-pin') {
return PinVerificationStatus.failure(e.body['attempts_remaining']);
}
rethrow;
} finally {
ref.invalidateSelf();
}
}
@override
Future<PinVerificationStatus> changePuk(String puk, String newPuk) async {
try {
await _session.command(
'change_puk',
params: {'puk': puk, 'new_puk': newPuk},
);
return const PinVerificationStatus.success();
} on RpcError catch (e) {
if (e.status == 'invalid-pin') {
return PinVerificationStatus.failure(e.body['attempts_remaining']);
}
rethrow;
} finally {
ref.invalidateSelf();
}
}
@override
Future<void> setManagementKey(String managementKey,
{ManagementKeyType managementKeyType = defaultManagementKeyType,
bool storeKey = false}) async {
await _session.command(
'set_key',
params: {
'key': managementKey,
'key_type': managementKeyType.value,
'store_key': storeKey,
},
);
ref.read(_managementKeyProvider(_devicePath).notifier).state =
managementKey;
ref.invalidateSelf();
}
@override
Future<PinVerificationStatus> unblockPin(String puk, String newPin) async {
try {
await _session.command(
'unblock_pin',
params: {'puk': puk, 'new_pin': newPin},
);
return const PinVerificationStatus.success();
} on RpcError catch (e) {
if (e.status == 'invalid-pin') {
return PinVerificationStatus.failure(e.body['attempts_remaining']);
}
rethrow;
} finally {
ref.invalidateSelf();
}
}
}
final _shownSlots = SlotId.values.map((slot) => slot.id).toList();
final desktopPivSlots = AsyncNotifierProvider.autoDispose
.family<PivSlotsNotifier, List<PivSlot>, DevicePath>(
_DesktopPivSlotsNotifier.new);
class _DesktopPivSlotsNotifier extends PivSlotsNotifier {
late RpcNodeSession _session;
@override
FutureOr<List<PivSlot>> build(DevicePath devicePath) async {
_session = ref.watch(_sessionProvider(devicePath));
final result = await _session.command('get', target: ['slots']);
return (result['children'] as Map<String, dynamic>)
.values
.where((e) => _shownSlots.contains(e['slot']))
.map((e) => PivSlot.fromJson(e))
.toList();
}
@override
Future<void> delete(SlotId slot) async {
await _session.command('delete', target: ['slots', slot.hexId]);
ref.invalidateSelf();
}
@override
Future<PivGenerateResult> generate(
SlotId slot,
KeyType keyType, {
required PivGenerateParameters parameters,
PinPolicy pinPolicy = PinPolicy.dfault,
TouchPolicy touchPolicy = TouchPolicy.dfault,
String? pin,
}) async {
final withContext = ref.watch(withContextProvider);
final signaler = Signaler();
UserInteractionController? controller;
try {
signaler.signals.listen((signal) async {
if (signal.status == 'touch') {
controller = await withContext(
(context) async {
final l10n = AppLocalizations.of(context)!;
return promptUserInteraction(
context,
icon: const Icon(Icons.touch_app),
title: l10n.s_touch_required,
description: l10n.l_touch_button_now,
);
},
);
}
});
final (type, subject, validFrom, validTo) = parameters.when(
certificate: (subject, validFrom, validTo) => (
GenerateType.certificate,
subject,
dateFormatter.format(validFrom),
dateFormatter.format(validTo),
),
csr: (subject) => (
GenerateType.csr,
subject,
null,
null,
),
);
final pin = ref.read(_pinProvider(_session.devicePath));
final result = await _session.command(
'generate',
target: [
'slots',
slot.hexId,
],
params: {
'key_type': keyType.value,
'pin_policy': pinPolicy.value,
'touch_policy': touchPolicy.value,
'subject': subject,
'generate_type': type.name,
'valid_from': validFrom,
'valid_to': validTo,
'pin': pin,
},
signal: signaler,
);
ref.invalidateSelf();
return PivGenerateResult.fromJson(
{'generate_type': type.name, ...result});
} finally {
controller?.close();
}
}
@override
Future<PivExamineResult> examine(String data, {String? password}) async {
final result = await _session.command('examine_file', params: {
'data': data,
'password': password,
});
if (result['status']) {
return PivExamineResult.fromJson({'runtimeType': 'result', ...result});
} else {
return PivExamineResult.invalidPassword();
}
}
@override
Future<bool> validateRfc4514(String value) async {
final result = await _session.command('validate_rfc4514', params: {
'data': value,
});
return result['status'];
}
@override
Future<PivImportResult> import(SlotId slot, String data,
{String? password,
PinPolicy pinPolicy = PinPolicy.dfault,
TouchPolicy touchPolicy = TouchPolicy.dfault}) async {
final result = await _session.command('import_file', target: [
'slots',
slot.hexId,
], params: {
'data': data,
'password': password,
'pin_policy': pinPolicy.value,
'touch_policy': touchPolicy.value,
});
ref.invalidateSelf();
return PivImportResult.fromJson(result);
}
@override
Future<(SlotMetadata?, String?)> read(SlotId slot) async {
final result = await _session.command('get', target: [
'slots',
slot.hexId,
]);
final data = result['data'];
final metadata = data['metadata'];
return (
metadata != null ? SlotMetadata.fromJson(metadata) : null,
data['certificate'] as String?,
);
}
}

View File

@ -94,7 +94,7 @@ Future<OathCode?> _calculateCode(
String _getIcon() {
if (Platform.isMacOS) {
return 'resources/icons/systray-template.eps';
return 'resources/icons/systray-template.png';
}
if (Platform.isWindows) {
return 'resources/icons/com.yubico.yubioath.ico';

45
lib/fido/keys.dart Normal file
View File

@ -0,0 +1,45 @@
/*
* Copyright (C) 2022-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';
const _prefix = 'fido.keys';
const _keyAction = '$_prefix.actions';
const _credentialAction = '$_prefix.credential.actions';
const _fingerprintAction = '$_prefix.fingerprint.actions';
// Key actions
const managePinAction = Key('$_keyAction.manage_pin');
const addFingerprintAction = Key('$_keyAction.add_fingerprint');
const resetAction = Key('$_keyAction.reset');
// Credential actions
const editCredentialAction = Key('$_credentialAction.edit');
const deleteCredentialAction = Key('$_credentialAction.delete');
// Fingerprint actions
const editFingerintAction = Key('$_fingerprintAction.edit');
const deleteFingerprintAction = Key('$_fingerprintAction.delete');
const saveButton = Key('$_prefix.save');
const deleteButton = Key('$_prefix.delete');
const unlockButton = Key('$_prefix.unlock');
const managementKeyField = Key('$_prefix.management_key');
const pinPukField = Key('$_prefix.pin_puk');
const newPinPukField = Key('$_prefix.new_pin_puk');
const confirmPinPukField = Key('$_prefix.confirm_pin_puk');
const subjectField = Key('$_prefix.subject');

View File

@ -41,6 +41,8 @@ class FidoState with _$FidoState {
info['options']['credentialMgmtPreview'] == true;
bool? get bioEnroll => info['options']['bioEnroll'];
bool get alwaysUv => info['options']['alwaysUv'] == true;
}
@freezed

View File

@ -14,16 +14,15 @@
* limitations under the License.
*/
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../app/models.dart';
import '../core/state.dart';
import 'models.dart';
final fidoStateProvider = StateNotifierProvider.autoDispose
.family<FidoStateNotifier, AsyncValue<FidoState>, DevicePath>(
(ref, devicePath) => throw UnimplementedError(),
final fidoStateProvider = AsyncNotifierProvider.autoDispose
.family<FidoStateNotifier, FidoState, DevicePath>(
() => throw UnimplementedError(),
);
abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
@ -32,36 +31,24 @@ abstract class FidoStateNotifier extends ApplicationStateNotifier<FidoState> {
Future<PinResult> unlock(String pin);
}
abstract class LockedCollectionNotifier<T>
extends StateNotifier<AsyncValue<List<T>>> {
LockedCollectionNotifier() : super(const AsyncValue.loading());
@protected
void setItems(List<T> items) {
if (mounted) {
state = AsyncValue.data(List.unmodifiable(items));
}
}
}
final fingerprintProvider = StateNotifierProvider.autoDispose.family<
FidoFingerprintsNotifier, AsyncValue<List<Fingerprint>>, DevicePath>(
(ref, arg) => throw UnimplementedError(),
final fingerprintProvider = AsyncNotifierProvider.autoDispose
.family<FidoFingerprintsNotifier, List<Fingerprint>, DevicePath>(
() => throw UnimplementedError(),
);
abstract class FidoFingerprintsNotifier
extends LockedCollectionNotifier<Fingerprint> {
extends AutoDisposeFamilyAsyncNotifier<List<Fingerprint>, DevicePath> {
Stream<FingerprintEvent> registerFingerprint({String? name});
Future<Fingerprint> renameFingerprint(Fingerprint fingerprint, String name);
Future<void> deleteFingerprint(Fingerprint fingerprint);
}
final credentialProvider = StateNotifierProvider.autoDispose.family<
FidoCredentialsNotifier, AsyncValue<List<FidoCredential>>, DevicePath>(
(ref, arg) => throw UnimplementedError(),
final credentialProvider = AsyncNotifierProvider.autoDispose
.family<FidoCredentialsNotifier, List<FidoCredential>, DevicePath>(
() => throw UnimplementedError(),
);
abstract class FidoCredentialsNotifier
extends LockedCollectionNotifier<FidoCredential> {
extends AutoDisposeFamilyAsyncNotifier<List<FidoCredential>, DevicePath> {
Future<void> deleteCredential(FidoCredential credential);
}

View File

@ -0,0 +1,55 @@
/*
* 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/models.dart';
import '../../app/shortcuts.dart';
import '../keys.dart' as keys;
List<ActionItem> buildFingerprintActions(AppLocalizations l10n) {
return [
ActionItem(
key: keys.editFingerintAction,
icon: const Icon(Icons.edit),
title: l10n.s_rename_fp,
subtitle: l10n.l_rename_fp_desc,
intent: const EditIntent(),
),
ActionItem(
key: keys.deleteFingerprintAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_fingerprint,
subtitle: l10n.l_delete_fingerprint_desc,
intent: const DeleteIntent(),
),
];
}
List<ActionItem> buildCredentialActions(AppLocalizations l10n) {
return [
ActionItem(
key: keys.deleteCredentialAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete),
title: l10n.s_delete_passkey,
subtitle: l10n.l_delete_account_desc,
intent: const DeleteIntent(),
),
];
}

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'actions.dart';
import 'delete_credential_dialog.dart';
class CredentialDialog extends ConsumerWidget {
final FidoCredential credential;
const CredentialDialog(this.credential, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
if (node == null) {
// The rest of this method assumes there is a device, and will throw an exception if not.
// This will never be shown, as the dialog will be immediately closed
return const SizedBox();
}
final l10n = AppLocalizations.of(context)!;
return Actions(
actions: {
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
final withContext = ref.read(withContextProvider);
final bool? deleted =
await ref.read(withContextProvider)((context) async =>
await showBlurDialog(
context: context,
builder: (context) => DeleteCredentialDialog(
node.path,
credential,
),
) ??
false);
// Pop the account dialog if deleted
if (deleted == true) {
await withContext((context) async {
Navigator.of(context).pop();
});
}
return deleted;
}),
},
child: FocusScope(
autofocus: true,
child: FsDialog(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 48, bottom: 32),
child: Column(
children: [
Text(
credential.userName,
style: Theme.of(context).textTheme.headlineSmall,
softWrap: true,
textAlign: TextAlign.center,
),
Text(
credential.rpId,
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).textTheme.bodySmall!.color,
),
),
const SizedBox(height: 16),
const Icon(Icons.person, size: 72),
],
),
),
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions: buildCredentialActions(l10n),
),
],
),
),
),
);
}
}

View File

@ -38,14 +38,14 @@ class DeleteCredentialDialog extends ConsumerWidget {
final label = credential.userName;
return ResponsiveDialog(
title: Text(l10n.s_delete_credential),
title: Text(l10n.s_delete_passkey),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.p_warning_delete_credential),
Text(l10n.l_credential(label)),
Text(l10n.p_warning_delete_passkey),
Text(l10n.l_passkey(label)),
]
.map((e) => Padding(
child: e,
@ -63,7 +63,7 @@ class DeleteCredentialDialog extends ConsumerWidget {
await ref.read(withContextProvider)(
(context) async {
Navigator.of(context).pop(true);
showMessage(context, l10n.s_credential_deleted);
showMessage(context, l10n.s_passkey_deleted);
},
);
},

View File

@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import 'actions.dart';
import 'delete_fingerprint_dialog.dart';
import 'rename_fingerprint_dialog.dart';
class FingerprintDialog extends ConsumerWidget {
final Fingerprint fingerprint;
const FingerprintDialog(this.fingerprint, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
final node = ref.watch(currentDeviceDataProvider).valueOrNull?.node;
if (node == null) {
// The rest of this method assumes there is a device, and will throw an exception if not.
// This will never be shown, as the dialog will be immediately closed
return const SizedBox();
}
final l10n = AppLocalizations.of(context)!;
return Actions(
actions: {
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
final withContext = ref.read(withContextProvider);
final Fingerprint? renamed =
await withContext((context) async => await showBlurDialog(
context: context,
builder: (context) => RenameFingerprintDialog(
node.path,
fingerprint,
),
));
if (renamed != null) {
// Replace the dialog with the renamed credential
await withContext((context) async {
Navigator.of(context).pop();
await showBlurDialog(
context: context,
builder: (context) {
return FingerprintDialog(renamed);
},
);
});
}
return renamed;
}),
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
final withContext = ref.read(withContextProvider);
final bool? deleted =
await ref.read(withContextProvider)((context) async =>
await showBlurDialog(
context: context,
builder: (context) => DeleteFingerprintDialog(
node.path,
fingerprint,
),
) ??
false);
// Pop the account dialog if deleted
if (deleted == true) {
await withContext((context) async {
Navigator.of(context).pop();
});
}
return deleted;
}),
},
child: FocusScope(
autofocus: true,
child: FsDialog(
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 48, bottom: 32),
child: Column(
children: [
Text(
fingerprint.label,
style: Theme.of(context).textTheme.headlineSmall,
softWrap: true,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
const Icon(Icons.fingerprint, size: 72),
],
),
),
ActionListSection.fromMenuActions(
context,
l10n.s_actions,
actions: buildFingerprintActions(l10n),
),
],
),
),
),
);
}
}

View File

@ -19,72 +19,90 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../widgets/list_title.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../models.dart';
import '../keys.dart' as keys;
import 'add_fingerprint_dialog.dart';
import 'pin_dialog.dart';
import 'reset_dialog.dart';
bool fidoShowActionsNotifier(FidoState state) {
return (state.alwaysUv && !state.hasPin) || state.bioEnroll == false;
}
Widget fidoBuildActions(
BuildContext context, DeviceNode node, FidoState state, int fingerprints) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context).colorScheme;
return SimpleDialog(
children: [
if (state.bioEnroll != null) ...[
ListTitle(l10n.s_setup,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
leading: const CircleAvatar(child: Icon(Icons.fingerprint_outlined)),
title: Text(l10n.s_add_fingerprint),
subtitle: state.unlocked
? Text(l10n.l_fingerprints_used(fingerprints))
: Text(state.hasPin
? l10n.l_unlock_pin_first
: l10n.l_set_pin_first),
enabled: state.unlocked && fingerprints < 5,
onTap: state.unlocked && fingerprints < 5
? () {
return FsDialog(
child: Column(
children: [
if (state.bioEnroll != null)
ActionListSection(
l10n.s_setup,
children: [
ActionListItem(
key: keys.addFingerprintAction,
actionStyle: ActionStyle.primary,
icon: const Icon(Icons.fingerprint_outlined),
title: l10n.s_add_fingerprint,
subtitle: state.unlocked
? l10n.l_fingerprints_used(fingerprints)
: state.hasPin
? l10n.l_unlock_pin_first
: l10n.l_set_pin_first,
trailing:
fingerprints == 0 ? const Icon(Icons.warning_amber) : null,
onTap: state.unlocked && fingerprints < 5
? (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => AddFingerprintDialog(node.path),
);
}
: null,
),
],
),
ActionListSection(
l10n.s_manage,
children: [
ActionListItem(
key: keys.managePinAction,
icon: const Icon(Icons.pin_outlined),
title: state.hasPin ? l10n.s_change_pin : l10n.s_set_pin,
subtitle: state.hasPin
? l10n.s_fido_pin_protection
: l10n.l_fido_pin_protection_optional,
trailing: state.alwaysUv && !state.hasPin
? const Icon(Icons.warning_amber)
: null,
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => AddFingerprintDialog(node.path),
builder: (context) => FidoPinDialog(node.path, state),
);
}
: null,
),
}),
ActionListItem(
key: keys.resetAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
title: l10n.s_reset_fido,
subtitle: l10n.l_factory_reset_this_app,
onTap: (context) {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
],
)
],
ListTitle(l10n.s_manage,
textStyle: Theme.of(context).textTheme.bodyLarge),
ListTile(
leading: const CircleAvatar(child: Icon(Icons.pin_outlined)),
title: Text(state.hasPin ? l10n.s_change_pin : l10n.s_set_pin),
subtitle: Text(state.hasPin
? l10n.s_fido_pin_protection
: l10n.l_fido_pin_protection_optional),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => FidoPinDialog(node.path, state),
);
}),
ListTile(
leading: CircleAvatar(
foregroundColor: theme.onError,
backgroundColor: theme.error,
child: const Icon(Icons.delete_outline),
),
title: Text(l10n.s_reset_fido),
subtitle: Text(l10n.l_factory_reset_this_app),
onTap: () {
Navigator.of(context).pop();
showBlurDialog(
context: context,
builder: (context) => ResetDialog(node),
);
},
),
],
),
);
}

View File

@ -43,6 +43,7 @@ class FidoLockedPage extends ConsumerWidget {
header: l10n.s_no_fingerprints,
message: l10n.l_set_pin_fingerprints,
keyActionsBuilder: _buildActions,
keyActionsBadge: fidoShowActionsNotifier(state),
);
} else {
return MessagePage(
@ -53,6 +54,7 @@ class FidoLockedPage extends ConsumerWidget {
: l10n.l_ready_to_use,
message: l10n.l_optionally_set_a_pin,
keyActionsBuilder: _buildActions,
keyActionsBadge: fidoShowActionsNotifier(state),
);
}
}

View File

@ -22,6 +22,7 @@ import 'package:logging/logging.dart';
import '../../app/logging.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/state.dart';
import '../../desktop/models.dart';
import '../../widgets/responsive_dialog.dart';
import '../models.dart';
@ -184,10 +185,14 @@ class _FidoPinDialogState extends ConsumerState<FidoPinDialog> {
} else {
errorMessage = e.toString();
}
showMessage(
context,
l10n.l_set_pin_failed(errorMessage),
duration: const Duration(seconds: 4),
await ref.read(withContextProvider)(
(context) async {
showMessage(
context,
l10n.l_set_pin_failed(errorMessage),
duration: const Duration(seconds: 4),
);
},
);
}
}

View File

@ -20,14 +20,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/models.dart';
import '../../app/shortcuts.dart';
import '../../app/views/app_list_item.dart';
import '../../app/views/app_page.dart';
import '../../app/views/graphics.dart';
import '../../app/views/message_page.dart';
import '../../widgets/list_title.dart';
import '../models.dart';
import '../state.dart';
import 'actions.dart';
import 'credential_dialog.dart';
import 'delete_credential_dialog.dart';
import 'delete_fingerprint_dialog.dart';
import 'fingerprint_dialog.dart';
import 'key_actions.dart';
import 'rename_fingerprint_dialog.dart';
@ -48,42 +53,27 @@ class FidoUnlockedPage extends ConsumerWidget {
}
final creds = data.value;
if (creds.isNotEmpty) {
children.add(ListTitle(l10n.s_credentials));
children.addAll(
creds.map(
(cred) => ListTile(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.person),
),
title: Text(
cred.userName,
softWrap: false,
overflow: TextOverflow.fade,
),
subtitle: Text(
cred.rpId,
softWrap: false,
overflow: TextOverflow.fade,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
showBlurDialog(
children.add(ListTitle(l10n.s_passkeys));
children.addAll(creds.map((cred) => Actions(
actions: {
OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) =>
DeleteCredentialDialog(node.path, cred),
);
},
icon: const Icon(Icons.delete_outline)),
],
),
),
),
);
barrierColor: Colors.transparent,
builder: (context) => CredentialDialog(cred),
)),
DeleteIntent: CallbackAction<DeleteIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => DeleteCredentialDialog(
node.path,
cred,
),
),
),
},
child: _CredentialListItem(cred),
)));
}
}
@ -97,40 +87,32 @@ class FidoUnlockedPage extends ConsumerWidget {
if (fingerprints.isNotEmpty) {
nFingerprints = fingerprints.length;
children.add(ListTitle(l10n.s_fingerprints));
children.addAll(fingerprints.map((fp) => ListTile(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.fingerprint),
),
title: Text(
fp.label,
softWrap: false,
overflow: TextOverflow.fade,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () {
showBlurDialog(
children.addAll(fingerprints.map((fp) => Actions(
actions: {
OpenIntent: CallbackAction<OpenIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) =>
RenameFingerprintDialog(node.path, fp),
);
},
icon: const Icon(Icons.edit_outlined)),
IconButton(
onPressed: () {
showBlurDialog(
barrierColor: Colors.transparent,
builder: (context) => FingerprintDialog(fp),
)),
EditIntent: CallbackAction<EditIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) =>
DeleteFingerprintDialog(node.path, fp),
);
},
icon: const Icon(Icons.delete_outline)),
],
),
builder: (context) => RenameFingerprintDialog(
node.path,
fp,
),
)),
DeleteIntent: CallbackAction<DeleteIntent>(
onInvoke: (_) => showBlurDialog(
context: context,
builder: (context) => DeleteFingerprintDialog(
node.path,
fp,
),
)),
},
child: _FingerprintListItem(fp),
)));
}
}
@ -140,6 +122,7 @@ class FidoUnlockedPage extends ConsumerWidget {
title: Text(l10n.s_webauthn),
keyActionsBuilder: (context) =>
fidoBuildActions(context, node, state, nFingerprints),
keyActionsBadge: fidoShowActionsNotifier(state),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, children: children),
);
@ -153,6 +136,7 @@ class FidoUnlockedPage extends ConsumerWidget {
message: l10n.l_add_one_or_more_fps,
keyActionsBuilder: (context) =>
fidoBuildActions(context, node, state, 0),
keyActionsBadge: fidoShowActionsNotifier(state),
);
}
@ -162,6 +146,7 @@ class FidoUnlockedPage extends ConsumerWidget {
header: l10n.l_no_discoverable_accounts,
message: l10n.l_register_sk_on_websites,
keyActionsBuilder: (context) => fidoBuildActions(context, node, state, 0),
keyActionsBadge: fidoShowActionsNotifier(state),
);
}
@ -172,3 +157,50 @@ class FidoUnlockedPage extends ConsumerWidget {
child: const CircularProgressIndicator(),
);
}
class _CredentialListItem extends StatelessWidget {
final FidoCredential credential;
const _CredentialListItem(this.credential);
@override
Widget build(BuildContext context) {
return AppListItem(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor: Theme.of(context).colorScheme.primary,
child: const Icon(Icons.person),
),
title: credential.userName,
subtitle: credential.rpId,
trailing: OutlinedButton(
onPressed: Actions.handler(context, const OpenIntent()),
child: const Icon(Icons.more_horiz),
),
buildPopupActions: (context) =>
buildCredentialActions(AppLocalizations.of(context)!),
);
}
}
class _FingerprintListItem extends StatelessWidget {
final Fingerprint fingerprint;
const _FingerprintListItem(this.fingerprint);
@override
Widget build(BuildContext context) {
return AppListItem(
leading: CircleAvatar(
foregroundColor: Theme.of(context).colorScheme.onSecondary,
backgroundColor: Theme.of(context).colorScheme.secondary,
child: const Icon(Icons.fingerprint),
),
title: fingerprint.label,
trailing: OutlinedButton(
onPressed: Actions.handler(context, const OpenIntent()),
child: const Icon(Icons.more_horiz),
),
buildPopupActions: (context) =>
buildFingerprintActions(AppLocalizations.of(context)!),
);
}
}

445
lib/l10n/app_de.arb Normal file
View File

@ -0,0 +1,445 @@
{
"@@locale": "de",
"@_readme": {
"notes": [
"Alle Zeichenketten beginnen mit einem Großbuchstaben.",
"Gruppieren Sie Zeichenketten nach Kategorie, aber fügen Sie nicht zu einem Bereich hinzu, wenn sie in mehreren Bereichen verwendet werden können.",
"Führen Sie check_strings.py für die .arb Datei aus, um Probleme zu finden. Passen Sie @_lint_rules nach Sprache an wie nötig."
],
"prefixes": {
"s_": "Ein einzelnes Wort oder wenige Wörter. Sollte kurz genug sein, um auf einer Schaltfläche oder einer Überschrift angezeigt zu werden.",
"l_": "Eine einzelne Zeile, kann umbgebrochen werden. Sollte nicht mehr als einen Satz umfassen und nicht mit einem Punkt enden.",
"p_": "Ein oder mehrere ganze Sätze mit allen Satzzeichen.",
"q_": "Eine Frage, die mit einem Fragezeichen endet."
}
},
"@_lint_rules": {
"s_max_words": 4,
"s_max_length": 32
},
"app_name": "Yubico Authenticator",
"s_save": "Speichern",
"s_cancel": "Abbrechen",
"s_close": "Schließen",
"s_delete": "Löschen",
"s_quit": "Beenden",
"s_unlock": "Entsperren",
"s_calculate": "Berechnen",
"s_label": "Beschriftung",
"s_name": "Name",
"s_usb": "USB",
"s_nfc": "NFC",
"s_show_window": "Fenster anzeigen",
"s_hide_window": "Fenster verstecken",
"q_rename_target": "{label} umbenennen?",
"@q_rename_target" : {
"placeholders": {
"label": {}
}
},
"s_about": "Über",
"s_appearance": "Aussehen",
"s_authenticator": "Authenticator",
"s_manage": "Verwalten",
"s_setup": "Einrichten",
"s_settings": "Einstellungen",
"s_webauthn": "WebAuthn",
"s_help_and_about": "Hilfe und Über",
"s_help_and_feedback": "Hilfe und Feedback",
"s_send_feedback": "Senden Sie uns Feedback",
"s_i_need_help": "Ich brauche Hilfe",
"s_troubleshooting": "Problembehebung",
"s_terms_of_use": "Nutzungsbedingungen",
"s_privacy_policy": "Datenschutzerklärung",
"s_open_src_licenses": "Open Source-Lizenzen",
"s_configure_yk": "YubiKey konfigurieren",
"s_please_wait": "Bitte warten\u2026",
"s_secret_key": "Geheimer Schlüssel",
"s_invalid_length": "Ungültige Länge",
"s_require_touch": "Berührung ist erforderlich",
"q_have_account_info": "Haben Sie Konto-Informationen?",
"s_run_diagnostics": "Diagnose ausführen",
"s_log_level": "Log-Level: {level}",
"@s_log_level": {
"placeholders": {
"level": {}
}
},
"s_character_count": "Anzahl Zeichen",
"s_learn_more": "Mehr\u00a0erfahren",
"@_language": {},
"s_language": "Sprache",
"l_enable_community_translations": "Übersetzungen der Gemeinschaft aktivieren",
"p_community_translations_desc": "Diese Übersetzungen werden von der Gemeinschaft erstellt und gewartet. Sie könnten Fehler enthalten oder unvollständig sein.",
"@_theme": {},
"s_app_theme": "App Theme",
"s_choose_app_theme": "App Theme auswählen",
"s_system_default": "Standard des Systems",
"s_light_mode": "Heller Modus",
"s_dark_mode": "Dunkler Modus",
"@_yubikey_selection": {},
"s_yk_information": "YubiKey Information",
"s_select_yk": "YubiKey auswählen",
"s_select_to_scan": "Zum Scannen auswählen",
"s_hide_device": "Gerät verstecken",
"s_show_hidden_devices": "Versteckte Geräte anzeigen",
"s_sn_serial": "S/N: {serial}",
"@s_sn_serial" : {
"placeholders": {
"serial": {}
}
},
"s_fw_version": "F/W: {version}",
"@s_fw_version" : {
"placeholders": {
"version": {}
}
},
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKey anschließen",
"l_insert_or_tap_yk": "YubiKey anschließen oder dagegenhalten",
"l_unplug_yk": "Entfernen Sie Ihren YubiKey",
"l_reinsert_yk": "Schließen Sie Ihren YubiKey wieder an",
"l_place_on_nfc_reader": "Halten Sie Ihren YubiKey zum NFC-Leser",
"l_replace_yk_on_reader": "Halten Sie Ihren YubiKey wieder zum Leser",
"l_remove_yk_from_reader": "Entfernen Sie Ihren YubiKey vom NFC-Leser",
"p_try_reinsert_yk": "Versuchen Sie Ihren YubiKey zu entfernen und wieder anzuschließen.",
"s_touch_required": "Berührung erforderlich",
"l_touch_button_now": "Berühren Sie jetzt die Schaltfläche auf Ihrem YubiKey",
"l_keep_touching_yk": "Berühren Sie Ihren YubiKey wiederholt\u2026",
"@_app_configuration": {},
"s_toggle_applications": "Anwendungen umschalten",
"l_min_one_interface": "Mindestens ein Interface muss aktiviert sein",
"s_reconfiguring_yk": "YubiKey wird neu konfiguriert\u2026",
"s_config_updated": "Konfiguration aktualisiert",
"l_config_updated_reinsert": "Konfiguration aktualisiert, entfernen Sie Ihren YubiKey und schließen ihn wieder an",
"s_app_not_supported": "Anwendung nicht unterstützt",
"l_app_not_supported_on_yk": "Der verwendete YubiKey unterstützt die Anwendung '{app}' nicht",
"@l_app_not_supported_on_yk" : {
"placeholders": {
"app": {}
}
},
"l_app_not_supported_desc": "Diese Anwendung wird nicht unterstützt",
"s_app_disabled": "Anwendung deaktiviert",
"l_app_disabled_desc": "Aktivieren Sie die Anwendung '{app}' auf Ihrem YubiKey für Zugriff",
"@l_app_disabled_desc" : {
"placeholders": {
"app": {}
}
},
"s_fido_disabled": "FIDO2 deaktiviert",
"l_webauthn_req_fido2": "WebAuthn erfordert, dass die FIDO2 Anwendung auf Ihrem YubiKey aktiviert ist",
"@_connectivity_issues": {},
"l_helper_not_responding": "Der Helper-Prozess antwortet nicht",
"l_yk_no_access": "Auf diesen YubiKey kann nicht zugegriffen werden",
"s_yk_inaccessible": "Gerät nicht zugänglich",
"l_open_connection_failed": "Konnte keine Verbindung öffnen",
"l_ccid_connection_failed": "Konnte keine Smartcard-Verbindung öffnen",
"p_ccid_service_unavailable": "Stellen Sie sicher, dass Ihr Smartcard-Service funktioniert.",
"p_pcscd_unavailable": "Stellen Sie sicher, dass pcscd installiert ist und ausgeführt wird.",
"l_no_yk_present": "Kein YubiKey vorhanden",
"s_unknown_type": "Unbekannter Typ",
"s_unknown_device": "Unbekanntes Gerät",
"s_unsupported_yk": "Nicht unterstützter YubiKey",
"s_yk_not_recognized": "Geräte nicht erkannt",
"@_general_errors": {},
"l_error_occured": "Es ist ein Fehler aufgetreten",
"s_application_error": "Anwendungs-Fehler",
"l_import_error": "Import-Fehler",
"l_file_not_found": "Datei nicht gefunden",
"l_file_too_big": "Datei ist zu groß",
"l_filesystem_error": "Fehler beim Dateisystem-Zugriff",
"@_pins": {},
"s_pin": "PIN",
"s_set_pin": "PIN setzen",
"s_change_pin": "PIN ändern",
"s_current_pin": "Derzeitige PIN",
"s_new_pin": "Neue PIN",
"s_confirm_pin": "PIN bestätigen",
"l_new_pin_len": "Neue PIN muss mindestens {length} Zeichen lang sein",
"@l_new_pin_len" : {
"placeholders": {
"length": {}
}
},
"s_pin_set": "PIN gesetzt",
"l_set_pin_failed": "PIN konnte nicht gesetzt werden: {message}",
"@l_set_pin_failed" : {
"placeholders": {
"message": {}
}
},
"l_wrong_pin_attempts_remaining": "Falsche PIN, {retries} Versuch(e) verbleibend",
"@l_wrong_pin_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"s_fido_pin_protection": "FIDO PIN Schutz",
"l_fido_pin_protection_optional": "Optionaler FIDO PIN Schutz",
"l_enter_fido2_pin": "Geben Sie die FIDO2 PIN für Ihren YubiKey ein",
"l_optionally_set_a_pin": "Setzen Sie optional eine PIN, um den Zugriff auf Ihren YubiKey zu schützen\nAls Sicherheitsschlüssel auf Webseiten registrieren",
"l_pin_blocked_reset": "PIN ist blockiert; setzen Sie die FIDO Anwendung auf Werkseinstellung zurück",
"l_set_pin_first": "Zuerst ist eine PIN erforderlich",
"l_unlock_pin_first": "Zuerst mit PIN entsperren",
"l_pin_soft_locked": "PIN wurde blockiert bis der YubiKey entfernt und wieder angeschlossen wird",
"p_enter_current_pin_or_reset": "Geben Sie Ihre aktuelle PIN ein. Wenn Sie die PIN nicht wissen, müssen Sie den YubiKey zurücksetzen.",
"p_enter_new_fido2_pin": "Geben Sie Ihre neue PIN ein. Eine PIN muss mindestens {length} Zeichen lang sein und kann Buchstaben, Ziffern und spezielle Zeichen enthalten.",
"@p_enter_new_fido2_pin" : {
"placeholders": {
"length": {}
}
},
"@_passwords": {},
"s_password": "Passwort",
"s_manage_password": "Passwort verwalten",
"s_set_password": "Passwort setzen",
"s_password_set": "Passwort gesetzt",
"l_optional_password_protection": "Optionaler Passwortschutz",
"s_new_password": "Neues Passwort",
"s_current_password": "Aktuelles Passwort",
"s_confirm_password": "Passwort bestätigen",
"s_wrong_password": "Falsches Passwort",
"s_remove_password": "Passwort entfernen",
"s_password_removed": "Passwort entfernt",
"s_remember_password": "Passwort speichern",
"s_clear_saved_password": "Gespeichertes Passwort entfernen",
"s_password_forgotten": "Passwort vergessen",
"l_keystore_unavailable": "Passwortspeicher des Betriebssystems nicht verfügbar",
"l_remember_pw_failed": "Konnte Passwort nicht speichern",
"l_unlock_first": "Zuerst mit Passwort entsperren",
"l_enter_oath_pw": "Das OATH-Passwort für Ihren YubiKey eingeben",
"p_enter_current_password_or_reset": "Geben Sie Ihr aktuelles Passwort ein. Wenn Sie Ihr Passwort nicht wissen, müssen Sie den YubiKey zurücksetzen.",
"p_enter_new_password": "Geben Sie Ihr neues Passwort ein. Ein Passwort kann Buchstaben, Ziffern und spezielle Zeichen enthalten.",
"@_oath_accounts": {},
"l_account": "Konto: {label}",
"@l_account" : {
"placeholders": {
"label": {}
}
},
"s_accounts": "Konten",
"s_no_accounts": "Keine Konten",
"s_add_account": "Konto hinzufügen",
"s_account_added": "Konto hinzugefügt",
"l_account_add_failed": "Fehler beim Hinzufügen des Kontos: {message}",
"@l_account_add_failed" : {
"placeholders": {
"message": {}
}
},
"l_account_name_required": "Ihr Konto muss einen Namen haben",
"l_name_already_exists": "Für diesen Aussteller existiert dieser Name bereits",
"l_invalid_character_issuer": "Ungültiges Zeichen: ':' ist im Aussteller nicht erlaubt",
"s_pinned": "Angepinnt",
"s_pin_account": "Konto anpinnen",
"s_unpin_account": "Konto nicht mehr anpinnen",
"s_no_pinned_accounts": "Keine angepinnten Konten",
"s_rename_account": "Konto umbenennen",
"s_account_renamed": "Konto umbenannt",
"p_rename_will_change_account_displayed": "Das ändert die Anzeige dieses Kontos in der Liste.",
"s_delete_account": "Konto löschen",
"s_account_deleted": "Konto gelöscht",
"p_warning_delete_account": "Vorsicht! Das löscht das Konto von Ihrem YubiKey.",
"p_warning_disable_credential": "Sie werden keine OTPs für dieses Konto mehr erstellen können. Deaktivieren Sie diese Anmeldeinformation zuerst auf der Webseite, um nicht aus dem Konto ausgesperrt zu werden.",
"s_account_name": "Kontoname",
"s_search_accounts": "Konten durchsuchen",
"l_accounts_used": "{used} von {capacity} Konten verwendet",
"@l_accounts_used" : {
"placeholders": {
"used": {},
"capacity": {}
}
},
"s_num_digits": "{num} Ziffern",
"@s_num_digits" : {
"placeholders": {
"num": {}
}
},
"s_num_sec": "{num} sek",
"@s_num_sec" : {
"placeholders": {
"num": {}
}
},
"s_issuer_optional": "Aussteller (optional)",
"s_counter_based": "Zähler-basiert",
"s_time_based": "Zeit-basiert",
"@_fido_credentials": {},
"l_credential": "Anmeldeinformation: {label}",
"@l_credential" : {
"placeholders": {
"label": {}
}
},
"s_credentials": "Anmeldeinformationen",
"l_ready_to_use": "Bereit zur Verwendung",
"l_register_sk_on_websites": "Als Sicherheitsschlüssel auf Webseiten registrieren",
"l_no_discoverable_accounts": "Keine erkennbaren Konten",
"s_delete_credential": "Anmeldeinformation löschen",
"s_credential_deleted": "Anmeldeinformation gelöscht",
"p_warning_delete_credential": "Das löscht die Anmeldeinformation von Ihrem YubiKey.",
"@_fingerprints": {},
"l_fingerprint": "Fingerabdruck: {label}",
"@l_fingerprint" : {
"placeholders": {
"label": {}
}
},
"s_fingerprints": "Fingerabdrücke",
"l_fingerprint_captured": "Fingerabdruck erfolgreich aufgenommen!",
"s_fingerprint_added": "Fingerabdruck hinzugefügt",
"l_setting_name_failed": "Fehler beim Setzen des Namens: {message}",
"@l_setting_name_failed" : {
"placeholders": {
"message": {}
}
},
"s_add_fingerprint": "Fingerabdruck hinzufügen",
"l_fp_step_1_capture": "Schritt 1/2: Fingerabdruck aufnehmen",
"l_fp_step_2_name": "Schritt 2/2: Fingerabdruck benennen",
"s_delete_fingerprint": "Fingerabdruck löschen",
"s_fingerprint_deleted": "Fingerabdruck gelöscht",
"p_warning_delete_fingerprint": "Das löscht den Fingerabdruck von Ihrem YubiKey.",
"s_no_fingerprints": "Keine Fingerabdrücke",
"l_set_pin_fingerprints": "Setzen Sie eine PIN um Fingerabdrücke zu registrieren",
"l_no_fps_added": "Es wurden keine Fingerabdrücke hinzugefügt",
"s_rename_fp": "Fingerabdruck umbenennen",
"s_fingerprint_renamed": "Fingerabdruck umbenannt",
"l_rename_fp_failed": "Fehler beim Umbenennen: {message}",
"@l_rename_fp_failed" : {
"placeholders": {
"message": {}
}
},
"l_add_one_or_more_fps": "Fügen Sie einen oder bis zu fünf Fingerabdrücke hinzu",
"l_fingerprints_used": "{used}/5 Fingerabdrücke registriert",
"@l_fingerprints_used": {
"placeholders": {
"used": {}
}
},
"p_press_fingerprint_begin": "Drücken Sie Ihren Finger gegen den YubiKey um zu beginnen.",
"p_will_change_label_fp": "Das ändert die Beschriftung des Fingerabdrucks.",
"@_permissions": {},
"s_enable_nfc": "NFC aktivieren",
"s_permission_denied": "Zugriff verweigert",
"l_elevating_permissions": "Erhöhe Berechtigungen\u2026",
"s_review_permissions": "Berechtigungen überprüfen",
"p_elevated_permissions_required": "Die Verwaltung dieses Geräts benötigt erhöhte Berechtigungen.",
"p_webauthn_elevated_permissions_required": "WebAuthn-Verwaltung benötigt erhöhte Berechtigungen.",
"p_need_camera_permission": "Yubico Authenticator benötigt Zugriff auf die Kamera um QR-Codes aufnehmen zu können.",
"@_qr_codes": {},
"s_qr_scan": "QR-Code aufnehmen",
"l_qr_scanned": "QR-Code aufgenommen",
"l_invalid_qr": "Ungültiger QR-Code",
"l_qr_not_found": "Kein QR-Code gefunden",
"l_qr_not_read": "Fehler beim Lesen des QR-Codes: {message}",
"@l_qr_not_read" : {
"placeholders": {
"message": {}
}
},
"l_point_camera_scan": "Halten Sie Ihre Kamera auf einen QR-Code um ihn aufzunehmen",
"q_want_to_scan": "Möchten Sie aufnehmen?",
"q_no_qr": "Kein QR-Code?",
"s_enter_manually": "Manuell eingeben",
"@_factory_reset": {},
"s_reset": "Zurücksetzen",
"s_factory_reset": "Werkseinstellungen",
"l_factory_reset_this_app": "Anwendung auf Werkseinstellung zurücksetzen",
"s_reset_oath": "OATH zurücksetzen",
"l_oath_application_reset": "OATH Anwendung zurücksetzen",
"s_reset_fido": "FIDO zurücksetzen",
"l_fido_app_reset": "FIDO Anwendung zurückgesetzt",
"l_press_reset_to_begin": "Drücken Sie Zurücksetzen um zu beginnen\u2026",
"l_reset_failed": "Fehler beim Zurücksetzen: {message}",
"@l_reset_failed" : {
"placeholders": {
"message": {}
}
},
"p_warning_factory_reset": "Achtung! Das löscht alle OATH TOTP/HOTP Konten unwiederbringlich von Ihrem YubiKey.",
"p_warning_disable_credentials": "Ihre OATH Anmeldeinformationen und jedes gesetzte Passwort wird von diesem YubiKey entfernt. Deaktivieren Sie diese zuerst auf den zugehörigen Webseiten, um nicht aus ihren Konten ausgesperrt zu werden.",
"p_warning_deletes_accounts": "Achtung! Das löscht alle U2F und FIDO2 Konten unwiederbringlich von Ihrem YubiKey.",
"p_warning_disable_accounts": "Ihre Anmeldeinformationen und jede gesetzte PIN wird von diesem YubiKey entfernt. Deaktivieren Sie diese zuerst auf den zugehörigen Webseiten, um nicht aus ihren Konten ausgesperrt zu werden.",
"@_copy_to_clipboard": {},
"l_copy_to_clipboard": "In die Zwischenablage kopieren",
"s_code_copied": "Code kopiert",
"l_code_copied_clipboard": "Code in die Zwischenablage kopiert",
"s_copy_log": "Log kopiert",
"l_log_copied": "Log in die Zwischenablage kopiert",
"l_diagnostics_copied": "Diagnosedaten in die Zwischenablage kopiert",
"p_target_copied_clipboard": "{label} in die Zwischenablage kopiert.",
"@p_target_copied_clipboard" : {
"placeholders": {
"label": {}
}
},
"@_custom_icons": {},
"s_custom_icons": "Eigene Icons",
"l_set_icons_for_accounts": "Icons für Konten setzen",
"p_custom_icons_description": "Icon-Pakete machen Ihre Konten mit bekannten Logos und Farben leichter unterscheidbar.",
"s_replace_icon_pack": "Icon-Paket ersetzen",
"l_loading_icon_pack": "Lade Icon-Paket\u2026",
"s_load_icon_pack": "Icon-Paket laden",
"s_remove_icon_pack": "Icon-Paket entfernen",
"l_icon_pack_removed": "Icon-Paket entfernt",
"l_remove_icon_pack_failed": "Fehler beim Entfernen des Icon-Pakets",
"s_choose_icon_pack": "Icon-Paket auswählen",
"l_icon_pack_imported": "Icon-Paket importiert",
"l_import_icon_pack_failed": "Fehler beim Importieren des Icon-Pakets: {message}",
"@l_import_icon_pack_failed": {
"placeholders": {
"message": {}
}
},
"l_invalid_icon_pack": "Ungültiges Icon-Paket",
"l_icon_pack_copy_failed": "Kopieren der Dateien des Icon-Pakets fehlgeschlagen",
"@_android_settings": {},
"s_nfc_options": "NFC Optionen",
"l_on_yk_nfc_tap": "Bei YubiKey NFC-Berührung",
"l_launch_ya": "Yubico Authenticator starten",
"l_copy_otp_clipboard": "OTP in Zwischenablage kopieren",
"l_launch_and_copy_otp": "App starten und OTP kopieren",
"l_kbd_layout_for_static": "Tastaturlayout (für statisches Passwort)",
"s_choose_kbd_layout": "Tastaturlayout auswählen",
"l_bypass_touch_requirement": "Notwendigkeit zur Berührung umgehen",
"l_bypass_touch_requirement_on": "Konten, die Berührung erfordern, werden automatisch über NFC angezeigt",
"l_bypass_touch_requirement_off": "Konten, die Berührung erfordern, benötigen eine zusätzliche NFC-Berührung",
"s_silence_nfc_sounds": "NFC-Töne stummschalten",
"l_silence_nfc_sounds_on": "Keine Töne werden bei NFC-Berührung abgespielt",
"l_silence_nfc_sounds_off": "Töne werden bei NFC-Berührung abgespielt",
"s_usb_options": "USB Optionen",
"l_launch_app_on_usb": "Starten, wenn YubiKey angesteckt wird",
"l_launch_app_on_usb_on": "Das verhindert, dass andere Anwendungen den YubiKey über USB nutzen",
"l_launch_app_on_usb_off": "Andere Anwendungen können den YubiKey über USB nutzen",
"s_allow_screenshots": "Bildschirmfotos erlauben",
"@_eof": {}
}

View File

@ -29,10 +29,13 @@
"s_quit": "Quit",
"s_unlock": "Unlock",
"s_calculate": "Calculate",
"s_import": "Import",
"s_overwrite": "Overwrite",
"s_label": "Label",
"s_name": "Name",
"s_usb": "USB",
"s_nfc": "NFC",
"s_options": "Options",
"s_show_window": "Show window",
"s_hide_window": "Hide window",
"q_rename_target": "Rename {label}?",
@ -41,13 +44,22 @@
"label": {}
}
},
"l_bullet": "• {item}",
"@l_bullet" : {
"placeholders": {
"item": {}
}
},
"s_about": "About",
"s_algorithm": "Algorithm",
"s_appearance": "Appearance",
"s_authenticator": "Authenticator",
"s_actions": "Actions",
"s_manage": "Manage",
"s_setup": "Setup",
"s_settings": "Settings",
"s_piv": "PIV",
"s_webauthn": "WebAuthn",
"s_help_and_about": "Help and about",
"s_help_and_feedback": "Help and feedback",
@ -60,6 +72,7 @@
"s_configure_yk": "Configure YubiKey",
"s_please_wait": "Please wait\u2026",
"s_secret_key": "Secret key",
"s_private_key": "Private key",
"s_invalid_length": "Invalid length",
"s_require_touch": "Require touch",
"q_have_account_info": "Have account info?",
@ -165,11 +178,17 @@
"@_pins": {},
"s_pin": "PIN",
"s_puk": "PUK",
"s_set_pin": "Set PIN",
"s_change_pin": "Change PIN",
"s_change_puk": "Change PUK",
"s_current_pin": "Current PIN",
"s_current_puk": "Current PUK",
"s_new_pin": "New PIN",
"s_new_puk": "New PUK",
"s_confirm_pin": "Confirm PIN",
"s_confirm_puk": "Confirm PUK",
"s_unblock_pin": "Unblock PIN",
"l_new_pin_len": "New PIN must be at least {length} characters",
"@l_new_pin_len" : {
"placeholders": {
@ -177,18 +196,31 @@
}
},
"s_pin_set": "PIN set",
"s_puk_set": "PUK set",
"l_set_pin_failed": "Failed to set PIN: {message}",
"@l_set_pin_failed" : {
"placeholders": {
"message": {}
}
},
"l_attempts_remaining": "{retries} attempt(s) remaining",
"@l_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_pin_attempts_remaining": "Wrong PIN, {retries} attempt(s) remaining",
"@l_wrong_pin_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_puk_attempts_remaining": "Wrong PUK, {retries} attempt(s) remaining",
"@l_wrong_puk_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"s_fido_pin_protection": "FIDO PIN protection",
"l_fido_pin_protection_optional": "Optional FIDO PIN protection",
"l_enter_fido2_pin": "Enter the FIDO2 PIN for your YubiKey",
@ -197,13 +229,24 @@
"l_set_pin_first": "A PIN is required first",
"l_unlock_pin_first": "Unlock with PIN first",
"l_pin_soft_locked": "PIN has been blocked until the YubiKey is removed and reinserted",
"p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to reset the YubiKey.",
"p_enter_current_pin_or_reset": "Enter your current PIN. If you don't know your PIN, you'll need to unblock it with the PUK or reset the YubiKey.",
"p_enter_current_puk_or_reset": "Enter your current PUK. If you don't know your PUK, you'll need to reset the YubiKey.",
"p_enter_new_fido2_pin": "Enter your new PIN. A PIN must be at least {length} characters long and may contain letters, numbers and special characters.",
"@p_enter_new_fido2_pin" : {
"placeholders": {
"length": {}
}
},
"s_pin_required": "PIN required",
"p_pin_required_desc": "The action you are about to perform requires the PIV PIN to be entered.",
"l_piv_pin_blocked": "Blocked, use PUK to reset",
"l_piv_pin_puk_blocked": "Blocked, factory reset needed",
"p_enter_new_piv_pin_puk": "Enter a new {name} to set. Must be 6-8 characters.",
"@p_enter_new_piv_pin_puk" : {
"placeholders": {
"name": {}
}
},
"@_passwords": {},
"s_password": "Password",
@ -227,6 +270,23 @@
"p_enter_current_password_or_reset": "Enter your current password. If you don't know your password, you'll need to reset the YubiKey.",
"p_enter_new_password": "Enter your new password. A password may contain letters, numbers and special characters.",
"@_management_key": {},
"s_management_key": "Management key",
"s_current_management_key": "Current management key",
"s_new_management_key": "New management key",
"l_change_management_key": "Change 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_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",
"s_protect_key": "Protect with PIN",
"l_pin_protected_key": "PIN can be used instead",
"l_wrong_key": "Wrong key",
"l_unlock_piv_management": "Unlock PIV management",
"p_unlock_piv_management_desc": "The action you are about to perform requires the PIV management key. Provide this key to unlock management functionality for this session.",
"@_oath_accounts": {},
"l_account": "Account: {label}",
"@l_account" : {
@ -237,6 +297,9 @@
"s_accounts": "Accounts",
"s_no_accounts": "No accounts",
"s_add_account": "Add account",
"s_add_accounts" : "Add account(s)",
"p_add_description" : "To scan a QR code, make sure the full code is visible on screen and press the button below. You can also drag a saved image from a folder onto this dialog. If you have the account credential details in writing, use the manual entry instead.",
"s_add_manually" : "Add manually",
"s_account_added": "Account added",
"l_account_add_failed": "Failed adding account: {message}",
"@l_account_add_failed" : {
@ -246,15 +309,20 @@
},
"l_account_name_required": "Your account must have a name",
"l_name_already_exists": "This name already exists for the issuer",
"l_account_already_exists": "This account already exists on the YubiKey",
"l_invalid_character_issuer": "Invalid character: ':' is not allowed in issuer",
"l_select_accounts" : "Select account(s) to add to the YubiKey",
"s_pinned": "Pinned",
"s_pin_account": "Pin account",
"s_unpin_account": "Unpin account",
"s_no_pinned_accounts": "No pinned accounts",
"l_pin_account_desc": "Keep your important accounts together",
"s_rename_account": "Rename account",
"l_rename_account_desc": "Edit the issuer/name of the account",
"s_account_renamed": "Account renamed",
"p_rename_will_change_account_displayed": "This will change how the account is displayed in the list.",
"s_delete_account": "Delete account",
"l_delete_account_desc": "Remove the account from your YubiKey",
"s_account_deleted": "Account deleted",
"p_warning_delete_account": "Warning! This action will delete the account from your YubiKey.",
"p_warning_disable_credential": "You will no longer be able to generate OTPs for this account. Make sure to first disable this credential from the website to avoid being locked out of your account.",
@ -282,21 +350,25 @@
"s_issuer_optional": "Issuer (optional)",
"s_counter_based": "Counter based",
"s_time_based": "Time based",
"l_copy_code_desc": "Easily paste the code into another app",
"s_calculate_code": "Calculate code",
"l_calculate_code_desc": "Get a new code from your YubiKey",
"@_fido_credentials": {},
"l_credential": "Credential: {label}",
"@l_credential" : {
"l_passkey": "Passkey: {label}",
"@l_passkey" : {
"placeholders": {
"label": {}
}
},
"s_credentials": "Credentials",
"s_passkeys": "Passkeys",
"l_ready_to_use": "Ready to use",
"l_register_sk_on_websites": "Register as a Security Key on websites",
"l_no_discoverable_accounts": "No discoverable accounts",
"s_delete_credential": "Delete credential",
"s_credential_deleted": "Credential deleted",
"p_warning_delete_credential": "This will delete the credential from your YubiKey.",
"l_no_discoverable_accounts": "No Passkeys stored",
"s_delete_passkey": "Delete Passkey",
"l_delete_passkey_desc": "Remove the Passkey from the YubiKey",
"s_passkey_deleted": "Passkey deleted",
"p_warning_delete_passkey": "This will delete the Passkey from your YubiKey.",
"@_fingerprints": {},
"l_fingerprint": "Fingerprint: {label}",
@ -318,12 +390,14 @@
"l_fp_step_1_capture": "Step 1/2: Capture fingerprint",
"l_fp_step_2_name": "Step 2/2: Name fingerprint",
"s_delete_fingerprint": "Delete fingerprint",
"l_delete_fingerprint_desc": "Remove the fingerprint from the YubiKey",
"s_fingerprint_deleted": "Fingerprint deleted",
"p_warning_delete_fingerprint": "This will delete the fingerprint from your YubiKey.",
"s_no_fingerprints": "No fingerprints",
"l_set_pin_fingerprints": "Set a PIN to register fingerprints",
"l_no_fps_added": "No fingerprints have been added",
"s_rename_fp": "Rename fingerprint",
"l_rename_fp_desc": "Change the label",
"s_fingerprint_renamed": "Fingerprint renamed",
"l_rename_fp_failed": "Error renaming: {message}",
"@l_rename_fp_failed" : {
@ -341,6 +415,83 @@
"p_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
"p_will_change_label_fp": "This will change the label of the fingerprint.",
"@_certificates": {},
"s_certificate": "Certificate",
"s_certificates": "Certificates",
"s_csr": "CSR",
"s_subject": "Subject",
"l_export_csr_file": "Save CSR to file",
"l_select_import_file": "Select file to import",
"l_export_certificate": "Export certificate",
"l_export_certificate_file": "Export certificate to file",
"l_export_certificate_desc": "Export the certificate to a file",
"l_certificate_exported": "Certificate exported",
"l_import_file": "Import file",
"l_import_desc": "Import a key and/or certificate",
"l_importing_file": "Importing file\u2026",
"s_file_imported": "File imported",
"l_delete_certificate": "Delete certificate",
"l_delete_certificate_desc": "Remove the certificate from your YubiKey",
"s_issuer": "Issuer",
"s_serial": "Serial",
"s_certificate_fingerprint": "Fingerprint",
"s_valid_from": "Valid from",
"s_valid_to": "Valid to",
"l_no_certificate": "No certificate loaded",
"l_key_no_certificate": "Key without certificate loaded",
"s_generate_key": "Generate key",
"l_generate_desc": "Generate a new certificate or CSR",
"p_generate_desc": "This will generate a new key on the YubiKey in PIV slot {slot}. The public key will be embedded into a self-signed certificate stored on the YubiKey, or in a certificate signing request (CSR) saved to file.",
"@p_generate_desc" : {
"placeholders": {
"slot": {}
}
},
"l_generating_private_key": "Generating private key\u2026",
"s_private_key_generated": "Private key generated",
"p_warning_delete_certificate": "Warning! This action will delete the certificate from your YubiKey.",
"q_delete_certificate_confirm": "Delete the certficate in PIV slot {slot}?",
"@q_delete_certificate_confirm" : {
"placeholders": {
"slot": {}
}
},
"l_certificate_deleted": "Certificate deleted",
"p_password_protected_file": "The selected file is password protected. Enter the password to proceed.",
"p_import_items_desc": "The following item(s) will be imported into PIV slot {slot}.",
"@p_import_items_desc" : {
"placeholders": {
"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": {},
"s_slot_display_name": "{name} ({hexid})",
"@s_slot_display_name" : {
"placeholders": {
"name": {},
"hexid": {}
}
},
"s_slot_9a": "Authentication",
"s_slot_9c": "Digital Signature",
"s_slot_9d": "Key Management",
"s_slot_9e": "Card Authentication",
"@_permissions": {},
"s_enable_nfc": "Enable NFC",
"s_permission_denied": "Permission denied",
@ -381,10 +532,14 @@
"message": {}
}
},
"s_reset_piv": "Reset PIV",
"l_piv_app_reset": "PIV application reset",
"p_warning_factory_reset": "Warning! This will irrevocably delete all OATH TOTP/HOTP accounts from your YubiKey.",
"p_warning_disable_credentials": "Your OATH credentials, as well as any password set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_deletes_accounts": "Warning! This will irrevocably delete all U2F and FIDO2 accounts from your YubiKey.",
"p_warning_disable_accounts": "Your credentials, as well as any PIN set, will be removed from this YubiKey. Make sure to first disable these from their respective web sites to avoid being locked out of your accounts.",
"p_warning_piv_reset": "Warning! All data stored for PIV will be irrevocably deleted from your YubiKey.",
"p_warning_piv_reset_desc": "This includes private keys and certificates. Your PIN, PUK, and management key will be reset to their factory default values.",
"@_copy_to_clipboard": {},
"l_copy_to_clipboard": "Copy to clipboard",
@ -441,5 +596,20 @@
"l_launch_app_on_usb_off": "Other apps can use the YubiKey over USB",
"s_allow_screenshots": "Allow screenshots",
"s_nfc_dialog_tap_key": "Tap your key",
"s_nfc_dialog_operation_success": "Success",
"s_nfc_dialog_operation_failed": "Failed",
"s_nfc_dialog_oath_reset": "Action: reset OATH applet",
"s_nfc_dialog_oath_unlock": "Action: unlock OATH applet",
"s_nfc_dialog_oath_set_password": "Action: set OATH password",
"s_nfc_dialog_oath_unset_password": "Action: remove OATH password",
"s_nfc_dialog_oath_add_account": "Action: add new account",
"s_nfc_dialog_oath_rename_account": "Action: rename account",
"s_nfc_dialog_oath_delete_account": "Action: delete account",
"s_nfc_dialog_oath_calculate_code": "Action: calculate OATH code",
"s_nfc_dialog_oath_failure": "OATH operation failed",
"s_nfc_dialog_oath_add_multiple_accounts": "Action: add multiple accounts",
"@_eof": {}
}

615
lib/l10n/app_fr.arb Normal file
View File

@ -0,0 +1,615 @@
{
"@@locale": "fr",
"@_readme": {
"notes": [
"Toutes les chaînes de caractères commencent par une lettre majuscule.",
"Regroupez les chaînes de caractères par catégories, mais ne les liez pas inutilement à une section de l'application si elles peuvent être réutilisées entre plusieurs.",
"Exécutez check_strings.py sur le fichier .arb pour détecter les problèmes et ajustez @_lint_rules si nécessaire par langue."
],
"prefixes": {
"s_": "Un seul ou peu de mots. Doit être assez court pour être affiché sur un bouton ou un entête.",
"l_": "Une seule ligne, peut être enroulée. Ne doit pas être plus d'une phrase et ne finis pas par un point.",
"p_": "Une ou plusieurs phrases avec ponctuation.",
"q_": "Une question se terminant par un point d'interrogation."
}
},
"@_lint_rules": {
"s_max_words": 5,
"s_max_length": 32
},
"app_name": "Yubico Authenticator",
"s_save": "Sauvegarder",
"s_cancel": "Annuler",
"s_close": "Fermer",
"s_delete": "Supprimer",
"s_quit": "Quitter",
"s_unlock": "Déverrouiller",
"s_calculate": "Calculer",
"s_import": "Importer",
"s_overwrite": "Écraser",
"s_label": "Étiquette",
"s_name": "Nom",
"s_usb": "USB",
"s_nfc": "NFC",
"s_options": "Options",
"s_show_window": "Afficher la fenêtre",
"s_hide_window": "Masquer la fenêtre",
"q_rename_target": "Renommer {label}?",
"@q_rename_target" : {
"placeholders": {
"label": {}
}
},
"l_bullet": "• {item}",
"@l_bullet" : {
"placeholders": {
"item": {}
}
},
"s_about": "À propos",
"s_algorithm": "Algorithme",
"s_appearance": "Apparence",
"s_authenticator": "Authenticator",
"s_actions": "Actions",
"s_manage": "Gérer",
"s_setup": "Configuration",
"s_settings": "Paramètres",
"s_piv": "PIV",
"s_webauthn": "WebAuthn",
"s_help_and_about": "Aide et à propos",
"s_help_and_feedback": "Aide et retours",
"s_send_feedback": "Envoyer nous un retour",
"s_i_need_help": "J'ai besoin d'aide",
"s_troubleshooting": "Dépannage",
"s_terms_of_use": "Termes d'utilisation",
"s_privacy_policy": "Politique de confidentialité",
"s_open_src_licenses": "Licenses Open source",
"s_configure_yk": "Configurer la YubiKey",
"s_please_wait": "Veuillez patienter\u2026",
"s_secret_key": "Clé secrète",
"s_private_key": "Clé privée",
"s_invalid_length": "Longueur invalide",
"s_require_touch": "Touché requis",
"q_have_account_info": "Avez-vous des informations de compte?",
"s_run_diagnostics": "Exécuter un diagnostique",
"s_log_level": "Niveau de log: {level}",
"@s_log_level": {
"placeholders": {
"level": {}
}
},
"s_character_count": "Nombre de caractères",
"s_learn_more": "En savoir\u00a0plus",
"@_language": {},
"s_language": "Langue",
"l_enable_community_translations": "Activer les traductions de la communauté",
"p_community_translations_desc": "Ces traductions sont fournies et maintenues par la communauté. Elles peuvent contenir des erreurs ou être incomplètes.",
"@_theme": {},
"s_app_theme": "Thèmes de l'application",
"s_choose_app_theme": "Choix du thème",
"s_system_default": "Thème du système",
"s_light_mode": "Thème clair",
"s_dark_mode": "Thème sombre",
"@_yubikey_selection": {},
"s_yk_information": "Informations de la YubiKey",
"s_select_yk": "Sélectionner une YubiKey",
"s_select_to_scan": "Sélectionnez pour scanner",
"s_hide_device": "Cacher l'appareil",
"s_show_hidden_devices": "Afficher les appareils cachés",
"s_sn_serial": "S/N: {serial}",
"@s_sn_serial" : {
"placeholders": {
"serial": {}
}
},
"s_fw_version": "F/W: {version}",
"@s_fw_version" : {
"placeholders": {
"version": {}
}
},
"@_yubikey_interactions": {},
"l_insert_yk": "Insérez votre YubiKey",
"l_insert_or_tap_yk": "Insérez ou touchez votre YubiKey",
"l_unplug_yk": "Déconnectez votre YubiKey",
"l_reinsert_yk": "Réinsérez votre YubiKey",
"l_place_on_nfc_reader": "Placez votre YubiKey sur le lecteur NFC",
"l_replace_yk_on_reader": "Placez votre YubiKey à nouveau sur le lecteur",
"l_remove_yk_from_reader": "Retirez votre YubiKey du lecteur NFC",
"p_try_reinsert_yk": "Essayez de réinsérer votre YubiKey.",
"s_touch_required": "Touché requis",
"l_touch_button_now": "Touchez votre YubiKey maintenant",
"l_keep_touching_yk": "Continuez de toucher votre YubiKey\u2026",
"@_app_configuration": {},
"s_toggle_applications": "Changer les applications",
"l_min_one_interface": "Au moins une interface doit être activée",
"s_reconfiguring_yk": "Reconfiguration de la YubiKey\u2026",
"s_config_updated": "Configuration mise à jour",
"l_config_updated_reinsert": "Configuration mise à jour; retirez et réinsérez votre YubiKey",
"s_app_not_supported": "Application non supportée",
"l_app_not_supported_on_yk": "La YubiKey utilisée ne supporte pas l'application '{app}'",
"@l_app_not_supported_on_yk" : {
"placeholders": {
"app": {}
}
},
"l_app_not_supported_desc": "Cette application n'est pas supportée",
"s_app_disabled": "Application désactivée",
"l_app_disabled_desc": "Activez l'application '{app}' sur votre YubiKey",
"@l_app_disabled_desc" : {
"placeholders": {
"app": {}
}
},
"s_fido_disabled": "FIDO2 désactivé",
"l_webauthn_req_fido2": "WebAuthn demande que le FIDO2 soit activé sur votre YubiKey",
"@_connectivity_issues": {},
"l_helper_not_responding": "Le processus Helper ne réponds pas",
"l_yk_no_access": "Cette YubiKey Ne peut pas être accédée",
"s_yk_inaccessible": "Appareil inaccessible",
"l_open_connection_failed": "Échec d'ouverture de connection",
"l_ccid_connection_failed": "Échec d'ouverture de connection smart card",
"p_ccid_service_unavailable": "Assurez vous que votre service smart card fonctionne.",
"p_pcscd_unavailable": "Assurez vous que pcscd est installé et opérationnel.",
"l_no_yk_present": "Aucune YubiKey détectée",
"s_unknown_type": "Type inconnu",
"s_unknown_device": "Appareil non reconnu",
"s_unsupported_yk": "YubiKey non supportée",
"s_yk_not_recognized": "Appareil non reconnu",
"@_general_errors": {},
"l_error_occured": "Une erreur est survenue",
"s_application_error": "Erreur d'application",
"l_import_error": "Erreur d'importation",
"l_file_not_found": "Fichier non trouvé",
"l_file_too_big": "Fichier trop volumineux",
"l_filesystem_error": "Erreur d'accès au système de fichier",
"@_pins": {},
"s_pin": "PIN",
"s_puk": "PUK",
"s_set_pin": "Entrez un PIN",
"s_change_pin": "Changez PIN",
"s_change_puk": "Changez PUK",
"s_current_pin": "PIN actuel",
"s_current_puk": "PUK actuel",
"s_new_pin": "Nouveau PIN",
"s_new_puk": "Nouveau PUK",
"s_confirm_pin": "Confirmez le PIN",
"s_confirm_puk": "Confirmez le PUK",
"s_unblock_pin": "Débloquer le PIN",
"l_new_pin_len": "Le nouveau PIN doit avoir au moins {length} caractères",
"@l_new_pin_len" : {
"placeholders": {
"length": {}
}
},
"s_pin_set": "PIN défini",
"s_puk_set": "PUK défini",
"l_set_pin_failed": "Échec du changement de PIN: {message}",
"@l_set_pin_failed" : {
"placeholders": {
"message": {}
}
},
"l_attempts_remaining": "Nombre de tentative(s) restante(s) : {retries}",
"@l_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_pin_attempts_remaining": "Mauvais PIN, {retries} tentative(s) restante(s)",
"@l_wrong_pin_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_puk_attempts_remaining": "Mauvais PUK, {retries} tentative(s) restante(s)",
"@l_wrong_puk_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"s_fido_pin_protection": "PIN de protection FIDO",
"l_fido_pin_protection_optional": "PIN de protection optionnel FIDO",
"l_enter_fido2_pin": "Entrez le PIN FIDO2 de votre YubiKey",
"l_optionally_set_a_pin": "(Optionnel) Entrez un PIN pour protéger l'accès de votre YubiKey\nEnregistrer en tant que clé de sécurité sur les sites web",
"l_pin_blocked_reset": "PIN bloqué; Réinitialisez à l'état d'usine le FIDO",
"l_set_pin_first": "Un PIN est d'abord requis",
"l_unlock_pin_first": "Débloquez avec un PIN d'abord",
"l_pin_soft_locked": "Le PIN est bloqué tant que votre YubiKey ne sera pas réinsérée",
"p_enter_current_pin_or_reset": "Entrez votre PIN. Si vous ne savez pas votre PIN, vous devez déverrouiller votre YubiKey avec un code PUK ou la réinitialiser.",
"p_enter_current_puk_or_reset": "Entrez votre PUK. Si vous ne savez pas votre PUK, vous devez déverrouiller réinitialiser votre YubiKey.",
"p_enter_new_fido2_pin": "Entrez votre nouveau PIN. Un code PIN doit avoir au moins {length} caractères et peut contenir des lettres, nombre et caractères spéciaux.",
"@p_enter_new_fido2_pin" : {
"placeholders": {
"length": {}
}
},
"s_pin_required": "PIN requis",
"p_pin_required_desc": "L'action que vous allez faire demande d'entrer le code PIN du PIV.",
"l_piv_pin_blocked": "Vous êtes bloqué, utilisez le code PUK pour réinitialiser",
"l_piv_pin_puk_blocked": "Bloqué, réinitialisation nécesssaire",
"p_enter_new_piv_pin_puk": "Entrez un nouveau {name} entre 6 et 8 caractères.",
"@p_enter_new_piv_pin_puk" : {
"placeholders": {
"name": {}
}
},
"@_passwords": {},
"s_password": "Mot de passe",
"s_manage_password": "Gérer les mots de passes",
"s_set_password": "Définir le mot de passe",
"s_password_set": "Mot de passe défini",
"l_optional_password_protection": "Mot de passe optionnel de protection",
"s_new_password": "Nouveau mot de passe",
"s_current_password": "Mot de passe actuel",
"s_confirm_password": "Confirmez le mot de passe",
"s_wrong_password": "Mauvais mot de passe",
"s_remove_password": "Supprimer le mot de passe",
"s_password_removed": "Mot de passe supprimé",
"s_remember_password": "Retenir le mot de passe",
"s_clear_saved_password": "Effacer le mot de passe",
"s_password_forgotten": "Mot de passe oublié",
"l_keystore_unavailable": "OS Keystore indisponible",
"l_remember_pw_failed": "Échec de la mémorisation du mot de passe",
"l_unlock_first": "Déverrouillez initialement avec un mot de passe",
"l_enter_oath_pw": "Entrez le mot de passe OATH de votre YubiKey",
"p_enter_current_password_or_reset": "Entrez votre mot de passe actuel. Si vous ne vous souvenez plus de votre mot de passe, vous devrez réinitialiser votre YubiKey.",
"p_enter_new_password": "Entrez votre nouveau mot de passe. Un mot de passe peut contenir des lettres, nombres et des caractères spéciaux.",
"@_management_key": {},
"s_management_key": "Gestion des clés",
"s_current_management_key": "Clé actuelle de gestion",
"s_new_management_key": "Nouvelle clé de gestion",
"l_change_management_key": "Changer la clé de gestion",
"p_change_management_key_desc": "Changer votre clé de gestion. Vous pouvez optionnellement autoriser le PIN à être utilisé à la place de la clé de gestion.",
"l_management_key_changed": "Ché de gestion changée",
"l_default_key_used": "Clé de gestion par défaut utilisée",
"s_generate_random": "Génération aléatoire",
"s_use_default": "Utiliser la valeur par défaut",
"l_warning_default_key": "Attention: Clé par défaut utilisée",
"s_protect_key": "Protection par PIN",
"l_pin_protected_key": "Un PIN peut être utilisé à la place",
"l_wrong_key": "Mauvaise clé",
"l_unlock_piv_management": "Déverrouiller la gestion PIV",
"p_unlock_piv_management_desc": "L'action que vous allez réaliser demande la clé de gestion PIV. Donner cette clé déverrouillera cette fonctionnalité pour cette session.",
"@_oath_accounts": {},
"l_account": "Compte: {label}",
"@l_account" : {
"placeholders": {
"label": {}
}
},
"s_accounts": "Comptes",
"s_no_accounts": "Aucun compte",
"s_add_account": "Ajouter un compte",
"s_add_accounts" : "Ajouter un/des compte(s)",
"p_add_description" : "Pour scanner un code QR, assurez-vous que le code complet est visible à l'écran et appuyez sur le bouton ci-dessous. Vous pouvez également faire glisser une image enregistrée dans un dossier vers cette boîte de dialogue. Si vous disposez des informations d'identification du compte par écrit, utilisez plutôt la saisie manuelle.",
"s_add_manually" : "Ajouter manuellement",
"s_account_added": "Compte ajouté",
"l_account_add_failed": "Échec d'ajout d'un compte: {message}",
"@l_account_add_failed" : {
"placeholders": {
"message": {}
}
},
"l_account_name_required": "Votre compte doit avoir un nom",
"l_name_already_exists": "Ce nom existe déjà pour cet émetteur",
"l_account_already_exists": "Ce compte existe déjà sur la YubiKey",
"l_invalid_character_issuer": "Caractère invalide: ':' n'est pas autorisé pour un émetteur",
"l_select_accounts" : "Sélectionner le/les compte(s) à ajouter à la YubiKey",
"s_pinned": "Épinglé",
"s_pin_account": "Épingler un compte",
"s_unpin_account": "Désépingler un compte",
"s_no_pinned_accounts": "Aucun compte épinglé",
"l_pin_account_desc": "Gardez vos comptes importants ensembles",
"s_rename_account": "Renommer le compte",
"l_rename_account_desc": "Éditer l'émetteur / le nom du compte",
"s_account_renamed": "Compte renommé",
"p_rename_will_change_account_displayed": "Cette action changera la façon dont le compte est affiché dans la liste.",
"s_delete_account": "Supprimer le compte",
"l_delete_account_desc": "Supprimer le compte de votre YubiKey",
"s_account_deleted": "Compte supprimé",
"p_warning_delete_account": "Attention! Cette action supprimera le compte de votre YubiKey.",
"p_warning_disable_credential": "Vous ne pourrez plus générer de code OTPs pour ce compte. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.",
"s_account_name": "Nom du compte",
"s_search_accounts": "Rechercher des comptes",
"l_accounts_used": "{used} sur {capacity} comptes utilisés",
"@l_accounts_used" : {
"placeholders": {
"used": {},
"capacity": {}
}
},
"s_num_digits": "{num} chiffres",
"@s_num_digits" : {
"placeholders": {
"num": {}
}
},
"s_num_sec": "{num} secondes",
"@s_num_sec" : {
"placeholders": {
"num": {}
}
},
"s_issuer_optional": "Émetteur (optionnel)",
"s_counter_based": "Basé sur un décompte",
"s_time_based": "Basé sur le temps",
"l_copy_code_desc": "Coller facilement le code depuis une autre application",
"s_calculate_code": "Calculer un code",
"l_calculate_code_desc": "Récupérer un nouveau code depuis votre YubiKey",
"@_fido_credentials": {},
"l_passkey": "Passkey: {label}",
"@l_passkey" : {
"placeholders": {
"label": {}
}
},
"s_passkeys": "Passkeys",
"l_ready_to_use": "Prêt à l'emploi",
"l_register_sk_on_websites": "Enregistrer comme clé de sécurité sur les sites internet",
"l_no_discoverable_accounts": "Aucune Passkey détectée",
"s_delete_passkey": "Supprimer une Passkey",
"l_delete_passkey_desc": "Supprimer la Passkey de votre YubiKey",
"s_passkey_deleted": "Passkey supprimée",
"p_warning_delete_passkey": "Cette action supprimera cette Passkey de votre YubiKey.",
"@_fingerprints": {},
"l_fingerprint": "Empreinte: {label}",
"@l_fingerprint" : {
"placeholders": {
"label": {}
}
},
"s_fingerprints": "Empreintes",
"l_fingerprint_captured": "Empreinte capturée avec succès!",
"s_fingerprint_added": "Empreinte ajoutée",
"l_setting_name_failed": "Erreur lors de l'ajout du nom: {message}",
"@l_setting_name_failed" : {
"placeholders": {
"message": {}
}
},
"s_add_fingerprint": "Ajouter une empreinte",
"l_fp_step_1_capture": "Étape 1/2: Entrez votre empreinte",
"l_fp_step_2_name": "Étape 2/2: Nommez votre empreinte",
"s_delete_fingerprint": "Supprimer l'empreinte",
"l_delete_fingerprint_desc": "Supprimer l'empreinte de votre YubiKey",
"s_fingerprint_deleted": "Empreinte supprimée",
"p_warning_delete_fingerprint": "Cette action supprimera cette empreinte de votre YubiKey.",
"s_no_fingerprints": "Aucune empreinte",
"l_set_pin_fingerprints": "Ajoutez un PIN pour enregistrer des empreintes",
"l_no_fps_added": "Aucune empreinte n'a été ajoutée",
"s_rename_fp": "Renommer une empreinte",
"l_rename_fp_desc": "Changer la description",
"s_fingerprint_renamed": "Empreinte renommée",
"l_rename_fp_failed": "Erreur lors du renommage: {message}",
"@l_rename_fp_failed" : {
"placeholders": {
"message": {}
}
},
"l_add_one_or_more_fps": "Ajouter une ou plusieurs (jusqu'a 5) empreintes",
"l_fingerprints_used": "{used}/5 empreintes enregistrées",
"@l_fingerprints_used": {
"placeholders": {
"used": {}
}
},
"p_press_fingerprint_begin": "Posez votre doigt sur votre YubiKey pour commencer.",
"p_will_change_label_fp": "Cette action changera la description de votre empreinte.",
"@_certificates": {},
"s_certificate": "Certificat",
"s_certificates": "Certificats",
"s_csr": "CSR",
"s_subject": "Sujet",
"l_export_csr_file": "Sauvegarder le CSR vers un fichier",
"l_select_import_file": "Sélectionnez un fichier à importer",
"l_export_certificate": "Exporter le certificat",
"l_export_certificate_file": "Exporter le certificat vers un fichier",
"l_export_certificate_desc": "Exporter le certificat vers un fichier",
"l_certificate_exported": "Certificat exporté",
"l_import_file": "Importer un fichier",
"l_import_desc": "Importer une clé et/ou un certificat",
"l_importing_file": "Importation d'un fichier\u2026",
"s_file_imported": "Fichier importé",
"l_delete_certificate": "Supprimer un certificat",
"l_delete_certificate_desc": "Supprimer un certificat de votre YubiKey",
"s_issuer": "Émetteur",
"s_serial": "Série",
"s_certificate_fingerprint": "Empreinte digitale",
"s_valid_from": "Valide à partir de",
"s_valid_to": "Valide jusqu'à",
"l_no_certificate": "Aucun certificat chargé",
"l_key_no_certificate": "Clé sans certificat chargé",
"s_generate_key": "Générer une clé",
"l_generate_desc": "Générer un nouveau certificat ou CSR",
"p_generate_desc": "Cette action génèrera une nouvelle clé sur l'emplacement PIV {slot} de votre YubiKey. La clé publique sera incorporée dans un certificat auto-signé stocké sur votre YubiKey, ou dans un fichier CSR (Certificate Signing Request).",
"@p_generate_desc" : {
"placeholders": {
"slot": {}
}
},
"l_generating_private_key": "Génération d'une clé privée\u2026",
"s_private_key_generated": "Clé privée générée",
"p_warning_delete_certificate": "Attention! Cette action supprimera le certificat de votre YubiKey.",
"q_delete_certificate_confirm": "Supprimer le certficat du slot PIV {slot}?",
"@q_delete_certificate_confirm" : {
"placeholders": {
"slot": {}
}
},
"l_certificate_deleted": "Certificat supprimé",
"p_password_protected_file": "Le fichier sélectionné est protégé par un mot de passe. Enterez le mot de passe pour continuer.",
"p_import_items_desc": "Les éléments suivants seront importés dans le slot PIV {slot}.",
"@p_import_items_desc" : {
"placeholders": {
"slot": {}
}
},
"p_subject_desc": "Un nom distinctif (DN) formaté conformément à la spécification RFC 4514.",
"l_rfc4514_invalid": "Format RFC 4514 invalide",
"rfc4514_examples": "Exemples:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
"p_cert_options_desc": "Algorithme de clé à utiliser, format de sortie et date d'expiration (certificat uniquement).",
"s_overwrite_slot": "Écraser l'emplacement",
"p_overwrite_slot_desc": "Cette opération écrase de manière permanente le contenu existant dans le slot {slot}.",
"@p_overwrite_slot_desc" : {
"placeholders": {
"slot": {}
}
},
"l_overwrite_cert": "Le certificat sera écrasé",
"l_overwrite_key": "La clé privée sera écrasée",
"l_overwrite_key_maybe": "Toute clé privée existante dans le slot sera écrasée",
"@_piv_slots": {},
"s_slot_display_name": "{name} ({hexid})",
"@s_slot_display_name" : {
"placeholders": {
"name": {},
"hexid": {}
}
},
"s_slot_9a": "Authentification",
"s_slot_9c": "Signature digitale",
"s_slot_9d": "Gestion des clés",
"s_slot_9e": "Authentification par carte",
"@_permissions": {},
"s_enable_nfc": "Activer le NFC",
"s_permission_denied": "Permission refusée",
"l_elevating_permissions": "Élevation des permissions\u2026",
"s_review_permissions": "Révision des permissions",
"p_elevated_permissions_required": "Gérer cet appareil demande des privilèges plus élevés.",
"p_webauthn_elevated_permissions_required": "La gestion WebAuthn demande des privilèges plus élevés.",
"p_need_camera_permission": "Yubico Authenticator a besoin des permission d'utiliser la caméra pour scanner les QR code.",
"@_qr_codes": {},
"s_qr_scan": "Scanner un QR code",
"l_qr_scanned": "QR code scanné",
"l_invalid_qr": "QR code invalide",
"l_qr_not_found": "Aucun QR code trouvé",
"l_qr_not_read": "Erreur de lecture du QR code: {message}",
"@l_qr_not_read" : {
"placeholders": {
"message": {}
}
},
"l_point_camera_scan": "Dirigez votre caméra vers le QR code pour le scanner",
"q_want_to_scan": "Voulez vous scanner un code?",
"q_no_qr": "Pas de QR code?",
"s_enter_manually": "Entrer un code manuellement",
"@_factory_reset": {},
"s_reset": "Réinitialiser",
"s_factory_reset": "Réinitialisation",
"l_factory_reset_this_app": "Réinitialiser cette application",
"s_reset_oath": "Réinitialiser l'OATH",
"l_oath_application_reset": "L'application OATH à été réinitialisée",
"s_reset_fido": "Réinitialiser le FIDO",
"l_fido_app_reset": "L'application FIDO à été réinitialisée",
"l_press_reset_to_begin": "Appuyez sur réinitialiser pour commencer\u2026",
"l_reset_failed": "Erreur pendant la réinitialisation: {message}",
"@l_reset_failed" : {
"placeholders": {
"message": {}
}
},
"s_reset_piv": "Réinitialiser le PIV",
"l_piv_app_reset": "L'application PIV à été réinitialisée",
"p_warning_factory_reset": "Attention! Cette action supprimera de manière irrévocable tous les comptes OATH TOTP/HOTP de votre YubiKey.",
"p_warning_disable_credentials": "Vos identifiants OATH, ainsi que vos mots de passes, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.",
"p_warning_deletes_accounts": "Attention! Cette action supprimera de manière irrévocable tous les comptes U2F et FIDO2 de votre YubiKey.",
"p_warning_disable_accounts": "Vos identifiants, ainsi que les codes PIN associés, seront supprimés de votre YubiKey. Assurez vous de désactiver les identifiants des sites pour ne pas être verrouillé hors de vos comptes.",
"p_warning_piv_reset": "Attention! Cette action supprimera de manière irrévocable toutes les données PIV stockées sur votre YubiKey.",
"p_warning_piv_reset_desc": "Cela inclus les clé privées et les certificats. Votre PIN, PUK, clé de management seront réinitialisés à leur valeurs d'usine.",
"@_copy_to_clipboard": {},
"l_copy_to_clipboard": "Copier vers le presse papier",
"s_code_copied": "Code copié",
"l_code_copied_clipboard": "Code copé vers le presse papier",
"s_copy_log": "Copier les logs",
"l_log_copied": "Logs copiés vers le presse papier",
"l_diagnostics_copied": "Données de diagnostique copiés vers le presse papier",
"p_target_copied_clipboard": "{label} copié vers le presse papier.",
"@p_target_copied_clipboard" : {
"placeholders": {
"label": {}
}
},
"@_custom_icons": {},
"s_custom_icons": "Icônes personnalisées",
"l_set_icons_for_accounts": "Sélectionner les icônes pour les comptes",
"p_custom_icons_description": "Les packs d'icônes peuvent rendre vos comptes plus facilement repérables grâce à des logos et couleurs familières.",
"s_replace_icon_pack": "Remplacer le pack d'icônes",
"l_loading_icon_pack": "Chargement du pack d'icônes\u2026",
"s_load_icon_pack": "Charger le pack d'icônes",
"s_remove_icon_pack": "Supprimer le pack d'icônes",
"l_icon_pack_removed": "Pack d'icônes supprimé",
"l_remove_icon_pack_failed": "Erreur lors de la suppression du pack d'icônes",
"s_choose_icon_pack": "Choisissez votre pack d'icônes",
"l_icon_pack_imported": "Pack d'icônes importé",
"l_import_icon_pack_failed": "Erreur lors de l'importation du pack d'icônes: {message}",
"@l_import_icon_pack_failed": {
"placeholders": {
"message": {}
}
},
"l_invalid_icon_pack": "Pack d'icônes invalide",
"l_icon_pack_copy_failed": "Échec de la copie des fichiers du pack d'icônes",
"@_android_settings": {},
"s_nfc_options": "Options NFC",
"l_on_yk_nfc_tap": "Lorsque le NFC de la YubiKey est en contact",
"l_launch_ya": "Démarrer Yubico Authenticator",
"l_copy_otp_clipboard": "Copier le code OTP vers le presse papier",
"l_launch_and_copy_otp": "Démarrer l'application et copier le code OTP",
"l_kbd_layout_for_static": "Arrangement clavier (pour les mot de passes statiques)",
"s_choose_kbd_layout": "Choisissez l'arrangement clavier",
"l_bypass_touch_requirement": "Contourner la nécessité de toucher la YubiKey",
"l_bypass_touch_requirement_on": "Les comptes nécessitant un contact sont automatiquement affichés via NFC",
"l_bypass_touch_requirement_off": "Les comptes nécessitant un contact sur la YubiKey doivent faire l'objet d'un contact NFC supplémentaire",
"s_silence_nfc_sounds": "Couper le son NFC",
"l_silence_nfc_sounds_on": "Aucun son ne sera joué lors du contact NFC",
"l_silence_nfc_sounds_off": "Du son sera joué lors du contact NFC",
"s_usb_options": "Options USB",
"l_launch_app_on_usb": "Lancer lorsque la YubiKey est connectée",
"l_launch_app_on_usb_on": "Cela empêchera que d'autres applications utilisent la YubiKey via USB",
"l_launch_app_on_usb_off": "D'autres applications peuvent utiliser la YubiKey via USB",
"s_allow_screenshots": "Autoriser les captures d'écrans",
"s_nfc_dialog_tap_key": "Effleurez votre YubiKey",
"s_nfc_dialog_operation_success": "Succès",
"s_nfc_dialog_operation_failed": "Échec",
"s_nfc_dialog_oath_reset": "Action: réinitialiser l'applet OATH",
"s_nfc_dialog_oath_unlock": "Action: déverouiller l'applet OATH",
"s_nfc_dialog_oath_set_password": "Action: définir le mot de passe OATH",
"s_nfc_dialog_oath_unset_password": "Action: supprimer le mot de passe OATH",
"s_nfc_dialog_oath_add_account": "Action: ajouter un nouveau compte",
"s_nfc_dialog_oath_rename_account": "Action: renommer le compte",
"s_nfc_dialog_oath_delete_account": "Action: supprimer le compte",
"s_nfc_dialog_oath_calculate_code": "Action: calculer le code OATH",
"s_nfc_dialog_oath_failure": "Échec de l'opération OATH",
"s_nfc_dialog_oath_add_multiple_accounts": "Action: ajouter plusieurs comptes",
"@_eof": {}
}

615
lib/l10n/app_ja.arb Normal file
View File

@ -0,0 +1,615 @@
{
"@@locale": "ja",
"@_readme": {
"notes": [
"All strings start with a Captial letter.",
"Group strings by category, but don't needlessly tie them to a section of the app if they can be re-used between several.",
"Run check_strings.py on the .arb file to detect problems, tweak @_lint_rules as needed per language."
],
"prefixes": {
"s_": "A single, or few words. Should be short enough to display on a button, or a header.",
"l_": "A single line, can be wrapped. Should not be more than one sentence, and not end with a period.",
"p_": "One or more full sentences, with proper punctuation.",
"q_": "A question, ending in question mark."
}
},
"@_lint_rules": {
"s_max_words": 4,
"s_max_length": 32
},
"app_name": "Yubico Authenticator",
"s_save": "保存",
"s_cancel": "キャンセル",
"s_close": "閉じる",
"s_delete": "消去",
"s_quit": "終了",
"s_unlock": "ロック解除",
"s_calculate": "計算",
"s_import": "インポート",
"s_overwrite": "上書き",
"s_label": "ラベル",
"s_name": "名前",
"s_usb": "USB",
"s_nfc": "NFC",
"s_options": "オプション",
"s_show_window": "ウィンドウを表示",
"s_hide_window": "ウィンドウを表示しない",
"q_rename_target": "{label}の名前を変更しますか?",
"@q_rename_target" : {
"placeholders": {
"label": {}
}
},
"l_bullet": "• {item}",
"@l_bullet" : {
"placeholders": {
"item": {}
}
},
"s_about": "情報",
"s_algorithm": "アルゴリズム",
"s_appearance": "外観",
"s_authenticator": "Authenticator",
"s_actions": "アクション",
"s_manage": "管理",
"s_setup": "セットアップ",
"s_settings": "設定",
"s_piv": "PIV",
"s_webauthn": "WebAuthn",
"s_help_and_about": "ヘルプと概要",
"s_help_and_feedback": "ヘルプとフィードバック",
"s_send_feedback": "フィードバックの送信",
"s_i_need_help": "ヘルプが必要",
"s_troubleshooting": "トラブルシューティング",
"s_terms_of_use": "利用規約",
"s_privacy_policy": "プライバシーポリシー",
"s_open_src_licenses": "オープンソースライセンス",
"s_configure_yk": "YubiKeyを構成する",
"s_please_wait": "お待ちください\u2026",
"s_secret_key": "秘密鍵",
"s_private_key": "秘密鍵",
"s_invalid_length": "無効な長さです",
"s_require_touch": "タッチが必要",
"q_have_account_info": "アカウント情報をお持ちですか?",
"s_run_diagnostics": "診断を実行する",
"s_log_level": "ログレベル: {level}",
"@s_log_level": {
"placeholders": {
"level": {}
}
},
"s_character_count": "文字数",
"s_learn_more": "もっと詳しく知る",
"@_language": {},
"s_language": "言語",
"l_enable_community_translations": "コミュニティ翻訳を有効にする",
"p_community_translations_desc": "これらの翻訳はコミュニティによって提供および維持されます。 エラーが含まれているか不完全である可能性があります。",
"@_theme": {},
"s_app_theme": "アプリのテーマ",
"s_choose_app_theme": "アプリのテーマを選択",
"s_system_default": "システムのデフォルト",
"s_light_mode": "ライトモード",
"s_dark_mode": "ダークモード",
"@_yubikey_selection": {},
"s_yk_information": "YubiKey情報",
"s_select_yk": "YubiKeyを選択",
"s_select_to_scan": "選択してスキャン",
"s_hide_device": "デバイスを非表示",
"s_show_hidden_devices": "非表示のデバイスを表示",
"s_sn_serial": "S/N: {serial}",
"@s_sn_serial" : {
"placeholders": {
"serial": {}
}
},
"s_fw_version": "F/W: {version}",
"@s_fw_version" : {
"placeholders": {
"version": {}
}
},
"@_yubikey_interactions": {},
"l_insert_yk": "YubiKeyを挿入する",
"l_insert_or_tap_yk": "YubiKeyを挿入またはタップする",
"l_unplug_yk": "YubiKeyを取り外す",
"l_reinsert_yk": "YubiKeyを再挿入する",
"l_place_on_nfc_reader": "YubiKeyをNFCリーダーに置く",
"l_replace_yk_on_reader": "YubiKeyをリーダーに戻す",
"l_remove_yk_from_reader": "YubiKeyをNFCリーダーから取り外す",
"p_try_reinsert_yk": "YubiKeyを取り外して再挿入してみてください",
"s_touch_required": "タッチが必要です",
"l_touch_button_now": "今すぐYubiKeyのボタンをタッチしてください",
"l_keep_touching_yk": "YubiKeyを繰り返しタッチし続けてください\u2026",
"@_app_configuration": {},
"s_toggle_applications": "アプリケーションの切替え",
"l_min_one_interface": "少なくとも 1 つのインターフェイスを有効にする必要があります",
"s_reconfiguring_yk": "YubiKeyを再構成しています\u2026",
"s_config_updated": "構成が更新されました",
"l_config_updated_reinsert": "設定が更新されました。YubiKeyを取り外して再挿入してください",
"s_app_not_supported": "アプリケーションがサポートされていません",
"l_app_not_supported_on_yk": "使用されているYubiKeyは '{app}' アプリケーションをサポートしていません",
"@l_app_not_supported_on_yk" : {
"placeholders": {
"app": {}
}
},
"l_app_not_supported_desc": "このアプリケーションはサポートされていません",
"s_app_disabled": "アプリケーションが無効になっています",
"l_app_disabled_desc": "YubiKeyの「{app}」アプリケーションへのアクセスを許可",
"@l_app_disabled_desc" : {
"placeholders": {
"app": {}
}
},
"s_fido_disabled": "FIDO2が無効になっています",
"l_webauthn_req_fido2": "WebAuthnでは、YubiKeyでFIDO2アプリケーションを有効にする必要があります",
"@_connectivity_issues": {},
"l_helper_not_responding": "ヘルパープロセスが応答していません",
"l_yk_no_access": "このYubiKeyにはアクセスできません",
"s_yk_inaccessible": "デバイスにアクセスできません",
"l_open_connection_failed": "接続を開けませんでした",
"l_ccid_connection_failed": "スマートカード接続を開けませんでした",
"p_ccid_service_unavailable": "スマートカード サービスが機能していることを確認してください",
"p_pcscd_unavailable": "pcscdがインストールされ、実行されていることを確認してください",
"l_no_yk_present": "YubiKeyが存在しません",
"s_unknown_type": "不明なタイプ",
"s_unknown_device": "認識されないデバイス",
"s_unsupported_yk": "サポートされていないYubiKey",
"s_yk_not_recognized": "デバイスが認識されない",
"@_general_errors": {},
"l_error_occured": "エラーが発生しました",
"s_application_error": "アプリケーションエラー",
"l_import_error": "インポートエラー",
"l_file_not_found": "ファイルが見つかりません",
"l_file_too_big": "ファイルサイズが大きすぎます",
"l_filesystem_error": "ファイルシステム操作エラー",
"@_pins": {},
"s_pin": "PIN",
"s_puk": "PUK",
"s_set_pin": "PINを設定する",
"s_change_pin": "PINを変更する",
"s_change_puk": "PUKを変更する",
"s_current_pin": "現在のPIN",
"s_current_puk": "現在のPUK",
"s_new_pin": "新しいPIN",
"s_new_puk": "新しいPUK",
"s_confirm_pin": "PINの確認",
"s_confirm_puk": "PUKの確認",
"s_unblock_pin": "ブロックを解除",
"l_new_pin_len": "新しいPINは少なくとも{length}文字である必要があります",
"@l_new_pin_len" : {
"placeholders": {
"length": {}
}
},
"s_pin_set": "PINの設定",
"s_puk_set": "PUKの設定",
"l_set_pin_failed": "PIN設定に失敗しました:{message}",
"@l_set_pin_failed" : {
"placeholders": {
"message": {}
}
},
"l_attempts_remaining": "あと{retries}回試行できます",
"@l_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_pin_attempts_remaining": "PINが間違っています。あと{retries}回試行できます",
"@l_wrong_pin_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"l_wrong_puk_attempts_remaining": "PUKが間違っています。あと{retries}回試行できます",
"@l_wrong_puk_attempts_remaining" : {
"placeholders": {
"retries": {}
}
},
"s_fido_pin_protection": "FIDO PINによる保護",
"l_fido_pin_protection_optional": "任意FIDO PINによる保護",
"l_enter_fido2_pin": "YubiKeyのFIDO2 PINを入力してください",
"l_optionally_set_a_pin": "YubiKeyアクセスを保護するために、任意でPINの設定ができます\nWebサイトにはセキュリティ キーとして登録されます",
"l_pin_blocked_reset": "PINはブロックされています。FIDOアプリケーションを出荷時設定にリセットしてください",
"l_set_pin_first": "最初にPINが必要です",
"l_unlock_pin_first": "最初にPINでロックを解除してください",
"l_pin_soft_locked": "YubiKeyを取り外して再挿入するまで、PINはブロックされています",
"p_enter_current_pin_or_reset": "現在のPINを入力してください。PIN がわからない場合は、PUK でブロック解除するか、YubiKey をリセットする必要があります",
"p_enter_current_puk_or_reset": "現在のPUKを入力してください。 PUK がわからない場合は、YubiKeyをリセットする必要があります",
"p_enter_new_fido2_pin": "新しいPINを入力してください。 PINは少なくとも{length}文字の長さである必要があり、文字、数字、特殊文字を含めることができます",
"@p_enter_new_fido2_pin" : {
"placeholders": {
"length": {}
}
},
"s_pin_required": "PINが必要",
"p_pin_required_desc": "実行しようとしている操作には、PIV PINの入力が必要です",
"l_piv_pin_blocked": "ブロックされています。PUK を使用してリセットしてください",
"l_piv_pin_puk_blocked": "ブロックされています。工場出荷リセットしてください",
"p_enter_new_piv_pin_puk": "新{name}を入力してください。6 8 文字でなければな りま せん。",
"@p_enter_new_piv_pin_puk" : {
"placeholders": {
"name": {}
}
},
"@_passwords": {},
"s_password": "パスワード",
"s_manage_password": "パスワードの管理",
"s_set_password": "パスワードを設定",
"s_password_set": "パスワード設定",
"l_optional_password_protection": "任意パスワードによる保護",
"s_new_password": "新しいパスワード",
"s_current_password": "現在のパスワード",
"s_confirm_password": "パスワードを確認",
"s_wrong_password": "間違ったパスワード",
"s_remove_password": "パスワードの削除",
"s_password_removed": "パスワードが削除されました",
"s_remember_password": "パスワードを覚える",
"s_clear_saved_password": "保存されたパスワードを削除する",
"s_password_forgotten": "パスワードを忘れた場合",
"l_keystore_unavailable": "OSキーストアは使用できません",
"l_remember_pw_failed": "パスワードを忘れました",
"l_unlock_first": "最初にパスワードでロックを解除します",
"l_enter_oath_pw": "YubiKeyのOATHパスワードを入力してください",
"p_enter_current_password_or_reset": "現在のパスワードを入力してください。パスワードがわからない場合は、YubiKeyをリセットする必要があります",
"p_enter_new_password": "新しいパスワードを入力してください。 パスワードには文字、数字、特殊文字を含めることができます",
"@_management_key": {},
"s_management_key": "Management key",
"s_current_management_key": "現在のManagement key",
"s_new_management_key": "新しいManagement key",
"l_change_management_key": "Management keyの変更",
"p_change_management_key_desc": "Management keyを変更してください。Management keyの代わりにPINを使用することも可能です",
"l_management_key_changed": "Management keyは変更されました",
"l_default_key_used": "デフォルトManagement keyが使用されています",
"s_generate_random": "ランダムに生成する",
"s_use_default": "デフォルトの使用",
"l_warning_default_key": "警告: デフォルトのキーが使用されています",
"s_protect_key": "PINで保護する",
"l_pin_protected_key": "代わりにPINを使用できます",
"l_wrong_key": "間違ったキー",
"l_unlock_piv_management": "PIV管理のロックの解除",
"p_unlock_piv_management_desc": "実行しようとしている操作にはPIVのManagement keyが必要です。このセッションの管理機能のロックを解除するために、キーを入力してください",
"@_oath_accounts": {},
"l_account": "アカウント:{label}",
"@l_account" : {
"placeholders": {
"label": {}
}
},
"s_accounts": "アカウント",
"s_no_accounts": "アカウントがありません",
"s_add_account": "アカウントの追加",
"s_add_accounts" : "アカウントの追加",
"p_add_description" : "QR コードをスキャンするには、コード全体が画面に表示されていることを確認し、下のボタンを押してください。保存した画像をこのダイアログにドラッグすることもできます。アカウントクレデンシャル情報を書面で持っている場合は、代わりに手動で入力をしてください。",
"s_add_manually" : "手動で追加",
"s_account_added": "アカウントが追加されました",
"l_account_add_failed": "アカウントの追加に失敗しました:{message}",
"@l_account_add_failed" : {
"placeholders": {
"message": {}
}
},
"l_account_name_required": "アカウントには名前が必要です",
"l_name_already_exists": "発行者名は既に使われています",
"l_account_already_exists": "このアカウントはすでに YubiKey に存在します",
"l_invalid_character_issuer": "無効な文字: ':' は発行者名で使用できません",
"l_select_accounts" : "YubiKey に追加するアカウントを選択してください",
"s_pinned": "固定",
"s_pin_account": "アカウントを固定する",
"s_unpin_account": "アカウントの固定を解除する",
"s_no_pinned_accounts": "固定されたアカウントはありません",
"l_pin_account_desc": "重要なアカウントは一緒にまとめてください",
"s_rename_account": "アカウント名の変更",
"l_rename_account_desc": "アカウントの発行者/名前の編集",
"s_account_renamed": "アカウント名が変更されました",
"p_rename_will_change_account_displayed": "これにより、リスト内でのアカウントの表示方法が変わります。",
"s_delete_account": "アカウントを削除する",
"l_delete_account_desc": "YubiKeyからアカウントの削除",
"s_account_deleted": "アカウントが削除されました",
"p_warning_delete_account": "警告この操作によってYubiKeyからアカウントが削除されます",
"p_warning_disable_credential": "このアカウントのOTPを生成できなくなります。 アカウントからロックアウトされないように、必ず最初にWebサイトからこのクレデンシャルを無効化してください",
"s_account_name": "アカウント名",
"s_search_accounts": "アカウントを検索",
"l_accounts_used": "{capacity}個のアカウントのうち{used}個が使用されています",
"@l_accounts_used" : {
"placeholders": {
"used": {},
"capacity": {}
}
},
"s_num_digits": "{num}桁",
"@s_num_digits" : {
"placeholders": {
"num": {}
}
},
"s_num_sec": "{num}秒",
"@s_num_sec" : {
"placeholders": {
"num": {}
}
},
"s_issuer_optional": "発行者(任意)",
"s_counter_based": "カウンターベース",
"s_time_based": "時間ベース",
"l_copy_code_desc": "コードを別のアプリに貼り付ける",
"s_calculate_code": " コードの計算",
"l_calculate_code_desc": "YubiKey から新しいコードの取得",
"@_fido_credentials": {},
"l_passkey": "パスキー: {label}",
"@l_passkey" : {
"placeholders": {
"label": {}
}
},
"s_passkeys": "パスキー",
"l_ready_to_use": "すぐに使用可能",
"l_register_sk_on_websites": "Webサイトにセキュリティキーとして登録する",
"l_no_discoverable_accounts": "パスキーは保存されていません",
"s_delete_passkey": "パスキーを削除",
"l_delete_passkey_desc": "YubiKeyからパスキーの削除",
"s_passkey_deleted": "パスキーが削除されました",
"p_warning_delete_passkey": "これにより、YubiKeyからパスキーが削除されます",
"@_fingerprints": {},
"l_fingerprint": "指紋:{label}",
"@l_fingerprint" : {
"placeholders": {
"label": {}
}
},
"s_fingerprints": "指紋",
"l_fingerprint_captured": "指紋の取得に成功しました!",
"s_fingerprint_added": "指紋が追加されました",
"l_setting_name_failed": "名前設定時名エラー:{message}",
"@l_setting_name_failed" : {
"placeholders": {
"message": {}
}
},
"s_add_fingerprint": "指紋を追加",
"l_fp_step_1_capture": "ステップ 1/2:指紋を取得する",
"l_fp_step_2_name": "ステップ 2/2:指紋の名前を付ける",
"s_delete_fingerprint": "指紋を削除",
"l_delete_fingerprint_desc": "YubiKeyから指紋の削除",
"s_fingerprint_deleted": "指紋が削除されました",
"p_warning_delete_fingerprint": "これによりYubiKeyから指紋が削除されます",
"s_no_fingerprints": "指紋は登録されていません",
"l_set_pin_fingerprints": "指紋登録のためにPINを設定してください",
"l_no_fps_added": "指紋は追加されていません",
"s_rename_fp": "指紋の名前を変更",
"l_rename_fp_desc": "ラベルの変更",
"s_fingerprint_renamed": "指紋の名前が変更されました",
"l_rename_fp_failed": "大友:名前変更エラー:{message}",
"@l_rename_fp_failed" : {
"placeholders": {
"message": {}
}
},
"l_add_one_or_more_fps": "1 つ以上 (最大 5 つ) の指紋を追加します",
"l_fingerprints_used": "{used}/5つの指紋が登録されました",
"@l_fingerprints_used": {
"placeholders": {
"used": {}
}
},
"p_press_fingerprint_begin": "YubiKeyに指を押し当てて開始します",
"p_will_change_label_fp": "これにより指紋のラベルが変更されます",
"@_certificates": {},
"s_certificate": "証明書",
"s_certificates": "証明書",
"s_csr": "CSR",
"s_subject": "サブジェクト",
"l_export_csr_file": "CSRをファイルに保存",
"l_select_import_file": "インポートするファイルの選択",
"l_export_certificate": "証明書をエクスポートする",
"l_export_certificate_file": "証明書をファイルにエクスポートする",
"l_export_certificate_desc": "証明書をファイルにエクスポートする",
"l_certificate_exported": "証明書がエクスポートされました",
"l_import_file": "ファイルのインポート",
"l_import_desc": "キーや証明書のインポート",
"l_importing_file": "ファイルのインポート中\u2026",
"s_file_imported": "ファイル をインポートしました",
"l_delete_certificate": "証明書を削除",
"l_delete_certificate_desc": "YubiKeyか証明書の削除",
"s_issuer": "発行者",
"s_serial": "シリアル番号",
"s_certificate_fingerprint": "指紋",
"s_valid_from": "有効期限の開始",
"s_valid_to": "有効期限の終了",
"l_no_certificate": "証明書はロードされていません",
"l_key_no_certificate": "証明書がロードされていない鍵",
"s_generate_key": "鍵の生成",
"l_generate_desc": "新しい証明書またはCSRの生成",
"p_generate_desc": "これにより、YubiKeyのPIVスロット{slot}に新しい鍵が生成されます。公開鍵は、YubiKeyに保存されている自己署名証明書、またはファイルに保存されている証明書署名要求(CSR)に埋め込まれます",
"@p_generate_desc" : {
"placeholders": {
"slot": {}
}
},
"l_generating_private_key": "秘密鍵を生成しています\u2026",
"s_private_key_generated": "秘密鍵を生成しました",
"p_warning_delete_certificate": "警告この操作によってYubiKeyから証明書が削除されます",
"q_delete_certificate_confirm": "PIVスロット{slot}の証明書を削除しますか?",
"@q_delete_certificate_confirm" : {
"placeholders": {
"slot": {}
}
},
"l_certificate_deleted": "証明書が削除されました",
"p_password_protected_file": "選択したファイルはパスワードで保護されています。パスワードを入力して続行します",
"p_import_items_desc": "次のアイテムはPIVスロット{slot}にインポートされます",
"@p_import_items_desc" : {
"placeholders": {
"slot": {}
}
},
"p_subject_desc": "RFC 4514フォーマットの識別名識別名 (DN)",
"l_rfc4514_invalid": "無効な RFC 4514 形式です",
"rfc4514_examples": "例:\nCN=Example Name\nCN=jsmith,DC=example,DC=net",
"p_cert_options_desc": "使用する鍵アルゴリズム、出力形式、および有効期限 (該当する場合)",
"s_overwrite_slot": "スロットの上書き",
"p_overwrite_slot_desc": "これにより、スロット{slot}内の既存のデータが永久に上書きされます",
"@p_overwrite_slot_desc" : {
"placeholders": {
"slot": {}
}
},
"l_overwrite_cert": "証明書は上書きされます",
"l_overwrite_key": "秘密鍵は上書きされます",
"l_overwrite_key_maybe": "スロット内の既存の秘密鍵は上書きされます",
"@_piv_slots": {},
"s_slot_display_name": "{name} ({hexid})",
"@s_slot_display_name" : {
"placeholders": {
"name": {},
"hexid": {}
}
},
"s_slot_9a": "認証",
"s_slot_9c": "デジタル署名",
"s_slot_9d": "鍵の管理",
"s_slot_9e": "カード認証",
"@_permissions": {},
"s_enable_nfc": "NFCを有効にする",
"s_permission_denied": "権限がありません",
"l_elevating_permissions": "権限の昇格\u2026",
"s_review_permissions": "権限の確認",
"p_elevated_permissions_required": "このデバイスを管理するには権限の昇格が必要です",
"p_webauthn_elevated_permissions_required": "WebAuthn管理には権限の昇格が必要です",
"p_need_camera_permission": "Yubico AuthenticatorにはQRコードをスキャンするためのカメラ権限が必要です",
"@_qr_codes": {},
"s_qr_scan": "QRコードをスキャン",
"l_qr_scanned": "スキャンしたQRコード",
"l_invalid_qr": "無効なQRコード",
"l_qr_not_found": "QRコードが見つかりませんでした",
"l_qr_not_read": "QRコードの読み取りに失敗しました:{message}",
"@l_qr_not_read" : {
"placeholders": {
"message": {}
}
},
"l_point_camera_scan": "カメラをQRコードに向けてスキャンする",
"q_want_to_scan": "スキャンしますか?",
"q_no_qr": "QRコードはありませんか",
"s_enter_manually": "手動で入力",
"@_factory_reset": {},
"s_reset": "リセット",
"s_factory_reset": "工場出荷リセット",
"l_factory_reset_this_app": "このアプリケーションを出荷時設定にリセット",
"s_reset_oath": "OATHのリセット",
"l_oath_application_reset": "OATHアプリケーションのリセット",
"s_reset_fido": "FIDOのリセット",
"l_fido_app_reset": "FIDOアプリケーションのリセット",
"l_press_reset_to_begin": "リセットを押して開始してください\u2026",
"l_reset_failed": "リセット実行中のエラー:{message}",
"@l_reset_failed" : {
"placeholders": {
"message": {}
}
},
"s_reset_piv": "PIVのリセット",
"l_piv_app_reset": "PIVアプリケーションのリセット",
"p_warning_factory_reset": "警告これによりすべてのOATH TOTP/HOTPアカウントがYubiKeyから削除されて、復旧不可能となります",
"p_warning_disable_credentials": "あなたのOATHクレデンシャル情報とパスワードは、このYubiKeyから削除されます。アカウントからロックアウトされないように、まずそれぞれのWebサイトからこれらを無効化してください",
"p_warning_deletes_accounts": ":警告これによりYubiKeyからすべてのU2FおよびFIDO2アカウントが削除されて、復旧不可能となります",
"p_warning_disable_accounts": "あなたのクレデンシャル情報とすべてのPINは、このYubiKeyから削除されます。 アカウントからロックアウトされないように、まずそれぞれのWebサイトでこれらを無効化してください",
"p_warning_piv_reset": "警告PIVデータは、YubiKeyから削除されて、復旧不可能となります",
"p_warning_piv_reset_desc": "これには秘密鍵と証明書が含まれます。 PIN、PUK、およびManagement keyは工場出荷時のデフォルト値にリセットされます",
"@_copy_to_clipboard": {},
"l_copy_to_clipboard": "クリップボードにコピー",
"s_code_copied": "コードをコピーしました",
"l_code_copied_clipboard": "コードをクリップボードにコピーしました",
"s_copy_log": "ログのコピー",
"l_log_copied": "ログをクリップボードにコピーしました",
"l_diagnostics_copied": "診断データをクリップボードにコピーしました",
"p_target_copied_clipboard": "{label}をクリップボードにコピーしました",
"@p_target_copied_clipboard" : {
"placeholders": {
"label": {}
}
},
"@_custom_icons": {},
"s_custom_icons": "カスタムアイコン",
"l_set_icons_for_accounts": "アカウントのアイコンの設定",
"p_custom_icons_description": "アイコンパックを使用すると、見慣れたロゴと色でアカウントをより簡単に区別できるようになります",
"s_replace_icon_pack": "アイコンパックを置き換える",
"l_loading_icon_pack": "アイコンパックをロード中\u2026",
"s_load_icon_pack": "アイコンパックのロード",
"s_remove_icon_pack": "アイコンパックの削除",
"l_icon_pack_removed": "アイコンパックが削除されました",
"l_remove_icon_pack_failed": "アイコンパックの削除中にエラーが発生しました",
"s_choose_icon_pack": "アイコンパックを選択",
"l_icon_pack_imported": "アイコンパックがインポートされました",
"l_import_icon_pack_failed": "アイコンパックのインポート中にエラーが発生しました:{message}",
"@l_import_icon_pack_failed": {
"placeholders": {
"message": {}
}
},
"l_invalid_icon_pack": "無効なアイコンパック",
"l_icon_pack_copy_failed": "アイコンパックのコピーに失敗しました",
"@_android_settings": {},
"s_nfc_options": "NFCオプション",
"l_on_yk_nfc_tap": "YubiKey NFCタップ時",
"l_launch_ya": "Yubico Authenticatorを起動",
"l_copy_otp_clipboard": "OTPをクリップボードにコピー",
"l_launch_and_copy_otp": "アプリを起動してOTPをコピー",
"l_kbd_layout_for_static": "キーボードレイアウト (静的パスワード用)",
"s_choose_kbd_layout": "キーボードレイアウトの選択",
"l_bypass_touch_requirement": "タッチ要件をバイパス",
"l_bypass_touch_requirement_on": "タッチが必要なアカウントはNFC経由で自動的に表示されます",
"l_bypass_touch_requirement_off": "タッチが必要なアカウントはNFC経由でさらにタップする必要があります",
"s_silence_nfc_sounds": "NFC音をミュートする",
"l_silence_nfc_sounds_on": "NFCタップでは音は鳴りません",
"l_silence_nfc_sounds_off": "NFCタップで音が再生されます",
"s_usb_options": "USBオプション",
"l_launch_app_on_usb": "YubiKeyが接続されているときに起動",
"l_launch_app_on_usb_on": "これにより他のアプリがUSB経由でYubiKeyを使用できなくなります",
"l_launch_app_on_usb_off": "他のアプリはUSB経由でYubiKeyを使用できます",
"s_allow_screenshots": "スクリーンショットを許可する",
"s_nfc_dialog_tap_key": "キーをタップする",
"s_nfc_dialog_operation_success": "成功",
"s_nfc_dialog_operation_failed": "失敗",
"s_nfc_dialog_oath_reset": "操作OATHアプレットのリセット",
"s_nfc_dialog_oath_unlock": "操作OATHアプレットのロック解除",
"s_nfc_dialog_oath_set_password": "操作OATHパスワードの設定",
"s_nfc_dialog_oath_unset_password": "操作OATHパスワードの削除",
"s_nfc_dialog_oath_add_account": "操作:新アカウントの追加",
"s_nfc_dialog_oath_rename_account": "操作:アカウント名の変更",
"s_nfc_dialog_oath_delete_account": "操作:アカウントの削除",
"s_nfc_dialog_oath_calculate_code": "操作OATHコードの計算",
"s_nfc_dialog_oath_failure": "OATH操作は失敗しました",
"s_nfc_dialog_oath_add_multiple_accounts": "操作:複数アカウントの追加",
"@_eof": {}
}

View File

@ -1,445 +1,547 @@
{
"@@locale": "pl",
"@_readme": {
"notes": [
"Wszystkie ciągi zaczynają się od wielkiej litery.",
"Pogrupuj ciągi znaków według kategorii, ale nie łącz ich nadmiernie z daną sekcją aplikacji, jeżeli mogą być ponownie wykorzystane w kilku różnych sekcjach.",
"Uruchom check_strings.py na pliku .arb, aby wykryć problemy, dostosuj @_lint_rules zgodnie z potrzebami dla danego języka."
],
"prefixes": {
"s_": "Jedno lub kilka słów. Powinny być na tyle krótkie, aby można je było wyświetlić na przycisku lub w nagłówku.",
"l_": "Pojedyncza linia może być zawinięta. Nie powinna być dłuższa niż jedno zdanie i nie powinna kończyć się kropką.",
"p_": "Jedno lub więcej pełnych zdań z odpowiednią interpunkcją.",
"q_": "Pytania kończą się znakiem zapytania."
}
},
"@_lint_rules": {
"s_max_words": 4,
"s_max_length": 32
},
"app_name": "Yubico Authenticator",
"s_save": "Zapisz",
"s_cancel": "Anuluj",
"s_close": "Zamknij",
"s_delete": "Usuń",
"s_quit": "Wyjdź",
"s_unlock": "Odblokuj",
"s_calculate": "Oblicz",
"s_label": "Etykieta",
"s_name": "Nazwa",
"s_usb": "USB",
"s_nfc": "NFC",
"s_show_window": "Pokaż okno",
"s_hide_window": "Ukryj okno",
"q_rename_target": "Zmienić nazwę {label}?",
"@q_rename_target" : {
"@l_account": {
"placeholders": {
"label": {}
}
},
"s_about": "O aplikacji",
"s_appearance": "Wygląd",
"s_authenticator": "Authenticator",
"s_manage": "Zarządzaj",
"s_setup": "Konfiguruj",
"s_settings": "Ustawienia",
"s_webauthn": "WebAuthn",
"s_help_and_about": "Pomoc i informacje",
"s_help_and_feedback": "Pomoc i opinie",
"s_send_feedback": "Prześlij opinię",
"s_i_need_help": "Pomoc",
"s_troubleshooting": "Rozwiązywanie problemów",
"s_terms_of_use": "Warunki użytkowania",
"s_privacy_policy": "Polityka prywatności",
"s_open_src_licenses": "Licencje open source",
"s_configure_yk": "Skonfiguruj YubiKey",
"s_please_wait": "Proszę czekać\u2026",
"s_secret_key": "Tajny klucz",
"s_invalid_length": "Nieprawidłowa długość",
"s_require_touch": "Wymagaj dotknięcia",
"q_have_account_info": "Masz dane konta?",
"s_run_diagnostics": "Uruchom diagnostykę",
"s_log_level": "Poziom logowania: {level}",
"@s_log_level": {
"placeholders": {
"level": {}
}
},
"s_character_count": "Liczba znaków",
"s_learn_more": "Dowiedz się więcej",
"@_language": {},
"s_language": "Język",
"l_enable_community_translations": "Włącz tłumaczenia społecznościowe",
"p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.",
"@_theme": {},
"s_app_theme": "Motyw aplikacji",
"s_choose_app_theme": "Wybierz motyw aplikacji",
"s_system_default": "Zgodny z systemem",
"s_light_mode": "Jasny",
"s_dark_mode": "Ciemny",
"@_yubikey_selection": {},
"s_yk_information": "Informacja o YubiKey",
"s_select_yk": "Wybierz YubiKey",
"s_select_to_scan": "Wybierz, aby skanować",
"s_hide_device": "Ukryj urządzenie",
"s_show_hidden_devices": "Pokaż ukryte urządzenia",
"s_sn_serial": "S/N: {serial}",
"@s_sn_serial" : {
"placeholders": {
"serial": {}
}
},
"s_fw_version": "F/W: {version}",
"@s_fw_version" : {
"placeholders": {
"version": {}
}
},
"@_yubikey_interactions": {},
"l_insert_yk": "Podłącz klucz YubiKey",
"l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey",
"l_unplug_yk": "Odłącz klucz YubiKey",
"l_reinsert_yk": "Ponownie podłącz YubiKey",
"l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC",
"l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku",
"l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC",
"p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.",
"s_touch_required": "Wymagane dotknięcie",
"l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey",
"l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey\u2026",
"@_app_configuration": {},
"s_toggle_applications": "Przełączanie funkcji",
"l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony",
"s_reconfiguring_yk": "Rekonfigurowanie YubiKey\u2026",
"s_config_updated": "Zaktualizowano konfigurację",
"l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey",
"s_app_not_supported": "Funkcja nie jest obsługiwana",
"l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji „{app}”",
"@l_app_not_supported_on_yk" : {
"placeholders": {
"app": {}
}
},
"l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana",
"s_app_disabled": "Wyłączona funkcja",
"l_app_disabled_desc": "Włącz funkcję „{app}” w kluczu YubiKey, aby uzyskać dostęp",
"@l_app_disabled_desc" : {
"placeholders": {
"app": {}
}
},
"s_fido_disabled": "FIDO2 wyłączone",
"l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey",
"@_connectivity_issues": {},
"l_helper_not_responding": "Proces pomocnika nie odpowiada",
"l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy",
"s_yk_inaccessible": "Urządzenie niedostępne",
"l_open_connection_failed": "Nie udało się nawiązać połączenia",
"l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną",
"p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.",
"p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.",
"l_no_yk_present": "Klucz YubiKey nie jest obecny",
"s_unknown_type": "Nieznany typ",
"s_unknown_device": "Nierozpoznane urządzenie",
"s_unsupported_yk": "Nieobsługiwany klucz YubiKey",
"s_yk_not_recognized": "Urządzenie nie rozpoznane",
"@_general_errors": {},
"l_error_occured": "Wystąpił błąd",
"s_application_error": "Błąd funkcji",
"l_import_error": "Błąd importowania",
"l_file_not_found": "Nie odnaleziono pliku",
"l_file_too_big": "Zbyt duży rozmiar pliku",
"l_filesystem_error": "Błąd operacji systemu plików",
"@_pins": {},
"s_pin": "PIN",
"s_set_pin": "Ustaw PIN",
"s_change_pin": "Zmień PIN",
"s_current_pin": "Aktualny PIN",
"s_new_pin": "Nowy PIN",
"s_confirm_pin": "Potwierdź PIN",
"l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków",
"@l_new_pin_len" : {
"placeholders": {
"length": {}
}
},
"s_pin_set": "PIN ustawiony",
"l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}",
"@l_set_pin_failed" : {
"l_account": "Konto: {label}",
"@l_account_add_failed": {
"placeholders": {
"message": {}
}
},
"l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało {retries} prób",
"@l_wrong_pin_attempts_remaining" : {
"l_account_add_failed": "Nie udało się dodać konta: {message}",
"l_account_already_exists": "To konto już istnieje w YubiKey",
"l_account_name_required": "Twoje konto musi mieć nazwę",
"@l_accounts_used": {
"placeholders": {
"capacity": {},
"used": {}
}
},
"l_accounts_used": "Użyto {used} z {capacity} kont",
"l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców",
"@l_app_disabled_desc": {
"placeholders": {
"app": {}
}
},
"l_app_disabled_desc": "Włącz funkcję '{app}' w kluczu YubiKey, aby uzyskać dostęp",
"l_app_not_supported_desc": "Ta funkcja nie jest obsługiwana",
"@l_app_not_supported_on_yk": {
"placeholders": {
"app": {}
}
},
"l_app_not_supported_on_yk": "Używany klucz YubiKey nie obsługuje funkcji '{app}'",
"@l_attempts_remaining": {
"placeholders": {
"retries": {}
}
},
"s_fido_pin_protection": "Ochrona FIDO kodem PIN",
"l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN",
"l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 dla klucza YubiKey",
"l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych",
"l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO",
"l_set_pin_first": "Najpierw wymagany jest kod PIN",
"l_unlock_pin_first": "Najpierw odblokuj kodem PIN",
"l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey",
"p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli nie znasz kodu PIN, musisz zresetować klucz YubiKey.",
"p_enter_new_fido2_pin": "Wprowadź nowy PIN. Kod PIN musi mieć co najmniej {length} znaków i może zawierać litery, cyfry i znaki specjalne.",
"@p_enter_new_fido2_pin" : {
"l_attempts_remaining": "Pozostało prób: {retries}",
"@l_bullet": {
"placeholders": {
"length": {}
"item": {}
}
},
"@_passwords": {},
"s_password": "Hasło",
"s_manage_password": "Zarządzaj hasłem",
"s_set_password": "Ustaw hasło",
"s_password_set": "Hasło zostało ustawione",
"l_optional_password_protection": "Opcjonalna ochrona hasłem",
"s_new_password": "Nowe hasło",
"s_current_password": "Aktualne hasło",
"s_confirm_password": "Potwierdź hasło",
"s_wrong_password": "Błędne hasło",
"s_remove_password": "Usuń hasło",
"s_password_removed": "Hasło zostało usunięte",
"s_remember_password": "Zapamiętaj hasło",
"s_clear_saved_password": "Usuń zapisane hasło",
"s_password_forgotten": "Hasło zostało zapomniane",
"l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny",
"l_remember_pw_failed": "Nie udało się zapamiętać hasła",
"l_unlock_first": "Najpierw odblokuj hasłem",
"l_bullet": "• {item}",
"l_bypass_touch_requirement": "Obejdź wymóg dotknięcia",
"l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC",
"l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC",
"l_calculate_code_desc": "Uzyskaj nowy kod z klucza YubiKey",
"l_ccid_connection_failed": "Nie udało się nawiązać połączenia z kartą inteligentną",
"l_certificate_deleted": "Certyfikat został usunięty",
"l_certificate_exported": "Wyeksportowano certyfikat",
"l_change_management_key": "Zmień klucz zarządzania",
"l_code_copied_clipboard": "Kod skopiowany do schowka",
"l_config_updated_reinsert": "Zaktualizowano konfigurację, podłącz ponownie klucz YubiKey",
"l_copy_code_desc": "Łatwe wklejanie kodu do innych aplikacji",
"l_copy_otp_clipboard": "Skopiuj OTP do schowka",
"l_copy_to_clipboard": "Skopiuj do schowka",
"l_default_key_used": "Używany jest domyślny klucz zarządzania",
"l_delete_account_desc": "Usuń konto z klucza YubiKey",
"l_delete_certificate": "Usuń certyfikat",
"l_delete_certificate_desc": "Usuń certyfikat z klucza YubiKey",
"l_delete_fingerprint_desc": "Usuń odcisk palca z klucza YubiKey",
"l_delete_passkey_desc": "Usuń klucz dostępu z klucza YubiKey",
"l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka",
"l_elevating_permissions": "Podnoszenie uprawnień…",
"l_enable_community_translations": "Tłumaczenia społecznościowe",
"l_enter_fido2_pin": "Wprowadź kod PIN FIDO2 klucza YubiKey",
"l_enter_oath_pw": "Wprowadź hasło OATH dla klucza YubiKey",
"p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli nie znasz hasła, musisz zresetować klucz YubiKey.",
"p_enter_new_password": "Wprowadź nowe hasło. Hasło może zawierać litery, cyfry i znaki specjalne.",
"@_oath_accounts": {},
"l_account": "Konto: {label}",
"@l_account" : {
"l_error_occured": "Wystąpił błąd",
"l_export_certificate": "Eksportuj certyfikat",
"l_export_certificate_desc": "Pozwala wyeksportować certyfikat do pliku",
"l_export_certificate_file": "Eksportuj certyfikat do pliku",
"l_export_csr_file": "Zapisz CSR do pliku",
"l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji",
"l_fido_app_reset": "Reset funkcji FIDO",
"l_fido_pin_protection_optional": "Opcjonalna ochrona FIDO kodem PIN",
"l_file_not_found": "Nie odnaleziono pliku",
"l_file_too_big": "Zbyt duży rozmiar pliku",
"l_filesystem_error": "Błąd operacji systemu plików",
"@l_fingerprint": {
"placeholders": {
"label": {}
}
},
"s_accounts": "Konta",
"s_no_accounts": "Brak kont",
"s_add_account": "Dodaj konto",
"s_account_added": "Konto zostało dodane",
"l_account_add_failed": "Nie udało się dodać konta: {message}",
"@l_account_add_failed" : {
"placeholders": {
"message": {}
}
},
"l_account_name_required": "Twoje konto musi mieć nazwę",
"l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy",
"l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy",
"s_pinned": "Przypięte",
"s_pin_account": "Przypnij konto",
"s_unpin_account": "Odepnij konto",
"s_no_pinned_accounts": "Brak przypiętych kont",
"s_rename_account": "Zmień nazwę konta",
"s_account_renamed": "Zmieniono nazwę konta",
"p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.",
"s_delete_account": "Usuń konto",
"s_account_deleted": "Konto zostało usunięte",
"p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.",
"p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.",
"s_account_name": "Nazwa konta",
"s_search_accounts": "Wyszukaj konta",
"l_accounts_used": "Użyto {used} z {capacity} kont",
"@l_accounts_used" : {
"placeholders": {
"used": {},
"capacity": {}
}
},
"s_num_digits": "{num} cyfr",
"@s_num_digits" : {
"placeholders": {
"num": {}
}
},
"s_num_sec": "{num} sek",
"@s_num_sec" : {
"placeholders": {
"num": {}
}
},
"s_issuer_optional": "Wydawca (opcjonalnie)",
"s_counter_based": "Na podstawie licznika",
"s_time_based": "Na podstawie czasu",
"@_fido_credentials": {},
"l_credential": "Poświadczenie: {label}",
"@l_credential" : {
"placeholders": {
"label": {}
}
},
"s_credentials": "Poświadczenia",
"l_ready_to_use": "Gotowe do użycia",
"l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych",
"l_no_discoverable_accounts": "Nie wykryto kont",
"s_delete_credential": "Usuń poświadczenie",
"s_credential_deleted": "Poświadczenie zostało usunięte",
"p_warning_delete_credential": "Spowoduje to usunięcie poświadczenia z klucza YubiKey.",
"@_fingerprints": {},
"l_fingerprint": "Odcisk palca: {label}",
"@l_fingerprint" : {
"placeholders": {
"label": {}
}
},
"s_fingerprints": "Odciski palców",
"l_fingerprint_captured": "Odcisk palca zarejestrowany pomyślnie!",
"s_fingerprint_added": "Dodano odcisk palca",
"l_setting_name_failed": "Błąd ustawienia nazwy: {message}",
"@l_setting_name_failed" : {
"placeholders": {
"message": {}
}
},
"s_add_fingerprint": "Dodaj odcisk palca",
"l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca",
"l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca",
"s_delete_fingerprint": "Usuń odcisk palca",
"s_fingerprint_deleted": "Odcisk palca został usunięty",
"p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.",
"s_no_fingerprints": "Brak odcisków palców",
"l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców",
"l_no_fps_added": "Nie dodano odcisków palców",
"s_rename_fp": "Zmień nazwę odcisku palca",
"s_fingerprint_renamed": "Zmieniono nazwę odcisku palca",
"l_rename_fp_failed": "Błąd zmiany nazwy: {message}",
"@l_rename_fp_failed" : {
"placeholders": {
"message": {}
}
},
"l_add_one_or_more_fps": "Dodaj jeden lub więcej (do pięciu) odcisków palców",
"l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców",
"@l_fingerprints_used": {
"placeholders": {
"used": {}
}
},
"p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.",
"p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.",
"@_permissions": {},
"s_enable_nfc": "Włącz NFC",
"s_permission_denied": "Odmowa dostępu",
"l_elevating_permissions": "Podnoszenie uprawnień\u2026",
"s_review_permissions": "Przegląd uprawnień",
"p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.",
"p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.",
"p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.",
"@_qr_codes": {},
"s_qr_scan": "Skanuj kod QR",
"l_qr_scanned": "Zeskanowany kod QR",
"l_invalid_qr": "Nieprawidłowy kod QR",
"l_qr_not_found": "Nie znaleziono kodu QR",
"l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}",
"@l_qr_not_read" : {
"placeholders": {
"message": {}
}
},
"l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować",
"q_want_to_scan": "Czy chcesz zeskanować?",
"q_no_qr": "Nie masz kodu QR?",
"s_enter_manually": "Wprowadź ręcznie",
"@_factory_reset": {},
"s_reset": "Zresetuj",
"s_factory_reset": "Ustawienia fabryczne",
"l_factory_reset_this_app": "Przywróć ustawienia fabryczne tej funkcji",
"s_reset_oath": "Zresetuj OATH",
"l_oath_application_reset": "Reset funkcji OATH",
"s_reset_fido": "Zresetuj FIDO",
"l_fido_app_reset": "Reset funkcji FIDO",
"l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć\u2026",
"l_reset_failed": "Błąd podczas resetowania: {message}",
"@l_reset_failed" : {
"placeholders": {
"message": {}
}
},
"p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.",
"p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.",
"p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.",
"p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.",
"@_copy_to_clipboard": {},
"l_copy_to_clipboard": "Skopiuj do schowka",
"s_code_copied": "Kod skopiowany",
"l_code_copied_clipboard": "Kod skopiowany do schowka",
"s_copy_log": "Kopiuj logi",
"l_log_copied": "Logi skopiowane do schowka",
"l_diagnostics_copied": "Dane diagnostyczne skopiowane do schowka",
"p_target_copied_clipboard": "{label} skopiowano do schowka.",
"@p_target_copied_clipboard" : {
"placeholders": {
"label": {}
}
},
"@_custom_icons": {},
"s_custom_icons": "Niestandardowe ikony",
"l_set_icons_for_accounts": "Ustaw ikony dla kont",
"p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.",
"s_replace_icon_pack": "Zastąp pakiet ikon",
"l_loading_icon_pack": "Wczytywanie pakietu ikon\u2026",
"s_load_icon_pack": "Wczytaj pakiet ikon",
"s_remove_icon_pack": "Usuń pakiet ikon",
"l_icon_pack_removed": "Usunięto pakiet ikon",
"l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon",
"s_choose_icon_pack": "Wybierz pakiet ikon",
"l_fingerprints_used": "Zarejestrowano {used}/5 odcisków palców",
"l_fp_step_1_capture": "Krok 1/2: Pobranie odcisku palca",
"l_fp_step_2_name": "Krok 2/2: Nazwij odcisk palca",
"l_generate_desc": "Generuj nowy certyfikat lub CSR",
"l_generating_private_key": "Generowanie prywatnego klucza…",
"l_helper_not_responding": "Proces pomocnika nie odpowiada",
"l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon",
"l_icon_pack_imported": "Zaimportowano pakiet ikon",
"l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}",
"l_icon_pack_removed": "Usunięto pakiet ikon",
"l_import_desc": "Zaimportuj klucz i/lub certyfikat",
"l_import_error": "Błąd importowania",
"l_import_file": "Importuj plik",
"@l_import_icon_pack_failed": {
"placeholders": {
"message": {}
}
},
"l_import_icon_pack_failed": "Błąd importu pakietu ikon: {message}",
"l_importing_file": "Importowanie pliku…",
"l_insert_or_tap_yk": "Podłącz lub przystaw YubiKey",
"l_insert_yk": "Podłącz klucz YubiKey",
"l_invalid_character_issuer": "Nieprawidłowy znak: „:” nie jest dozwolony w polu wydawcy",
"l_invalid_icon_pack": "Nieprawidłowy pakiet ikon",
"l_icon_pack_copy_failed": "Nie udało się skopiować plików z pakietu ikon",
"@_android_settings": {},
"s_nfc_options": "Opcje NFC",
"l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC",
"l_launch_ya": "Uruchom Yubico Authenticator",
"l_copy_otp_clipboard": "Skopiuj OTP do schowka",
"l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP",
"l_invalid_qr": "Nieprawidłowy kod QR",
"l_kbd_layout_for_static": "Układ klawiatury (dla hasła statycznego)",
"s_choose_kbd_layout": "Wybierz układ klawiatury",
"l_bypass_touch_requirement": "Obejdź wymóg dotyku",
"l_bypass_touch_requirement_on": "Konta, które wymagają dotknięcia, są automatycznie wyświetlane przez NFC",
"l_bypass_touch_requirement_off": "Konta, które wymagają dotknięcia, potrzebują dodatkowego przyłożenia do NFC",
"s_silence_nfc_sounds": "Wycisz dźwięki NFC",
"l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC",
"l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC",
"s_usb_options": "Opcje USB",
"l_keep_touching_yk": "Wielokrotnie dotykaj klucza YubiKey…",
"l_key_no_certificate": "Załadowano klucz bez certyfikatu",
"l_keystore_unavailable": "Magazyn kluczy systemu operacyjnego jest niedostępny",
"l_launch_and_copy_otp": "Uruchom aplikację i skopiuj OTP",
"l_launch_app_on_usb": "Uruchom po podłączeniu YubiKey",
"l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB",
"l_launch_app_on_usb_off": "Inne aplikacje mogą korzystać z YubiKey przez USB",
"l_launch_app_on_usb_on": "Uniemożliwia to innym aplikacjom korzystanie z YubiKey przez USB",
"l_launch_ya": "Uruchom Yubico Authenticator",
"l_loading_icon_pack": "Wczytywanie pakietu ikon…",
"l_log_copied": "Logi skopiowane do schowka",
"l_management_key_changed": "Zmieniono klucz zarządzania",
"l_min_one_interface": "Przynajmniej jeden interfejs musi być włączony",
"l_name_already_exists": "Ta nazwa już istnieje dla tego wydawcy",
"@l_new_pin_len": {
"placeholders": {
"length": {}
}
},
"l_new_pin_len": "Nowy PIN musi mieć co najmniej {length} znaków",
"l_no_certificate": "Nie załadowano certyfikatu",
"l_no_discoverable_accounts": "Nie wykryto kont",
"l_no_fps_added": "Nie dodano odcisków palców",
"l_no_yk_present": "Nie wykryto YubiKey",
"l_oath_application_reset": "Reset funkcji OATH",
"l_on_yk_nfc_tap": "Podczas kontaktu YubiKey z NFC",
"l_open_connection_failed": "Nie udało się nawiązać połączenia",
"l_optional_password_protection": "Opcjonalna ochrona hasłem",
"l_optionally_set_a_pin": "Opcjonalnie ustaw PIN, aby chronić dostęp do YubiKey\nZarejestruj jako klucz bezpieczeństwa na stronach internetowych",
"l_overwrite_cert": "Certyfikat zostanie nadpisany",
"l_overwrite_key": "Klucz prywatny zostanie nadpisany",
"l_overwrite_key_maybe": "Każdy istniejący klucz prywatny w slocie zostanie nadpisany",
"@l_passkey": {
"placeholders": {
"label": {}
}
},
"l_passkey": "Klucz dostępu: {label}",
"l_pin_account_desc": "Przechowuj ważne konta razem",
"l_pin_blocked_reset": "PIN jest zablokowany; przywróć ustawienia fabryczne funkcji FIDO",
"l_pin_protected_key": "Zamiast tego można użyć kodu PIN",
"l_pin_soft_locked": "PIN został zablokowany do momentu ponownego podłączenia klucza YubiKey",
"l_piv_app_reset": "Funkcja PIV została zresetowana",
"l_piv_pin_blocked": "Zablokowano, użyj PUK, aby zresetować",
"l_piv_pin_puk_blocked": "Zablokowano, konieczny reset do ustawień fabrycznych",
"l_place_on_nfc_reader": "Przyłóż klucz YubiKey do czytnika NFC",
"l_point_camera_scan": "Skieruj aparat na kod QR, by go zeskanować",
"l_press_reset_to_begin": "Naciśnij reset, aby rozpocząć…",
"l_qr_not_found": "Nie znaleziono kodu QR",
"@l_qr_not_read": {
"placeholders": {
"message": {}
}
},
"l_qr_not_read": "Odczytanie kodu QR nie powiodło się: {message}",
"l_qr_scanned": "Zeskanowany kod QR",
"l_ready_to_use": "Gotowe do użycia",
"l_register_sk_on_websites": "Zarejestruj jako klucz bezpieczeństwa na stronach internetowych",
"l_reinsert_yk": "Ponownie podłącz YubiKey",
"l_remember_pw_failed": "Nie udało się zapamiętać hasła",
"l_remove_icon_pack_failed": "Błąd podczas usuwania pakietu ikon",
"l_remove_yk_from_reader": "Odsuń klucz YubiKey od czytnika NFC",
"l_rename_account_desc": "Edytuj wydawcę/nazwę konta",
"l_rename_fp_desc": "Zmień etykietę",
"@l_rename_fp_failed": {
"placeholders": {
"message": {}
}
},
"l_rename_fp_failed": "Błąd zmiany nazwy: {message}",
"l_replace_yk_on_reader": "Umieść klucz YubiKey z powrotem na czytniku",
"@l_reset_failed": {
"placeholders": {
"message": {}
}
},
"l_reset_failed": "Błąd podczas resetowania: {message}",
"l_rfc4514_invalid": "Nieprawidłowy format RFC 4514",
"l_select_accounts": "Wybierz konta, które chcesz dodać do YubiKey",
"l_select_import_file": "Wybierz plik do zaimportowania",
"l_set_icons_for_accounts": "Ustaw ikony dla kont",
"@l_set_pin_failed": {
"placeholders": {
"message": {}
}
},
"l_set_pin_failed": "Nie udało się ustawić kodu PIN: {message}",
"l_set_pin_fingerprints": "Ustaw kod PIN, aby zarejestrować odciski palców",
"l_set_pin_first": "Najpierw wymagany jest kod PIN",
"@l_setting_name_failed": {
"placeholders": {
"message": {}
}
},
"l_setting_name_failed": "Błąd ustawienia nazwy: {message}",
"l_silence_nfc_sounds_off": "Dźwięki będą odtwarzane po przyłożeniu do NFC",
"l_silence_nfc_sounds_on": "Dźwięki nie będą odtwarzane po przyłożeniu do NFC",
"l_touch_button_now": "Dotknij teraz przycisku na kluczu YubiKey",
"l_unlock_first": "Najpierw odblokuj hasłem",
"l_unlock_pin_first": "Najpierw odblokuj kodem PIN",
"l_unlock_piv_management": "Odblokuj zarządzanie PIV",
"l_unplug_yk": "Odłącz klucz YubiKey",
"l_warning_default_key": "Uwaga: Używany jest klucz domyślny",
"l_webauthn_req_fido2": "WebAuthn wymaga włączenia funkcji FIDO2 w kluczu YubiKey",
"l_wrong_key": "Błędny klucz",
"@l_wrong_pin_attempts_remaining": {
"placeholders": {
"retries": {}
}
},
"l_wrong_pin_attempts_remaining": "Błędny PIN, pozostało prób: {retries}",
"@l_wrong_puk_attempts_remaining": {
"placeholders": {
"retries": {}
}
},
"l_wrong_puk_attempts_remaining": "Nieprawidłowy PUK, pozostało prób: {retries}",
"l_yk_no_access": "Dostęp do tego klucza YubiKey jest niemożliwy",
"p_add_description": "W celu zeskanowania kodu QR, upewnij się, że pełny kod jest widoczny na ekranie a następnie naciśnij poniższy przycisk. Jeśli posiadasz dane uwierzytelniające do konta w tekstowej formie, skorzystaj z opcji ręcznego wprowadzania danych.",
"p_ccid_service_unavailable": "Upewnij się, że usługa kart inteligentnych działa.",
"p_cert_options_desc": "Algorytm klucza do użycia, format wyjściowy i data wygaśnięcia (tylko certyfikat).",
"p_change_management_key_desc": "Zmień swój klucz zarządzania. Opcjonalnie możesz zezwolić na używanie kodu PIN zamiast klucza zarządzania.",
"p_community_translations_desc": "Tłumaczenia te są dostarczane i utrzymywane przez społeczność. Mogą zawierać błędy lub być niekompletne.",
"p_custom_icons_description": "Pakiety ikon mogą sprawić, że Twoje konta będą łatwiejsze do odróżnienia dzięki znanym logo i kolorom.",
"p_elevated_permissions_required": "Zarządzanie tym urządzeniem wymaga podwyższonych uprawnień.",
"p_enter_current_password_or_reset": "Wprowadź aktualne hasło. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
"p_enter_current_pin_or_reset": "Wprowadź aktualny kod PIN. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
"p_enter_current_puk_or_reset": "Wprowadź aktualny kod PUK. Jeśli go nie znasz, musisz zresetować klucz YubiKey.",
"@p_enter_new_fido2_pin": {
"placeholders": {
"length": {}
}
},
"p_enter_new_fido2_pin": "Wprowadź nowy kod PIN. Musi zawierać co najmniej {length} znaków. Może zawierać litery, cyfry i znaki specjalne.",
"p_enter_new_password": "Wprowadź nowe hasło. Może ono zawierać litery, cyfry i znaki specjalne.",
"@p_enter_new_piv_pin_puk": {
"placeholders": {
"name": {}
}
},
"p_enter_new_piv_pin_puk": "Wprowadź nową {name} do ustawienia. Musi składać się z 6-8 znaków.",
"@p_generate_desc": {
"placeholders": {
"slot": {}
}
},
"p_generate_desc": "Spowoduje to wygenerowanie nowego klucza w kluczu YubiKey w slocie PIV {slot}. Klucz publiczny zostanie osadzony w samopodpisanym certyfikacie przechowywanym w kluczu YubiKey lub w żądaniu podpisania certyfikatu (CSR) zapisanym w pliku.",
"@p_import_items_desc": {
"placeholders": {
"slot": {}
}
},
"p_import_items_desc": "Następujące elementy zostaną zaimportowane do slotu PIV {slot}.",
"p_need_camera_permission": "Yubico Authenticator wymaga dostępu do aparatu w celu skanowania kodów QR.",
"@p_overwrite_slot_desc": {
"placeholders": {
"slot": {}
}
},
"p_overwrite_slot_desc": "Spowoduje to trwałe nadpisanie istniejącej zawartości w slocie {slot}.",
"p_password_protected_file": "Wybrany plik jest chroniony hasłem. Wprowadź je, aby kontynuować.",
"p_pcscd_unavailable": "Upewnij się, że pcscd jest zainstalowany i uruchomiony.",
"p_pin_required_desc": "Czynność, którą zamierzasz wykonać, wymaga wprowadzenia kodu PIN PIV.",
"p_press_fingerprint_begin": "Przytrzymaj palec na kluczu YubiKey, aby rozpocząć.",
"p_rename_will_change_account_displayed": "Spowoduje to zmianę sposobu wyświetlania konta na liście.",
"p_subject_desc": "Nazwa wyróżniająca (DN) sformatowana zgodnie ze specyfikacją RFC 4514.",
"@p_target_copied_clipboard": {
"placeholders": {
"label": {}
}
},
"p_target_copied_clipboard": "{label} skopiowano do schowka.",
"p_try_reinsert_yk": "Spróbuj ponownie podłączyć klucz YubiKey.",
"p_unlock_piv_management_desc": "Czynność, którą zamierzasz wykonać, wymaga klucza zarządzania PIV. Podaj ten klucz, aby odblokować funkcje zarządzania dla tej sesji.",
"p_warning_delete_account": "Uwaga! Ta czynność spowoduje usunięcie konta z klucza YubiKey.",
"p_warning_delete_certificate": "Uwaga! Ta czynność spowoduje usunięcie certyfikatu z klucza YubiKey.",
"p_warning_delete_fingerprint": "Spowoduje to usunięcie odcisku palca z twojego YubiKey.",
"p_warning_delete_passkey": "Spowoduje to usunięcie klucza dostępu z klucza YubiKey.",
"p_warning_deletes_accounts": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont U2F i FIDO2 z klucza YubiKey.",
"p_warning_disable_accounts": "Twoje poświadczenia, a także wszelkie ustawione kody PIN, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.",
"p_warning_disable_credential": "Nie będzie już możliwe generowanie OTP dla tego konta. Upewnij się, że najpierw wyłączono te dane uwierzytelniające w witrynie, aby uniknąć zablokowania konta.",
"p_warning_disable_credentials": "Twoje poświadczenia OATH, jak również wszelkie ustawione hasła, zostaną usunięte z tego klucza YubiKey. Upewnij się, że najpierw wyłączono je w odpowiednich witrynach internetowych, aby uniknąć zablokowania kont.",
"p_warning_factory_reset": "Uwaga! Spowoduje to nieodwracalne usunięcie wszystkich kont OATH TOTP/HOTP z klucza YubiKey.",
"p_warning_piv_reset": "Ostrzeżenie! Wszystkie dane przechowywane dla PIV zostaną nieodwracalnie usunięte z klucza YubiKey.",
"p_warning_piv_reset_desc": "Obejmuje to klucze prywatne i certyfikaty. Kod PIN, PUK i klucz zarządzania zostaną zresetowane do domyślnych wartości fabrycznych.",
"p_webauthn_elevated_permissions_required": "Zarządzanie WebAuthn wymaga podwyższonych uprawnień.",
"p_will_change_label_fp": "Spowoduje to zmianę etykiety odcisku palca.",
"@q_delete_certificate_confirm": {
"placeholders": {
"slot": {}
}
},
"q_delete_certificate_confirm": "Usunąć certyfikat ze slotu PIV {slot}?",
"q_have_account_info": "Masz dane konta?",
"q_no_qr": "Nie masz kodu QR?",
"@q_rename_target": {
"placeholders": {
"label": {}
}
},
"q_rename_target": "Zmienić nazwę {label}?",
"q_want_to_scan": "Czy chcesz zeskanować?",
"rfc4514_examples": "Przykłady:\nCN=Przykładowa Nazwa\nCN=jkowalski,DC=przyklad,DC=pl",
"s_about": "O aplikacji",
"s_account_added": "Konto zostało dodane",
"s_account_deleted": "Konto zostało usunięte",
"s_account_name": "Nazwa konta",
"s_account_renamed": "Zmieniono nazwę konta",
"s_accounts": "Konta",
"s_actions": "Działania",
"s_add_account": "Dodaj konto",
"s_add_accounts": "Dodaj konto(-a)",
"s_add_fingerprint": "Dodaj odcisk palca",
"s_add_manually": "Dodaj ręcznie",
"s_algorithm": "Algorytm",
"s_allow_screenshots": "Zezwalaj na zrzuty ekranu",
"@_eof": {}
}
"s_app_disabled": "Wyłączona funkcja",
"s_app_not_supported": "Funkcja nie jest obsługiwana",
"s_app_theme": "Motyw aplikacji",
"s_appearance": "Wygląd",
"s_application_error": "Błąd funkcji",
"s_authenticator": "Authenticator",
"s_calculate": "Oblicz",
"s_calculate_code": "Oblicz kod",
"s_cancel": "Anuluj",
"s_certificate": "Certyfikat",
"s_certificate_fingerprint": "Odcisk palca",
"s_certificates": "Certyfikaty",
"s_change_pin": "Zmień PIN",
"s_change_puk": "Zmień PUK",
"s_character_count": "Liczba znaków",
"s_choose_app_theme": "Wybierz motyw aplikacji",
"s_choose_icon_pack": "Wybierz pakiet ikon",
"s_choose_kbd_layout": "Wybierz układ klawiatury",
"s_clear_saved_password": "Usuń zapisane hasło",
"s_close": "Zamknij",
"s_code_copied": "Kod skopiowany",
"s_config_updated": "Zaktualizowano konfigurację",
"s_configure_yk": "Skonfiguruj YubiKey",
"s_confirm_password": "Potwierdź hasło",
"s_confirm_pin": "Potwierdź PIN",
"s_confirm_puk": "Potwierdź PUK",
"s_copy_log": "Kopiuj logi",
"s_counter_based": "Na podstawie licznika",
"s_csr": "CSR",
"s_current_management_key": "Aktualny klucz zarządzania",
"s_current_password": "Aktualne hasło",
"s_current_pin": "Aktualny PIN",
"s_current_puk": "Aktualny PUK",
"s_custom_icons": "Niestandardowe ikony",
"s_dark_mode": "Ciemny",
"s_delete": "Usuń",
"s_delete_account": "Usuń konto",
"s_delete_fingerprint": "Usuń odcisk palca",
"s_delete_passkey": "Usuń klucz dostępu",
"s_enable_nfc": "Włącz NFC",
"s_enter_manually": "Wprowadź ręcznie",
"s_factory_reset": "Ustawienia fabryczne",
"s_fido_disabled": "FIDO2 wyłączone",
"s_fido_pin_protection": "Ochrona FIDO kodem PIN",
"s_file_imported": "Plik został zaimportowany",
"s_fingerprint_added": "Dodano odcisk palca",
"s_fingerprint_deleted": "Odcisk palca został usunięty",
"s_fingerprint_renamed": "Zmieniono nazwę odcisku palca",
"s_fingerprints": "Odciski palców",
"@s_fw_version": {
"placeholders": {
"version": {}
}
},
"s_fw_version": "F/W: {version}",
"s_generate_key": "Generuj klucz",
"s_generate_random": "Generuj losowo",
"s_help_and_about": "Pomoc i informacje",
"s_help_and_feedback": "Pomoc i opinie",
"s_hide_device": "Ukryj urządzenie",
"s_hide_window": "Ukryj okno",
"s_i_need_help": "Pomoc",
"s_import": "Importuj",
"s_invalid_length": "Nieprawidłowa długość",
"s_issuer": "Wydawca",
"s_issuer_optional": "Wydawca (opcjonalnie)",
"s_label": "Etykieta",
"s_language": "Język",
"s_learn_more": "Dowiedz się więcej",
"s_light_mode": "Jasny",
"s_load_icon_pack": "Wczytaj pakiet ikon",
"@s_log_level": {
"placeholders": {
"level": {}
}
},
"s_log_level": "Poziom logowania: {level}",
"s_manage": "Zarządzaj",
"s_manage_password": "Zarządzaj hasłem",
"s_management_key": "Klucz zarządzania",
"s_name": "Nazwa",
"s_new_management_key": "Nowy klucz zarządzania",
"s_new_password": "Nowe hasło",
"s_new_pin": "Nowy PIN",
"s_new_puk": "Nowy PUK",
"s_nfc": "NFC",
"s_nfc_dialog_oath_add_account": "Działanie: dodaj nowe konto",
"s_nfc_dialog_oath_add_multiple_accounts": "Działanie: dodawanie wielu kont",
"s_nfc_dialog_oath_calculate_code": "Działanie: oblicz kod OATH",
"s_nfc_dialog_oath_delete_account": "Działanie: usuń konto",
"s_nfc_dialog_oath_failure": "Operacja OATH nie powiodła się",
"s_nfc_dialog_oath_rename_account": "Działanie: zmień nazwę konta",
"s_nfc_dialog_oath_reset": "Działanie: resetuj aplet OATH",
"s_nfc_dialog_oath_set_password": "Działanie: ustaw hasło OATH",
"s_nfc_dialog_oath_unlock": "Działanie: odblokuj aplet OATH",
"s_nfc_dialog_oath_unset_password": "Działanie: usuń hasło OATH",
"s_nfc_dialog_operation_failed": "Niepowodzenie",
"s_nfc_dialog_operation_success": "Powodzenie",
"s_nfc_dialog_tap_key": "Przystaw swój klucz",
"s_nfc_options": "Opcje NFC",
"s_no_accounts": "Brak kont",
"s_no_fingerprints": "Brak odcisków palców",
"s_no_pinned_accounts": "Brak przypiętych kont",
"@s_num_digits": {
"placeholders": {
"num": {}
}
},
"s_num_digits": "{num} cyfr",
"@s_num_sec": {
"placeholders": {
"num": {}
}
},
"s_num_sec": "{num} sek",
"s_open_src_licenses": "Licencje open source",
"s_options": "Opcje",
"s_overwrite": "Nadpisz",
"s_overwrite_slot": "Nadpisz slot",
"s_passkey_deleted": "Usunięto klucz dostępu",
"s_passkeys": "Klucze dostępu",
"s_password": "Hasło",
"s_password_forgotten": "Hasło zostało zapomniane",
"s_password_removed": "Hasło zostało usunięte",
"s_password_set": "Hasło zostało ustawione",
"s_permission_denied": "Odmowa dostępu",
"s_pin": "PIN",
"s_pin_account": "Przypnij konto",
"s_pin_required": "Wymagany PIN",
"s_pin_set": "PIN ustawiony",
"s_pinned": "Przypięte",
"s_piv": "PIV",
"s_please_wait": "Proszę czekać…",
"s_privacy_policy": "Polityka prywatności",
"s_private_key": "Klucz prywatny",
"s_private_key_generated": "Wygenerowano klucz prywatny",
"s_protect_key": "Zabezpiecz kodem PIN",
"s_puk": "PUK",
"s_puk_set": "PUK ustawiony",
"s_qr_scan": "Skanuj kod QR",
"s_quit": "Wyjdź",
"s_reconfiguring_yk": "Rekonfigurowanie YubiKey…",
"s_remember_password": "Zapamiętaj hasło",
"s_remove_icon_pack": "Usuń pakiet ikon",
"s_remove_password": "Usuń hasło",
"s_rename_account": "Zmień nazwę konta",
"s_rename_fp": "Zmień nazwę odcisku palca",
"s_replace_icon_pack": "Zastąp pakiet ikon",
"s_require_touch": "Wymagaj dotknięcia",
"s_reset": "Zresetuj",
"s_reset_fido": "Zresetuj FIDO",
"s_reset_oath": "Zresetuj OATH",
"s_reset_piv": "Resetuj PIV",
"s_review_permissions": "Przegląd uprawnień",
"s_run_diagnostics": "Uruchom diagnostykę",
"s_save": "Zapisz",
"s_search_accounts": "Wyszukaj konta",
"s_secret_key": "Tajny klucz",
"s_select_to_scan": "Wybierz, aby skanować",
"s_select_yk": "Wybierz YubiKey",
"s_send_feedback": "Prześlij opinię",
"s_serial": "Nr. seryjny",
"s_set_password": "Ustaw hasło",
"s_set_pin": "Ustaw PIN",
"s_settings": "Ustawienia",
"s_setup": "Konfiguruj",
"s_show_hidden_devices": "Pokaż ukryte urządzenia",
"s_show_window": "Pokaż okno",
"s_silence_nfc_sounds": "Wycisz dźwięki NFC",
"s_slot_9a": "Uwierzytelnienie",
"s_slot_9c": "Cyfrowy podpis",
"s_slot_9d": "Menedżer kluczy",
"s_slot_9e": "Autoryzacja karty",
"@s_slot_display_name": {
"placeholders": {
"hexid": {},
"name": {}
}
},
"s_slot_display_name": "{name} ({hexid})",
"@s_sn_serial": {
"placeholders": {
"serial": {}
}
},
"s_sn_serial": "S/N: {serial}",
"s_subject": "Temat",
"s_system_default": "Zgodny z systemem",
"s_terms_of_use": "Warunki użytkowania",
"s_time_based": "Na podstawie czasu",
"s_toggle_applications": "Przełączanie funkcji",
"s_touch_required": "Wymagane dotknięcie",
"s_troubleshooting": "Rozwiązywanie problemów",
"s_unblock_pin": "Odblokuj PIN",
"s_unknown_device": "Nierozpoznane urządzenie",
"s_unknown_type": "Nieznany typ",
"s_unlock": "Odblokuj",
"s_unpin_account": "Odepnij konto",
"s_unsupported_yk": "Nieobsługiwany klucz YubiKey",
"s_usb": "USB",
"s_usb_options": "Opcje USB",
"s_use_default": "Użyj domyślnego",
"s_valid_from": "Ważny od",
"s_valid_to": "Ważny do",
"s_webauthn": "WebAuthn",
"s_wrong_password": "Błędne hasło",
"s_yk_inaccessible": "Urządzenie niedostępne",
"s_yk_information": "Informacja o YubiKey",
"s_yk_not_recognized": "Urządzenie nie rozpoznane"
}

View File

@ -20,9 +20,9 @@ import 'package:yubico_authenticator/management/models.dart';
import '../app/models.dart';
import '../core/state.dart';
final managementStateProvider = StateNotifierProvider.autoDispose
.family<ManagementStateNotifier, AsyncValue<DeviceInfo>, DevicePath>(
(ref, devicePath) => throw UnimplementedError(),
final managementStateProvider = AsyncNotifierProvider.autoDispose
.family<ManagementStateNotifier, DeviceInfo, DevicePath>(
() => throw UnimplementedError(),
);
abstract class ManagementStateNotifier

View File

@ -20,11 +20,11 @@ import 'dart:io';
import 'package:archive/archive.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:io/io.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:yubico_authenticator/app/logging.dart';
import 'package:io/io.dart';
import 'icon_cache.dart';
import 'icon_pack.dart';
@ -116,7 +116,16 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
final unpackDirectory = Directory(join(tempDirectory.path, 'unpack'));
final archive = ZipDecoder().decodeBytes(bytes, verify: true);
Archive archive;
try {
archive = ZipDecoder().decodeBytes(bytes, verify: true);
} on Exception catch (_) {
_log.error('File is not an icon pack: zip decoding failed');
_lastError = l10n.l_invalid_icon_pack;
state = AsyncValue.error('File is not an icon pack', StackTrace.current);
return false;
}
for (final file in archive) {
final filename = file.name;
if (file.size > 0) {
@ -172,7 +181,8 @@ class IconPackManager extends StateNotifier<AsyncValue<IconPack?>> {
} catch (e) {
_log.error('Failed to copy icon pack files to destination: $e');
_lastError = l10n.l_icon_pack_copy_failed;
state = AsyncValue.error('Failed to copy icon pack files.', StackTrace.current);
state = AsyncValue.error(
'Failed to copy icon pack files.', StackTrace.current);
return false;
}

View File

@ -17,18 +17,29 @@
import 'package:flutter/material.dart';
const _prefix = 'oath.keys';
const setOrManagePasswordAction = Key('$_prefix.set_or_manage_password');
const addAccountAction = Key('$_prefix.add_account');
const resetAction = Key('$_prefix.reset');
const customIconsAction = Key('$_prefix.custom_icons');
const noAccountsView = Key('$_prefix.no_accounts');
const _keyAction = '$_prefix.actions';
const _accountAction = '$_prefix.account.actions';
// This is global so we can access it from the global Ctrl+F shortcut.
final searchAccountsField = GlobalKey();
// Key actions
const setOrManagePasswordAction =
Key('$_keyAction.action.set_or_manage_password');
const addAccountAction = Key('$_keyAction.add_account');
const migrateAccountAction = Key('$_keyAction.migrate_account');
const resetAction = Key('$_keyAction.reset');
const customIconsAction = Key('$_keyAction.custom_icons');
// Credential actions
const copyAction = Key('$_accountAction.copy');
const calculateAction = Key('$_accountAction.calculate');
const togglePinAction = Key('$_accountAction.toggle_pin');
const editAction = Key('$_accountAction.edit');
const deleteAction = Key('$_accountAction.delete');
const noAccountsView = Key('$_prefix.no_accounts');
const passwordField = Key('$_prefix.password');
const currentPasswordField = Key('$_prefix.current_password');
const newPasswordField = Key('$_prefix.new_password');

View File

@ -14,6 +14,9 @@
* limitations under the License.
*/
import 'dart:typed_data';
import 'dart:convert';
import 'package:base32/base32.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@ -120,10 +123,83 @@ class CredentialData with _$CredentialData {
factory CredentialData.fromJson(Map<String, dynamic> json) =>
_$CredentialDataFromJson(json);
factory CredentialData.fromUri(Uri uri) {
if (uri.scheme.toLowerCase() != 'otpauth') {
throw ArgumentError('Invalid scheme, must be "otpauth://"');
static List<CredentialData> fromUri(Uri uri) {
if (uri.scheme.toLowerCase() == 'otpauth-migration') {
return CredentialData.fromMigration(uri);
} else if (uri.scheme.toLowerCase() == 'otpauth') {
return [CredentialData.fromOtpauth(uri)];
} else {
throw ArgumentError('Invalid scheme');
}
}
static List<CredentialData> fromMigration(Uri uri) {
// Parse single protobuf encoded integer
(int value, Uint8List rem) protoInt(Uint8List data) {
final extras = data.takeWhile((b) => b & 0x80 != 0).length;
int value = 0;
for (int i = extras; i >= 0; i--) {
value = (value << 7) | (data[i] & 0x7F);
}
return (value, data.sublist(1 + extras));
}
// Parse a single protobuf value from a buffer
(int tag, dynamic value, Uint8List rem) protoValue(Uint8List data) {
final first = data[0];
final int len;
(len, data) = protoInt(data.sublist(1));
final index = first >> 3;
switch (first & 0x07) {
case 0:
return (index, len, data);
case 2:
return (index, data.sublist(0, len), data.sublist(len));
}
throw ArgumentError('Unsupported value type!');
}
// Parse a protobuf message into map of tags and values
Map<int, dynamic> protoMap(Uint8List data) {
Map<int, dynamic> values = {};
while (data.isNotEmpty) {
final (tag, value, rem) = protoValue(data);
values[tag] = value;
data = rem;
}
return values;
}
// Parse encoded credentials from data (tag 1) ignoring trailing extra data
Iterable<Map<int, dynamic>> splitCreds(Uint8List rem) sync* {
Uint8List credrem;
while (rem[0] == 0x0a) {
(_, credrem, rem) = protoValue(rem);
yield protoMap(credrem);
}
}
// Convert parsed credential values into CredentialData objects
return splitCreds(base64.decode(uri.queryParameters['data']!))
.map((values) => CredentialData(
secret: base32.encode(values[1]),
name: utf8.decode(values[2], allowMalformed: true),
issuer: values[3] != null
? utf8.decode(values[3], allowMalformed: true)
: null,
hashAlgorithm: switch (values[4]) {
2 => HashAlgorithm.sha256,
3 => HashAlgorithm.sha512,
_ => HashAlgorithm.sha1,
},
digits: values[5] == 2 ? 8 : defaultDigits,
oathType: values[6] == 1 ? OathType.hotp : OathType.totp,
counter: values[7] ?? defaultCounter,
))
.toList();
}
factory CredentialData.fromOtpauth(Uri uri) {
final oathType = OathType.values.byName(uri.host.toLowerCase());
final params = uri.queryParameters;
String? issuer;

View File

@ -37,9 +37,9 @@ class SearchNotifier extends StateNotifier<String> {
}
}
final oathStateProvider = StateNotifierProvider.autoDispose
.family<OathStateNotifier, AsyncValue<OathState>, DevicePath>(
(ref, devicePath) => throw UnimplementedError(),
final oathStateProvider = AsyncNotifierProvider.autoDispose
.family<OathStateNotifier, OathState, DevicePath>(
() => throw UnimplementedError(),
);
abstract class OathStateNotifier extends ApplicationStateNotifier<OathState> {

View File

@ -23,6 +23,8 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../app/views/fs_dialog.dart';
import '../../app/views/action_list.dart';
import '../../core/models.dart';
import '../../core/state.dart';
import '../models.dart';
@ -37,60 +39,6 @@ class AccountDialog extends ConsumerWidget {
const AccountDialog(this.credential, {super.key});
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
final l10n = AppLocalizations.of(context)!;
final actions = helper.buildActions();
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
final copy =
actions.firstWhere(((e) => e.text == l10n.l_copy_to_clipboard));
final delete = actions.firstWhere(((e) => e.text == l10n.s_delete_account));
final colors = {
copy: (theme.primary, theme.onPrimary),
delete: (theme.error, theme.onError),
};
// If we can't copy, but can calculate, highlight that button instead
if (copy.intent == null) {
final calculates = actions.where(((e) => e.text == l10n.s_calculate));
if (calculates.isNotEmpty) {
colors[calculates.first] = (theme.primary, theme.onPrimary);
}
}
return actions.map((e) {
final intent = e.intent;
final (firstColor, secondColor) =
colors[e] ?? (theme.secondary, theme.onSecondary);
final tooltip = e.trailing != null ? '${e.text}\n${e.trailing}' : e.text;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: CircleAvatar(
backgroundColor: intent != null ? firstColor : theme.secondary,
foregroundColor: secondColor,
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: intent != null ? firstColor : theme.secondary,
foregroundColor: secondColor,
disabledBackgroundColor: theme.onSecondary.withOpacity(0.2),
fixedSize: const Size.square(38),
),
icon: e.icon,
iconSize: 22,
tooltip: tooltip,
onPressed: intent != null
? () {
Actions.invoke(context, intent);
}
: null,
),
),
);
}).toList();
}
@override
Widget build(BuildContext context, WidgetRef ref) {
// TODO: Solve this in a cleaner way
@ -111,15 +59,16 @@ class AccountDialog extends ConsumerWidget {
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
final credentials = ref.read(credentialsProvider);
final withContext = ref.read(withContextProvider);
final OathCredential? renamed =
final renamed =
await withContext((context) async => await showBlurDialog(
context: context,
builder: (context) => RenameAccountDialog(
node,
credential,
credentials,
),
));
context: context,
builder: (context) => RenameAccountDialog.forOathCredential(
ref,
node,
credential,
credentials?.map((e) => (e.issuer, e.name)).toList() ??
[],
)));
if (renamed != null) {
// Replace the dialog with the renamed credential
await withContext((context) async {
@ -168,42 +117,16 @@ class AccountDialog extends ConsumerWidget {
}
return FocusScope(
autofocus: true,
child: AlertDialog(
title: Center(
child: Text(
helper.title,
style: Theme.of(context).textTheme.headlineSmall,
softWrap: true,
textAlign: TextAlign.center,
),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
content: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
child: FsDialog(
child: Column(
children: [
if (subtitle != null)
Text(
subtitle,
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).textTheme.bodySmall!.color,
),
),
const SizedBox(height: 12.0),
DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: const BorderRadius.all(Radius.circular(30.0)),
),
child: Center(
child: FittedBox(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 8.0),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 32),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
@ -220,20 +143,36 @@ class AccountDialog extends ConsumerWidget {
],
),
),
),
Text(
helper.title,
style: Theme.of(context).textTheme.headlineSmall,
softWrap: true,
textAlign: TextAlign.center,
),
if (subtitle != null)
Text(
subtitle,
softWrap: true,
textAlign: TextAlign.center,
// This is what ListTile uses for subtitle
style:
Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context)
.textTheme
.bodySmall!
.color,
),
),
],
),
),
ActionListSection.fromMenuActions(
context,
AppLocalizations.of(context)!.s_actions,
actions: helper.buildActions(),
),
],
),
actionsPadding: const EdgeInsets.symmetric(vertical: 10.0),
actions: [
Center(
child: FittedBox(
alignment: Alignment.center,
child: Row(children: _buildActions(context, helper)),
),
)
],
),
);
},

View File

@ -29,6 +29,7 @@ import '../../widgets/circle_timer.dart';
import '../../widgets/custom_icons.dart';
import '../models.dart';
import '../state.dart';
import '../keys.dart' as keys;
import 'actions.dart';
/// Support class for presenting an OATH account.
@ -53,7 +54,7 @@ class AccountHelper {
String get title => credential.issuer ?? credential.name;
String? get subtitle => credential.issuer != null ? credential.name : null;
List<MenuAction> buildActions() => _ref
List<ActionItem> buildActions() => _ref
.watch(currentDeviceDataProvider)
.maybeWhen(
data: (data) {
@ -61,38 +62,51 @@ class AccountHelper {
credential.touchRequired || credential.oathType == OathType.hotp;
final ready = expired || credential.oathType == OathType.hotp;
final pinned = _ref.watch(favoritesProvider).contains(credential.id);
final l10n = AppLocalizations.of(_context)!;
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
final canCopy = code != null && !expired;
return [
MenuAction(
text: l10n.l_copy_to_clipboard,
ActionItem(
key: keys.copyAction,
icon: const Icon(Icons.copy),
intent: code == null || expired ? null : const CopyIntent(),
trailing: shortcut,
title: l10n.l_copy_to_clipboard,
subtitle: l10n.l_copy_code_desc,
shortcut: Platform.isMacOS ? '\u2318 C' : 'Ctrl+C',
actionStyle: canCopy ? ActionStyle.primary : null,
intent: canCopy ? const CopyIntent() : null,
),
if (manual)
MenuAction(
text: l10n.s_calculate,
ActionItem(
key: keys.calculateAction,
actionStyle: !canCopy ? ActionStyle.primary : null,
icon: const Icon(Icons.refresh),
title: l10n.s_calculate,
subtitle: l10n.l_calculate_code_desc,
intent: ready ? const CalculateIntent() : null,
),
MenuAction(
text: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
ActionItem(
key: keys.togglePinAction,
icon: pinned
? pushPinStrokeIcon
: const Icon(Icons.push_pin_outlined),
title: pinned ? l10n.s_unpin_account : l10n.s_pin_account,
subtitle: l10n.l_pin_account_desc,
intent: const TogglePinIntent(),
),
if (data.info.version.isAtLeast(5, 3))
MenuAction(
ActionItem(
key: keys.editAction,
icon: const Icon(Icons.edit_outlined),
text: l10n.s_rename_account,
title: l10n.s_rename_account,
subtitle: l10n.l_rename_account_desc,
intent: const EditIntent(),
),
MenuAction(
text: l10n.s_delete_account,
ActionItem(
key: keys.deleteAction,
actionStyle: ActionStyle.error,
icon: const Icon(Icons.delete_outline),
title: l10n.s_delete_account,
subtitle: l10n.l_delete_account_desc,
intent: const DeleteIntent(),
),
];
@ -157,6 +171,7 @@ class _CodeLabel extends StatelessWidget {
// This helps with vertical centering on desktop
applyHeightToFirstAscent: !isDesktop,
),
semanticsLabel: code?.value.characters.map((c) => '$c ' ).toString(),
),
);
}

View File

@ -22,8 +22,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
import '../../app/state.dart';
import '../../core/state.dart';
import '../../widgets/menu_list_tile.dart';
import '../../app/views/app_list_item.dart';
import '../models.dart';
import '../state.dart';
import 'account_dialog.dart';
@ -48,15 +47,6 @@ String _a11yCredentialLabel(String? issuer, String name, String? code) {
class _AccountViewState extends ConsumerState<AccountView> {
OathCredential get credential => widget.credential;
final _focusNode = FocusNode();
int _lastTap = 0;
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
Color _iconColor(int shade) {
final colors = [
Colors.red[shade],
@ -87,23 +77,6 @@ class _AccountViewState extends ConsumerState<AccountView> {
return colors[label.hashCode % colors.length]!;
}
List<PopupMenuItem> _buildPopupMenu(
BuildContext context, AccountHelper helper) {
return helper.buildActions().map((e) {
final intent = e.intent;
return buildMenuItem(
leading: e.icon,
title: Text(e.text),
action: intent != null
? () {
Actions.invoke(context, intent);
}
: null,
trailing: e.trailing,
);
}).toList();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
@ -116,6 +89,7 @@ class _AccountViewState extends ConsumerState<AccountView> {
OpenIntent: CallbackAction<OpenIntent>(onInvoke: (_) async {
await showBlurDialog(
context: context,
barrierColor: Colors.transparent,
builder: (context) => AccountDialog(credential),
);
return null;
@ -123,12 +97,16 @@ class _AccountViewState extends ConsumerState<AccountView> {
EditIntent: CallbackAction<EditIntent>(onInvoke: (_) async {
final node = ref.read(currentDeviceProvider)!;
final credentials = ref.read(credentialsProvider);
return await ref.read(withContextProvider)(
(context) async => await showBlurDialog(
context: context,
builder: (context) =>
RenameAccountDialog(node, credential, credentials),
));
final withContext = ref.read(withContextProvider);
return await withContext((context) async => await showBlurDialog(
context: context,
builder: (context) => RenameAccountDialog.forOathCredential(
ref,
node,
credential,
credentials?.map((e) => (e.issuer, e.name)).toList() ?? [],
),
));
}),
DeleteIntent: CallbackAction<DeleteIntent>(onInvoke: (_) async {
final node = ref.read(currentDeviceProvider)!;
@ -165,83 +143,27 @@ class _AccountViewState extends ConsumerState<AccountView> {
child: Semantics(
label: _a11yCredentialLabel(
credential.issuer, credential.name, helper.code?.value),
child: InkWell(
focusNode: _focusNode,
borderRadius: BorderRadius.circular(30),
onSecondaryTapDown: (details) {
showMenu(
context: context,
position: RelativeRect.fromLTRB(
details.globalPosition.dx,
details.globalPosition.dy,
details.globalPosition.dx,
0,
),
items: _buildPopupMenu(context, helper),
);
},
onTap: () {
if (isDesktop) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - _lastTap < 500) {
setState(() {
_lastTap = 0;
});
Actions.maybeInvoke(context, const CopyIntent());
} else {
_focusNode.requestFocus();
setState(() {
_lastTap = now;
});
}
} else {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
}
},
onLongPress: () {
Actions.maybeInvoke(context, const CopyIntent());
},
child: ListTile(
leading: showAvatar
? AccountIcon(
issuer: credential.issuer,
defaultWidget: circleAvatar)
: null,
title: Text(
helper.title,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
),
subtitle: subtitle != null
? Text(
subtitle,
overflow: TextOverflow.fade,
maxLines: 1,
softWrap: false,
)
: null,
trailing: Focus(
skipTraversal: true,
descendantsAreTraversable: false,
child: helper.code != null
? FilledButton.tonalIcon(
icon: helper.buildCodeIcon(),
label: helper.buildCodeLabel(),
onPressed: () {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
},
)
: FilledButton.tonal(
onPressed: () {
Actions.maybeInvoke<OpenIntent>(
context, const OpenIntent());
},
child: helper.buildCodeIcon()),
),
),
child: AppListItem(
leading: showAvatar
? AccountIcon(
issuer: credential.issuer,
defaultWidget: circleAvatar)
: null,
title: helper.title,
subtitle: subtitle,
trailing: helper.code != null
? FilledButton.tonalIcon(
icon: helper.buildCodeIcon(),
label: helper.buildCodeLabel(),
onPressed:
Actions.handler(context, const OpenIntent()),
)
: FilledButton.tonal(
onPressed:
Actions.handler(context, const OpenIntent()),
child: helper.buildCodeIcon()),
activationIntent: const CopyIntent(),
buildPopupActions: (_) => helper.buildActions(),
),
));
});

View File

@ -1,3 +1,19 @@
/*
* 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

Some files were not shown because too many files have changed in this diff Show More