mirror of
https://github.com/Yubico/yubioath-flutter.git
synced 2024-11-22 00:12:09 +03:00
Merge branch 'main' into adamve/nfc_activity_widget
This commit is contained in:
commit
9a7a1e76a3
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@ -1,6 +1,15 @@
|
||||
version: 2
|
||||
enable-beta-ecosystems: true # required for the pub package manager
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
|
||||
- package-ecosystem: "pub"
|
||||
directory: "/"
|
||||
schedule:
|
||||
|
6
.github/workflows/android.yml
vendored
6
.github/workflows/android.yml
vendored
@ -8,12 +8,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: 'app'
|
||||
|
||||
@ -63,7 +63,7 @@ jobs:
|
||||
run: android/scripts/collect-artifacts.sh ${GITHUB_REF}
|
||||
working-directory: ./app
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: yubico-authenticator-android
|
||||
path: app/artifacts/*
|
||||
|
2
.github/workflows/check-strings.yml
vendored
2
.github/workflows/check-strings.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read variables from repo
|
||||
run: cat .github/workflows/env >> $GITHUB_ENV
|
||||
|
10
.github/workflows/codeql-analysis.yml
vendored
10
.github/workflows/codeql-analysis.yml
vendored
@ -28,18 +28,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
setup-python-dependencies: false
|
||||
|
||||
- name: set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: 'app'
|
||||
|
||||
@ -75,7 +75,7 @@ jobs:
|
||||
|
||||
- if: matrix.language == 'python'
|
||||
name: autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
4
.github/workflows/env
vendored
4
.github/workflows/env
vendored
@ -1,2 +1,2 @@
|
||||
FLUTTER=3.13.4
|
||||
PYVER=3.12.0
|
||||
FLUTTER=3.22.2
|
||||
PYVER=3.12.4
|
||||
|
36
.github/workflows/linux.yml
vendored
36
.github/workflows/linux.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: .github/workflows/env
|
||||
|
||||
@ -30,10 +30,13 @@ jobs:
|
||||
apt-get install -qq git python$PYVER_MINOR-dev python$PYVER_MINOR-venv
|
||||
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
ln -s `which python$PYVER_MINOR` /usr/local/bin/python
|
||||
ln -s `which python$PYVER_MINOR` /usr/local/bin/python3
|
||||
PYVER_TEMP=`/usr/local/bin/python --version`
|
||||
export PYVERINST=${PYVER_TEMP#* }
|
||||
echo "PYVERINST=$PYVERINST" >> $GITHUB_ENV
|
||||
echo "Installed python version: $PYVERINST"
|
||||
python -m ensurepip --user
|
||||
python -m pip install -U pip pipx
|
||||
|
||||
- name: Verify Python version
|
||||
if: ${{ env.PYVERINST != env.PYVER }}
|
||||
@ -43,7 +46,7 @@ jobs:
|
||||
echo "Expected: $PYVER"
|
||||
exit 1
|
||||
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check app versions
|
||||
run: |
|
||||
@ -52,28 +55,19 @@ jobs:
|
||||
|
||||
- name: Cache helper
|
||||
id: cache-helper
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
build/linux/helper
|
||||
assets/licenses/helper.json
|
||||
key: ${{ runner.os }}-py${{ env.PYVER }}-${{ hashFiles('helper/**') }}
|
||||
|
||||
- name: Install helper dependencies
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
apt-get install -qq swig libpcsclite-dev build-essential cmake
|
||||
python -m ensurepip --user
|
||||
python -m pip install -U pip pipx
|
||||
# pipx ensurepath
|
||||
echo "export PATH=$PATH:$HOME/.local/bin" >> ~/.bashrc
|
||||
. ~/.bashrc # Needed to ensure poetry on PATH
|
||||
pipx install poetry
|
||||
|
||||
- name: Build the Helper
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
. ~/.bashrc # Needed to ensure poetry on PATH
|
||||
apt-get install -qq swig libpcsclite-dev build-essential cmake
|
||||
export PATH=$PATH:$HOME/.local/bin # Needed to ensure pipx/poetry on PATH
|
||||
pipx install poetry
|
||||
./build-helper.sh
|
||||
|
||||
- name: Install Flutter dependencies
|
||||
@ -87,14 +81,18 @@ jobs:
|
||||
|
||||
- name: Configure Flutter
|
||||
run: |
|
||||
git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-$FLUTTER-x64
|
||||
git config --global --add safe.directory $FLUTTER_ROOT
|
||||
flutter config --enable-linux-desktop
|
||||
flutter --version
|
||||
|
||||
- name: Run tests
|
||||
- name: Run lints/tests
|
||||
env:
|
||||
SKIP: ${{ steps.cache-helper.outputs.cache-hit == 'true' && 'mypy,flake8,black,bandit' || ''}}
|
||||
run: |
|
||||
export PATH=$PATH:$HOME/.local/bin # Needed to ensure pip/pre-commit on PATH
|
||||
pipx install pre-commit
|
||||
pre-commit run --all-files
|
||||
flutter test
|
||||
flutter analyze
|
||||
|
||||
- name: Build the app
|
||||
run: flutter build linux
|
||||
@ -123,7 +121,7 @@ jobs:
|
||||
tar -czf deploy/${BASENAME}.tar.gz -C build "${BASENAME}"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: yubioath-desktop-linux
|
||||
path: deploy
|
||||
|
29
.github/workflows/macos.yml
vendored
29
.github/workflows/macos.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read variables from repo
|
||||
run: cat .github/workflows/env >> $GITHUB_ENV
|
||||
@ -18,21 +18,25 @@ jobs:
|
||||
python3 set-version.py
|
||||
git diff --exit-code
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYVER }}
|
||||
|
||||
- name: Set up CocoaPods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
podfile-path: macos/Podfile.lock
|
||||
|
||||
- name: Cache helper
|
||||
id: cache-helper
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
build/macos/helper
|
||||
assets/licenses/helper.json
|
||||
key: ${{ runner.os }}-py${{ env.PYVER }}-${{ hashFiles('helper/**') }}
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYVER }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
@ -53,10 +57,13 @@ jobs:
|
||||
- run: flutter config --enable-macos-desktop
|
||||
- run: flutter --version
|
||||
|
||||
- name: Run tests
|
||||
- name: Run lints/tests
|
||||
env:
|
||||
SKIP: ${{ steps.cache-helper.outputs.cache-hit == 'true' && 'mypy,flake8,black,bandit' || ''}}
|
||||
run: |
|
||||
pip install pre-commit
|
||||
pre-commit run --all-files
|
||||
flutter test
|
||||
flutter analyze
|
||||
|
||||
- name: Build the app
|
||||
run: |
|
||||
@ -88,7 +95,7 @@ jobs:
|
||||
mv macos/release-macos.sh deploy
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: yubioath-desktop-macos
|
||||
path: deploy
|
||||
|
45
.github/workflows/windows.yml
vendored
45
.github/workflows/windows.yml
vendored
@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Read variables from repo
|
||||
shell: bash
|
||||
@ -19,30 +19,28 @@ jobs:
|
||||
python set-version.py
|
||||
git diff --exit-code
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYVER }}
|
||||
|
||||
- name: Update pip
|
||||
run: python -m pip install --upgrade pip
|
||||
|
||||
- name: Cache helper
|
||||
id: cache-helper
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
build/windows/helper
|
||||
assets/licenses/helper.json
|
||||
key: ${{ runner.os }}-py${{ env.PYVER }}-${{ hashFiles('helper/**') }}
|
||||
|
||||
- name: Set up Python
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYVER }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install poetry
|
||||
|
||||
- name: Build the Helper
|
||||
if: steps.cache-helper.outputs.cache-hit != 'true'
|
||||
run: .\build-helper.bat
|
||||
run: |
|
||||
pip install poetry
|
||||
.\build-helper.bat
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
@ -51,10 +49,13 @@ jobs:
|
||||
- run: flutter config --enable-windows-desktop
|
||||
- run: flutter --version
|
||||
|
||||
- name: Run tests
|
||||
- name: Run lints/tests
|
||||
env:
|
||||
SKIP: ${{ steps.cache-helper.outputs.cache-hit == 'true' && 'mypy,flake8,black,bandit' || ''}}
|
||||
run: |
|
||||
pip install pre-commit
|
||||
pre-commit run --all-files
|
||||
flutter test
|
||||
flutter analyze
|
||||
|
||||
- name: Build the app
|
||||
run: |
|
||||
@ -66,7 +67,7 @@ jobs:
|
||||
|
||||
- name: Move .dll files
|
||||
run: |
|
||||
$dest = "build\windows\runner\Release"
|
||||
$dest = "build\windows\x64\runner\Release"
|
||||
cp $dest\helper\_internal/MSVCP140.dll $dest\
|
||||
cp $dest\helper\_internal/VCRUNTIME140.dll $dest\
|
||||
cp $dest\helper\_internal/VCRUNTIME140_1.dll $dest\
|
||||
@ -74,8 +75,8 @@ jobs:
|
||||
- name: Create an unsigned .msi installer package
|
||||
run: |
|
||||
$env:PATH += ";$env:WIX\bin"
|
||||
$env:SRCDIR = "build\windows\runner\Release\"
|
||||
heat dir .\build\windows\runner\Release\ -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
|
||||
$env:SRCDIR = "build\windows\x64\runner\Release\"
|
||||
heat dir .\build\windows\x64\runner\Release\ -out fragment.wxs -gg -scom -srd -sfrag -dr INSTALLDIR -cg ApplicationFiles -var env.SRCDIR
|
||||
candle .\fragment.wxs .\resources\win\yubioath-desktop.wxs -ext WixUtilExtension -arch x64
|
||||
light fragment.wixobj yubioath-desktop.wixobj -ext WixUIExtension -ext WixUtilExtension -o yubioath-desktop.msi
|
||||
|
||||
@ -85,13 +86,13 @@ jobs:
|
||||
$branch = $arr[2]
|
||||
$dest = "deploy\yubioath-desktop-$branch-windows"
|
||||
mkdir $dest
|
||||
mv build\windows\runner\Release\* $dest\
|
||||
mv build\windows\x64\runner\Release\* $dest\
|
||||
mv yubioath-desktop.msi deploy
|
||||
mv resources\win\release-win.ps1 deploy
|
||||
mv resources deploy
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: yubioath-desktop-windows
|
||||
path: deploy
|
||||
|
47
.pre-commit-config.yaml
Normal file
47
.pre-commit-config.yaml
Normal file
@ -0,0 +1,47 @@
|
||||
repos:
|
||||
# Flutter
|
||||
- repo: https://github.com/dluksza/flutter-analyze-pre-commit
|
||||
rev: "4afcaa82fc368d40d486256bf4edba329bf667bb"
|
||||
hooks:
|
||||
- id: dart-format
|
||||
files: \.dart$
|
||||
require_serial: true
|
||||
- id: flutter-analyze
|
||||
require_serial: true
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: arb-reformatter
|
||||
name: reformat-strings
|
||||
files: \.arb$
|
||||
language: script
|
||||
entry: arb_reformatter.py
|
||||
require_serial: true
|
||||
- id: update-android-strings
|
||||
name: update-android-strings
|
||||
files: \.arb$
|
||||
language: script
|
||||
entry: update_android_strings.py
|
||||
require_serial: true
|
||||
|
||||
# Python
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy
|
||||
files: helper/
|
||||
language: script
|
||||
entry: run-mypy.sh
|
||||
require_serial: true
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.11.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/PyCQA/bandit
|
||||
rev: 1.7.5
|
||||
hooks:
|
||||
- id: bandit
|
||||
files: helper/ # keep in sync with .bandit file
|
53
NEWS
53
NEWS
@ -1,3 +1,50 @@
|
||||
* Version 7.0.1 (released 2024-05-30) Android only release
|
||||
** Fix: Opening the app by NFC tap needs another tap to reveal accounts.
|
||||
** Fix: NFC devices attached to mobile phone prevent usage of USB YubiKeys.
|
||||
** Fix: Invalid colors shown in customization views for Android Dynamic color.
|
||||
** Fix: Fingerprints are shown in random order.
|
||||
|
||||
* Version 7.0.0 (released 2024-05-06)
|
||||
** UI: Add home screen with device information, customization options, and factory reset.
|
||||
** UI: Add search filtering to Passkeys and display more information.
|
||||
** Localization: Add official support for French and Japanese.
|
||||
** PIV (desktop): Support managing retired key slots.
|
||||
** Management (desktop): Support toggling applications when Configuration Lock code is set.
|
||||
** OTP (desktop): Support managing slots when OTP Access Code is set.
|
||||
** Android: Support for FIDO (managing PIN, Passkeys, Fingerprints, and factory reset).
|
||||
** Linux: Add ability to use external program for copying to clipboard (see README).
|
||||
** Additional features/support for YubiKey 5.7:
|
||||
*** Handling of PIN complexity.
|
||||
*** PIV: New key algorithms: RSA3072, RSA4096, Ed25519, and X25519.
|
||||
*** PIV: The ability to move and delete keys.
|
||||
|
||||
* Version 6.4.0 (released 2024-02-20)
|
||||
** UI: Major UI overhaul, with improvements including:
|
||||
*** Add new UI layouts for wider windows to better utilize screen space.
|
||||
*** Add YubiKey personalization through custom naming and theme color.
|
||||
*** Split FIDO/WebAuthn into multiple sections.
|
||||
*** Move factory reset functionality into a single dialog, from the individual sections.
|
||||
** Add support for Yubico OTP provisioning.
|
||||
** PIV: Display more information about keys and certificates.
|
||||
** PIV: Add output format for public key when generating keys.
|
||||
** Desktop: Window hidden/shown state no longer saved when closing the app,
|
||||
use --hidden to start the app in a hidden to systray state.
|
||||
** Desktop: Fix FIDO reset over NFC.
|
||||
** Windows: Add option to launch Windows Settings for FIDO management.
|
||||
** Android: Increase read timeout for NFC, improving compatibility with older YubiKeys.
|
||||
|
||||
* Version 6.3.1 (released 2023-12-12)
|
||||
** Add command line options: --hidden/--shown, --log-file FILE.
|
||||
** Disable autocorrect in text fields.
|
||||
** Improve UI for toggling USB interfaces on YubiKey <= 4.
|
||||
** OATH: Fix displaying of credentials with empty (but not null) issuer.
|
||||
** PIV: Fix "Save" button not updating when changing Management Key.
|
||||
** FIDO: Improved handling of forcePinChange.
|
||||
** Add keyboard shortcut to calculate new code (Ctrl/Cmd + R).
|
||||
** Android: Support loading QR code from file.
|
||||
** Android: Add a "Do nothing" option to NFC tap.
|
||||
** Android: Improve handing of USB device removal.
|
||||
|
||||
* 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.
|
||||
@ -66,7 +113,7 @@
|
||||
** Bugfix: Show firmware version for YubiKey NEO correctly
|
||||
** Windows: Show correct version number in .msi installers
|
||||
** macOS: Fix issue with window positioning
|
||||
** macOS: Fix occacional crashes on startup
|
||||
** macOS: Fix occasional crashes on startup
|
||||
** Linux: Fix the app icon and desktop entry for the Snap package.
|
||||
|
||||
* Version 5.0.3 (released 2020-04-14)
|
||||
@ -101,7 +148,7 @@ to uninstall the older version before using the .msi
|
||||
** Feature: Select favorite credentials, available from the System Tray/Menu Bar
|
||||
** Show some information about the connected YubiKey, such as firmware version and serial number
|
||||
** Add experimental support for external smart card readers, enabling the use of a YubiKey over NFC
|
||||
** Add initial accessability support
|
||||
** Add initial accessibility support
|
||||
|
||||
* Version 4.3.6b (released 2019-06-11)
|
||||
** Fixes problem where YubiKey was not being detected on macOS.
|
||||
@ -226,7 +273,7 @@ to uninstall the older version before using the .msi
|
||||
|
||||
* Version 4.0.1 (released 2017-03-27)
|
||||
** Bugfix: Follow color schemes better.
|
||||
** Removed some spacing thath caused the layout to be slightly off.
|
||||
** Removed some spacing that caused the layout to be slightly off.
|
||||
** Improved focus switching between search bar and credentials.
|
||||
** Added keyboard shortcut (Ctrl/Cmd + F) for focus on the search bar.
|
||||
** Select the top credential during search.
|
||||
|
68
README.adoc
68
README.adoc
@ -2,39 +2,28 @@
|
||||
|
||||
image:splash.png[]
|
||||
|
||||
Store your unique credential on a hardware-backed security key and take it
|
||||
wherever you go from mobile to desktop. No more storing sensitive secrets on
|
||||
your mobile phone, leaving your account vulnerable to takeovers. With the
|
||||
Yubico Authenticator you can raise the bar for security.
|
||||
|
||||
* The Yubico Authenticator will work with any USB or NFC-enabled YubiKeys
|
||||
|
||||
The Yubico Authenticator securely generates a code used to verify your identity
|
||||
as you are logging into various services. No connectivity needed!
|
||||
Manage your YubiKey and access one-time passwords with this full-featured
|
||||
companion app to the YubiKey.
|
||||
|
||||
=== Features include
|
||||
* Secure - Hardware-backed strong two-factor authentication with secret stored
|
||||
on the YubiKey, not on your phone or computer
|
||||
* Portable - Get the same set of codes across our other Yubico Authenticator
|
||||
apps for desktops as well as for all leading mobile platforms
|
||||
* Flexible - Support for time-based and counter-based code generation
|
||||
* USB or NFC usage - Insert the YubiKey into the USB port, or use the YubiKey
|
||||
with NFC with a mobile phone that is NFC-enabled or a desktop NFC reader to
|
||||
store your credential on the YubiKey
|
||||
* Easy Setup - QR codes available from the services you wish to protect with
|
||||
strong authentication
|
||||
* User Presence - Require a touch on the YubiKey sensor to generate new codes
|
||||
for sensitive accounts
|
||||
* Compatible - Secure all the services currently compatible with other
|
||||
Authenticator apps
|
||||
* Versatile - Support for multiple work and personal accounts
|
||||
* Display information about your YubiKey such as serial number, firmware version,
|
||||
and supported capabilities
|
||||
* Manage and access OATH one-time passwords stored securely on your YubiKey
|
||||
* Configure PIN, fingerprints, and manage passkeys for WebAuthn/FIDO
|
||||
* Configure PIN/PUK/Management key, and manage private keys and certificates for PIV
|
||||
* Provision Yubico OTP, static passwords, and other YubiKey slot-based credentials
|
||||
* Configure enabled features, and factory reset YubiKey data
|
||||
* Compatible with any USB or NFC-enabled YubiKey
|
||||
|
||||
Store your unique credential on a hardware-backed security key and take it
|
||||
wherever you go from mobile to desktop. No more storing sensitive secrets on
|
||||
your mobile phone, leaving your accounts vulnerable to takeovers. With the
|
||||
YubiKey and Yubico Authenticator you can raise the bar for security. No
|
||||
connectivity needed!
|
||||
|
||||
Experience security the modern way with the Yubico Authenticator.
|
||||
Visit https://yubico.com to learn more.
|
||||
|
||||
NOTE: Yubico Authenticator 6 uses a new codebase built using the Flutter
|
||||
framework. The previous Qt codebase can be found in the `legacy` branch.
|
||||
|
||||
=== Supported platforms
|
||||
|
||||
*Supported* - these are platforms we build and test on and commit to supporting.
|
||||
@ -45,8 +34,8 @@ framework. The previous Qt codebase can be found in the `legacy` branch.
|
||||
||Supported|Best-effort
|
||||
|
||||
|Windows
|
||||
|Windows 10 & above, 64-bit
|
||||
|Windows 8.1, 64-bit
|
||||
|Windows 10 & above, x64
|
||||
|Windows 10 & above, x64
|
||||
|
||||
|macOS
|
||||
|macOS 11 (Big Sur) & above
|
||||
@ -54,19 +43,34 @@ framework. The previous Qt codebase can be found in the `legacy` branch.
|
||||
|
||||
|Linux
|
||||
|Ubuntu 22.04 & above
|
||||
|Ubuntu 18.04 (or equivalent)
|
||||
|Ubuntu 20.04 (or equivalent)
|
||||
|
||||
|Android
|
||||
|Android 11 & above
|
||||
|Android 5 (Lollipop)
|
||||
|===
|
||||
|
||||
On Linux systems, make sure the `pcscd` service is installed and running.
|
||||
|
||||
=== Installation
|
||||
Downloads for all supported operating systems are available
|
||||
https://www.yubico.com/products/yubico-authenticator/[here].
|
||||
|
||||
==== Linux
|
||||
On Linux platforms you will need pcscd installed and running to be able to
|
||||
communicate with a YubiKey over the SmartCard interface. Additionally, you may
|
||||
need to set permissions for your user to access YubiKeys via the HID
|
||||
interfaces. The relevant permissions are described
|
||||
https://developers.yubico.com/yubikey-manager/Device_Permissions.html[here].
|
||||
|
||||
For some configurations running Wayland, copying an OTP to clipboard only works
|
||||
when the app has focus. If you are unable to reliably copy to clipboard from
|
||||
the systray icon, you can use a separate binary which take the payload to stdin
|
||||
by defining the environment variable `_YA_TRAY_CLIPBOARD`. Note that this must
|
||||
be an absolute path to a binary owned by root:root, and should not be
|
||||
world-writable.
|
||||
For example: `_YA_TRAY_CLIPBOARD=/usr/bin/wl-copy`.
|
||||
|
||||
NOTE: Only use a trusted binary, OTPs will be sent to this when copied to clipboard from the systray!
|
||||
|
||||
=== Command line interface
|
||||
Looking for a command line option? Try our
|
||||
https://github.com/Yubico/yubikey-manager/[YubiKey Manager CLI] tool.
|
||||
|
@ -22,10 +22,10 @@ linter:
|
||||
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
|
||||
# producing the lint.
|
||||
rules:
|
||||
# avoid_print: false # Uncomment to disable the `avoid_print` rule
|
||||
prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||
unawaited_futures: true # Explicitly mark futures which are not being awaited
|
||||
use_super_parameters: true
|
||||
- prefer_single_quotes
|
||||
- unawaited_futures # Explicitly mark futures which are not being awaited
|
||||
- directives_ordering # Force ordering of imports
|
||||
- prefer_relative_imports
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
@ -36,3 +36,8 @@ analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
- "**/*.freezed.dart"
|
||||
- "build/**/intermediates/**/AndroidManifest.xml"
|
||||
errors:
|
||||
invalid_annotation_target: ignore # see https://github.com/rrousselGit/freezed/issues/488
|
||||
plugins:
|
||||
- custom_lint
|
||||
|
@ -1,3 +1,11 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlinx-serialization'
|
||||
id 'dev.flutter.flutter-gradle-plugin'
|
||||
id 'com.google.android.gms.oss-licenses-plugin'
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
if (localPropertiesFile.exists()) {
|
||||
@ -6,31 +14,16 @@ if (localPropertiesFile.exists()) {
|
||||
}
|
||||
}
|
||||
|
||||
def flutterRoot = localProperties.getProperty('flutter.sdk')
|
||||
if (flutterRoot == null) {
|
||||
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
|
||||
}
|
||||
|
||||
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
|
||||
if (flutterVersionCode == null) {
|
||||
//noinspection GroovyUnusedAssignment
|
||||
flutterVersionCode = '1'
|
||||
}
|
||||
|
||||
def flutterVersionName = localProperties.getProperty('flutter.versionName')
|
||||
if (flutterVersionName == null) {
|
||||
//noinspection GroovyUnusedAssignment
|
||||
flutterVersionName = '1.0'
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlinx-serialization'
|
||||
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
|
||||
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
|
||||
|
||||
import com.android.build.OutputFile
|
||||
|
||||
android {
|
||||
|
||||
namespace 'com.yubico.authenticator'
|
||||
@ -58,7 +51,6 @@ android {
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
@ -78,7 +70,7 @@ android {
|
||||
applicationVariants.all { variant ->
|
||||
variant.outputs.each { output ->
|
||||
def abiCodes = ['armeabi-v7a': 1, 'arm64-v8a': 2, x86: 3, x86_64: 4]
|
||||
def abiCode = abiCodes.get(output.getFilter(OutputFile.ABI))
|
||||
def abiCode = abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI))
|
||||
output.versionCodeOverride = variant.versionCode * 10 + (abiCode != null ? abiCode : 0)
|
||||
}
|
||||
}
|
||||
@ -99,17 +91,16 @@ dependencies {
|
||||
api "com.yubico.yubikit:fido:$project.yubiKitVersion"
|
||||
api "com.yubico.yubikit:support:$project.yubiKitVersion"
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0'
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3'
|
||||
|
||||
// Lifecycle
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.3'
|
||||
|
||||
implementation "androidx.core:core-ktx:1.12.0"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.6.1'
|
||||
implementation "androidx.core:core-ktx:1.13.1"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.8.1'
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
|
||||
implementation 'com.google.android.material:material:1.10.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
|
||||
implementation 'com.github.tony19:logback-android:3.0.0'
|
||||
|
||||
|
@ -61,14 +61,14 @@ def collectLicenses(File rootDir, File ossPluginResDir, File outDir) {
|
||||
|
||||
// add zxing_licenses which are not detected
|
||||
println "adding zxing licenses"
|
||||
licenseList.add(PackageName: "ZXing Core (3.3.0)", PackageLicense: "https://www.apache.org/licenses/LICENSE-2.0.txt")
|
||||
licenseList.add(PackageName: "ZXing Core (3.5.2)", PackageLicense: "https://www.apache.org/licenses/LICENSE-2.0.txt")
|
||||
licenseList.add(PackageName: "ZXing Android Core (3.3.0)", PackageLicense: "https://www.apache.org/licenses/LICENSE-2.0.txt")
|
||||
|
||||
outFile.write(new JsonOutput().toJson(licenseList))
|
||||
println "Created ${outFile.absolutePath}"
|
||||
|
||||
// copy license assets to flutter resources
|
||||
def licensesDir = new File(rootDir, "licenses/");
|
||||
def licensesDir = new File(rootDir, "licenses/")
|
||||
copy {
|
||||
from(licensesDir.absolutePath) {
|
||||
include "**/*txt"
|
||||
@ -82,7 +82,7 @@ def collectLicenses(File rootDir, File ossPluginResDir, File outDir) {
|
||||
metadata.delete()
|
||||
}
|
||||
|
||||
task collectLicenses() {
|
||||
tasks.register('collectLicenses') {
|
||||
dependsOn(":app:releaseOssLicensesTask")
|
||||
doLast {
|
||||
def ossPluginResDir = new File(project.buildDir, "generated/third_party_licenses/release/res/raw/")
|
||||
|
@ -37,7 +37,7 @@ class AppContext(messenger: BinaryMessenger, coroutineScope: CoroutineScope, pri
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setContext(subPageIndex: Int): String {
|
||||
private fun setContext(subPageIndex: Int): String {
|
||||
val appContext = OperationContext.getByValue(subPageIndex)
|
||||
appViewModel.setAppContext(appContext)
|
||||
logger.debug("App context is now {}", appContext)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -21,7 +21,10 @@ import com.yubico.yubikit.core.YubiKeyDevice
|
||||
/**
|
||||
* Provides behavior to run when a YubiKey is inserted/tapped for a specific view of the app.
|
||||
*/
|
||||
interface AppContextManager {
|
||||
suspend fun processYubiKey(device: YubiKeyDevice)
|
||||
fun dispose()
|
||||
abstract class AppContextManager {
|
||||
abstract suspend fun processYubiKey(device: YubiKeyDevice)
|
||||
|
||||
open fun dispose() {}
|
||||
|
||||
open fun onPause() {}
|
||||
}
|
@ -33,6 +33,9 @@ class AppPreferences(context: Context) {
|
||||
|
||||
const val PREF_CLIP_KBD_LAYOUT = "flutter.prefClipKbdLayout"
|
||||
const val DEFAULT_CLIP_KBD_LAYOUT = "US"
|
||||
|
||||
const val PREF_ENABLE_COMMUNITY_TRANSLATIONS =
|
||||
"flutter.APP_STATE_ENABLE_COMMUNITY_TRANSLATIONS"
|
||||
}
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AppPreferences::class.java)
|
||||
@ -66,6 +69,9 @@ class AppPreferences(context: Context) {
|
||||
val openAppOnUsb: Boolean
|
||||
get() = prefs.getBoolean(PREF_USB_OPEN_APP, false)
|
||||
|
||||
val communityTranslationsEnabled: Boolean
|
||||
get() = prefs.getBoolean(PREF_ENABLE_COMMUNITY_TRANSLATIONS, false)
|
||||
|
||||
fun registerListener(listener: OnSharedPreferenceChangeListener) {
|
||||
logger.debug("registering change listener")
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -32,6 +32,16 @@ import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
interface JsonSerializable {
|
||||
fun toJson() : String
|
||||
}
|
||||
|
||||
sealed interface ViewModelData {
|
||||
data object Empty : ViewModelData
|
||||
data object Loading : ViewModelData
|
||||
data class Value<T : JsonSerializable>(val data: T) : ViewModelData
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a LiveData value, sending each change to Flutter via an EventChannel.
|
||||
*/
|
||||
@ -61,6 +71,48 @@ inline fun <reified T> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, mess
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes a ViewModelData LiveData value, sending each change to Flutter via an EventChannel.
|
||||
*/
|
||||
@JvmName("streamViewModelData")
|
||||
inline fun <reified T : ViewModelData> LiveData<T>.streamTo(lifecycleOwner: LifecycleOwner, messenger: BinaryMessenger, channelName: String): Closeable {
|
||||
val channel = EventChannel(messenger, channelName)
|
||||
var sink: EventChannel.EventSink? = null
|
||||
|
||||
val get: (ViewModelData) -> String = {
|
||||
when (it) {
|
||||
is ViewModelData.Empty -> NULL
|
||||
is ViewModelData.Loading -> LOADING
|
||||
is ViewModelData.Value<*> -> it.data.toJson()
|
||||
}
|
||||
}
|
||||
|
||||
channel.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
sink = events
|
||||
events.success(
|
||||
value?.let {
|
||||
get(it)
|
||||
} ?: NULL
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
sink = null
|
||||
}
|
||||
})
|
||||
|
||||
val observer = Observer<T> {
|
||||
sink?.success(get(it))
|
||||
}
|
||||
observe(lifecycleOwner, observer)
|
||||
|
||||
return Closeable {
|
||||
removeObserver(observer)
|
||||
channel.setStreamHandler(null)
|
||||
}
|
||||
}
|
||||
|
||||
typealias MethodHandler = suspend (method: String, args: Map<String, Any?>) -> String
|
||||
|
||||
/**
|
||||
|
@ -37,7 +37,7 @@ import android.os.Build
|
||||
* @param sdkVersion the version this instance uses for compatibility checking. The release app
|
||||
* uses `Build.VERSION.SDK_INT`, tests use appropriate other values.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate", "unused")
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class CompatUtil(private val sdkVersion: Int) {
|
||||
/**
|
||||
* Wrapper class holding values computed by [CompatUtil]
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -20,6 +20,8 @@ import kotlinx.serialization.json.Json
|
||||
|
||||
const val NULL = "null"
|
||||
|
||||
const val LOADING = "\"loading\""
|
||||
|
||||
val jsonSerializer = Json {
|
||||
// creates properties for default values
|
||||
encodeDefaults = true
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -43,13 +43,19 @@ import androidx.activity.viewModels
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.FidoManager
|
||||
import com.yubico.authenticator.fido.FidoViewModel
|
||||
import com.yubico.authenticator.logging.FlutterLog
|
||||
import com.yubico.authenticator.management.ManagementHandler
|
||||
import com.yubico.authenticator.oath.AppLinkMethodChannel
|
||||
import com.yubico.authenticator.oath.OathManager
|
||||
import com.yubico.authenticator.oath.OathViewModel
|
||||
import com.yubico.authenticator.yubikit.NfcActivityDispatcher
|
||||
import com.yubico.authenticator.yubikit.NfcActivityListener
|
||||
import com.yubico.authenticator.yubikit.NfcActivityState
|
||||
import com.yubico.authenticator.yubikit.getDeviceInfo
|
||||
import com.yubico.yubikit.android.YubiKitManager
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcConfiguration
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcNotAvailable
|
||||
@ -72,8 +78,9 @@ import java.util.concurrent.Executors
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
private val viewModel: MainViewModel by viewModels()
|
||||
private val oathViewModel: OathViewModel by viewModels()
|
||||
private val fidoViewModel: FidoViewModel by viewModels()
|
||||
|
||||
private val nfcConfiguration = NfcConfiguration()
|
||||
private val nfcConfiguration = NfcConfiguration().timeout(2000)
|
||||
|
||||
private var hasNfc: Boolean = false
|
||||
|
||||
@ -129,9 +136,13 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
logger.debug("Starting nfc discovery")
|
||||
yubikit.startNfcDiscovery(
|
||||
nfcConfiguration.disableNfcDiscoverySound(appPreferences.silenceNfcSounds),
|
||||
this,
|
||||
::processYubiKey
|
||||
)
|
||||
this
|
||||
) { nfcYubiKeyDevice ->
|
||||
if (!deviceManager.isUsbKeyConnected()) {
|
||||
launchProcessYubiKey(nfcYubiKeyDevice)
|
||||
}
|
||||
}
|
||||
|
||||
hasNfc = true
|
||||
} catch (e: NfcNotAvailable) {
|
||||
hasNfc = false
|
||||
@ -152,7 +163,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
logger.debug("YubiKey was disconnected, stopping usb discovery")
|
||||
stopUsbDiscovery()
|
||||
}
|
||||
processYubiKey(device)
|
||||
launchProcessYubiKey(device)
|
||||
}
|
||||
}
|
||||
|
||||
@ -186,6 +197,8 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
contextManager?.onPause()
|
||||
|
||||
appPreferences.unregisterListener(sharedPreferencesListener)
|
||||
|
||||
if (!preserveConnectionOnPause) {
|
||||
@ -233,7 +246,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
val device = NfcYubiKeyDevice(tag, nfcConfiguration.timeout, executor)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
contextManager?.processYubiKey(device)
|
||||
processYubiKey(device)
|
||||
device.remove {
|
||||
executor.shutdown()
|
||||
startNfcDiscovery()
|
||||
@ -246,7 +259,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
startNfcDiscovery()
|
||||
}
|
||||
|
||||
val usbManager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||
val usbManager = getSystemService(USB_SERVICE) as UsbManager
|
||||
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) {
|
||||
val device = intent.parcelableExtra<UsbDevice>(UsbManager.EXTRA_DEVICE)
|
||||
if (device != null) {
|
||||
@ -288,24 +301,53 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun processYubiKey(device: YubiKeyDevice) {
|
||||
private suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
val deviceInfo = getDeviceInfo(device)
|
||||
deviceManager.setDeviceInfo(deviceInfo)
|
||||
|
||||
if (deviceInfo == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val supportedContexts = DeviceManager.getSupportedContexts(deviceInfo)
|
||||
logger.debug("Connected key supports: {}", supportedContexts)
|
||||
if (!supportedContexts.contains(viewModel.appContext.value)) {
|
||||
val preferredContext = DeviceManager.getPreferredContext(supportedContexts)
|
||||
logger.debug(
|
||||
"Current context ({}) is not supported by the key. Using preferred context {}",
|
||||
viewModel.appContext.value,
|
||||
preferredContext
|
||||
)
|
||||
switchContext(preferredContext)
|
||||
}
|
||||
|
||||
if (contextManager == null) {
|
||||
switchContext(DeviceManager.getPreferredContext(supportedContexts))
|
||||
}
|
||||
|
||||
contextManager?.let {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
it.processYubiKey(device)
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
device.remove {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
|
||||
}
|
||||
try {
|
||||
it.processYubiKey(device)
|
||||
if (device is NfcYubiKeyDevice) {
|
||||
device.remove {
|
||||
appMethodChannel.nfcActivityStateChanged(NfcActivityState.READY)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error("Error processing YubiKey in AppContextManager", e)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logger.error("Error processing YubiKey in AppContextManager", e)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchProcessYubiKey(device: YubiKeyDevice) {
|
||||
lifecycleScope.launch {
|
||||
processYubiKey(device)
|
||||
}
|
||||
}
|
||||
|
||||
private var contextManager: AppContextManager? = null
|
||||
private lateinit var deviceManager: DeviceManager
|
||||
private lateinit var appContext: AppContext
|
||||
private lateinit var dialogManager: DialogManager
|
||||
private lateinit var appPreferences: AppPreferences
|
||||
@ -313,18 +355,21 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
private lateinit var flutterStreams: List<Closeable>
|
||||
private lateinit var appMethodChannel: AppMethodChannel
|
||||
private lateinit var appLinkMethodChannel: AppLinkMethodChannel
|
||||
private lateinit var messenger: BinaryMessenger
|
||||
private lateinit var managementHandler: ManagementHandler
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
flutterLog = FlutterLog(messenger)
|
||||
deviceManager = DeviceManager(this, viewModel)
|
||||
appContext = AppContext(messenger, this.lifecycleScope, viewModel)
|
||||
dialogManager = DialogManager(messenger, this.lifecycleScope)
|
||||
appPreferences = AppPreferences(this)
|
||||
appMethodChannel = AppMethodChannel(messenger)
|
||||
appLinkMethodChannel = AppLinkMethodChannel(messenger)
|
||||
managementHandler = ManagementHandler(messenger, deviceManager, dialogManager)
|
||||
|
||||
nfcActivityListener.appMethodChannel = appMethodChannel
|
||||
|
||||
@ -332,29 +377,67 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
viewModel.deviceInfo.streamTo(this, messenger, "android.devices.deviceInfo"),
|
||||
oathViewModel.sessionState.streamTo(this, messenger, "android.oath.sessionState"),
|
||||
oathViewModel.credentials.streamTo(this, messenger, "android.oath.credentials"),
|
||||
fidoViewModel.sessionState.streamTo(this, messenger, "android.fido.sessionState"),
|
||||
fidoViewModel.credentials.streamTo(this, messenger, "android.fido.credentials"),
|
||||
fidoViewModel.fingerprints.streamTo(this, messenger, "android.fido.fingerprints"),
|
||||
fidoViewModel.resetState.streamTo(this, messenger, "android.fido.reset"),
|
||||
fidoViewModel.registerFingerprint.streamTo(this, messenger, "android.fido.registerFp"),
|
||||
)
|
||||
|
||||
viewModel.appContext.observe(this) {
|
||||
switchContext(it)
|
||||
viewModel.connectedYubiKey.value?.let(::launchProcessYubiKey)
|
||||
}
|
||||
}
|
||||
|
||||
private fun switchContext(appContext: OperationContext) {
|
||||
// TODO: refactor this when more OperationContext are handled
|
||||
// only recreate the contextManager object if it cannot be reused
|
||||
if (appContext == OperationContext.Home ||
|
||||
(appContext == OperationContext.Oath && contextManager is OathManager) ||
|
||||
(appContext in listOf(
|
||||
OperationContext.FidoPasskeys,
|
||||
OperationContext.FidoFingerprints
|
||||
) && contextManager is FidoManager)
|
||||
) {
|
||||
// no need to dispose this context
|
||||
} else {
|
||||
contextManager?.dispose()
|
||||
contextManager = when (it) {
|
||||
contextManager = null
|
||||
}
|
||||
|
||||
if (contextManager == null) {
|
||||
contextManager = when (appContext) {
|
||||
OperationContext.Oath -> OathManager(
|
||||
this,
|
||||
messenger,
|
||||
viewModel,
|
||||
deviceManager,
|
||||
oathViewModel,
|
||||
dialogManager,
|
||||
appPreferences,
|
||||
nfcActivityListener
|
||||
)
|
||||
|
||||
OperationContext.FidoFingerprints,
|
||||
OperationContext.FidoPasskeys -> FidoManager(
|
||||
messenger,
|
||||
this,
|
||||
deviceManager,
|
||||
fidoViewModel,
|
||||
viewModel,
|
||||
dialogManager
|
||||
)
|
||||
|
||||
else -> null
|
||||
}
|
||||
viewModel.connectedYubiKey.value?.let(::processYubiKey)
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
nfcActivityListener.appMethodChannel = null
|
||||
flutterStreams.forEach { it.close() }
|
||||
contextManager?.dispose()
|
||||
deviceManager.dispose()
|
||||
super.cleanUpFlutterEngine(flutterEngine)
|
||||
}
|
||||
|
||||
@ -420,6 +503,11 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
methodCall.arguments as Boolean,
|
||||
)
|
||||
)
|
||||
|
||||
"getPrimaryColor" -> result.success(
|
||||
getPrimaryColor(this@MainActivity)
|
||||
)
|
||||
|
||||
"getAndroidSdkVersion" -> result.success(
|
||||
Build.VERSION.SDK_INT
|
||||
)
|
||||
@ -445,7 +533,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
"hasCamera" -> {
|
||||
val cameraService =
|
||||
getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
||||
getSystemService(CAMERA_SERVICE) as CameraManager
|
||||
result.success(
|
||||
cameraService.cameraIdList.any {
|
||||
cameraService.getCameraCharacteristics(it)
|
||||
@ -467,6 +555,7 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
startActivity(Intent(ACTION_NFC_SETTINGS))
|
||||
result.success(true)
|
||||
}
|
||||
|
||||
else -> logger.warn("Unknown app method: {}", methodCall.method)
|
||||
}
|
||||
}
|
||||
@ -502,6 +591,30 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
return FLAG_SECURE != (window.attributes.flags and FLAG_SECURE)
|
||||
}
|
||||
|
||||
private fun getPrimaryColor(context: Context): Int? {
|
||||
if (DynamicColors.isDynamicColorAvailable()) {
|
||||
val dynamicColorContext = DynamicColors.wrapContextIfAvailable(
|
||||
context,
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_DynamicColors_DayNight
|
||||
)
|
||||
|
||||
val typedArray = dynamicColorContext.obtainStyledAttributes(
|
||||
intArrayOf(
|
||||
android.R.attr.colorPrimary,
|
||||
)
|
||||
)
|
||||
try {
|
||||
return if (typedArray.hasValue(0))
|
||||
typedArray.getColor(0, 0)
|
||||
else
|
||||
null
|
||||
} finally {
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
private fun forcePortraitOrientation() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
@ -511,5 +624,5 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
|
||||
private fun isPortraitOnly() = resources.getBoolean(R.bool.portrait_only);
|
||||
private fun isPortraitOnly() = resources.getBoolean(R.bool.portrait_only)
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -23,10 +23,20 @@ import com.yubico.authenticator.device.Info
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
|
||||
enum class OperationContext(val value: Int) {
|
||||
Oath(0), Yubikey(1), Invalid(-1);
|
||||
Home(0),
|
||||
Oath(1),
|
||||
FidoU2f(2),
|
||||
FidoFingerprints(3),
|
||||
FidoPasskeys(4),
|
||||
YubiOtp(5),
|
||||
Piv(6),
|
||||
OpenPgp(7),
|
||||
HsmAuth(8),
|
||||
Management(9),
|
||||
Invalid(-1);
|
||||
|
||||
companion object {
|
||||
fun getByValue(value: Int) = values().firstOrNull { it.value == value } ?: Invalid
|
||||
fun getByValue(value: Int) = entries.firstOrNull { it.value == value } ?: Invalid
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,14 +45,14 @@ class MainViewModel : ViewModel() {
|
||||
val appContext: LiveData<OperationContext> = _appContext
|
||||
fun setAppContext(appContext: OperationContext) {
|
||||
// Don't reset the context unless it actually changes
|
||||
if(appContext != _appContext.value) {
|
||||
if (appContext != _appContext.value) {
|
||||
_appContext.postValue(appContext)
|
||||
}
|
||||
}
|
||||
|
||||
private val _connectedYubiKey = MutableLiveData<UsbYubiKeyDevice?>()
|
||||
val connectedYubiKey: LiveData<UsbYubiKeyDevice?> = _connectedYubiKey
|
||||
fun setConnectedYubiKey(device: UsbYubiKeyDevice, onDisconnect: () -> Unit ) {
|
||||
fun setConnectedYubiKey(device: UsbYubiKeyDevice, onDisconnect: () -> Unit) {
|
||||
_connectedYubiKey.postValue(device)
|
||||
device.setOnClosed {
|
||||
_connectedYubiKey.postValue(null)
|
||||
|
@ -16,6 +16,7 @@
|
||||
|
||||
package com.yubico.authenticator
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.nfc.NdefMessage
|
||||
@ -24,13 +25,12 @@ import android.nfc.Tag
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
|
||||
import com.yubico.authenticator.ndef.KeyboardLayout
|
||||
import com.yubico.yubikit.core.util.NdefUtils
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
typealias ResourceId = Int
|
||||
|
||||
@ -39,6 +39,10 @@ class NdefActivity : Activity() {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(NdefActivity::class.java)
|
||||
|
||||
companion object {
|
||||
private val officialLocalization = arrayOf(Locale.JAPAN, Locale.FRANCE, Locale.US)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
appPreferences = AppPreferences(this)
|
||||
@ -66,8 +70,8 @@ class NdefActivity : Activity() {
|
||||
compatUtil.until(Build.VERSION_CODES.TIRAMISU) {
|
||||
showToast(
|
||||
when (otpSlotContent.type) {
|
||||
OtpType.Otp -> R.string.otp_success_set_otp_to_clipboard
|
||||
OtpType.Password -> R.string.otp_success_set_password_to_clipboard
|
||||
OtpType.Otp -> R.string.p_ndef_set_otp
|
||||
OtpType.Password -> R.string.p_ndef_set_password
|
||||
}, Toast.LENGTH_SHORT
|
||||
)
|
||||
}
|
||||
@ -77,16 +81,19 @@ class NdefActivity : Activity() {
|
||||
illegalArgumentException.message ?: "Failure when handling YubiKey OTP",
|
||||
illegalArgumentException
|
||||
)
|
||||
showToast(R.string.otp_parse_failure, Toast.LENGTH_LONG)
|
||||
showToast(R.string.p_ndef_parse_failure, Toast.LENGTH_LONG)
|
||||
} catch (_: UnsupportedOperationException) {
|
||||
showToast(R.string.otp_set_clip_failure, Toast.LENGTH_LONG)
|
||||
showToast(R.string.p_ndef_set_clip_failure, Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
if (appPreferences.openAppOnNfcTap) {
|
||||
val mainAppIntent = Intent(this, MainActivity::class.java).apply {
|
||||
// Pass the NFC Tag to the main Activity.
|
||||
putExtra(NfcAdapter.EXTRA_TAG, intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG))
|
||||
putExtra(
|
||||
NfcAdapter.EXTRA_TAG,
|
||||
intent.parcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
|
||||
)
|
||||
}
|
||||
startActivity(mainAppIntent)
|
||||
}
|
||||
@ -96,9 +103,32 @@ class NdefActivity : Activity() {
|
||||
}
|
||||
|
||||
private fun showToast(value: ResourceId, length: Int) {
|
||||
Toast.makeText(this, value, length).show()
|
||||
val context = if (appPreferences.communityTranslationsEnabled)
|
||||
this
|
||||
else {
|
||||
val configuration = resources.configuration
|
||||
configuration.setLocale(getLocale())
|
||||
createConfigurationContext(configuration)
|
||||
}
|
||||
Toast.makeText(context, value, length).show()
|
||||
}
|
||||
|
||||
private fun getLocale() : Locale =
|
||||
compatUtil.from(Build.VERSION_CODES.N) {
|
||||
getLocaleN()
|
||||
}.otherwise {
|
||||
@Suppress("deprecation")
|
||||
officialLocalization.firstOrNull {
|
||||
it == resources.configuration.locale
|
||||
} ?: Locale.US
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
private fun getLocaleN() : Locale =
|
||||
resources.configuration.locales.getFirstMatch(
|
||||
officialLocalization.map { it.toLanguageTag() }.toTypedArray()
|
||||
) ?: Locale.US
|
||||
|
||||
private fun parseOtpFromIntent(): OtpSlotValue {
|
||||
val parcelable = intent.parcelableArrayExtra<NdefMessage>(NfcAdapter.EXTRA_NDEF_MESSAGES)
|
||||
requireNotNull(parcelable) { "Null NDEF message" }
|
||||
|
@ -0,0 +1,175 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.device
|
||||
|
||||
import androidx.collection.ArraySet
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.OperationContext
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
interface DeviceListener {
|
||||
// a USB device is connected
|
||||
fun onConnected(device: YubiKeyDevice) {}
|
||||
|
||||
// a USB device is disconnected
|
||||
fun onDisconnected() {}
|
||||
|
||||
// the app has been paused for more than DeviceManager.NFC_DATA_CLEANUP_DELAY
|
||||
fun onTimeout() {}
|
||||
}
|
||||
|
||||
class DeviceManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val appViewModel: MainViewModel
|
||||
) {
|
||||
var clearDeviceInfoOnDisconnect: Boolean = true
|
||||
|
||||
private val deviceListeners = HashSet<DeviceListener>()
|
||||
|
||||
fun addDeviceListener(listener: DeviceListener) {
|
||||
deviceListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeDeviceListener(listener: DeviceListener) {
|
||||
deviceListeners.remove(listener)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
|
||||
private val logger = LoggerFactory.getLogger(DeviceManager::class.java)
|
||||
|
||||
private val capabilityContextMap = mapOf(
|
||||
Capability.OATH to listOf(OperationContext.Oath),
|
||||
Capability.FIDO2 to listOf(
|
||||
OperationContext.FidoFingerprints,
|
||||
OperationContext.FidoPasskeys
|
||||
)
|
||||
)
|
||||
|
||||
fun getSupportedContexts(deviceInfo: Info): ArraySet<OperationContext> {
|
||||
val operationContexts = ArraySet<OperationContext>()
|
||||
|
||||
val capabilities = (
|
||||
if (deviceInfo.isNfc)
|
||||
deviceInfo.config.enabledCapabilities.nfc else
|
||||
deviceInfo.config.enabledCapabilities.usb
|
||||
) ?: 0
|
||||
|
||||
capabilityContextMap.forEach { entry ->
|
||||
if (capabilities and entry.key.bit == entry.key.bit) {
|
||||
operationContexts.addAll(entry.value)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Device supports following contexts: {}", operationContexts)
|
||||
return operationContexts
|
||||
}
|
||||
|
||||
fun getPreferredContext(contexts: ArraySet<OperationContext>): OperationContext {
|
||||
// custom sort
|
||||
for (context in contexts) {
|
||||
if (context == OperationContext.Oath) {
|
||||
return context
|
||||
} else if (context == OperationContext.FidoPasskeys) {
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
return OperationContext.Oath
|
||||
}
|
||||
}
|
||||
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
private var startTimeMs: Long = -1
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
startTimeMs = currentTimeMs
|
||||
super.onPause(owner)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
if (canInvoke) {
|
||||
if (appViewModel.connectedYubiKey.value == null) {
|
||||
// no USB YubiKey is connected, reset known data on resume
|
||||
logger.debug("Removing NFC data after resume.")
|
||||
if (clearDeviceInfoOnDisconnect) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
}
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val currentTimeMs
|
||||
get() = System.currentTimeMillis()
|
||||
|
||||
private val canInvoke: Boolean
|
||||
get() = startTimeMs != -1L && currentTimeMs - startTimeMs > NFC_DATA_CLEANUP_DELAY
|
||||
}
|
||||
|
||||
private val usbObserver = Observer<UsbYubiKeyDevice?> { yubiKeyDevice ->
|
||||
if (yubiKeyDevice == null) {
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onDisconnected()
|
||||
}
|
||||
if (clearDeviceInfoOnDisconnect) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
}
|
||||
} else {
|
||||
deviceListeners.forEach { listener ->
|
||||
listener.onConnected(yubiKeyDevice)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
}
|
||||
|
||||
fun setDeviceInfo(deviceInfo: Info?) {
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
}
|
||||
|
||||
fun isUsbKeyConnected(): Boolean {
|
||||
return appViewModel.connectedYubiKey.value != null
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
}
|
||||
|
||||
suspend fun <T> withKey(onNfc: suspend () -> T, onUsb: suspend (UsbYubiKeyDevice) -> T) =
|
||||
appViewModel.connectedYubiKey.value?.let {
|
||||
onUsb(it)
|
||||
} ?: onNfc()
|
||||
}
|
@ -49,8 +49,12 @@ data class Info(
|
||||
val isNfc: Boolean,
|
||||
@SerialName("usb_pid")
|
||||
val usbPid: Int?,
|
||||
@SerialName("pin_complexity")
|
||||
val pinComplexity: Boolean,
|
||||
@SerialName("supported_capabilities")
|
||||
val supportedCapabilities: Capabilities
|
||||
val supportedCapabilities: Capabilities,
|
||||
@SerialName("fips_capable")
|
||||
val fipsCapable: Int,
|
||||
) {
|
||||
constructor(name: String, isNfc: Boolean, usbPid: Int?, deviceInfo: DeviceInfo) : this(
|
||||
config = Config(deviceInfo.config),
|
||||
@ -63,9 +67,11 @@ data class Info(
|
||||
name = name,
|
||||
isNfc = isNfc,
|
||||
usbPid = usbPid,
|
||||
pinComplexity = deviceInfo.pinComplexity,
|
||||
supportedCapabilities = Capabilities(
|
||||
nfc = deviceInfo.capabilitiesFor(Transport.NFC),
|
||||
usb = deviceInfo.capabilitiesFor(Transport.USB),
|
||||
)
|
||||
),
|
||||
fipsCapable = deviceInfo.fipsCapable
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.yubico.authenticator.device
|
||||
|
||||
import com.yubico.yubikit.core.Transport
|
||||
import com.yubico.yubikit.management.Capability
|
||||
import com.yubico.yubikit.management.FormFactor
|
||||
|
||||
val UnknownDevice = Info(
|
||||
@ -18,5 +20,32 @@ val UnknownDevice = Info(
|
||||
name = "Unrecognized device",
|
||||
isNfc = false,
|
||||
usbPid = null,
|
||||
supportedCapabilities = Capabilities()
|
||||
)
|
||||
pinComplexity = false,
|
||||
supportedCapabilities = Capabilities(),
|
||||
fipsCapable = 0
|
||||
)
|
||||
|
||||
fun unknownDeviceWithCapability(transport: Transport, bit: Int = 0) : Info {
|
||||
val isNfc = transport == Transport.NFC
|
||||
val capabilities = Capabilities(
|
||||
nfc = if (isNfc) bit else null,
|
||||
usb = if (!isNfc) bit else null
|
||||
)
|
||||
return UnknownDevice.copy(
|
||||
isNfc = isNfc,
|
||||
config = UnknownDevice.config.copy(enabledCapabilities = capabilities),
|
||||
supportedCapabilities = capabilities
|
||||
)
|
||||
}
|
||||
|
||||
fun unknownOathDeviceInfo(transport: Transport) : Info {
|
||||
return unknownDeviceWithCapability(transport, Capability.OATH.bit).copy(
|
||||
name = "OATH device"
|
||||
)
|
||||
}
|
||||
|
||||
fun unknownFido2DeviceInfo(transport: Transport) : Info {
|
||||
return unknownDeviceWithCapability(transport, Capability.FIDO2.bit).copy(
|
||||
name = "FIDO2 device"
|
||||
)
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
const val dialogDescriptionFidoIndex = 200
|
||||
|
||||
enum class FidoActionDescription(private val value: Int) {
|
||||
Reset(0),
|
||||
Unlock(1),
|
||||
SetPin(2),
|
||||
DeleteCredential(3),
|
||||
DeleteFingerprint(4),
|
||||
RenameFingerprint(5),
|
||||
RegisterFingerprint(6),
|
||||
ActionFailure(7);
|
||||
|
||||
val id: Int
|
||||
get() = value + dialogDescriptionFidoIndex
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.DialogTitle
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.fido.FidoConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class FidoConnectionHelper(
|
||||
private val deviceManager: DeviceManager,
|
||||
private val dialogManager: DialogManager
|
||||
) {
|
||||
private var pendingAction: FidoAction? = null
|
||||
|
||||
fun invokePending(fidoSession: YubiKitFidoSession) {
|
||||
pendingAction?.let { action ->
|
||||
action.invoke(Result.success(fidoSession))
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelPending() {
|
||||
pendingAction?.let { action ->
|
||||
action.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun <T> useSession(
|
||||
actionDescription: FidoActionDescription,
|
||||
action: (YubiKitFidoSession) -> T
|
||||
): T {
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(actionDescription,action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionUsb(
|
||||
device: UsbYubiKeyDevice,
|
||||
block: (YubiKitFidoSession) -> T
|
||||
): T = device.withConnection<FidoConnection, T> {
|
||||
block(YubiKitFidoSession(it))
|
||||
}
|
||||
|
||||
suspend fun <T> useSessionNfc(
|
||||
actionDescription: FidoActionDescription,
|
||||
block: (YubiKitFidoSession) -> T
|
||||
): T {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
pendingAction = {
|
||||
outer.resumeWith(runCatching {
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
dialogManager.showDialog(
|
||||
DialogTitle.TapKey,
|
||||
actionDescription.id
|
||||
) {
|
||||
logger.debug("Cancelled Dialog {}", actionDescription.name)
|
||||
pendingAction?.invoke(Result.failure(CancellationException()))
|
||||
pendingAction = null
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
throw error
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(FidoConnectionHelper::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,616 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.yubico.authenticator.AppContextManager
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.asString
|
||||
import com.yubico.authenticator.device.DeviceListener
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.FidoCredential
|
||||
import com.yubico.authenticator.fido.data.FidoFingerprint
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
import com.yubico.authenticator.fido.data.SessionInfo
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.authenticator.setHandler
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyConnection
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.application.CommandState
|
||||
import com.yubico.yubikit.core.fido.CtapException
|
||||
import com.yubico.yubikit.core.fido.FidoConnection
|
||||
import com.yubico.yubikit.core.internal.Logger
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.fido.ctap.BioEnrollment
|
||||
import com.yubico.yubikit.fido.ctap.ClientPin
|
||||
import com.yubico.yubikit.fido.ctap.CredentialManagement
|
||||
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
|
||||
import com.yubico.yubikit.fido.ctap.FingerprintBioEnrollment
|
||||
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol
|
||||
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol
|
||||
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1
|
||||
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import org.json.JSONObject
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.util.Arrays
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
typealias FidoAction = (Result<YubiKitFidoSession, Exception>) -> Unit
|
||||
|
||||
class FidoManager(
|
||||
messenger: BinaryMessenger,
|
||||
lifecycleOwner: LifecycleOwner,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
mainViewModel: MainViewModel,
|
||||
dialogManager: DialogManager,
|
||||
) : AppContextManager(), DeviceListener {
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
private object HexCodec {
|
||||
fun bytesToHexString(bytes: ByteArray) : String = bytes.toHexString()
|
||||
fun hexStringToBytes(hex: String) : ByteArray = hex.hexToByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun getPreferredPinUvAuthProtocol(infoData: InfoData): PinUvAuthProtocol {
|
||||
val pinUvAuthProtocols = infoData.pinUvAuthProtocols
|
||||
val pinSupported = infoData.options["clientPin"] != null
|
||||
if (pinSupported) {
|
||||
for (protocol in pinUvAuthProtocols) {
|
||||
if (protocol == PinUvAuthProtocolV1.VERSION) {
|
||||
return PinUvAuthProtocolV1()
|
||||
}
|
||||
if (protocol == PinUvAuthProtocolV2.VERSION) {
|
||||
return PinUvAuthProtocolV2()
|
||||
}
|
||||
}
|
||||
}
|
||||
return PinUvAuthDummyProtocol()
|
||||
}
|
||||
}
|
||||
|
||||
private val connectionHelper = FidoConnectionHelper(deviceManager, dialogManager)
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
|
||||
private val fidoChannel = MethodChannel(messenger, "android.fido.methods")
|
||||
|
||||
private val logger = LoggerFactory.getLogger(FidoManager::class.java)
|
||||
|
||||
private val pinStore = FidoPinStore()
|
||||
|
||||
private var pinRetries : Int? = null
|
||||
|
||||
private val resetHelper =
|
||||
FidoResetHelper(
|
||||
lifecycleOwner,
|
||||
deviceManager,
|
||||
fidoViewModel,
|
||||
mainViewModel,
|
||||
connectionHelper,
|
||||
pinStore
|
||||
)
|
||||
|
||||
init {
|
||||
pinRetries = null
|
||||
|
||||
deviceManager.addDeviceListener(this)
|
||||
|
||||
fidoChannel.setHandler(coroutineScope) { method, args ->
|
||||
when (method) {
|
||||
"reset" -> resetHelper.reset()
|
||||
|
||||
"cancelReset" -> resetHelper.cancelReset()
|
||||
|
||||
"unlock" -> unlock(
|
||||
(args["pin"] as String).toCharArray()
|
||||
)
|
||||
|
||||
"setPin" -> setPin(
|
||||
(args["pin"] as String?)?.toCharArray(),
|
||||
(args["newPin"] as String).toCharArray(),
|
||||
)
|
||||
|
||||
"deleteCredential" -> deleteCredential(
|
||||
args["rpId"] as String,
|
||||
args["credentialId"] as String
|
||||
)
|
||||
|
||||
"deleteFingerprint" -> deleteFingerprint(
|
||||
args["templateId"] as String
|
||||
)
|
||||
|
||||
"renameFingerprint" -> renameFingerprint(
|
||||
args["templateId"] as String,
|
||||
args["name"] as String
|
||||
)
|
||||
|
||||
"registerFingerprint" -> registerFingerprint(
|
||||
args["name"] as String?,
|
||||
)
|
||||
|
||||
"cancelRegisterFingerprint" -> cancelRegisterFingerprint()
|
||||
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
super.dispose()
|
||||
deviceManager.removeDeviceListener(this)
|
||||
fidoChannel.setMethodCallHandler(null)
|
||||
fidoViewModel.clearSessionState()
|
||||
fidoViewModel.updateCredentials(null)
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
override suspend fun processYubiKey(device: YubiKeyDevice) {
|
||||
try {
|
||||
if (device.supportsConnection(FidoConnection::class.java)) {
|
||||
device.withConnection<FidoConnection, Unit> { connection ->
|
||||
processYubiKey(connection, device)
|
||||
}
|
||||
} else {
|
||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||
processYubiKey(connection, device)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// something went wrong, try to get DeviceInfo from any available connection type
|
||||
logger.error("Failure when processing YubiKey: ", e)
|
||||
|
||||
// Clear any cached FIDO state
|
||||
fidoViewModel.clearSessionState()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processYubiKey(connection: YubiKeyConnection, device: YubiKeyDevice) {
|
||||
val fidoSession =
|
||||
if (connection is FidoConnection) {
|
||||
YubiKitFidoSession(connection)
|
||||
} else {
|
||||
YubiKitFidoSession(connection as SmartCardConnection)
|
||||
}
|
||||
|
||||
val previousSession = fidoViewModel.currentSession()?.info
|
||||
val currentSession = SessionInfo(fidoSession.cachedInfo)
|
||||
logger.debug(
|
||||
"Previous session: {}, current session: {}",
|
||||
previousSession,
|
||||
currentSession
|
||||
)
|
||||
|
||||
val sameDevice = currentSession == previousSession
|
||||
|
||||
if (device is NfcYubiKeyDevice && (sameDevice || resetHelper.inProgress)) {
|
||||
connectionHelper.invokePending(fidoSession)
|
||||
} else {
|
||||
|
||||
if (!sameDevice) {
|
||||
// different key
|
||||
logger.debug("This is a different key than previous, invalidating the PIN token")
|
||||
pinStore.setPin(null)
|
||||
connectionHelper.cancelPending()
|
||||
if (resetHelper.inProgress) {
|
||||
logger.debug("Cannot reset this key")
|
||||
resetHelper.cancelReset()
|
||||
}
|
||||
}
|
||||
|
||||
val infoData = fidoSession.cachedInfo
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(infoData))
|
||||
|
||||
pinRetries = if (infoData.options["clientPin"] == true) clientPin.pinRetries.count else null
|
||||
|
||||
fidoViewModel.setSessionState(
|
||||
Session(infoData, pinStore.hasPin(), pinRetries)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPinPermissionsCM(fidoSession: YubiKitFidoSession): Int {
|
||||
return if (CredentialManagement.isSupported(fidoSession.cachedInfo))
|
||||
ClientPin.PIN_PERMISSION_CM else 0
|
||||
}
|
||||
|
||||
private fun getPinPermissionsBE(fidoSession: YubiKitFidoSession): Int {
|
||||
return if (BioEnrollment.isSupported(fidoSession.cachedInfo))
|
||||
ClientPin.PIN_PERMISSION_BE else 0
|
||||
}
|
||||
|
||||
private fun unlockSession(
|
||||
fidoSession: YubiKitFidoSession,
|
||||
clientPin: ClientPin,
|
||||
pin: CharArray
|
||||
): String {
|
||||
|
||||
val pinPermissionsCM = getPinPermissionsCM(fidoSession)
|
||||
val pinPermissionsBE = getPinPermissionsBE(fidoSession)
|
||||
val permissions = pinPermissionsCM or pinPermissionsBE
|
||||
|
||||
val token = if (permissions != 0) {
|
||||
clientPin.getPinToken(pin, permissions, null)
|
||||
} else {
|
||||
clientPin.getPinToken(pin, permissions, "yubico-authenticator.example.com")
|
||||
null
|
||||
}
|
||||
|
||||
pinStore.setPin(pin)
|
||||
|
||||
pinRetries = clientPin.pinRetries.count
|
||||
|
||||
fidoViewModel.setSessionState(
|
||||
Session(
|
||||
fidoSession.info,
|
||||
pinStore.hasPin(),
|
||||
pinRetries
|
||||
)
|
||||
)
|
||||
|
||||
token?.let {
|
||||
val credentials = getCredentials(fidoSession, clientPin, token)
|
||||
logger.debug("Creds: {}", credentials)
|
||||
fidoViewModel.updateCredentials(credentials)
|
||||
|
||||
if (pinPermissionsBE != 0) {
|
||||
val fingerprints = getFingerprints(fidoSession, clientPin, token)
|
||||
logger.debug("Fingerprints: {}", fingerprints)
|
||||
fidoViewModel.updateFingerprints(fingerprints)
|
||||
}
|
||||
}
|
||||
|
||||
return JSONObject(mapOf("success" to true)).toString()
|
||||
}
|
||||
|
||||
private fun catchPinErrors(
|
||||
fidoSession: YubiKitFidoSession,
|
||||
clientPin: ClientPin,
|
||||
block: () -> String
|
||||
): String =
|
||||
try {
|
||||
block()
|
||||
} catch (ctapException: CtapException) {
|
||||
if (ctapException.ctapError == CtapException.ERR_PIN_INVALID ||
|
||||
ctapException.ctapError == CtapException.ERR_PIN_BLOCKED ||
|
||||
ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED ||
|
||||
ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION
|
||||
) {
|
||||
pinStore.setPin(null)
|
||||
fidoViewModel.updateCredentials(null)
|
||||
pinRetries = clientPin.pinRetries.count
|
||||
|
||||
fidoViewModel.setSessionState(
|
||||
Session(
|
||||
fidoSession.info,
|
||||
pinStore.hasPin(),
|
||||
pinRetries
|
||||
)
|
||||
)
|
||||
|
||||
if (ctapException.ctapError == CtapException.ERR_PIN_POLICY_VIOLATION) {
|
||||
JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
"pinViolation" to true
|
||||
)
|
||||
).toString()
|
||||
} else {
|
||||
JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
"pinRetries" to pinRetries,
|
||||
"authBlocked" to (ctapException.ctapError == CtapException.ERR_PIN_AUTH_BLOCKED),
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
} else {
|
||||
throw ctapException
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun unlock(pin: CharArray): String =
|
||||
connectionHelper.useSession(FidoActionDescription.Unlock) { fidoSession ->
|
||||
|
||||
try {
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
catchPinErrors(fidoSession, clientPin) {
|
||||
unlockSession(fidoSession, clientPin, pin)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
// something failed, keep the session locked
|
||||
fidoViewModel.currentSession()?.let {
|
||||
fidoViewModel.setSessionState(it.copy(info = it.info, unlocked = false))
|
||||
}
|
||||
throw e
|
||||
} finally {
|
||||
Arrays.fill(pin, 0.toChar())
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOrChangePin(
|
||||
fidoSession: YubiKitFidoSession,
|
||||
clientPin: ClientPin,
|
||||
pin: CharArray?,
|
||||
newPin: CharArray
|
||||
) {
|
||||
val infoData = fidoSession.cachedInfo
|
||||
val hasPin = infoData.options["clientPin"] == true
|
||||
|
||||
if (hasPin) {
|
||||
clientPin.changePin(pin!!, newPin)
|
||||
} else {
|
||||
clientPin.setPin(newPin)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setPin(pin: CharArray?, newPin: CharArray): String =
|
||||
connectionHelper.useSession(FidoActionDescription.SetPin) { fidoSession ->
|
||||
try {
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
catchPinErrors(fidoSession, clientPin) {
|
||||
setOrChangePin(fidoSession, clientPin, pin, newPin)
|
||||
unlockSession(fidoSession, clientPin, newPin)
|
||||
}
|
||||
} finally {
|
||||
Arrays.fill(newPin, 0.toChar())
|
||||
pin?.let {
|
||||
Arrays.fill(it, 0.toChar())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCredentials(
|
||||
fidoSession: YubiKitFidoSession,
|
||||
clientPin: ClientPin,
|
||||
pinUvAuthToken: ByteArray
|
||||
): List<FidoCredential> =
|
||||
try {
|
||||
fidoViewModel.updateCredentials(null)
|
||||
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
|
||||
val rpIds = credMan.enumerateRps()
|
||||
|
||||
val credentials = rpIds.map { rpData ->
|
||||
credMan.enumerateCredentials(rpData.rpIdHash).map { credentialData ->
|
||||
FidoCredential(
|
||||
rpData.rp["id"] as String,
|
||||
(credentialData.credentialId["id"] as ByteArray).asString(),
|
||||
(credentialData.user["id"] as ByteArray).asString(),
|
||||
credentialData.user["name"] as String,
|
||||
publicKeyCredentialDescriptor = credentialData.credentialId,
|
||||
displayName = credentialData.user["displayName"] as String?,
|
||||
)
|
||||
}
|
||||
}.reduceOrNull { credentials, credentialList ->
|
||||
credentials + credentialList
|
||||
}
|
||||
|
||||
credentials ?: emptyList()
|
||||
} finally {
|
||||
|
||||
}
|
||||
|
||||
private suspend fun deleteCredential(rpId: String, credentialId: String): String =
|
||||
connectionHelper.useSession(FidoActionDescription.DeleteCredential) { fidoSession ->
|
||||
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
val permissions = getPinPermissionsCM(fidoSession)
|
||||
val token = clientPin.getPinToken(pinStore.getPin(), permissions, null)
|
||||
val credMan = CredentialManagement(fidoSession, clientPin.pinUvAuth, token)
|
||||
|
||||
val credentialDescriptor =
|
||||
fidoViewModel.credentials.value?.firstOrNull {
|
||||
it.credentialId == credentialId && it.rpId == rpId
|
||||
}?.publicKeyCredentialDescriptor
|
||||
|
||||
credentialDescriptor?.let {
|
||||
credMan.deleteCredential(credentialDescriptor)
|
||||
fidoViewModel.removeCredential(rpId, credentialId)
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to true,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
// could not find the credential to delete
|
||||
JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private fun getFingerprints(
|
||||
fidoSession: YubiKitFidoSession,
|
||||
clientPin: ClientPin,
|
||||
pinUvAuthToken: ByteArray
|
||||
): List<FidoFingerprint> {
|
||||
val bioEnrollment =
|
||||
FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, pinUvAuthToken)
|
||||
|
||||
val enrollments: Map<ByteArray, String?> = bioEnrollment.enumerateEnrollments()
|
||||
return enrollments.map { enrollment ->
|
||||
FidoFingerprint(HexCodec.bytesToHexString(enrollment.key), enrollment.value)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private suspend fun deleteFingerprint(templateId: String): String =
|
||||
connectionHelper.useSession(FidoActionDescription.DeleteFingerprint) { fidoSession ->
|
||||
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
val token =
|
||||
clientPin.getPinToken(
|
||||
pinStore.getPin(),
|
||||
getPinPermissionsBE(fidoSession),
|
||||
null
|
||||
)
|
||||
|
||||
|
||||
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||
bioEnrollment.removeEnrollment(HexCodec.hexStringToBytes(templateId))
|
||||
fidoViewModel.removeFingerprint(templateId)
|
||||
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to true,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private suspend fun renameFingerprint(templateId: String, name: String): String =
|
||||
connectionHelper.useSession(FidoActionDescription.RenameFingerprint) { fidoSession ->
|
||||
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
val token =
|
||||
clientPin.getPinToken(
|
||||
pinStore.getPin(),
|
||||
getPinPermissionsBE(fidoSession),
|
||||
null
|
||||
)
|
||||
|
||||
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||
bioEnrollment.setName(HexCodec.hexStringToBytes(templateId), name)
|
||||
fidoViewModel.renameFingerprint(templateId, name)
|
||||
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to true,
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
private var state : CommandState? = null
|
||||
private fun cancelRegisterFingerprint(): String {
|
||||
state?.cancel()
|
||||
return NULL
|
||||
}
|
||||
|
||||
private suspend fun registerFingerprint(name: String?): String =
|
||||
connectionHelper.useSession(FidoActionDescription.RegisterFingerprint) { fidoSession ->
|
||||
state?.cancel()
|
||||
state = CommandState()
|
||||
val clientPin =
|
||||
ClientPin(fidoSession, getPreferredPinUvAuthProtocol(fidoSession.cachedInfo))
|
||||
|
||||
val token =
|
||||
clientPin.getPinToken(
|
||||
pinStore.getPin(),
|
||||
getPinPermissionsBE(fidoSession),
|
||||
null
|
||||
)
|
||||
|
||||
val bioEnrollment = FingerprintBioEnrollment(fidoSession, clientPin.pinUvAuth, token)
|
||||
|
||||
val fingerprintEnrollmentContext = bioEnrollment.enroll(null)
|
||||
var templateId: ByteArray? = null
|
||||
while (templateId == null) {
|
||||
try {
|
||||
templateId = fingerprintEnrollmentContext.capture(state)
|
||||
fidoViewModel.updateRegisterFpState(
|
||||
createCaptureEvent(fingerprintEnrollmentContext.remaining!!)
|
||||
)
|
||||
} catch (captureError: FingerprintBioEnrollment.CaptureError) {
|
||||
fidoViewModel.updateRegisterFpState(createCaptureErrorEvent(captureError.code))
|
||||
} catch (ctapException: CtapException) {
|
||||
when (ctapException.ctapError) {
|
||||
CtapException.ERR_KEEPALIVE_CANCEL -> {
|
||||
fingerprintEnrollmentContext.cancel()
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
"status" to "user-cancelled"
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
CtapException.ERR_USER_ACTION_TIMEOUT -> {
|
||||
fingerprintEnrollmentContext.cancel()
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
"status" to "user-action-timeout"
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
else -> throw ctapException
|
||||
}
|
||||
} catch (io: IOException) {
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to false,
|
||||
"status" to "connection-error"
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
||||
if (!name.isNullOrBlank()) {
|
||||
bioEnrollment.setName(templateId, name)
|
||||
Logger.debug(logger, "Set name to {}", name)
|
||||
}
|
||||
|
||||
val templateIdHexString = HexCodec.bytesToHexString(templateId)
|
||||
fidoViewModel.addFingerprint(FidoFingerprint(templateIdHexString, name))
|
||||
fidoViewModel.setSessionState(Session(fidoSession.info, pinStore.hasPin(), pinRetries))
|
||||
|
||||
return@useSession JSONObject(
|
||||
mapOf(
|
||||
"success" to true,
|
||||
"template_id" to templateIdHexString,
|
||||
"name" to name
|
||||
)
|
||||
).toString()
|
||||
}
|
||||
|
||||
|
||||
override fun onDisconnected() {
|
||||
if (!resetHelper.inProgress) {
|
||||
fidoViewModel.clearSessionState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTimeout() {
|
||||
fidoViewModel.clearSessionState()
|
||||
}
|
||||
}
|
24
lib/app/views/graphics.dart → android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoPinStore.kt
Executable file → Normal file
24
lib/app/views/graphics.dart → android/app/src/main/kotlin/com/yubico/authenticator/fido/FidoPinStore.kt
Executable file → Normal file
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -14,11 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
final Image noAccounts = _graphic('no-accounts');
|
||||
final Image noFingerprints = _graphic('no-fingerprints');
|
||||
final Image noPermission = _graphic('no-permission');
|
||||
final Image manageAccounts = _graphic('manage-accounts');
|
||||
class FidoPinStore {
|
||||
private var pin: CharArray? = null
|
||||
|
||||
Image _graphic(String name) => Image.asset('assets/graphics/$name.png');
|
||||
fun hasPin(): Boolean {
|
||||
return pin != null
|
||||
}
|
||||
|
||||
fun getPin(): CharArray {
|
||||
return pin!!
|
||||
}
|
||||
|
||||
fun setPin(newPin: CharArray?) {
|
||||
pin?.fill(0.toChar())
|
||||
pin = newPin?.clone()
|
||||
}
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.yubico.authenticator.MainViewModel
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
import com.yubico.authenticator.fido.data.YubiKitFidoSession
|
||||
import com.yubico.yubikit.core.application.CommandState
|
||||
import com.yubico.yubikit.core.fido.CtapException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
enum class FidoResetState(val value: String) {
|
||||
Remove("remove"),
|
||||
Insert("insert"),
|
||||
Touch("touch")
|
||||
}
|
||||
|
||||
@Serializable
|
||||
sealed class FidoRegisterFpEvent(val status: String)
|
||||
|
||||
@Serializable
|
||||
data class FidoRegisterFpCaptureEvent(private val remaining: Int) : FidoRegisterFpEvent("capture")
|
||||
|
||||
@Serializable
|
||||
data class FidoRegisterFpCaptureErrorEvent(val code: Int) : FidoRegisterFpEvent("capture-error")
|
||||
|
||||
|
||||
fun createCaptureEvent(remaining: Int): FidoRegisterFpCaptureEvent {
|
||||
return FidoRegisterFpCaptureEvent(remaining)
|
||||
}
|
||||
|
||||
fun createCaptureErrorEvent(code: Int) : FidoRegisterFpCaptureErrorEvent {
|
||||
return FidoRegisterFpCaptureErrorEvent(code)
|
||||
}
|
||||
|
||||
class FidoResetHelper(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val fidoViewModel: FidoViewModel,
|
||||
private val mainViewModel: MainViewModel,
|
||||
private val connectionHelper: FidoConnectionHelper,
|
||||
private val pinStore: FidoPinStore
|
||||
) {
|
||||
|
||||
var inProgress = false
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private var resetCommandState: CommandState? = null
|
||||
private var cancelReset: Boolean = false
|
||||
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
super.onStop(owner)
|
||||
if (inProgress) {
|
||||
logger.debug("Cancelling ongoing reset")
|
||||
cancelReset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun reset(): String {
|
||||
try {
|
||||
withContext(Dispatchers.Main) {
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
deviceManager.clearDeviceInfoOnDisconnect = false
|
||||
inProgress = true
|
||||
fidoViewModel.updateResetState(FidoResetState.Remove)
|
||||
val usb = deviceManager.isUsbKeyConnected()
|
||||
if (usb) {
|
||||
resetOverUSB()
|
||||
} else {
|
||||
resetOverNfc()
|
||||
}
|
||||
logger.info("FIDO reset complete")
|
||||
} catch (e: CancellationException) {
|
||||
logger.debug("FIDO reset cancelled")
|
||||
} finally {
|
||||
withContext(Dispatchers.Main) {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
}
|
||||
inProgress = false
|
||||
deviceManager.clearDeviceInfoOnDisconnect = true
|
||||
}
|
||||
return NULL
|
||||
}
|
||||
|
||||
fun cancelReset(): String {
|
||||
cancelReset = true
|
||||
resetCommandState?.cancel()
|
||||
inProgress = false
|
||||
return NULL
|
||||
}
|
||||
|
||||
private suspend fun waitForUsbDisconnect() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch {
|
||||
cancelReset = false
|
||||
while (deviceManager.isUsbKeyConnected()) {
|
||||
if (cancelReset) {
|
||||
logger.debug("Reset was cancelled while waiting for YubiKey to be disconnected")
|
||||
continuation.resumeWithException(CancellationException())
|
||||
return@launch
|
||||
}
|
||||
logger.debug("Waiting for YubiKey to be disconnected")
|
||||
delay(300)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun waitForConnection() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch {
|
||||
fidoViewModel.updateResetState(FidoResetState.Insert)
|
||||
while (!deviceManager.isUsbKeyConnected()) {
|
||||
if (cancelReset) {
|
||||
// the key is not connected, clean device info
|
||||
mainViewModel.setDeviceInfo(null)
|
||||
continuation.resumeWithException(CancellationException())
|
||||
return@launch
|
||||
}
|
||||
logger.debug("Waiting for YubiKey to be connected")
|
||||
delay(300)
|
||||
}
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resetAfterTouch() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch(Dispatchers.Main) {
|
||||
fidoViewModel.updateResetState(FidoResetState.Touch)
|
||||
logger.debug("Waiting for touch")
|
||||
deviceManager.withKey { usbYubiKeyDevice ->
|
||||
connectionHelper.useSessionUsb(usbYubiKeyDevice) { fidoSession ->
|
||||
resetCommandState = CommandState()
|
||||
try {
|
||||
if (cancelReset) {
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} else {
|
||||
doReset(fidoSession)
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
} catch (e: CtapException) {
|
||||
when (e.ctapError) {
|
||||
CtapException.ERR_KEEPALIVE_CANCEL -> {
|
||||
logger.debug("Received ERR_KEEPALIVE_CANCEL during FIDO reset")
|
||||
}
|
||||
|
||||
CtapException.ERR_ACTION_TIMEOUT -> {
|
||||
logger.debug("Received ERR_ACTION_TIMEOUT during FIDO reset")
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.error("Received CtapException during FIDO reset: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} catch (e: IOException) {
|
||||
// communication error, key was removed?
|
||||
logger.error("IOException during FIDO reset: ", e)
|
||||
// treat it as cancellation
|
||||
continuation.resumeWithException(CancellationException())
|
||||
} finally {
|
||||
resetCommandState = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resetOverUSB() {
|
||||
waitForUsbDisconnect()
|
||||
waitForConnection()
|
||||
resetAfterTouch()
|
||||
}
|
||||
|
||||
private suspend fun resetOverNfc() = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch {
|
||||
fidoViewModel.updateResetState(FidoResetState.Touch)
|
||||
try {
|
||||
connectionHelper.useSessionNfc(FidoActionDescription.Reset) { fidoSession ->
|
||||
doReset(fidoSession)
|
||||
continuation.resume(Unit)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// on NFC, clean device info in this situation
|
||||
mainViewModel.setDeviceInfo(null)
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun doReset(fidoSession: YubiKitFidoSession) {
|
||||
logger.debug("Calling FIDO reset")
|
||||
fidoSession.reset(resetCommandState)
|
||||
fidoViewModel.setSessionState(Session(fidoSession.info, true, null))
|
||||
fidoViewModel.updateCredentials(emptyList())
|
||||
pinStore.setPin(null)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(FidoResetHelper::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.yubico.authenticator.ViewModelData
|
||||
import com.yubico.authenticator.fido.data.FidoCredential
|
||||
import com.yubico.authenticator.fido.data.FidoFingerprint
|
||||
import com.yubico.authenticator.fido.data.Session
|
||||
|
||||
class FidoViewModel : ViewModel() {
|
||||
private val _sessionState = MutableLiveData<ViewModelData>()
|
||||
val sessionState: LiveData<ViewModelData> = _sessionState
|
||||
|
||||
fun currentSession() : Session? = (_sessionState.value as? ViewModelData.Value<*>)?.data as? Session?
|
||||
|
||||
fun setSessionState(sessionState: Session) {
|
||||
_sessionState.postValue(ViewModelData.Value(sessionState))
|
||||
}
|
||||
|
||||
fun clearSessionState() {
|
||||
_sessionState.postValue(ViewModelData.Empty)
|
||||
}
|
||||
|
||||
private val _credentials = MutableLiveData<List<FidoCredential>?>()
|
||||
val credentials: LiveData<List<FidoCredential>?> = _credentials
|
||||
|
||||
fun updateCredentials(credentials: List<FidoCredential>?) {
|
||||
_credentials.postValue(credentials)
|
||||
}
|
||||
|
||||
fun removeCredential(rpId: String, credentialId: String) {
|
||||
_credentials.postValue(_credentials.value?.filter {
|
||||
it.credentialId != credentialId || it.rpId != rpId
|
||||
})
|
||||
}
|
||||
|
||||
private val _resetState = MutableLiveData(FidoResetState.Remove.value)
|
||||
val resetState: LiveData<String> = _resetState
|
||||
|
||||
fun updateResetState(resetState: FidoResetState) {
|
||||
_resetState.postValue(resetState.value)
|
||||
}
|
||||
|
||||
private val _fingerprints = MutableLiveData<List<FidoFingerprint>>()
|
||||
val fingerprints: LiveData<List<FidoFingerprint>> = _fingerprints
|
||||
|
||||
fun updateFingerprints(fingerprints: List<FidoFingerprint>) {
|
||||
_fingerprints.postValue(fingerprints)
|
||||
}
|
||||
|
||||
fun addFingerprint(fingerprint: FidoFingerprint) {
|
||||
_fingerprints.postValue(_fingerprints.value?.plus(fingerprint))
|
||||
}
|
||||
|
||||
fun removeFingerprint(templateId: String) {
|
||||
_fingerprints.postValue(_fingerprints.value?.filter {
|
||||
it.templateId != templateId
|
||||
})
|
||||
}
|
||||
|
||||
fun renameFingerprint(templateId: String, name: String) {
|
||||
_fingerprints.postValue(_fingerprints.value?.map {
|
||||
if (it.templateId == templateId) {
|
||||
FidoFingerprint(templateId, name)
|
||||
} else it
|
||||
})
|
||||
}
|
||||
|
||||
private val _registerFingerprint = MutableLiveData<FidoRegisterFpEvent>()
|
||||
val registerFingerprint: LiveData<FidoRegisterFpEvent> = _registerFingerprint
|
||||
|
||||
fun updateRegisterFpState(registerFpState: FidoRegisterFpEvent) {
|
||||
_registerFingerprint.postValue(registerFpState)
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.Transient
|
||||
|
||||
@Serializable
|
||||
data class FidoCredential(
|
||||
@SerialName("rp_id")
|
||||
val rpId: String,
|
||||
@SerialName("credential_id")
|
||||
val credentialId: String,
|
||||
@SerialName("user_id")
|
||||
val userId: String,
|
||||
@SerialName("user_name")
|
||||
val userName: String,
|
||||
@SerialName("display_name")
|
||||
val displayName: String?,
|
||||
@Transient
|
||||
val publicKeyCredentialDescriptor: Map<String, Any?> = emptyMap()
|
||||
)
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido.data
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class FidoFingerprint(
|
||||
@SerialName("template_id")
|
||||
val templateId: String,
|
||||
@SerialName("name")
|
||||
val name: String?
|
||||
)
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.fido.data
|
||||
|
||||
import com.yubico.authenticator.JsonSerializable
|
||||
import com.yubico.authenticator.jsonSerializer
|
||||
import com.yubico.yubikit.fido.ctap.Ctap2Session.InfoData
|
||||
import kotlinx.serialization.*
|
||||
|
||||
typealias YubiKitFidoSession = com.yubico.yubikit.fido.ctap.Ctap2Session
|
||||
|
||||
@Serializable
|
||||
data class Options(
|
||||
val clientPin: Boolean,
|
||||
val credMgmt: Boolean,
|
||||
val credentialMgmtPreview: Boolean,
|
||||
val bioEnroll: Boolean?,
|
||||
val alwaysUv: Boolean
|
||||
) {
|
||||
constructor(infoData: InfoData) : this(
|
||||
infoData.getOptionsBoolean("clientPin") == true,
|
||||
infoData.getOptionsBoolean("credMgmt") == true,
|
||||
infoData.getOptionsBoolean("credentialMgmtPreview") == true,
|
||||
infoData.getOptionsBoolean("bioEnroll"),
|
||||
infoData.getOptionsBoolean("alwaysUv") == true,
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun InfoData.getOptionsBoolean(
|
||||
key: String
|
||||
): Boolean? = options[key] as? Boolean?
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class SessionInfo(
|
||||
val options: Options,
|
||||
val aaguid: ByteArray,
|
||||
@SerialName("min_pin_length")
|
||||
val minPinLength: Int,
|
||||
@SerialName("force_pin_change")
|
||||
val forcePinChange: Boolean,
|
||||
@SerialName("remaining_disc_creds")
|
||||
val remainingDiscoverableCredentials: Int?
|
||||
) {
|
||||
constructor(infoData: InfoData) : this(
|
||||
Options(infoData),
|
||||
infoData.aaguid,
|
||||
infoData.minPinLength,
|
||||
infoData.forcePinChange,
|
||||
infoData.remainingDiscoverableCredentials
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as SessionInfo
|
||||
|
||||
if (options != other.options) return false
|
||||
if (!aaguid.contentEquals(other.aaguid)) return false
|
||||
if (minPinLength != other.minPinLength) return false
|
||||
if (forcePinChange != other.forcePinChange) return false
|
||||
if (remainingDiscoverableCredentials != other.remainingDiscoverableCredentials) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = options.hashCode()
|
||||
result = 31 * result + aaguid.contentHashCode()
|
||||
result = 31 * result + minPinLength
|
||||
result = 31 * result + forcePinChange.hashCode()
|
||||
result = 31 * result + (remainingDiscoverableCredentials ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Session(
|
||||
val info: SessionInfo,
|
||||
val unlocked: Boolean,
|
||||
@SerialName("pin_retries")
|
||||
val pinRetries: Int?
|
||||
) : JsonSerializable {
|
||||
constructor(infoData: InfoData, unlocked: Boolean, pinRetries: Int?) : this(
|
||||
SessionInfo(infoData), unlocked, pinRetries
|
||||
)
|
||||
|
||||
override fun toJson(): String {
|
||||
return jsonSerializer.encodeToString(this)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -70,7 +70,7 @@ class FlutterLog(messenger: BinaryMessenger) {
|
||||
}
|
||||
|
||||
private fun logLevelFromArgument(argValue: String?): Log.LogLevel? =
|
||||
Log.LogLevel.values().firstOrNull { it.name == argValue?.uppercase() }
|
||||
Log.LogLevel.entries.firstOrNull { it.name == argValue?.uppercase() }
|
||||
|
||||
private fun loggerError(message: String) {
|
||||
log(Log.LogLevel.ERROR,"FlutterLog", message, null)
|
||||
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.management
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.DialogTitle
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.yubikit.withConnection
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
typealias YubiKitManagementSession = com.yubico.yubikit.management.ManagementSession
|
||||
typealias ManagementAction = (Result<YubiKitManagementSession, Exception>) -> Unit
|
||||
|
||||
class ManagementConnectionHelper(
|
||||
private val deviceManager: DeviceManager,
|
||||
private val dialogManager: DialogManager
|
||||
) {
|
||||
private var action: ManagementAction? = null
|
||||
|
||||
suspend fun <T> useSession(
|
||||
actionDescription: ManagementActionDescription,
|
||||
action: (YubiKitManagementSession) -> T
|
||||
): T {
|
||||
return deviceManager.withKey(
|
||||
onNfc = { useSessionNfc(actionDescription, action) },
|
||||
onUsb = { useSessionUsb(it, action) })
|
||||
}
|
||||
|
||||
private suspend fun <T> useSessionUsb(
|
||||
device: UsbYubiKeyDevice,
|
||||
block: (YubiKitManagementSession) -> T
|
||||
): T = device.withConnection<SmartCardConnection, T> {
|
||||
block(YubiKitManagementSession(it))
|
||||
}
|
||||
|
||||
private suspend fun <T> useSessionNfc(
|
||||
actionDescription: ManagementActionDescription,
|
||||
block: (YubiKitManagementSession) -> T
|
||||
): T {
|
||||
try {
|
||||
val result = suspendCoroutine { outer ->
|
||||
action = {
|
||||
outer.resumeWith(runCatching {
|
||||
block.invoke(it.value)
|
||||
})
|
||||
}
|
||||
dialogManager.showDialog(
|
||||
DialogTitle.TapKey,
|
||||
actionDescription.id
|
||||
) {
|
||||
logger.debug("Cancelled Dialog {}", actionDescription.name)
|
||||
action?.invoke(Result.failure(CancellationException()))
|
||||
action = null
|
||||
}
|
||||
}
|
||||
return result
|
||||
} catch (cancelled: CancellationException) {
|
||||
throw cancelled
|
||||
} catch (error: Throwable) {
|
||||
throw error
|
||||
} finally {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ManagementConnectionHelper::class.java)
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.yubico.authenticator.management
|
||||
|
||||
import com.yubico.authenticator.DialogManager
|
||||
import com.yubico.authenticator.NULL
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.setHandler
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
const val dialogDescriptionManagementIndex = 300
|
||||
|
||||
enum class ManagementActionDescription(private val value: Int) {
|
||||
DeviceReset(0), ActionFailure(1);
|
||||
|
||||
val id: Int
|
||||
get() = value + dialogDescriptionManagementIndex
|
||||
}
|
||||
|
||||
class ManagementHandler(
|
||||
messenger: BinaryMessenger,
|
||||
deviceManager: DeviceManager,
|
||||
dialogManager: DialogManager
|
||||
) {
|
||||
private val channel = MethodChannel(messenger, "android.management.methods")
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val connectionHelper = ManagementConnectionHelper(deviceManager, dialogManager)
|
||||
|
||||
init {
|
||||
channel.setHandler(coroutineScope) { method, _ ->
|
||||
when (method) {
|
||||
"deviceReset" -> deviceReset()
|
||||
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun deviceReset(): String =
|
||||
connectionHelper.useSession(ManagementActionDescription.DeviceReset) { managementSession ->
|
||||
managementSession.deviceReset()
|
||||
NULL
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,13 +19,13 @@ package com.yubico.authenticator.oath
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.Observer
|
||||
import com.yubico.authenticator.*
|
||||
import com.yubico.authenticator.device.Capabilities
|
||||
import com.yubico.authenticator.device.Info
|
||||
import com.yubico.authenticator.device.DeviceListener
|
||||
import com.yubico.authenticator.device.DeviceManager
|
||||
import com.yubico.authenticator.device.UnknownDevice
|
||||
import com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.CodeType
|
||||
@ -50,14 +50,13 @@ import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.Transport
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
|
||||
import com.yubico.yubikit.core.smartcard.ApduException
|
||||
import com.yubico.yubikit.core.smartcard.AppId
|
||||
import com.yubico.yubikit.core.smartcard.SW
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardProtocol
|
||||
import com.yubico.yubikit.core.util.Result
|
||||
import com.yubico.yubikit.oath.CredentialData
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.*
|
||||
@ -74,15 +73,15 @@ typealias OathAction = (Result<YubiKitOathSession, Exception>) -> Unit
|
||||
class OathManager(
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
messenger: BinaryMessenger,
|
||||
private val appViewModel: MainViewModel,
|
||||
private val deviceManager: DeviceManager,
|
||||
private val oathViewModel: OathViewModel,
|
||||
private val dialogManager: DialogManager,
|
||||
private val appPreferences: AppPreferences,
|
||||
private val nfcActivityListener: NfcActivityListener
|
||||
) : AppContextManager {
|
||||
) : AppContextManager(), DeviceListener {
|
||||
|
||||
companion object {
|
||||
const val NFC_DATA_CLEANUP_DELAY = 30L * 1000 // 30s
|
||||
val OTP_AID = byteArrayOf(0xa0.toByte(), 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x01)
|
||||
private val memoryKeyProvider = ClearingMemProvider()
|
||||
}
|
||||
|
||||
private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
|
||||
@ -90,7 +89,6 @@ class OathManager(
|
||||
|
||||
private val oathChannel = MethodChannel(messenger, "android.oath.methods")
|
||||
|
||||
private val memoryKeyProvider = ClearingMemProvider()
|
||||
private val keyManager by lazy {
|
||||
KeyManager(
|
||||
compatUtil.from(Build.VERSION_CODES.M) {
|
||||
@ -111,60 +109,23 @@ class OathManager(
|
||||
private var refreshJob: Job? = null
|
||||
private var addToAny = false
|
||||
|
||||
// provides actions for lifecycle events
|
||||
private val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
|
||||
private var startTimeMs: Long = -1
|
||||
|
||||
override fun onPause(owner: LifecycleOwner) {
|
||||
startTimeMs = currentTimeMs
|
||||
|
||||
// cancel any pending actions, except for addToAny
|
||||
if (!addToAny) {
|
||||
pendingAction?.let {
|
||||
logger.debug("Cancelling pending action/closing nfc dialog.")
|
||||
it.invoke(Result.failure(CancellationException()))
|
||||
coroutineScope.launch {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
pendingAction = null
|
||||
override fun onPause() {
|
||||
// cancel any pending actions, except for addToAny
|
||||
if (!addToAny) {
|
||||
pendingAction?.let {
|
||||
logger.debug("Cancelling pending action/closing nfc dialog.")
|
||||
it.invoke(Result.failure(CancellationException()))
|
||||
coroutineScope.launch {
|
||||
dialogManager.closeDialog()
|
||||
}
|
||||
pendingAction = null
|
||||
}
|
||||
|
||||
super.onPause(owner)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
super.onResume(owner)
|
||||
if (canInvoke) {
|
||||
if (appViewModel.connectedYubiKey.value == null) {
|
||||
// no USB YubiKey is connected, reset known data on resume
|
||||
logger.debug("Removing NFC data after resume.")
|
||||
appViewModel.setDeviceInfo(null)
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val currentTimeMs
|
||||
get() = System.currentTimeMillis()
|
||||
|
||||
private val canInvoke: Boolean
|
||||
get() = startTimeMs != -1L && currentTimeMs - startTimeMs > NFC_DATA_CLEANUP_DELAY
|
||||
}
|
||||
|
||||
private val usbObserver = Observer<UsbYubiKeyDevice?> {
|
||||
refreshJob?.cancel()
|
||||
if (it == null) {
|
||||
appViewModel.setDeviceInfo(null)
|
||||
oathViewModel.setSessionState(null)
|
||||
}
|
||||
}
|
||||
|
||||
private val credentialObserver = Observer<List<CredentialWithCode>?> { codes ->
|
||||
refreshJob?.cancel()
|
||||
if (codes != null && appViewModel.connectedYubiKey.value != null) {
|
||||
if (codes != null && deviceManager.isUsbKeyConnected()) {
|
||||
val expirations = codes
|
||||
.filter { it.credential.codeType == CodeType.TOTP && !it.credential.touchRequired }
|
||||
.mapNotNull { it.code?.validTo }
|
||||
@ -193,7 +154,7 @@ class OathManager(
|
||||
}
|
||||
|
||||
init {
|
||||
appViewModel.connectedYubiKey.observe(lifecycleOwner, usbObserver)
|
||||
deviceManager.addDeviceListener(this)
|
||||
oathViewModel.credentials.observe(lifecycleOwner, credentialObserver)
|
||||
|
||||
// OATH methods callable from Flutter:
|
||||
@ -239,15 +200,15 @@ class OathManager(
|
||||
else -> throw NotImplementedError()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
lifecycleOwner.lifecycle.removeObserver(lifecycleObserver)
|
||||
appViewModel.connectedYubiKey.removeObserver(usbObserver)
|
||||
super.dispose()
|
||||
deviceManager.removeDeviceListener(this)
|
||||
oathViewModel.credentials.removeObserver(credentialObserver)
|
||||
oathChannel.setMethodCallHandler(null)
|
||||
oathViewModel.clearSession()
|
||||
oathViewModel.updateCredentials(mapOf())
|
||||
coroutineScope.cancel()
|
||||
}
|
||||
|
||||
@ -255,7 +216,7 @@ class OathManager(
|
||||
try {
|
||||
device.withConnection<SmartCardConnection, Unit> { connection ->
|
||||
val session = getOathSession(connection)
|
||||
val previousId = oathViewModel.sessionState.value?.deviceId
|
||||
val previousId = oathViewModel.currentSession()?.deviceId
|
||||
if (session.deviceId == previousId && device is NfcYubiKeyDevice) {
|
||||
// Run any pending action
|
||||
pendingAction?.let { action ->
|
||||
@ -305,12 +266,12 @@ class OathManager(
|
||||
if (session.version.isLessThan(4, 0, 0) && connection.transport == Transport.NFC) {
|
||||
// NEO over NFC, select OTP applet before reading info
|
||||
try {
|
||||
SmartCardProtocol(connection).select(OTP_AID)
|
||||
SmartCardProtocol(connection).select(AppId.OTP)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failed to recognize this OATH device.")
|
||||
logger.error("Failed to recognize this OATH device.", e)
|
||||
// we know this is NFC device and it supports OATH
|
||||
val oathCapabilities = Capabilities(nfc = 0x20)
|
||||
appViewModel.setDeviceInfo(
|
||||
deviceManager.setDeviceInfo(
|
||||
UnknownDevice.copy(
|
||||
config = UnknownDevice.config.copy(enabledCapabilities = oathCapabilities),
|
||||
name = "Unknown OATH device",
|
||||
@ -321,19 +282,6 @@ class OathManager(
|
||||
return@withConnection
|
||||
}
|
||||
}
|
||||
|
||||
// Update deviceInfo since the deviceId has changed
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val deviceInfo = DeviceUtil.readInfo(connection, pid)
|
||||
appViewModel.setDeviceInfo(
|
||||
Info(
|
||||
name = DeviceUtil.getName(deviceInfo, pid?.type),
|
||||
isNfc = device.transport == Transport.NFC,
|
||||
usbPid = pid?.value,
|
||||
deviceInfo = deviceInfo
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
logger.debug(
|
||||
@ -341,25 +289,9 @@ class OathManager(
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
// OATH not enabled/supported, try to get DeviceInfo over other USB interfaces
|
||||
logger.error("Failed to connect to CCID", e)
|
||||
|
||||
if (device.transport == Transport.USB || e is ApplicationNotAvailableException) {
|
||||
val deviceInfo = try {
|
||||
getDeviceInfo(device)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
logger.debug("Device was not recognized")
|
||||
UnknownDevice.copy(isNfc = device.transport == Transport.NFC)
|
||||
} catch (e: Exception) {
|
||||
logger.error("Failure getting device info", e)
|
||||
null
|
||||
}
|
||||
|
||||
logger.debug("Setting device info: {}", deviceInfo)
|
||||
appViewModel.setDeviceInfo(deviceInfo)
|
||||
}
|
||||
|
||||
logger.error("Failed to connect to CCID: ", e)
|
||||
// Clear any cached OATH state
|
||||
oathViewModel.setSessionState(null)
|
||||
oathViewModel.clearSession()
|
||||
}
|
||||
}
|
||||
|
||||
@ -373,7 +305,7 @@ class OathManager(
|
||||
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!")
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
val credential = session.putCredential(credentialData, requireTouch)
|
||||
@ -400,7 +332,7 @@ class OathManager(
|
||||
logger.trace("Adding following accounts: {}", uris)
|
||||
|
||||
addToAny = true
|
||||
return useOathSessionNfc(OathActionDescription.AddMultipleAccounts) { session ->
|
||||
return useOathSession(OathActionDescription.AddMultipleAccounts) { session ->
|
||||
var successCount = 0
|
||||
for (index in uris.indices) {
|
||||
|
||||
@ -454,7 +386,7 @@ class OathManager(
|
||||
oathViewModel.setSessionState(Session(it, remembered))
|
||||
|
||||
// fetch credentials after unlocking only if the YubiKey is connected over USB
|
||||
if (appViewModel.connectedYubiKey.value != null) {
|
||||
if (deviceManager.isUsbKeyConnected()) {
|
||||
oathViewModel.updateCredentials(calculateOathCodes(it))
|
||||
}
|
||||
}
|
||||
@ -499,10 +431,10 @@ class OathManager(
|
||||
throw Exception("Unset password failed")
|
||||
}
|
||||
|
||||
private suspend fun forgetPassword(): String {
|
||||
private fun forgetPassword(): String {
|
||||
keyManager.clearAll()
|
||||
logger.debug("Cleared all keys.")
|
||||
oathViewModel.sessionState.value?.let {
|
||||
oathViewModel.currentSession()?.let {
|
||||
oathViewModel.setSessionState(
|
||||
it.copy(
|
||||
isLocked = it.isAccessKeySet,
|
||||
@ -567,7 +499,7 @@ class OathManager(
|
||||
} ?: emptyMap())
|
||||
}
|
||||
|
||||
appViewModel.connectedYubiKey.value?.let { usbYubiKeyDevice ->
|
||||
deviceManager.withKey { usbYubiKeyDevice ->
|
||||
try {
|
||||
useOathSessionUsb(usbYubiKeyDevice) { session ->
|
||||
try {
|
||||
@ -695,7 +627,7 @@ class OathManager(
|
||||
|
||||
|
||||
private fun calculateOathCodes(session: YubiKitOathSession): Map<Credential, Code?> {
|
||||
val isUsbKey = appViewModel.connectedYubiKey.value != null
|
||||
val isUsbKey = deviceManager.isUsbKeyConnected()
|
||||
var timestamp = System.currentTimeMillis()
|
||||
if (!isUsbKey) {
|
||||
// NFC, need to pad timer to avoid immediate expiration
|
||||
@ -724,9 +656,10 @@ class OathManager(
|
||||
|
||||
// callers can decide whether the session should be unlocked first
|
||||
unlockOnConnect.set(unlock)
|
||||
return appViewModel.connectedYubiKey.value?.let {
|
||||
useOathSessionUsb(it, action)
|
||||
} ?: useOathSessionNfc(oathActionDescription, action)
|
||||
return deviceManager.withKey(
|
||||
onUsb = { useOathSessionUsb(it, action) },
|
||||
onNfc = { useOathSessionNfc(oathActionDescription, action) }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> useOathSessionUsb(
|
||||
@ -782,5 +715,16 @@ class OathManager(
|
||||
(credential != null) && credential.id.asString() == credentialId
|
||||
} ?: throw Exception("Failed to find account")
|
||||
|
||||
override fun onConnected(device: YubiKeyDevice) {
|
||||
refreshJob?.cancel()
|
||||
}
|
||||
|
||||
override fun onDisconnected() {
|
||||
refreshJob?.cancel()
|
||||
oathViewModel.clearSession()
|
||||
}
|
||||
|
||||
override fun onTimeout() {
|
||||
oathViewModel.clearSession()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -19,30 +19,39 @@ package com.yubico.authenticator.oath
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.yubico.authenticator.ViewModelData
|
||||
import com.yubico.authenticator.oath.data.Code
|
||||
import com.yubico.authenticator.oath.data.Credential
|
||||
import com.yubico.authenticator.oath.data.CredentialWithCode
|
||||
import com.yubico.authenticator.oath.data.Session
|
||||
|
||||
class OathViewModel: ViewModel() {
|
||||
private val _sessionState = MutableLiveData<Session?>()
|
||||
val sessionState: LiveData<Session?> = _sessionState
|
||||
|
||||
private val _sessionState = MutableLiveData<ViewModelData>()
|
||||
val sessionState: LiveData<ViewModelData> = _sessionState
|
||||
|
||||
fun currentSession() : Session? = (_sessionState.value as? ViewModelData.Value<*>)?.data as? Session?
|
||||
|
||||
// Sets session and credentials after performing OATH reset
|
||||
// Note: we cannot use [setSessionState] because resetting OATH changes deviceId
|
||||
fun resetOathSession(sessionState: Session, credentials: Map<Credential, Code?>) {
|
||||
_sessionState.postValue(sessionState)
|
||||
_sessionState.postValue(ViewModelData.Value(sessionState))
|
||||
updateCredentials(credentials)
|
||||
}
|
||||
|
||||
fun setSessionState(sessionState: Session?) {
|
||||
val oldDeviceId = _sessionState.value?.deviceId
|
||||
_sessionState.postValue(sessionState)
|
||||
if(oldDeviceId != sessionState?.deviceId) {
|
||||
fun setSessionState(sessionState: Session) {
|
||||
val oldDeviceId = currentSession()?.deviceId
|
||||
_sessionState.postValue(ViewModelData.Value(sessionState))
|
||||
if(oldDeviceId != sessionState.deviceId) {
|
||||
_credentials.postValue(null)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSession() {
|
||||
_sessionState.postValue(ViewModelData.Empty)
|
||||
_credentials.postValue(null)
|
||||
}
|
||||
|
||||
private val _credentials = MutableLiveData<List<CredentialWithCode>?>()
|
||||
val credentials: LiveData<List<CredentialWithCode>?> = _credentials
|
||||
|
||||
@ -59,7 +68,7 @@ class OathViewModel: ViewModel() {
|
||||
}
|
||||
|
||||
fun addCredential(credential: Credential, code: Code?): CredentialWithCode {
|
||||
require(credential.deviceId == _sessionState.value?.deviceId) {
|
||||
require(credential.deviceId == currentSession()?.deviceId) {
|
||||
"Cannot add credential for different deviceId"
|
||||
}
|
||||
return CredentialWithCode(credential, code).also {
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
* Copyright (C) 2023-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -25,10 +25,8 @@ typealias YubiKitCode = com.yubico.yubikit.oath.Code
|
||||
data class Code(
|
||||
val value: String? = null,
|
||||
@SerialName("valid_from")
|
||||
@Suppress("unused")
|
||||
val validFrom: Long,
|
||||
@SerialName("valid_to")
|
||||
@Suppress("unused")
|
||||
val validTo: Long
|
||||
) {
|
||||
|
||||
|
@ -16,10 +16,13 @@
|
||||
|
||||
package com.yubico.authenticator.oath.data
|
||||
|
||||
import com.yubico.authenticator.JsonSerializable
|
||||
import com.yubico.authenticator.device.Version
|
||||
import com.yubico.authenticator.jsonSerializer
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
typealias YubiKitOathSession = com.yubico.yubikit.oath.OathSession
|
||||
|
||||
@ -35,7 +38,7 @@ data class Session(
|
||||
val isRemembered: Boolean,
|
||||
@SerialName("locked")
|
||||
val isLocked: Boolean
|
||||
) {
|
||||
) : JsonSerializable {
|
||||
@SerialName("keystore")
|
||||
@Suppress("unused")
|
||||
val keystoreState: String = "unknown"
|
||||
@ -52,4 +55,8 @@ data class Session(
|
||||
isRemembered,
|
||||
oathSession.isLocked
|
||||
)
|
||||
|
||||
override fun toJson(): String {
|
||||
return jsonSerializer.encodeToString(this)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -16,7 +16,6 @@
|
||||
|
||||
package com.yubico.authenticator.oath.keystore
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import com.yubico.yubikit.oath.AccessKey
|
||||
import java.util.*
|
||||
import javax.crypto.Mac
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -18,18 +18,24 @@ package com.yubico.authenticator.yubikit
|
||||
|
||||
import com.yubico.authenticator.device.Info
|
||||
import com.yubico.authenticator.compatUtil
|
||||
import com.yubico.authenticator.device.unknownDeviceWithCapability
|
||||
import com.yubico.authenticator.device.unknownFido2DeviceInfo
|
||||
import com.yubico.authenticator.device.unknownOathDeviceInfo
|
||||
import com.yubico.yubikit.android.transport.nfc.NfcYubiKeyDevice
|
||||
import com.yubico.yubikit.android.transport.usb.UsbYubiKeyDevice
|
||||
import com.yubico.yubikit.core.YubiKeyDevice
|
||||
import com.yubico.yubikit.core.application.ApplicationNotAvailableException
|
||||
import com.yubico.yubikit.core.fido.FidoConnection
|
||||
import com.yubico.yubikit.core.otp.OtpConnection
|
||||
import com.yubico.yubikit.core.smartcard.SmartCardConnection
|
||||
import com.yubico.yubikit.fido.ctap.Ctap2Session
|
||||
import com.yubico.yubikit.management.DeviceInfo
|
||||
import com.yubico.yubikit.oath.OathSession
|
||||
import com.yubico.yubikit.support.DeviceUtil
|
||||
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
suspend fun getDeviceInfo(device: YubiKeyDevice): Info {
|
||||
suspend fun getDeviceInfo(device: YubiKeyDevice): Info? {
|
||||
val pid = (device as? UsbYubiKeyDevice)?.pid
|
||||
val logger = LoggerFactory.getLogger("getDeviceInfo")
|
||||
|
||||
@ -45,10 +51,34 @@ suspend fun getDeviceInfo(device: YubiKeyDevice): Info {
|
||||
logger.debug("FIDO connection not available: {}", t.message)
|
||||
return SkyHelper(compatUtil).getDeviceInfo(device)
|
||||
}.getOrElse {
|
||||
logger.debug("Failed to recognize device: {}", it.message)
|
||||
throw it
|
||||
// this is not a YubiKey
|
||||
logger.debug("Probing unknown device")
|
||||
try {
|
||||
device.openConnection(SmartCardConnection::class.java).use { smartCardConnection ->
|
||||
try {
|
||||
// if OATH session is available use it
|
||||
OathSession(smartCardConnection)
|
||||
logger.debug("Device supports OATH")
|
||||
return unknownOathDeviceInfo(device.transport)
|
||||
} catch (applicationNotAvailable: ApplicationNotAvailableException) {
|
||||
try {
|
||||
// probe for CTAP2 availability
|
||||
Ctap2Session(smartCardConnection)
|
||||
logger.debug("Device supports FIDO2")
|
||||
return unknownFido2DeviceInfo(device.transport)
|
||||
} catch (applicationNotAvailable: ApplicationNotAvailableException) {
|
||||
logger.debug("Device not recognized")
|
||||
return unknownDeviceWithCapability(device.transport)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// no smart card connectivity
|
||||
logger.error("Failure getting device info", e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val name = DeviceUtil.getName(deviceInfo, pid?.type)
|
||||
return Info(name, device is NfcYubiKeyDevice, pid?.value, deviceInfo)
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,9 @@ class SkyHelper(private val compatUtil: CompatUtil) {
|
||||
name = (device.usbDevice.productName ?: "Yubico Security Key"),
|
||||
isNfc = false,
|
||||
usbPid = pid.value,
|
||||
supportedCapabilities = Capabilities(usb = 0)
|
||||
pinComplexity = false,
|
||||
supportedCapabilities = Capabilities(usb = 0),
|
||||
fipsCapable = 0
|
||||
)
|
||||
}
|
||||
|
||||
|
2
android/app/src/main/res/values-de/strings.xml
Normal file
2
android/app/src/main/res/values-de/strings.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources />
|
7
android/app/src/main/res/values-fr/strings.xml
Normal file
7
android/app/src/main/res/values-fr/strings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="p_ndef_set_otp">Code OTP copié de la YubiKey dans le presse-papiers.</string>
|
||||
<string name="p_ndef_set_password">Mot de passe copié de la YubiKey dans le presse-papiers.</string>
|
||||
<string name="p_ndef_parse_failure">Impossible d\'analyser le code OTP de la YubiKey.</string>
|
||||
<string name="p_ndef_set_clip_failure">Presse-papiers inaccessible lors de la tentative de copie du code OTP depuis la YubiKey.</string>
|
||||
</resources>
|
7
android/app/src/main/res/values-ja/strings.xml
Normal file
7
android/app/src/main/res/values-ja/strings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="p_ndef_set_otp">OTPコードがYubiKeyからクリップボードに正常にコピーされました。</string>
|
||||
<string name="p_ndef_set_password">パスワードがYubiKeyからクリップボードに正常にコピーされました。</string>
|
||||
<string name="p_ndef_parse_failure">YubiKeyからのOTPコードを解析できませんでした。</string>
|
||||
<string name="p_ndef_set_clip_failure">YubiKeyからのOTPコードのコピー試行時にクリップボードにアクセスできませんでした。</string>
|
||||
</resources>
|
7
android/app/src/main/res/values-pl/strings.xml
Normal file
7
android/app/src/main/res/values-pl/strings.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="p_ndef_set_otp">OTP zostało skopiowane do schowka.</string>
|
||||
<string name="p_ndef_set_password">Hasło statyczne zostało skopiowane do schowka.</string>
|
||||
<string name="p_ndef_parse_failure">Błąd czytania OTP z YubiKey.</string>
|
||||
<string name="p_ndef_set_clip_failure">Błąd kopiowania OTP do schowka.</string>
|
||||
</resources>
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_label">Yubico Authenticator</string>
|
||||
<string name="otp_success_set_otp_to_clipboard">Successfully copied OTP code from YubiKey to clipboard.</string>
|
||||
<string name="otp_success_set_password_to_clipboard">Successfully copied password from YubiKey to clipboard.</string>
|
||||
<string name="otp_parse_failure">Failed to parse OTP code from YubiKey.</string>
|
||||
<string name="otp_set_clip_failure">Failed to access clipboard when trying to copy OTP code from YubiKey.</string>
|
||||
<string name="app_label" translatable="false">Yubico Authenticator</string>
|
||||
<string name="p_ndef_set_otp">Successfully copied OTP code from YubiKey to clipboard.</string>
|
||||
<string name="p_ndef_set_password">Successfully copied password from YubiKey to clipboard.</string>
|
||||
<string name="p_ndef_parse_failure">Failed to parse OTP code from YubiKey.</string>
|
||||
<string name="p_ndef_set_clip_failure">Failed to access clipboard when trying to copy OTP code from YubiKey.</string>
|
||||
</resources>
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Yubico.
|
||||
* Copyright (C) 2023-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -17,7 +17,6 @@
|
||||
package com.yubico.authenticator.device
|
||||
|
||||
import com.yubico.authenticator.jsonSerializer
|
||||
import com.yubico.yubikit.management.DeviceConfig
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
@ -17,6 +17,7 @@
|
||||
package com.yubico.authenticator.oath
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.yubico.authenticator.ViewModelData
|
||||
import com.yubico.authenticator.device.Version
|
||||
import com.yubico.authenticator.oath.OathTestHelper.code
|
||||
import com.yubico.authenticator.oath.OathTestHelper.emptyCredentials
|
||||
@ -40,12 +41,12 @@ class ModelTest {
|
||||
private fun connectDevice(deviceId: String) {
|
||||
viewModel.setSessionState(
|
||||
Session(
|
||||
deviceId,
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false
|
||||
)
|
||||
deviceId,
|
||||
Version(1, 2, 3),
|
||||
isAccessKeySet = false,
|
||||
isRemembered = false,
|
||||
isLocked = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -116,7 +117,7 @@ class ModelTest {
|
||||
|
||||
viewModel.updateCredentials(m2)
|
||||
|
||||
assertEquals("device1", viewModel.sessionState.value?.deviceId)
|
||||
assertEquals("device1", viewModel.currentSession()?.deviceId)
|
||||
assertEquals(3, viewModel.credentials.value!!.size)
|
||||
assertTrue(viewModel.credentials.value!!.find { it.credential == cred1 } != null)
|
||||
assertTrue(viewModel.credentials.value!!.find { it.credential == cred2 } != null)
|
||||
@ -387,9 +388,9 @@ class ModelTest {
|
||||
val deviceId = "device"
|
||||
connectDevice(deviceId)
|
||||
viewModel.updateCredentials(mapOf(totp() to code()))
|
||||
viewModel.setSessionState(null)
|
||||
viewModel.clearSession()
|
||||
|
||||
assertNull(viewModel.sessionState.value)
|
||||
assertEquals(ViewModelData.Empty, viewModel.sessionState.value)
|
||||
assertNull(viewModel.credentials.value)
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022-2023 Yubico.
|
||||
* Copyright (C) 2022-2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -42,7 +42,7 @@ class SkyHelperTest {
|
||||
fun `supports three specific UsbPids`() {
|
||||
val skyHelper = SkyHelper(CompatUtil(33))
|
||||
|
||||
for (pid in UsbPid.values()) {
|
||||
for (pid in UsbPid.entries) {
|
||||
val ykDevice = getUsbYubiKeyDeviceMock().also {
|
||||
`when`(it.pid).thenReturn(pid)
|
||||
}
|
||||
|
@ -1,18 +1,3 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
@ -24,9 +9,9 @@ allprojects {
|
||||
targetSdkVersion = 34
|
||||
compileSdkVersion = 34
|
||||
|
||||
yubiKitVersion = "2.4.0-beta01"
|
||||
yubiKitVersion = "2.6.0"
|
||||
junitVersion = "4.13.2"
|
||||
mockitoVersion = "5.6.0"
|
||||
mockitoVersion = "5.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,14 @@ group 'com.yubico.authenticator.flutter_plugins.qrscanner_zxing'
|
||||
version '1.0'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
ext.kotlin_version = '2.0.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.2'
|
||||
classpath 'com.android.tools.build:gradle:8.5.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
@ -50,7 +50,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
def camerax_version = "1.3.0"
|
||||
def camerax_version = "1.3.4"
|
||||
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
|
||||
implementation "androidx.camera:camera-view:${camerax_version}"
|
||||
implementation "androidx.camera:camera-camera2:${camerax_version}"
|
||||
|
@ -1,6 +1,6 @@
|
||||
#Mon Oct 16 08:48:17 CEST 2023
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
@ -107,17 +107,15 @@ internal class QRScannerView(
|
||||
}
|
||||
|
||||
private fun requestPermissions(activity: Activity) {
|
||||
coroutineScope.launch {
|
||||
methodChannel.invokeMethod(
|
||||
"beforePermissionsRequest", null
|
||||
)
|
||||
methodChannel.invokeMethod(
|
||||
"beforePermissionsRequest", null
|
||||
)
|
||||
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
PERMISSIONS_TO_REQUEST,
|
||||
PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
PERMISSIONS_TO_REQUEST,
|
||||
PERMISSION_REQUEST_CODE
|
||||
)
|
||||
}
|
||||
|
||||
private val qrScannerView = View.inflate(context, R.layout.qr_scanner_view, null)
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2022 Yubico.
|
||||
* Copyright (C) 2022,2024 Yubico.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@ -42,7 +42,7 @@ class PermissionsResultRegistrar {
|
||||
requestCode,
|
||||
permissions,
|
||||
grantResults
|
||||
) ?: false
|
||||
) == true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.9.10'
|
||||
ext.kotlin_version = '2.0.0'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.1.2'
|
||||
classpath 'com.android.tools.build:gradle:8.5.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
|
@ -20,7 +20,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class CutoutOverlay extends StatelessWidget {
|
||||
final int marginPct;
|
||||
const CutoutOverlay({Key? key, required this.marginPct}) : super(key: key);
|
||||
const CutoutOverlay({super.key, required this.marginPct});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -26,7 +26,7 @@ void main() {
|
||||
}
|
||||
|
||||
class QRCodeScannerExampleApp extends StatelessWidget {
|
||||
const QRCodeScannerExampleApp({Key? key}) : super(key: key);
|
||||
const QRCodeScannerExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -41,7 +41,7 @@ class QRCodeScannerExampleApp extends StatelessWidget {
|
||||
}
|
||||
|
||||
class AppHomePage extends StatelessWidget {
|
||||
const AppHomePage({Key? key, required this.title}) : super(key: key);
|
||||
const AppHomePage({super.key, required this.title});
|
||||
final String title;
|
||||
|
||||
@override
|
||||
@ -104,7 +104,7 @@ class AppHomePage extends StatelessWidget {
|
||||
}
|
||||
|
||||
class QRScannerPage extends StatefulWidget {
|
||||
const QRScannerPage({Key? key}) : super(key: key);
|
||||
const QRScannerPage({super.key});
|
||||
|
||||
@override
|
||||
QRScannerPageState createState() => QRScannerPageState();
|
||||
|
@ -37,18 +37,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
|
||||
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.2"
|
||||
version: "1.18.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.8"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -61,18 +61,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
version: "2.1.2"
|
||||
file_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030
|
||||
sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.5.0"
|
||||
version: "6.2.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -82,18 +82,18 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.2"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: f185ac890306b5779ecbd611f52502d8d4d63d27703ef73161ca0407e815f02c
|
||||
sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.16"
|
||||
version: "2.0.20"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -104,54 +104,78 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker
|
||||
sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.4"
|
||||
leak_tracker_flutter_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_flutter_testing
|
||||
sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
leak_tracker_testing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: leak_tracker_testing
|
||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
version: "3.0.0"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: matcher
|
||||
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
|
||||
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.16"
|
||||
version: "0.12.16+1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
|
||||
sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.0"
|
||||
version: "0.8.0"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
|
||||
sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
version: "1.12.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path
|
||||
sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
|
||||
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
version: "1.9.0"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.6"
|
||||
version: "2.1.8"
|
||||
qrscanner_zxing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -176,18 +200,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stack_trace
|
||||
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
|
||||
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.11.0"
|
||||
version: "1.11.1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: stream_channel
|
||||
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
|
||||
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
string_scanner:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -208,10 +232,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
|
||||
sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
version: "0.7.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -220,22 +244,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
web:
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web
|
||||
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
|
||||
name: vm_service
|
||||
sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.4-beta"
|
||||
version: "14.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "350a11abd2d1d97e0cc7a28a81b781c08002aa2864d9e3f192ca0ffa18b06ed3"
|
||||
sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.9"
|
||||
version: "5.5.1"
|
||||
sdks:
|
||||
dart: ">=3.1.0-185.0.dev <4.0.0"
|
||||
flutter: ">=3.7.0"
|
||||
dart: ">=3.4.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
@ -13,13 +13,13 @@ dependencies:
|
||||
qrscanner_zxing:
|
||||
path: ../
|
||||
|
||||
cupertino_icons: ^1.0.2
|
||||
file_picker: ^5.3.2
|
||||
cupertino_icons: ^1.0.6
|
||||
file_picker: ^6.1.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
flutter_lints: ^3.0.1
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
@ -24,23 +24,25 @@ import 'package:flutter/services.dart';
|
||||
|
||||
class QRScannerZxingView extends StatefulWidget {
|
||||
final int marginPct;
|
||||
|
||||
/// Called when a code has been detected.
|
||||
final Function(String rawData) onDetect;
|
||||
|
||||
/// Called before the system UI with runtime permissions request is
|
||||
/// displayed.
|
||||
final Function()? beforePermissionsRequest;
|
||||
|
||||
/// Called after the view is completely initialized.
|
||||
///
|
||||
/// permissionsGranted is true if the user granted camera permissions.
|
||||
final Function(bool permissionsGranted) onViewInitialized;
|
||||
|
||||
const QRScannerZxingView(
|
||||
{Key? key,
|
||||
{super.key,
|
||||
required this.marginPct,
|
||||
required this.onDetect,
|
||||
this.beforePermissionsRequest,
|
||||
required this.onViewInitialized})
|
||||
: super(key: key);
|
||||
required this.onViewInitialized});
|
||||
|
||||
@override
|
||||
QRScannerZxingViewState createState() => QRScannerZxingViewState();
|
||||
|
@ -1,21 +1,20 @@
|
||||
name: qrscanner_zxing
|
||||
description: Android-only QR Scanner plugin based on zxing and CameraX API's
|
||||
version: 1.0.0
|
||||
homepage:
|
||||
|
||||
environment:
|
||||
sdk: ">=2.17.0-266.1.beta <3.0.0"
|
||||
flutter: ">=2.5.0"
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
plugin_platform_interface: ^2.0.2
|
||||
plugin_platform_interface: ^2.1.6
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^2.0.0
|
||||
flutter_lints: ^3.0.1
|
||||
|
||||
flutter:
|
||||
plugin:
|
||||
|
@ -1,6 +1,6 @@
|
||||
#Mon Aug 15 14:34:17 CEST 2022
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
@ -7,7 +7,7 @@ fi
|
||||
|
||||
export REF=$(echo ${GITHUB_REF} | cut -d '/' -f 3,4,5,6,7 | sed -r 's/\//_/g')
|
||||
export FLUTTER_APK=build/app/outputs/flutter-apk
|
||||
export NATIVE_LIBS=build/app/intermediates/merged_native_libs/release/out/lib
|
||||
export NATIVE_LIBS=build/app/intermediates/merged_native_libs/release/mergeReleaseNativeLibs/out/lib
|
||||
|
||||
rm -rf artifacts
|
||||
mkdir artifacts
|
||||
|
@ -1,11 +1,35 @@
|
||||
pluginManagement {
|
||||
def flutterSdkPath = {
|
||||
def properties = new Properties()
|
||||
file("local.properties").withInputStream { properties.load(it) }
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
return flutterSdkPath
|
||||
}()
|
||||
|
||||
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
resolutionStrategy {
|
||||
eachPlugin {
|
||||
// https://github.com/google/play-services-plugins/issues/223
|
||||
if (requested.id.id == "com.google.android.gms.oss-licenses-plugin") {
|
||||
useModule("com.google.android.gms:oss-licenses-plugin:${requested.version}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||
id "com.android.application" version "8.5.0" apply false
|
||||
id "org.jetbrains.kotlin.android" version "2.0.0" apply false
|
||||
id "org.jetbrains.kotlin.plugin.serialization" version "2.0.0" apply false
|
||||
id "com.google.android.gms.oss-licenses-plugin" version "0.10.6" apply false
|
||||
}
|
||||
|
||||
include ':app'
|
||||
|
||||
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
|
||||
def properties = new Properties()
|
||||
|
||||
assert localPropertiesFile.exists()
|
||||
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
|
||||
|
||||
def flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
|
||||
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
|
||||
|
@ -5,24 +5,33 @@ import os
|
||||
|
||||
|
||||
def read_file_lines(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return file.readlines()
|
||||
|
||||
|
||||
def read_file_json(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as file:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
return json.load(file)
|
||||
|
||||
|
||||
def write_to_file(file_path, text):
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
if file.read() == text:
|
||||
return False
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
file.write(text)
|
||||
return True
|
||||
|
||||
|
||||
# Translation table for unicode characters we want to keep in escaped form.
|
||||
trans = str.maketrans({
|
||||
'\u00a0': r"\u00a0", # No-Break Space (NBSP)
|
||||
'\u2026': r"\u2026" # Horizontal Ellipsis
|
||||
})
|
||||
trans = str.maketrans(
|
||||
{
|
||||
"\u00a0": r"\u00a0", # No-Break Space (NBSP)
|
||||
"\u2026": r"\u2026", # Horizontal Ellipsis
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Move keys in target into same order as in source.
|
||||
# Keys not present in source are removed from target.
|
||||
@ -68,16 +77,16 @@ def update_arb_file(source_path, target_path, language_code):
|
||||
if line.strip() == "":
|
||||
target_lines.insert(i, "")
|
||||
|
||||
write_to_file(target_path, "\n".join(target_lines).strip() + "\n")
|
||||
return write_to_file(target_path, "\n".join(target_lines).strip() + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
source_file_path = 'lib/l10n/app_en.arb'
|
||||
target_directory = 'lib/l10n'
|
||||
source_file_path = "lib/l10n/app_en.arb"
|
||||
target_directory = "lib/l10n"
|
||||
|
||||
for file_name in os.listdir(target_directory):
|
||||
if file_name.startswith('app_') and file_name.endswith('.arb'):
|
||||
if file_name.startswith("app_") and file_name.endswith(".arb"):
|
||||
target_file_path = os.path.join(target_directory, file_name)
|
||||
language_code = file_name.split('_')[1].split('.')[0]
|
||||
update_arb_file(source_file_path, target_file_path, language_code)
|
||||
print(f'File updated: {file_name}')
|
||||
language_code = file_name.split("_")[1].split(".")[0]
|
||||
if update_arb_file(source_file_path, target_file_path, language_code):
|
||||
print(f"File updated: {file_name}")
|
||||
|
BIN
assets/fonts/Roboto-Bold.ttf
Normal file
BIN
assets/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.0 KiB |
@ -34,21 +34,15 @@ if [ "$OS" = "macos" ]; then
|
||||
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
|
||||
poetry run pip uninstall -y cryptography cffi pillow
|
||||
# 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
|
||||
@ -56,8 +50,8 @@ if [ "$OS" = "macos" ]; then
|
||||
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)
|
||||
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
|
||||
|
@ -19,6 +19,7 @@ import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
non_words = (":",)
|
||||
errors = []
|
||||
|
||||
|
||||
@ -46,8 +47,9 @@ def check_prefixes(k, v, s_max_words, s_max_len, p_ending_chars, q_ending_chars)
|
||||
if k.startswith("s_"):
|
||||
if len(v) > s_max_len:
|
||||
errs.append(f"Too long ({len(v)} chars)")
|
||||
if len(v.split()) > s_max_words:
|
||||
errs.append(f"Too many words ({len(v.split())})")
|
||||
n_words = len([w for w in v.split() if w not in non_words])
|
||||
if n_words > s_max_words:
|
||||
errs.append(f"Too many words ({n_words})")
|
||||
if k.startswith("l_") or k.startswith("s_"):
|
||||
if v.endswith("."):
|
||||
errs.append("Ends with '.'")
|
||||
@ -89,7 +91,7 @@ def lint_strings(strings, rules):
|
||||
k,
|
||||
v,
|
||||
rules.get("s_max_words", 4),
|
||||
rules.get("s_max_len", 32),
|
||||
rules.get("s_max_length", 32),
|
||||
rules.get("p_ending_chars", ".!"),
|
||||
rules.get("q_ending_chars", "?"),
|
||||
)
|
||||
@ -106,7 +108,7 @@ if len(sys.argv) != 2:
|
||||
|
||||
|
||||
target = sys.argv[1]
|
||||
with open(target, encoding='utf-8') 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("@")}
|
||||
@ -115,7 +117,7 @@ print(target, f"- checking {len(strings)} strings")
|
||||
lint_strings(strings, values.get("@_lint_rules", {}))
|
||||
check_duplicate_values(strings)
|
||||
|
||||
with open(os.path.join(os.path.dirname(target), 'app_en.arb'), encoding='utf-8') as f:
|
||||
with open(os.path.join(os.path.dirname(target), "app_en.arb"), encoding="utf-8") as f:
|
||||
reference_values = json.load(f)
|
||||
errors.extend(check_keys_exist_in_reference(reference_values, values))
|
||||
|
||||
|
127
crowdin.yaml
Normal file
127
crowdin.yaml
Normal file
@ -0,0 +1,127 @@
|
||||
#
|
||||
# Your Crowdin credentials
|
||||
#
|
||||
"project_id_env": "CROWDIN_PROJECT_ID"
|
||||
"api_token_env": "CROWDIN_PERSONAL_TOKEN"
|
||||
"base_path": "."
|
||||
"base_url": "https://api.crowdin.com"
|
||||
|
||||
#
|
||||
# Choose file structure in Crowdin
|
||||
# e.g. true or false
|
||||
#
|
||||
"preserve_hierarchy": false
|
||||
|
||||
#
|
||||
# Files configuration
|
||||
#
|
||||
files: [
|
||||
{
|
||||
#
|
||||
# Source files filter
|
||||
# e.g. "/resources/en/*.json"
|
||||
#
|
||||
"source": "/lib/l10n/app_en.arb",
|
||||
|
||||
#
|
||||
# Where translations will be placed
|
||||
# e.g. "/resources/%two_letters_code%/%original_file_name%"
|
||||
#
|
||||
"translation": "/lib/l10n/app_%two_letters_code%.arb",
|
||||
|
||||
#
|
||||
# Files or directories for ignore
|
||||
# e.g. ["/**/?.txt", "/**/[0-9].txt", "/**/*\?*.txt"]
|
||||
#
|
||||
# "ignore": [],
|
||||
|
||||
#
|
||||
# The dest allows you to specify a file name in Crowdin
|
||||
# e.g. "/messages.json"
|
||||
#
|
||||
# "dest": "",
|
||||
|
||||
#
|
||||
# File type
|
||||
# e.g. "json"
|
||||
#
|
||||
# "type": "",
|
||||
|
||||
#
|
||||
# The parameter "update_option" is optional. If it is not set, after the files update the translations for changed strings will be removed. Use to fix typos and for minor changes in the source strings
|
||||
# e.g. "update_as_unapproved" or "update_without_changes"
|
||||
#
|
||||
# "update_option": "",
|
||||
|
||||
#
|
||||
# Start block (for XML only)
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether to translate tags attributes.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_attributes": 1,
|
||||
|
||||
#
|
||||
# Defines whether to translate texts placed inside the tags.
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "translate_content": 1,
|
||||
|
||||
#
|
||||
# This is an array of strings, where each item is the XPaths to DOM element that should be imported
|
||||
# e.g. ["/content/text", "/content/text[@value]"]
|
||||
#
|
||||
# "translatable_elements": [],
|
||||
|
||||
#
|
||||
# Defines whether to split long texts into smaller text segments
|
||||
# e.g. 0 or 1 (Default is 1)
|
||||
#
|
||||
# "content_segmentation": 1,
|
||||
|
||||
#
|
||||
# End block (for XML only)
|
||||
#
|
||||
|
||||
#
|
||||
# Start .properties block
|
||||
#
|
||||
|
||||
#
|
||||
# Defines whether single quote should be escaped by another single quote or backslash in exported translations
|
||||
# e.g. 0 or 1 or 2 or 3 (Default is 3)
|
||||
# 0 - do not escape single quote;
|
||||
# 1 - escape single quote by another single quote;
|
||||
# 2 - escape single quote by backslash;
|
||||
# 3 - escape single quote by another single quote only in strings containing variables ( {0} ).
|
||||
#
|
||||
# "escape_quotes": 3,
|
||||
|
||||
#
|
||||
# Defines whether any special characters (=, :, ! and #) should be escaped by backslash in exported translations.
|
||||
# e.g. 0 or 1 (Default is 0)
|
||||
# 0 - do not escape special characters
|
||||
# 1 - escape special characters by a backslash
|
||||
#
|
||||
# "escape_special_characters": 0
|
||||
#
|
||||
|
||||
#
|
||||
# End .properties block
|
||||
#
|
||||
|
||||
#
|
||||
# Does the first line contain header?
|
||||
# e.g. true or false
|
||||
#
|
||||
# "first_line_contains_header": true,
|
||||
|
||||
#
|
||||
# for spreadsheets
|
||||
# e.g. "identifier,source_phrase,context,uk,ru,fr"
|
||||
#
|
||||
# "scheme": "",
|
||||
}
|
||||
]
|
38
dart_test.yaml
Normal file
38
dart_test.yaml
Normal file
@ -0,0 +1,38 @@
|
||||
# define available tags
|
||||
tags:
|
||||
# Tests which we want to run on desktop
|
||||
desktop:
|
||||
timeout: none
|
||||
|
||||
# Tests which we want to run on Android
|
||||
android:
|
||||
timeout: none
|
||||
|
||||
# Tests consuming quiet a lot of time
|
||||
slow:
|
||||
timeout: none
|
||||
|
||||
# Minimal tests
|
||||
# quick verification that the framework is working
|
||||
minimal:
|
||||
timeout: none
|
||||
|
||||
# OATH tests
|
||||
oath:
|
||||
timeout: none
|
||||
|
||||
# OTP tests
|
||||
otp:
|
||||
timeout: none
|
||||
|
||||
# PIV tests
|
||||
piv:
|
||||
timeout: none
|
||||
|
||||
# Management tests
|
||||
management:
|
||||
timeout: none
|
||||
|
||||
# Passkey tests
|
||||
passkey:
|
||||
timeout: none
|
@ -12,7 +12,7 @@ Once you have an icon pack on your computer or Android device and wish to use
|
||||
it, follow these steps:
|
||||
|
||||
1. Open the Yubico Authenticator app and insert or tap your OATH-enabled YubiKey.
|
||||
2. Make sure the `Authenticator` section is chosen, where you can see your list of accounts.
|
||||
2. Make sure the `Accounts` section is chosen, where you can see your list of accounts.
|
||||
3. Press the `Configure YubiKey` button, and select `Custom icons` from the menu.
|
||||
4. Press the `Load icon pack` button, select the icon pack zip-file, and wait for it to load.
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
This document describes how to build and package Yubico Authenticator from
|
||||
source.
|
||||
|
||||
NOTE: Yubico Authenticator 6 uses a new codebase built using the Flutter
|
||||
NOTE: Yubico Authenticator 6+ uses a new codebase built using the Flutter
|
||||
framework. The previous Qt codebase can be found in the `legacy` branch.
|
||||
|
||||
=== Requirements
|
||||
@ -45,7 +45,7 @@ Make sure the http://www.swig.org/[swig] executable is in your PATH.
|
||||
|
||||
$ sudo apt install swig libu2f-udev pcscd libpcsclite-dev
|
||||
|
||||
==== Linux (RPM-based distributons)
|
||||
==== Linux (RPM-based distributions)
|
||||
|
||||
# Tested on Fedora 34
|
||||
$ sudo dnf install pcsc-lite-devel python3-devel swig
|
||||
@ -90,7 +90,7 @@ These do not require a YubiKey, and are relatively quick to run.
|
||||
The integration tests are slower but cover more end-to-end functionality. The
|
||||
require an attached YubiKey to run, and will make modifications to the data
|
||||
stored on that YubiKey. For instructions on running these tests, see
|
||||
link:Integration_Tests.adoc[these instructions].
|
||||
link:../integration_test/testdoc.adoc[these instructions].
|
||||
|
||||
|
||||
=== Packaging for MacOS
|
||||
|
@ -1,46 +0,0 @@
|
||||
== Integration tests
|
||||
|
||||
The `integration_test` directory contains a set of semi-automated tests which are to be run on a physical device with a real YubiKey. The tests are divided by test area into several files:
|
||||
|
||||
|===
|
||||
|Test file | Test area | Notes
|
||||
|
||||
|`management_test.dart`
|
||||
|Test turning OTP app off and on
|
||||
|Currently only Desktop
|
||||
|
||||
|`oath_test.dart`
|
||||
|Test oath app functionality: add, update and remove account, set, change and remove password
|
||||
|
|
||||
|===
|
||||
|
||||
To run the tests:
|
||||
|
||||
1. connect your YubiKey via USB to your host
|
||||
|
||||
2. pass a specific test file into the flutter test framework. The command needs to be run in the repository root. Example:
|
||||
|
||||
flutter test integration_test/oath_test.dart
|
||||
|
||||
|
||||
=== Notes
|
||||
|
||||
==== USB only
|
||||
Currently the tests work only with USB connected keys on both desktop and Android.
|
||||
|
||||
==== Avoiding data loss
|
||||
To avoid data loss, only approved YubiKeys will be accepted by the framework. Update `approved_yubikeys.dart` file with serial numbers of your testing YubiKeys:
|
||||
|
||||
var approvedYubiKeys = <String>['12345678', '98765432'];
|
||||
|
||||
==== Preparing YubiKeys for testing
|
||||
Before running the tests, remove passwords from the YubiKeys used for testing.
|
||||
|
||||
==== Android permission
|
||||
On Android, you have to confirm USB permissions when the first test is executed and Camera permission first time we are adding account. We implemented a custom test driver which can be used to avoid the Camera permission grant dialog. Use this command to run with the test driver:
|
||||
|
||||
flutter drive --driver=integration_test/android/test_driver.dart --target=integration_test/oath_test.dart -d DEVICE
|
||||
|
||||
where DEVICE is obtained with
|
||||
|
||||
flutter devices
|
@ -15,9 +15,11 @@
|
||||
# limitations under the License.
|
||||
|
||||
from helper import run_rpc_pipes, run_rpc_socket
|
||||
from typing import cast
|
||||
|
||||
import socket
|
||||
import sys
|
||||
import io
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -32,8 +34,8 @@ if __name__ == "__main__":
|
||||
|
||||
run_rpc_socket(sock)
|
||||
else:
|
||||
sys.stdin.reconfigure(encoding="utf-8")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.stderr.reconfigure(encoding="utf-8")
|
||||
cast(io.TextIOWrapper, sys.stdin).reconfigure(encoding="utf-8")
|
||||
cast(io.TextIOWrapper, sys.stdout).reconfigure(encoding="utf-8")
|
||||
cast(io.TextIOWrapper, sys.stderr).reconfigure(encoding="utf-8")
|
||||
|
||||
run_rpc_pipes(sys.stdout, sys.stdin)
|
||||
|
@ -24,7 +24,7 @@ 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.
|
||||
# MacOS: If the running Python process is "universal", build a universal2 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:
|
||||
|
@ -75,6 +75,11 @@ class AuthRequiredException(RpcException):
|
||||
super().__init__("auth-required", "Authentication is required")
|
||||
|
||||
|
||||
class PinComplexityException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__("pin-complexity", "PIN does not meet complexity requirements")
|
||||
|
||||
|
||||
class ChildResetException(Exception):
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
@ -42,6 +42,7 @@ from yubikit.logging import LOG_LEVEL
|
||||
from ykman.pcsc import list_devices, YK_READER_NAME
|
||||
from smartcard.Exceptions import SmartcardException, NoCardException
|
||||
from smartcard.pcsc.PCSCExceptions import EstablishContextException
|
||||
from smartcard.CardMonitoring import CardObserver, CardMonitor
|
||||
from hashlib import sha256
|
||||
from dataclasses import asdict
|
||||
from typing import Mapping, Tuple
|
||||
@ -61,11 +62,15 @@ def _is_admin():
|
||||
|
||||
|
||||
class ConnectionException(RpcException):
|
||||
def __init__(self, connection, exc_type):
|
||||
def __init__(self, device, connection, exc_type):
|
||||
super().__init__(
|
||||
"connection-error",
|
||||
f"Error connecting to {connection} interface",
|
||||
dict(connection=connection, exc_type=type(exc_type).__name__),
|
||||
dict(
|
||||
device=device,
|
||||
connection=connection,
|
||||
exc_type=type(exc_type).__name__,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@ -181,10 +186,28 @@ class DevicesNode(RpcNode):
|
||||
self._list_state = 0
|
||||
self._devices = {}
|
||||
self._device_mapping = {}
|
||||
self._failing_connection = {}
|
||||
self._retries = 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
with self._get_state:
|
||||
return super().__call__(*args, **kwargs)
|
||||
try:
|
||||
return super().__call__(*args, **kwargs)
|
||||
except ConnectionException as e:
|
||||
if self._failing_connection == e.body:
|
||||
self._retries += 1
|
||||
else:
|
||||
self._failing_connection = e.body
|
||||
self._retries = 0
|
||||
if self._retries > 2:
|
||||
raise
|
||||
logger.debug("Connection failed, attempt to recover", exc_info=True)
|
||||
raise ChildResetException(f"{e}")
|
||||
|
||||
def close(self):
|
||||
self._list_state = 0
|
||||
self._device_mapping = {}
|
||||
super().close()
|
||||
|
||||
@action(closes_child=False)
|
||||
def scan(self, *ignored):
|
||||
@ -263,9 +286,6 @@ class AbstractDeviceNode(RpcNode):
|
||||
|
||||
|
||||
class UsbDeviceNode(AbstractDeviceNode):
|
||||
def __init__(self, device, info):
|
||||
super().__init__(device, info)
|
||||
|
||||
def _supports_connection(self, conn_type):
|
||||
return self._device.pid.supports_connection(conn_type)
|
||||
|
||||
@ -289,7 +309,7 @@ class UsbDeviceNode(AbstractDeviceNode):
|
||||
return self._create_connection(SmartCardConnection)
|
||||
except (ValueError, SmartcardException, EstablishContextException) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException("ccid", e)
|
||||
raise ConnectionException(self._device.fingerprint, "ccid", e)
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(OtpConnection))
|
||||
def otp(self):
|
||||
@ -297,7 +317,7 @@ class UsbDeviceNode(AbstractDeviceNode):
|
||||
return self._create_connection(OtpConnection)
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException("otp", e)
|
||||
raise ConnectionException(self._device.fingerprint, "otp", e)
|
||||
|
||||
@child(condition=lambda self: self._supports_connection(FidoConnection))
|
||||
def fido(self):
|
||||
@ -305,18 +325,56 @@ class UsbDeviceNode(AbstractDeviceNode):
|
||||
return self._create_connection(FidoConnection)
|
||||
except (ValueError, OSError) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException("fido", e)
|
||||
raise ConnectionException(self._device.fingerprint, "fido", e)
|
||||
|
||||
|
||||
class _ReaderObserver(CardObserver):
|
||||
def __init__(self, device):
|
||||
self.device = device
|
||||
self.card = None
|
||||
self.data = None
|
||||
|
||||
def update(self, observable, actions):
|
||||
added, removed = actions
|
||||
for card in added:
|
||||
if card.reader == self.device.reader.name:
|
||||
if card != self.card:
|
||||
self.card = card
|
||||
break
|
||||
else:
|
||||
self.card = None
|
||||
self.data = None
|
||||
logger.debug(f"NFC card: {self.card}")
|
||||
|
||||
|
||||
class ReaderDeviceNode(AbstractDeviceNode):
|
||||
def __init__(self, device, info):
|
||||
super().__init__(device, info)
|
||||
self._observer = _ReaderObserver(device)
|
||||
self._monitor = CardMonitor()
|
||||
self._monitor.addObserver(self._observer)
|
||||
|
||||
def close(self):
|
||||
self._monitor.deleteObserver(self._observer)
|
||||
super().close()
|
||||
|
||||
def get_data(self):
|
||||
try:
|
||||
with self._device.open_connection(SmartCardConnection) as conn:
|
||||
return dict(self._read_data(conn), present=True)
|
||||
except NoCardException:
|
||||
return dict(present=False, status="no-card")
|
||||
except ValueError:
|
||||
return dict(present=False, status="unknown-device")
|
||||
if self._observer.data is None:
|
||||
card = self._observer.card
|
||||
if card is None:
|
||||
return dict(present=False, status="no-card")
|
||||
try:
|
||||
with self._device.open_connection(SmartCardConnection) as conn:
|
||||
self._observer.data = dict(self._read_data(conn), present=True)
|
||||
except NoCardException:
|
||||
return dict(present=False, status="no-card")
|
||||
except ValueError:
|
||||
self._observer.data = dict(present=False, status="unknown-device")
|
||||
return self._observer.data
|
||||
|
||||
@action(closes_child=False)
|
||||
def get(self, params, event, signal):
|
||||
return super().get(params, event, signal)
|
||||
|
||||
@child
|
||||
def ccid(self):
|
||||
@ -326,7 +384,7 @@ class ReaderDeviceNode(AbstractDeviceNode):
|
||||
return ConnectionNode(self._device, connection, info)
|
||||
except (ValueError, SmartcardException, EstablishContextException) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException("ccid", e)
|
||||
raise ConnectionException(self._device.fingerprint, "ccid", e)
|
||||
|
||||
@child
|
||||
def fido(self):
|
||||
@ -337,7 +395,7 @@ class ReaderDeviceNode(AbstractDeviceNode):
|
||||
return ConnectionNode(self._device, connection, info)
|
||||
except (ValueError, SmartcardException, EstablishContextException) as e:
|
||||
logger.warning("Error opening connection", exc_info=True)
|
||||
raise ConnectionException("fido", e)
|
||||
raise ConnectionException(self._device.fingerprint, "fido", e)
|
||||
|
||||
|
||||
class ConnectionNode(RpcNode):
|
||||
@ -413,7 +471,9 @@ class ConnectionNode(RpcNode):
|
||||
or ( # SmartCardConnection can be used over NFC, or on 5.3 and later.
|
||||
isinstance(self._connection, SmartCardConnection)
|
||||
and (
|
||||
self._transport == TRANSPORT.NFC or self._info.version >= (5, 3, 0)
|
||||
self._transport == TRANSPORT.NFC
|
||||
or self._info.version >= (5, 3, 0)
|
||||
or self._info.version[0] == 3
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -19,12 +19,14 @@ from .base import (
|
||||
RpcException,
|
||||
TimeoutException,
|
||||
AuthRequiredException,
|
||||
PinComplexityException,
|
||||
)
|
||||
from fido2.ctap import CtapError
|
||||
from fido2.ctap2 import Ctap2, ClientPin
|
||||
from fido2.ctap2.credman import CredentialManagement
|
||||
from fido2.ctap2.bio import BioEnrollment, FPBioEnrollment, CaptureError
|
||||
from fido2.pcsc import CtapPcscDevice
|
||||
from fido2.hid import CtapHidDevice
|
||||
from yubikit.core.fido import FidoConnection
|
||||
from ykman.hid import list_ctap_devices as list_ctap
|
||||
from ykman.pcsc import list_devices as list_ccid
|
||||
@ -46,6 +48,21 @@ class PinValidationException(RpcException):
|
||||
)
|
||||
|
||||
|
||||
class InactivityException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"user-action-timeout",
|
||||
"Failed to add fingerprint due to user inactivity.",
|
||||
)
|
||||
|
||||
|
||||
class KeyMismatchException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"key-mismatch", "Re-inserted YubiKey does not match initial device"
|
||||
)
|
||||
|
||||
|
||||
def _ctap_id(ctap):
|
||||
return (ctap.info.aaguid, ctap.info.firmware_version)
|
||||
|
||||
@ -60,6 +77,8 @@ def _handle_pin_error(e, client_pin):
|
||||
raise PinValidationException(
|
||||
pin_retries, e.code == CtapError.ERR.PIN_AUTH_BLOCKED
|
||||
)
|
||||
if e.code == CtapError.ERR.PIN_POLICY_VIOLATION:
|
||||
raise PinComplexityException()
|
||||
raise e
|
||||
|
||||
|
||||
@ -69,7 +88,6 @@ class Ctap2Node(RpcNode):
|
||||
self.ctap = Ctap2(connection)
|
||||
self._info = self.ctap.info
|
||||
self.client_pin = ClientPin(self.ctap)
|
||||
self._auth_blocked = False
|
||||
self._token = None
|
||||
|
||||
def get_data(self):
|
||||
@ -77,7 +95,6 @@ class Ctap2Node(RpcNode):
|
||||
logger.debug(f"Info: {self._info}")
|
||||
data = dict(
|
||||
info=asdict(self._info),
|
||||
auth_blocked=self._auth_blocked,
|
||||
unlocked=self._token is not None,
|
||||
)
|
||||
if self._info.options.get("clientPin"):
|
||||
@ -94,8 +111,10 @@ class Ctap2Node(RpcNode):
|
||||
data.update(uv_retries=uv_retries)
|
||||
return data
|
||||
|
||||
def _prepare_reset_nfc(self, event, signal):
|
||||
reader_name = self.ctap.device._name
|
||||
@staticmethod
|
||||
def _prepare_reset_nfc(device, event, signal):
|
||||
# TODO: Don't use private member _name.
|
||||
reader_name = device._name
|
||||
devices = list_ccid(reader_name)
|
||||
if not devices or devices[0].reader.name != reader_name:
|
||||
raise ValueError("Unable to isolate NFC reader")
|
||||
@ -106,10 +125,12 @@ class Ctap2Node(RpcNode):
|
||||
removed = False
|
||||
while not event.wait(0.5):
|
||||
try:
|
||||
with dev.open_connection(FidoConnection):
|
||||
if removed:
|
||||
sleep(1.0) # Wait for the device to settle
|
||||
return dev.open_connection(FidoConnection)
|
||||
conn = dev.open_connection(FidoConnection)
|
||||
if removed:
|
||||
conn.close()
|
||||
sleep(1.0) # Wait for the device to settle
|
||||
return dev.open_connection(FidoConnection)
|
||||
conn.close()
|
||||
except CardConnectionException:
|
||||
pass # Expected, ignore
|
||||
except NoCardException:
|
||||
@ -119,8 +140,9 @@ class Ctap2Node(RpcNode):
|
||||
|
||||
raise TimeoutException()
|
||||
|
||||
def _prepare_reset_usb(self, event, signal):
|
||||
dev_path = self.ctap.device.descriptor.path
|
||||
@staticmethod
|
||||
def _prepare_reset_usb(device, event, signal):
|
||||
dev_path = device.descriptor.path
|
||||
logger.debug(f"Reset over USB: {dev_path}")
|
||||
|
||||
signal("reset", dict(state="remove"))
|
||||
@ -148,25 +170,31 @@ class Ctap2Node(RpcNode):
|
||||
@action
|
||||
def reset(self, params, event, signal):
|
||||
target = _ctap_id(self.ctap)
|
||||
if isinstance(self.ctap.device, CtapPcscDevice):
|
||||
connection = self._prepare_reset_nfc(event, signal)
|
||||
device = self.ctap.device
|
||||
if isinstance(device, CtapPcscDevice):
|
||||
connection = self._prepare_reset_nfc(device, event, signal)
|
||||
elif isinstance(device, CtapHidDevice):
|
||||
connection = self._prepare_reset_usb(device, event, signal)
|
||||
else:
|
||||
connection = self._prepare_reset_usb(event, signal)
|
||||
raise TypeError("Unsupported connection type")
|
||||
|
||||
logger.debug("Performing reset...")
|
||||
self.ctap = Ctap2(connection)
|
||||
if target != _ctap_id(self.ctap):
|
||||
raise ValueError("Re-inserted YubiKey does not match initial device")
|
||||
self.ctap.reset(event=event)
|
||||
raise KeyMismatchException()
|
||||
try:
|
||||
self.ctap.reset(event=event)
|
||||
except CtapError as e:
|
||||
if e.code == CtapError.ERR.USER_ACTION_TIMEOUT:
|
||||
raise InactivityException()
|
||||
self._info = self.ctap.get_info()
|
||||
self._auth_blocked = False
|
||||
self._token = None
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self._info.options["clientPin"])
|
||||
def unlock(self, params, event, signal):
|
||||
pin = params.pop("pin")
|
||||
permissions = 0
|
||||
permissions = ClientPin.PERMISSION(0)
|
||||
if CredentialManagement.is_supported(self._info):
|
||||
permissions |= ClientPin.PERMISSION.CREDENTIAL_MGMT
|
||||
if BioEnrollment.is_supported(self._info):
|
||||
@ -255,12 +283,14 @@ class CredentialsRpNode(RpcNode):
|
||||
self.refresh()
|
||||
|
||||
def refresh(self):
|
||||
self.refresh_rps()
|
||||
self._creds = {
|
||||
cred[CredentialManagement.RESULT.CREDENTIAL_ID]["id"].hex(): dict(
|
||||
credential_id=cred[CredentialManagement.RESULT.CREDENTIAL_ID],
|
||||
user_id=cred[CredentialManagement.RESULT.USER]["id"],
|
||||
user_name=cred[CredentialManagement.RESULT.USER]["name"],
|
||||
display_name=cred[CredentialManagement.RESULT.USER].get(
|
||||
"displayName", None
|
||||
),
|
||||
)
|
||||
for cred in self.credman.enumerate_creds(self.data["rp_id_hash"])
|
||||
}
|
||||
@ -273,17 +303,17 @@ class CredentialsRpNode(RpcNode):
|
||||
return CredentialNode(
|
||||
self.credman,
|
||||
self._creds[name],
|
||||
self.refresh,
|
||||
self.refresh_rps,
|
||||
)
|
||||
return super().create_child(name)
|
||||
|
||||
|
||||
class CredentialNode(RpcNode):
|
||||
def __init__(self, credman, credential_data, refresh):
|
||||
def __init__(self, credman, credential_data, refresh_rps):
|
||||
super().__init__()
|
||||
self.credman = credman
|
||||
self.data = credential_data
|
||||
self.refresh = refresh
|
||||
self.refresh_rps = refresh_rps
|
||||
|
||||
def get_data(self):
|
||||
return self.data
|
||||
@ -291,7 +321,7 @@ class CredentialNode(RpcNode):
|
||||
@action
|
||||
def delete(self, params, event, signal):
|
||||
self.credman.delete_cred(self.data["credential_id"])
|
||||
self.refresh()
|
||||
self.refresh_rps()
|
||||
|
||||
|
||||
class FingerprintsNode(RpcNode):
|
||||
@ -333,6 +363,10 @@ class FingerprintsNode(RpcNode):
|
||||
signal("capture", dict(remaining=enroller.remaining))
|
||||
except CaptureError as e:
|
||||
signal("capture-error", dict(code=e.code))
|
||||
except CtapError as e:
|
||||
if e.code == CtapError.ERR.USER_ACTION_TIMEOUT:
|
||||
raise InactivityException()
|
||||
raise
|
||||
if name:
|
||||
self.bio.set_name(template_id, name)
|
||||
self._templates[template_id] = name
|
||||
|
@ -13,7 +13,7 @@
|
||||
# limitations under the License.
|
||||
|
||||
from .base import RpcNode, action
|
||||
from yubikit.core import require_version, NotSupportedError, TRANSPORT
|
||||
from yubikit.core import require_version, NotSupportedError, TRANSPORT, Connection
|
||||
from yubikit.core.smartcard import SmartCardConnection
|
||||
from yubikit.core.otp import OtpConnection
|
||||
from yubikit.core.fido import FidoConnection
|
||||
@ -21,6 +21,7 @@ from yubikit.management import ManagementSession, DeviceConfig, Mode, CAPABILITY
|
||||
from ykman.device import list_all_devices
|
||||
from dataclasses import asdict
|
||||
from time import sleep
|
||||
from typing import Type
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -29,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
class ManagementNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
self._connection_type = type(connection)
|
||||
self._connection_type: Type[Connection] = type(connection)
|
||||
self.session = ManagementSession(connection)
|
||||
|
||||
def get_data(self):
|
||||
@ -54,11 +55,13 @@ class ManagementNode(RpcNode):
|
||||
if self._connection_type.usb_interface in ifaces:
|
||||
connection_types = [self._connection_type]
|
||||
else:
|
||||
connection_types = [
|
||||
t
|
||||
for t in [SmartCardConnection, OtpConnection, FidoConnection]
|
||||
if t.usb_interface in ifaces
|
||||
types: list[Type[Connection]] = [
|
||||
SmartCardConnection,
|
||||
OtpConnection,
|
||||
# mypy doesn't support ABC.register()
|
||||
FidoConnection, # type: ignore
|
||||
]
|
||||
connection_types = [t for t in types if t.usb_interface in ifaces]
|
||||
|
||||
self.session.close()
|
||||
logger.debug("Waiting for device to re-appear...")
|
||||
@ -97,3 +100,10 @@ class ManagementNode(RpcNode):
|
||||
params.pop("auto_eject_timeout"),
|
||||
)
|
||||
return dict()
|
||||
|
||||
@action(
|
||||
condition=lambda self: issubclass(self._connection_type, SmartCardConnection)
|
||||
)
|
||||
def device_reset(self, params, event, signal):
|
||||
self.session.device_reset()
|
||||
return dict()
|
||||
|
@ -20,6 +20,7 @@ from .base import (
|
||||
ChildResetException,
|
||||
TimeoutException,
|
||||
AuthRequiredException,
|
||||
PinComplexityException,
|
||||
)
|
||||
from yubikit.core import NotSupportedError, BadResponseError, InvalidPinError
|
||||
from yubikit.core.smartcard import ApduError, SW
|
||||
@ -75,10 +76,20 @@ class InvalidPinException(RpcException):
|
||||
|
||||
@unique
|
||||
class GENERATE_TYPE(str, Enum):
|
||||
PUBLIC_KEY = "publicKey"
|
||||
CSR = "csr"
|
||||
CERTIFICATE = "certificate"
|
||||
|
||||
|
||||
def _handle_pin_puk_error(e):
|
||||
if isinstance(e, ApduError):
|
||||
if e.sw == SW.CONDITIONS_NOT_SATISFIED:
|
||||
raise PinComplexityException()
|
||||
if isinstance(e, InvalidPinError):
|
||||
raise InvalidPinException(cause=e)
|
||||
raise e
|
||||
|
||||
|
||||
class PivNode(RpcNode):
|
||||
def __init__(self, connection):
|
||||
super().__init__()
|
||||
@ -166,6 +177,7 @@ class PivNode(RpcNode):
|
||||
key = None
|
||||
|
||||
if self._pivman_data.has_derived_key:
|
||||
assert self._pivman_data.salt # nosec
|
||||
key = derive_management_key(pin, self._pivman_data.salt)
|
||||
elif self._pivman_data.has_stored_key:
|
||||
pivman_prot = get_pivman_protected_data(self.session)
|
||||
@ -206,21 +218,30 @@ class PivNode(RpcNode):
|
||||
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)
|
||||
try:
|
||||
pivman_change_pin(self.session, old_pin, new_pin)
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
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)
|
||||
try:
|
||||
self.session.change_puk(old_puk, new_puk)
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
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)
|
||||
try:
|
||||
self.session.unblock_pin(puk, new_pin)
|
||||
except Exception as e:
|
||||
_handle_pin_puk_error(e)
|
||||
return dict()
|
||||
|
||||
@action
|
||||
@ -296,12 +317,25 @@ def _choose_cert(certs):
|
||||
def _get_cert_info(cert):
|
||||
if cert is None:
|
||||
return None
|
||||
try: # Prefer timezone-aware variant (cryptography >= 42)
|
||||
not_before = cert.not_valid_before_utc
|
||||
not_after = cert.not_valid_after_utc
|
||||
except AttributeError:
|
||||
not_before = cert.not_valid_before
|
||||
not_after = cert.not_valid_after
|
||||
|
||||
try:
|
||||
key_type = KEY_TYPE.from_public_key(cert.public_key())
|
||||
except ValueError:
|
||||
key_type = None
|
||||
|
||||
return dict(
|
||||
key_type=key_type,
|
||||
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(),
|
||||
not_valid_before=not_before.isoformat(),
|
||||
not_valid_after=not_after.isoformat(),
|
||||
fingerprint=cert.fingerprint(hashes.SHA256()),
|
||||
)
|
||||
|
||||
@ -340,7 +374,7 @@ class SlotsNode(RpcNode):
|
||||
f"{int(slot):02x}": dict(
|
||||
slot=int(slot),
|
||||
name=slot.name,
|
||||
has_key=metadata is not None if self._has_metadata else None,
|
||||
metadata=_metadata_dict(metadata),
|
||||
cert_info=_get_cert_info(cert),
|
||||
)
|
||||
for slot, (metadata, cert) in self._slots.items()
|
||||
@ -354,6 +388,17 @@ class SlotsNode(RpcNode):
|
||||
return super().create_child(name)
|
||||
|
||||
|
||||
def _metadata_dict(metadata):
|
||||
if not metadata:
|
||||
return None
|
||||
data = asdict(metadata)
|
||||
data["public_key"] = metadata.public_key.public_bytes(
|
||||
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
|
||||
).decode()
|
||||
del data["public_key_encoded"]
|
||||
return data
|
||||
|
||||
|
||||
class SlotNode(RpcNode):
|
||||
def __init__(self, session, slot, metadata, certificate, refresh):
|
||||
super().__init__()
|
||||
@ -367,18 +412,47 @@ class SlotNode(RpcNode):
|
||||
return dict(
|
||||
id=f"{int(self.slot):02x}",
|
||||
name=self.slot.name,
|
||||
metadata=asdict(self.metadata) if self.metadata else None,
|
||||
metadata=_metadata_dict(self.metadata),
|
||||
certificate=self.certificate.public_bytes(encoding=Encoding.PEM).decode()
|
||||
if self.certificate
|
||||
else None,
|
||||
)
|
||||
|
||||
@action(condition=lambda self: self.certificate)
|
||||
@action(condition=lambda self: self.certificate or self.metadata)
|
||||
def delete(self, params, event, signal):
|
||||
self.session.delete_certificate(self.slot)
|
||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||
delete_cert = params.pop("delete_cert", False)
|
||||
delete_key = params.pop("delete_key", False)
|
||||
|
||||
if not delete_cert and not delete_key:
|
||||
raise ValueError("Missing delete option")
|
||||
|
||||
if delete_cert:
|
||||
self.session.delete_certificate(self.slot)
|
||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||
self.certificate = None
|
||||
if delete_key:
|
||||
self.session.delete_key(self.slot)
|
||||
self._refresh()
|
||||
return dict()
|
||||
|
||||
@action(condition=lambda self: self.metadata)
|
||||
def move_key(self, params, event, signal):
|
||||
destination = params.pop("destination")
|
||||
overwrite_key = params.pop("overwrite_key")
|
||||
include_certificate = params.pop("include_certificate")
|
||||
|
||||
if include_certificate:
|
||||
source_object = self.session.get_object(OBJECT_ID.from_slot(self.slot))
|
||||
destination = SLOT(int(destination, base=16))
|
||||
if overwrite_key:
|
||||
self.session.delete_key(destination)
|
||||
self.session.move_key(self.slot, destination)
|
||||
if include_certificate:
|
||||
self.session.put_object(OBJECT_ID.from_slot(destination), source_object)
|
||||
self.session.delete_certificate(self.slot)
|
||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||
self.certificate = None
|
||||
self._refresh()
|
||||
self.certificate = None
|
||||
return dict()
|
||||
|
||||
@action
|
||||
@ -417,7 +491,7 @@ class SlotNode(RpcNode):
|
||||
self._refresh()
|
||||
|
||||
return dict(
|
||||
metadata=asdict(metadata) if metadata else None,
|
||||
metadata=_metadata_dict(metadata),
|
||||
public_key=private_key.public_key()
|
||||
.public_bytes(
|
||||
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
|
||||
@ -442,6 +516,9 @@ class SlotNode(RpcNode):
|
||||
public_key = self.session.generate_key(
|
||||
self.slot, key_type, pin_policy, touch_policy
|
||||
)
|
||||
public_key_pem = public_key.public_bytes(
|
||||
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
|
||||
).decode()
|
||||
|
||||
if pin_policy != PIN_POLICY.NEVER:
|
||||
# TODO: Check if verified?
|
||||
@ -451,14 +528,17 @@ class SlotNode(RpcNode):
|
||||
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)
|
||||
if generate_type == GENERATE_TYPE.PUBLIC_KEY:
|
||||
result = public_key_pem
|
||||
elif generate_type == GENERATE_TYPE.CSR:
|
||||
csr = generate_csr(self.session, self.slot, public_key, subject)
|
||||
result = csr.public_bytes(encoding=Encoding.PEM).decode()
|
||||
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(
|
||||
cert = generate_self_signed_certificate(
|
||||
self.session,
|
||||
self.slot,
|
||||
public_key,
|
||||
@ -466,16 +546,12 @@ class SlotNode(RpcNode):
|
||||
datetime.datetime.strptime(valid_from, _date_format),
|
||||
datetime.datetime.strptime(valid_to, _date_format),
|
||||
)
|
||||
self.session.put_certificate(self.slot, result)
|
||||
result = cert.public_bytes(encoding=Encoding.PEM).decode()
|
||||
self.session.put_certificate(self.slot, cert)
|
||||
self.session.put_object(OBJECT_ID.CHUID, generate_chuid())
|
||||
else:
|
||||
raise ValueError("Unsupported GENERATE_TYPE")
|
||||
raise ValueError(f"Unsupported GENERATE_TYPE: {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(),
|
||||
)
|
||||
return dict(public_key=public_key_pem, result=result)
|
||||
|
@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .base import RpcException
|
||||
import mss
|
||||
import zxingcpp
|
||||
import base64
|
||||
@ -21,7 +22,7 @@ import sys
|
||||
import subprocess # nosec
|
||||
import tempfile
|
||||
from mss.exception import ScreenShotError
|
||||
from PIL import Image
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
|
||||
def _capture_screen():
|
||||
@ -64,11 +65,22 @@ def _capture_screen():
|
||||
raise ValueError("Unable to capture screenshot")
|
||||
|
||||
|
||||
class InvalidImageException(RpcException):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"invalid-image",
|
||||
"The provided file is not a valid image",
|
||||
)
|
||||
|
||||
|
||||
def scan_qr(image_data=None):
|
||||
if image_data:
|
||||
msg = base64.b64decode(image_data)
|
||||
buf = io.BytesIO(msg)
|
||||
img = Image.open(buf)
|
||||
try:
|
||||
msg = base64.b64decode(image_data)
|
||||
buf = io.BytesIO(msg)
|
||||
img = Image.open(buf)
|
||||
except UnidentifiedImageError:
|
||||
raise InvalidImageException()
|
||||
else:
|
||||
img = _capture_screen()
|
||||
|
||||
|
@ -14,7 +14,8 @@
|
||||
|
||||
from .base import RpcNode, action, child
|
||||
|
||||
from yubikit.core import NotSupportedError
|
||||
from yubikit.core import NotSupportedError, CommandError
|
||||
from yubikit.core.otp import modhex_encode, modhex_decode
|
||||
from yubikit.yubiotp import (
|
||||
YubiOtpSession,
|
||||
SLOT,
|
||||
@ -25,7 +26,17 @@ from yubikit.yubiotp import (
|
||||
YubiOtpSlotConfiguration,
|
||||
StaticTicketSlotConfiguration,
|
||||
)
|
||||
from ykman.otp import generate_static_pw, format_csv
|
||||
from yubikit.oath import parse_b32_key
|
||||
from ykman.scancodes import KEYBOARD_LAYOUT, encode
|
||||
|
||||
from typing import Dict
|
||||
import struct
|
||||
|
||||
_FAIL_MSG = (
|
||||
"Failed to write to the YubiKey. Make sure the device does not "
|
||||
"have restricted access"
|
||||
)
|
||||
|
||||
|
||||
class YubiOtpNode(RpcNode):
|
||||
@ -54,7 +65,10 @@ class YubiOtpNode(RpcNode):
|
||||
|
||||
@action
|
||||
def swap(self, params, event, signal):
|
||||
self.session.swap_slots()
|
||||
try:
|
||||
self.session.swap_slots()
|
||||
except CommandError:
|
||||
raise ValueError(_FAIL_MSG)
|
||||
return dict()
|
||||
|
||||
@child
|
||||
@ -65,6 +79,29 @@ class YubiOtpNode(RpcNode):
|
||||
def two(self):
|
||||
return SlotNode(self.session, SLOT.TWO)
|
||||
|
||||
@action(closes_child=False)
|
||||
def serial_modhex(self, params, event, signal):
|
||||
serial = params["serial"]
|
||||
return dict(encoded=modhex_encode(b"\xff\x00" + struct.pack(b">I", serial)))
|
||||
|
||||
@action(closes_child=False)
|
||||
def generate_static(self, params, event, signal):
|
||||
layout, length = params["layout"], int(params["length"])
|
||||
return dict(password=generate_static_pw(length, KEYBOARD_LAYOUT[layout]))
|
||||
|
||||
@action(closes_child=False)
|
||||
def keyboard_layouts(self, params, event, signal):
|
||||
return {layout.name: [sc for sc in layout.value] for layout in KEYBOARD_LAYOUT}
|
||||
|
||||
@action(closes_child=False)
|
||||
def format_yubiotp_csv(self, params, even, signal):
|
||||
serial = params["serial"]
|
||||
public_id = modhex_decode(params["public_id"])
|
||||
private_id = bytes.fromhex(params["private_id"])
|
||||
key = bytes.fromhex(params["key"])
|
||||
|
||||
return dict(csv=format_csv(serial, public_id, private_id, key))
|
||||
|
||||
|
||||
_CONFIG_TYPES = dict(
|
||||
hmac_sha1=HmacSha1SlotConfiguration,
|
||||
@ -113,7 +150,12 @@ class SlotNode(RpcNode):
|
||||
|
||||
@action(condition=lambda self: self._maybe_configured(self.slot))
|
||||
def delete(self, params, event, signal):
|
||||
self.session.delete_slot(self.slot, params.pop("cur_acc_code", None))
|
||||
try:
|
||||
access_code = params.pop("curr_acc_code", None)
|
||||
access_code = bytes.fromhex(access_code) if access_code else None
|
||||
self.session.delete_slot(self.slot, access_code)
|
||||
except CommandError:
|
||||
raise ValueError(_FAIL_MSG)
|
||||
|
||||
@action(condition=lambda self: self._can_calculate(self.slot))
|
||||
def calculate(self, params, event, signal):
|
||||
@ -121,7 +163,7 @@ class SlotNode(RpcNode):
|
||||
response = self.session.calculate_hmac_sha1(self.slot, challenge, event)
|
||||
return dict(response=response)
|
||||
|
||||
def _apply_config(self, config, params):
|
||||
def _apply_options(self, config, options):
|
||||
for option in (
|
||||
"serial_api_visible",
|
||||
"serial_usb_visible",
|
||||
@ -140,39 +182,63 @@ class SlotNode(RpcNode):
|
||||
"short_ticket",
|
||||
"manual_update",
|
||||
):
|
||||
if option in params:
|
||||
getattr(config, option)(params.pop(option))
|
||||
if option in options:
|
||||
getattr(config, option)(options.pop(option))
|
||||
|
||||
for option in ("tabs", "delay", "pacing", "strong_password"):
|
||||
if option in params:
|
||||
getattr(config, option)(*params.pop(option))
|
||||
if option in options:
|
||||
getattr(config, option)(*options.pop(option))
|
||||
|
||||
if "token_id" in params:
|
||||
token_id, *args = params.pop("token_id")
|
||||
if "token_id" in options:
|
||||
token_id, *args = options.pop("token_id")
|
||||
config.token_id(bytes.fromhex(token_id), *args)
|
||||
|
||||
return config
|
||||
|
||||
def _get_config(self, type, **kwargs):
|
||||
config = None
|
||||
|
||||
if type in _CONFIG_TYPES:
|
||||
if type == "hmac_sha1":
|
||||
config = _CONFIG_TYPES[type](bytes.fromhex(kwargs["key"]))
|
||||
elif type == "hotp":
|
||||
config = _CONFIG_TYPES[type](parse_b32_key(kwargs["key"]))
|
||||
elif type == "static_password":
|
||||
config = _CONFIG_TYPES[type](
|
||||
encode(
|
||||
kwargs["password"], KEYBOARD_LAYOUT[kwargs["keyboard_layout"]]
|
||||
)
|
||||
)
|
||||
elif type == "yubiotp":
|
||||
config = _CONFIG_TYPES[type](
|
||||
fixed=modhex_decode(kwargs["public_id"]),
|
||||
uid=bytes.fromhex(kwargs["private_id"]),
|
||||
key=bytes.fromhex(kwargs["key"]),
|
||||
)
|
||||
else:
|
||||
raise ValueError("No supported configuration type provided.")
|
||||
return config
|
||||
|
||||
@action
|
||||
def put(self, params, event, signal):
|
||||
config = None
|
||||
for key in _CONFIG_TYPES:
|
||||
if key in params:
|
||||
if config is not None:
|
||||
raise ValueError("Only one configuration type can be provided.")
|
||||
config = _CONFIG_TYPES[key](
|
||||
*(bytes.fromhex(arg) for arg in params.pop(key))
|
||||
)
|
||||
if config is None:
|
||||
raise ValueError("No supported configuration type provided.")
|
||||
self._apply_config(config, params)
|
||||
self.session.put_configuration(
|
||||
self.slot,
|
||||
config,
|
||||
params.pop("acc_code", None),
|
||||
params.pop("cur_acc_code", None),
|
||||
)
|
||||
return dict()
|
||||
type = params.pop("type")
|
||||
options = params.pop("options", {})
|
||||
access_code = params.pop("curr_acc_code", None)
|
||||
access_code = bytes.fromhex(access_code) if access_code else None
|
||||
args = params
|
||||
|
||||
config = self._get_config(type, **args)
|
||||
self._apply_options(config, options)
|
||||
try:
|
||||
self.session.put_configuration(
|
||||
self.slot,
|
||||
config,
|
||||
access_code,
|
||||
access_code,
|
||||
)
|
||||
return dict()
|
||||
except CommandError:
|
||||
raise ValueError(_FAIL_MSG)
|
||||
|
||||
@action(
|
||||
condition=lambda self: self._state.version >= (2, 2, 0)
|
||||
@ -180,7 +246,7 @@ class SlotNode(RpcNode):
|
||||
)
|
||||
def update(self, params, event, signal):
|
||||
config = UpdateConfiguration()
|
||||
self._apply_config(config, params)
|
||||
self._apply_options(config, params)
|
||||
self.session.update_configuration(
|
||||
self.slot,
|
||||
config,
|
||||
|
594
helper/poetry.lock
generated
Executable file → Normal file
594
helper/poetry.lock
generated
Executable file → Normal file
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
@ -11,6 +11,21 @@ files = [
|
||||
{file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backports-tarfile"
|
||||
version = "1.2.0"
|
||||
description = "Backport of CPython tarfile module"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"},
|
||||
{file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.16.0"
|
||||
@ -102,58 +117,67 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "41.0.4"
|
||||
version = "42.0.8"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{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"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"},
|
||||
{file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"},
|
||||
{file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"},
|
||||
{file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"},
|
||||
{file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"},
|
||||
{file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = ">=1.12"
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[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)"]
|
||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||
nox = ["nox"]
|
||||
pep8test = ["black", "check-sdist", "mypy", "ruff"]
|
||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
||||
sdist = ["build"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.3"
|
||||
version = "1.2.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
|
||||
{file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
|
||||
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
|
||||
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -161,49 +185,49 @@ test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "fido2"
|
||||
version = "1.1.2"
|
||||
version = "1.1.3"
|
||||
description = "FIDO2/WebAuthn library for implementing clients and servers."
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
python-versions = ">=3.8,<4.0"
|
||||
files = [
|
||||
{file = "fido2-1.1.2-py3-none-any.whl", hash = "sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8"},
|
||||
{file = "fido2-1.1.2.tar.gz", hash = "sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573"},
|
||||
{file = "fido2-1.1.3-py3-none-any.whl", hash = "sha256:6be34c0b9fe85e4911fd2d103cce7ae8ce2f064384a7a2a3bd970b3ef7702931"},
|
||||
{file = "fido2-1.1.3.tar.gz", hash = "sha256:26100f226d12ced621ca6198528ce17edf67b78df4287aee1285fee3cd5aa9fc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=2.6,<35 || >35,<44"
|
||||
cryptography = ">=2.6,<35 || >35,<45"
|
||||
|
||||
[package.extras]
|
||||
pcsc = ["pyscard (>=1.9,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "6.8.0"
|
||||
version = "8.0.0"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
|
||||
{file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
|
||||
{file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
|
||||
{file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=0.5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
perf = ["ipython"]
|
||||
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"]
|
||||
test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-resources"
|
||||
version = "6.1.0"
|
||||
version = "6.4.0"
|
||||
description = "Read resources from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "importlib_resources-6.1.0-py3-none-any.whl", hash = "sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83"},
|
||||
{file = "importlib_resources-6.1.0.tar.gz", hash = "sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9"},
|
||||
{file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
|
||||
{file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -211,7 +235,7 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff", "zipp (>=3.17)"]
|
||||
testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@ -226,13 +250,13 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-classes"
|
||||
version = "3.3.0"
|
||||
version = "3.4.0"
|
||||
description = "Utility functions for Python class constructs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jaraco.classes-3.3.0-py3-none-any.whl", hash = "sha256:10afa92b6743f25c0cf5f37c6bb6e18e2c5bb84a16527ccfc0040ea377e7aaeb"},
|
||||
{file = "jaraco.classes-3.3.0.tar.gz", hash = "sha256:c063dd08e89217cee02c8d5e5ec560f2c8ce6cdc2fcdc2e68f7b2e5547ed3621"},
|
||||
{file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"},
|
||||
{file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -240,7 +264,43 @@ more-itertools = "*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-context"
|
||||
version = "5.3.0"
|
||||
description = "Useful decorators and context managers"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"},
|
||||
{file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-functools"
|
||||
version = "4.0.1"
|
||||
description = "Functools like those found in stdlib"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"},
|
||||
{file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
more-itertools = "*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "jeepney"
|
||||
@ -259,27 +319,29 @@ trio = ["async_generator", "trio"]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "23.13.1"
|
||||
version = "25.2.1"
|
||||
description = "Store and access your passwords safely."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"},
|
||||
{file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"},
|
||||
{file = "keyring-25.2.1-py3-none-any.whl", hash = "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50"},
|
||||
{file = "keyring-25.2.1.tar.gz", hash = "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
|
||||
importlib-resources = {version = "*", markers = "python_version < \"3.9\""}
|
||||
"jaraco.classes" = "*"
|
||||
"jaraco.context" = "*"
|
||||
"jaraco.functools" = "*"
|
||||
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
|
||||
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
|
||||
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
|
||||
|
||||
[package.extras]
|
||||
completion = ["shtab"]
|
||||
docs = ["furo", "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)"]
|
||||
completion = ["shtab (>=1.1.0)"]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
@ -297,13 +359,13 @@ altgraph = ">=0.17"
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "10.1.0"
|
||||
version = "10.3.0"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "more-itertools-10.1.0.tar.gz", hash = "sha256:626c369fa0eb37bac0291bce8259b332fd59ac792fa5497b59837309cd5b114a"},
|
||||
{file = "more_itertools-10.1.0-py3-none-any.whl", hash = "sha256:64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6"},
|
||||
{file = "more-itertools-10.3.0.tar.gz", hash = "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463"},
|
||||
{file = "more_itertools-10.3.0-py3-none-any.whl", hash = "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -317,15 +379,73 @@ files = [
|
||||
{file = "mss-9.0.1.tar.gz", hash = "sha256:6eb7b9008cf27428811fa33aeb35f3334db81e3f7cc2dd49ec7c6e5a94b39f12"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.10.1"
|
||||
description = "Optional static typing for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
|
||||
{file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
|
||||
{file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
|
||||
{file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
|
||||
{file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
|
||||
{file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
|
||||
{file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
|
||||
{file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mypy-extensions = ">=1.0.0"
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = ">=4.1.0"
|
||||
|
||||
[package.extras]
|
||||
dmypy = ["psutil (>=4.0)"]
|
||||
install-types = ["pip"]
|
||||
mypyc = ["setuptools (>=50)"]
|
||||
reports = ["lxml"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
version = "24.1"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
|
||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -341,80 +461,110 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.0.1"
|
||||
version = "10.4.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{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"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"},
|
||||
{file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"},
|
||||
{file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"},
|
||||
{file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"},
|
||||
{file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"},
|
||||
{file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"},
|
||||
{file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"},
|
||||
{file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"},
|
||||
{file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"},
|
||||
{file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
|
||||
docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
|
||||
fpx = ["olefile"]
|
||||
mic = ["olefile"]
|
||||
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
|
||||
typing = ["typing-extensions"]
|
||||
xmp = ["defusedxml"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.3.0"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"},
|
||||
{file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"},
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -423,79 +573,87 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.21"
|
||||
version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
|
||||
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.0.0"
|
||||
version = "6.8.0"
|
||||
description = "PyInstaller bundles a Python application and all its dependencies into a single package."
|
||||
optional = false
|
||||
python-versions = "<3.13,>=3.8"
|
||||
files = [
|
||||
{file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"},
|
||||
{file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"},
|
||||
{file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:5ff6bc2784c1026f8e2f04aa3760cbed41408e108a9d4cf1dd52ee8351a3f6e1"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:39ac424d2ee2457d2ab11a5091436e75a0cccae207d460d180aa1fcbbafdd528"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_i686.whl", hash = "sha256:355832a3acc7de90a255ecacd4b9f9e166a547a79c8905d49f14e3a75c1acdb9"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:6303c7a009f47e6a96ef65aed49f41e36ece8d079b9193ca92fe807403e5fe80"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2b71509468c811968c0b5decb5bbe85b6292ea52d7b1f26313d2aabb673fa9a5"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ff31c5b99e05a4384bbe2071df67ec8b2b347640a375eae9b40218be2f1754c6"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:000c36b13fe4cd8d0d8c2bc855b1ddcf39867b5adf389e6b5ca45b25fa3e619d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fe0af018d7d5077180e3144ada89a4da5df8d07716eb7e9482834a56dc57a4e8"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win32.whl", hash = "sha256:d257f6645c7334cbd66f38a4fac62c3ad614cc46302b2b5d9f8cc48c563bce0e"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_amd64.whl", hash = "sha256:81cccfa9b16699b457f4788c5cc119b50f3cd4d0db924955f15c33f2ad27a50d"},
|
||||
{file = "pyinstaller-6.8.0-py3-none-win_arm64.whl", hash = "sha256:1c3060a263758cf7f0144ab4c016097b20451b2469d468763414665db1bb743d"},
|
||||
{file = "pyinstaller-6.8.0.tar.gz", hash = "sha256:3f4b6520f4423fe19bcc2fd63ab7238851ae2bdcbc98f25bc5d2f97cc62012e9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
altgraph = "*"
|
||||
importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
|
||||
macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""}
|
||||
packaging = ">=20.0"
|
||||
packaging = ">=22.0"
|
||||
pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""}
|
||||
pyinstaller-hooks-contrib = ">=2021.4"
|
||||
pyinstaller-hooks-contrib = ">=2024.6"
|
||||
pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""}
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[package.extras]
|
||||
completion = ["argcomplete"]
|
||||
hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2023.9"
|
||||
version = "2024.7"
|
||||
description = "Community maintained hooks for PyInstaller"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"},
|
||||
{file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7-py2.py3-none-any.whl", hash = "sha256:8bf0775771fbaf96bcd2f4dfd6f7ae6c1dd1b1efe254c7e50477b3c08e7841d8"},
|
||||
{file = "pyinstaller_hooks_contrib-2024.7.tar.gz", hash = "sha256:fd5f37dcf99bece184e40642af88be16a9b89613ecb958a8bd1136634fc9fac5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
|
||||
packaging = ">=22.0"
|
||||
setuptools = ">=42.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "pyscard"
|
||||
version = "2.0.7"
|
||||
version = "2.0.10"
|
||||
description = "Smartcard module for Python."
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pyscard-2.0.7-cp310-cp310-win32.whl", hash = "sha256:06666a597e1293421fa90e0d4fc2418add447b10b7dc85f49b3cafc23480f046"},
|
||||
{file = "pyscard-2.0.7-cp310-cp310-win_amd64.whl", hash = "sha256:a2266345bd387854298153264bff8b74f494581880a76e3e8679460c1b090fab"},
|
||||
{file = "pyscard-2.0.7-cp311-cp311-win32.whl", hash = "sha256:beacdcdc3d1516e195f7a38ec3966c5d4df7390c8f036cb41f6fef72bc5cc646"},
|
||||
{file = "pyscard-2.0.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e37b697327e8dc4848c481428d1cbd10b7ae2ce037bc799e5b8bbd2fc3ab5ed"},
|
||||
{file = "pyscard-2.0.7-cp37-cp37m-win32.whl", hash = "sha256:a0c5edbedafba62c68160884f878d9f53996d7219a3fc11b1cea6bab59c7f34a"},
|
||||
{file = "pyscard-2.0.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f704ad40dc40306e1c0981941789518ab16aa1f84443b1d52ec0264884092b3b"},
|
||||
{file = "pyscard-2.0.7-cp38-cp38-win32.whl", hash = "sha256:59a466ab7ae20188dd197664b9ca1ea9524d115a5aa5b16b575a6b772cdcb73c"},
|
||||
{file = "pyscard-2.0.7-cp38-cp38-win_amd64.whl", hash = "sha256:da70aa5b7be5868b88cdb6d4a419d2791b6165beeb90cd01d2748033302a0f43"},
|
||||
{file = "pyscard-2.0.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2d4bdc1f4e0e6c46e417ac1bc9d5990f7cfb24a080e890d453781405f7bd29dc"},
|
||||
{file = "pyscard-2.0.7-cp39-cp39-win32.whl", hash = "sha256:39e030c47878b37ae08038a917959357be6468da52e8b144e84ffc659f50e6e2"},
|
||||
{file = "pyscard-2.0.7-cp39-cp39-win_amd64.whl", hash = "sha256:5a5865675be294c8d91f22dc91e7d897c4138881e5295fb6b2cd821f7c0389d9"},
|
||||
{file = "pyscard-2.0.7.tar.gz", hash = "sha256:278054525fa75fbe8b10460d87edcd03a70ad94d688b11345e4739987f85c1bf"},
|
||||
{file = "pyscard-2.0.10-cp310-cp310-win32.whl", hash = "sha256:2ae1ece465ccd060e0a268cad1a213414ce8f7a8346bdb00b8470cf4b7826915"},
|
||||
{file = "pyscard-2.0.10-cp310-cp310-win_amd64.whl", hash = "sha256:c7197af995768e522665c3d01099224a268e1791b0dd5b8762364063a07503fa"},
|
||||
{file = "pyscard-2.0.10-cp311-cp311-win32.whl", hash = "sha256:af334ecff0a9415e4baa6c6b0c476148dfc81490671dad8eec5eff091bcdfdde"},
|
||||
{file = "pyscard-2.0.10-cp311-cp311-win_amd64.whl", hash = "sha256:c5281b4a124ac27e854b3630eb026e9ac6c4b984e7958586a3cb2b6403c2d11b"},
|
||||
{file = "pyscard-2.0.10-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:3d66ac3c7f6014351847b8efdbc684e9ac0a4ebb60fcb8a573c182e0f13b6fa4"},
|
||||
{file = "pyscard-2.0.10-cp312-cp312-win32.whl", hash = "sha256:eea9aad08d3baa6c5542d7c5c86e4975873c3260379ed2205c1ede713cf3ffb9"},
|
||||
{file = "pyscard-2.0.10-cp312-cp312-win_amd64.whl", hash = "sha256:5fbbc848c93641677bab855ef313b4c4153e63c82bcdf2aba4d79f99699398b5"},
|
||||
{file = "pyscard-2.0.10-cp37-cp37m-win32.whl", hash = "sha256:ebdd8ad859a2df2c9c919932bfd862076345e95e14027123a261bd814e327fb4"},
|
||||
{file = "pyscard-2.0.10-cp37-cp37m-win_amd64.whl", hash = "sha256:1349a5f2113090d9f58947158bcd6ab94d4c8287c461fe86909cd766631a55d8"},
|
||||
{file = "pyscard-2.0.10-cp38-cp38-win32.whl", hash = "sha256:f9f54e3a5b15cd825119f056c517f7cb34da76c934819548a38f77d8f4eea978"},
|
||||
{file = "pyscard-2.0.10-cp38-cp38-win_amd64.whl", hash = "sha256:3de4e46ebfb5f6ae9e9ce225e62c784dceb83dd304b9caad3dd4a00447224c71"},
|
||||
{file = "pyscard-2.0.10-cp39-cp39-win32.whl", hash = "sha256:6f79249bf169ab5f0c5272a0d36576d153a6132ad3e9728c5275a93e99de0f61"},
|
||||
{file = "pyscard-2.0.10-cp39-cp39-win_amd64.whl", hash = "sha256:b4acd0deb624cd931572be306aab3fee7a0e527d2daa8a8bf943e291bc043315"},
|
||||
{file = "pyscard-2.0.10.tar.gz", hash = "sha256:4b9b865df03b29522e80ebae17790a8b3a096a9d885cda19363b44b1a6bf5c1c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@ -504,13 +662,13 @@ pyro = ["Pyro"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.2"
|
||||
version = "8.2.2"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
|
||||
{file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
|
||||
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
|
||||
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -518,11 +676,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
pluggy = ">=1.5,<2.0"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32"
|
||||
@ -575,19 +733,18 @@ jeepney = ">=0.6"
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "68.2.2"
|
||||
version = "70.2.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
|
||||
{file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
|
||||
{file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"},
|
||||
{file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
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"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
|
||||
test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
@ -600,71 +757,98 @@ files = [
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-pillow"
|
||||
version = "10.2.0.20240520"
|
||||
description = "Typing stubs for Pillow"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "types-Pillow-10.2.0.20240520.tar.gz", hash = "sha256:130b979195465fa1e1676d8e81c9c7c30319e8e95b12fae945e8f0d525213107"},
|
||||
{file = "types_Pillow-10.2.0.20240520-py3-none-any.whl", hash = "sha256:33c36494b380e2a269bb742181bea5d9b00820367822dbd3760f07210a1da23d"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yubikey-manager"
|
||||
version = "5.2.0"
|
||||
version = "5.5.1"
|
||||
description = "Tool for managing your YubiKey configuration."
|
||||
optional = false
|
||||
python-versions = ">=3.7,<4.0"
|
||||
python-versions = "<4.0,>=3.8"
|
||||
files = [
|
||||
{file = "yubikey_manager-5.2.0-py3-none-any.whl", hash = "sha256:6e0c82605f92012363ae3d69673eec6c7876e2e366aa049cff66cc6734049165"},
|
||||
{file = "yubikey_manager-5.2.0.tar.gz", hash = "sha256:45e0f09e3cee2375b6f930dd5d89c1d3a7ca5d5cccb599b16a12f8f7d989fd36"},
|
||||
{file = "yubikey_manager-5.5.1-py3-none-any.whl", hash = "sha256:611a6cd088bb18f1ee8d11dfb2c4218ac086d5e40dc718f4a4183b9c4d0e932f"},
|
||||
{file = "yubikey_manager-5.5.1.tar.gz", hash = "sha256:2b1f4e70813973c646eb301c8f2513faf5e4736dd3c564422efdce0349c02afd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0,<9.0"
|
||||
cryptography = ">=3.0,<44"
|
||||
cryptography = ">=3.0,<45"
|
||||
fido2 = ">=1.0,<2.0"
|
||||
keyring = ">=23.4,<24.0"
|
||||
keyring = ">=23.4,<26"
|
||||
pyscard = ">=2.0,<3.0"
|
||||
pywin32 = {version = ">=223", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.17.0"
|
||||
version = "3.19.2"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
|
||||
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
|
||||
{file = "zipp-3.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
|
||||
{file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
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"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "zxing-cpp"
|
||||
version = "2.1.0"
|
||||
version = "2.2.0"
|
||||
description = "Python bindings for the zxing-cpp barcode library"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "zxing_cpp-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb18dcd86fcc26c8d86e7de03645827141f0f7194dcec3e09d46f0a9dda64cc2"},
|
||||
{file = "zxing-cpp-2.2.0.tar.gz", hash = "sha256:11884ef9d1a61e47ad89836339da9e1040cb28b083fb37462bc58e8d46f135bc"},
|
||||
{file = "zxing_cpp-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:eb8ede507dad76d0ed606c17be943cfe554909cdb517f7650da076e8dc9648c0"},
|
||||
{file = "zxing_cpp-2.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e8bae00edea7f6350ced4f954ca2c3386afbf6d85d3303126f9cf8584cec454"},
|
||||
{file = "zxing_cpp-2.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8256ed05d0978e87847e91e2aff03b21c19cf4256dcb8af8a335de8361f027be"},
|
||||
{file = "zxing_cpp-2.2.0-cp310-cp310-win32.whl", hash = "sha256:5e788ec26d10ac057f5027357ab404b1de2dd3ce216c1d0be20437dcd37f1afc"},
|
||||
{file = "zxing_cpp-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:b69dcee874b59201a586e8fc77a7f83a92cd09d029d22a8b7b4f7112db2c675f"},
|
||||
{file = "zxing_cpp-2.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7d63385278ed2c674c756fe3448769b813351584e111cb61e6901a5e3dbf646"},
|
||||
{file = "zxing_cpp-2.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d9e7369bba46727e4a7fa1b5bd2b630696ec042df60063c13f2d844887950b1"},
|
||||
{file = "zxing_cpp-2.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8602d7cad833007df497b8db7c1216fc7d21e077eb02206b45f4fc061b082913"},
|
||||
{file = "zxing_cpp-2.2.0-cp311-cp311-win32.whl", hash = "sha256:e2059fb6d47eb80122856d1524611339bffe27ad7e4cf59bdb6d642989e238b4"},
|
||||
{file = "zxing_cpp-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:70f9f13c4c91cb0747c0dc7ef247b39e19d38d07efd695fac17d11832f93d44a"},
|
||||
{file = "zxing_cpp-2.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:177d5ed00c505e1728a90dc4742c6f0a4d9c2db56101809762f10f98fb822fa2"},
|
||||
{file = "zxing_cpp-2.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9868fbb9770ef2a2b72b6fe67d7528b9e4858ba6a130e59969ecb2e71271cd1"},
|
||||
{file = "zxing_cpp-2.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0b35274af536ac9091d446ba0f69840feba62525feddf24b7b8991d924d6543"},
|
||||
{file = "zxing_cpp-2.2.0-cp312-cp312-win32.whl", hash = "sha256:c2ff0059eef121ae7769ba4baeee52172438ca770cb4054e1d114beeda66226d"},
|
||||
{file = "zxing_cpp-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:e3cdd28dd42176f59aa7fcbd052ec1d60c1fa29363b3fa5a8e41ecf8ce6e9be7"},
|
||||
{file = "zxing_cpp-2.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7a7a616f6c8e02a92f8ab38d91bc14fecfbb3bc6bf2b69c4e5d7bb54eeaea141"},
|
||||
{file = "zxing_cpp-2.2.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16148c7a7926f2f897c69c695f4b334cd1a777bbfa0269204245fad288b47cf"},
|
||||
{file = "zxing_cpp-2.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25d0dae34bdd8745103cd93c0cb315a0de4bb2876e564a35704be971ac07fcd4"},
|
||||
{file = "zxing_cpp-2.2.0-cp38-cp38-win32.whl", hash = "sha256:3297daded419c87b2720ce9620817db4742814d7e31bfddeeb6329266a61f54f"},
|
||||
{file = "zxing_cpp-2.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:7d620de9309d6f8d79dcb87b5c32bc8d72a1dbdc54369c31552a5ab277e25e78"},
|
||||
{file = "zxing_cpp-2.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55a3f24aeb3e71a2090f3ded10d3b44865e60b640cebcb9c5f10e188158314ea"},
|
||||
{file = "zxing_cpp-2.2.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4303e904a174df3c3f7f1817cf42a10521514865d9cdfcc9205e5ff0243c16fa"},
|
||||
{file = "zxing_cpp-2.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70026b8370fc34c257fa76d84a07374a78a8a46cb00d36f285d32c60e9fbf6dd"},
|
||||
{file = "zxing_cpp-2.2.0-cp39-cp39-win32.whl", hash = "sha256:92acc610eb6100cc5dd4c0f583de22e137c051a0f6e653dba6aef07e8c32810f"},
|
||||
{file = "zxing_cpp-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0bcd8855da4a9ef9e92799446353b5b8fa3eb5da21346089f0f09dcbb8bef1b6"},
|
||||
]
|
||||
|
||||
[package.source]
|
||||
type = "file"
|
||||
url = "zxing_cpp-2.1.0-cp312-cp312-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 = "1c10a85b3420fb1057c6371465b7007ff63f562e5cfed9178f25eee2185c45c3"
|
||||
content-hash = "123356b2b1ed4b00f453721795be4c7c10a3583c7d7b42368127f4c46461e50c"
|
||||
|
@ -10,17 +10,16 @@ packages = [
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
yubikey-manager = "5.2.0"
|
||||
yubikey-manager = "^5.5"
|
||||
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-cp312-cp312-win_amd64.whl", markers = "sys_platform == 'win32'"}
|
||||
]
|
||||
Pillow = "^10.0.0"
|
||||
Pillow = "^10.2.0"
|
||||
zxing-cpp = "^2.2.0"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pyinstaller = {version = "^6.0", python = "<3.13"}
|
||||
pytest = "^7.4.0"
|
||||
pytest = "^8.0.0"
|
||||
mypy = "^1.7.1"
|
||||
types-Pillow = "^10.2.0.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@ -28,3 +27,11 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
[tool.mypy]
|
||||
files = "."
|
||||
check_untyped_defs = true
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["smartcard.*", "zxingcpp"]
|
||||
ignore_missing_imports = true
|
||||
|
@ -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, 1, 0),
|
||||
prodvers=(6, 3, 1, 0),
|
||||
filevers=(7, 0, 2, 0),
|
||||
prodvers=(7, 0, 2, 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.1-dev.0'),
|
||||
StringStruct('FileVersion', '7.0.2-dev.0'),
|
||||
StringStruct('LegalCopyright', 'Copyright (c) Yubico'),
|
||||
StringStruct('OriginalFilename', 'authenticator-helper.exe'),
|
||||
StringStruct('ProductName', 'Yubico Authenticator'),
|
||||
StringStruct('ProductVersion', '6.3.1-dev.0')])
|
||||
StringStruct('ProductVersion', '7.0.2-dev.0')])
|
||||
]),
|
||||
VarFileInfo([VarStruct('Translation', [1033, 1200])])
|
||||
]
|
||||
|
Binary file not shown.
138
integration_test/keyless_test.dart
Normal file
138
integration_test/keyless_test.dart
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@Tags(['desktop', 'android'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:yubico_authenticator/app/views/keys.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
|
||||
import 'utils/keyless_test_util.dart';
|
||||
import 'utils/test_util.dart';
|
||||
|
||||
void main() {
|
||||
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
||||
|
||||
group('Startup', () {
|
||||
appTestKeyless('App starts', (WidgetTester tester) async {},
|
||||
tags: 'minimal');
|
||||
});
|
||||
group('Settings', () {
|
||||
appTestKeyless('Click through all Themes', (WidgetTester tester) async {
|
||||
var settingDrawerButton = find.byKey(settingDrawerIcon).hitTestable();
|
||||
await tester.tap(settingDrawerButton);
|
||||
await tester.longWait();
|
||||
await tester.tap(find.byKey(themeModeSetting));
|
||||
await tester.longWait();
|
||||
await tester
|
||||
.tap(find.byKey(themeModeOption(ThemeMode.light)).hitTestable());
|
||||
await tester.longWait();
|
||||
await tester.tap(find.byKey(themeModeSetting));
|
||||
await tester.longWait();
|
||||
await tester
|
||||
.tap(find.byKey(themeModeOption(ThemeMode.dark)).hitTestable());
|
||||
await tester.longWait();
|
||||
await tester.tap(find.byKey(themeModeSetting));
|
||||
await tester.longWait();
|
||||
await tester
|
||||
.tap(find.byKey(themeModeOption(ThemeMode.system)).hitTestable());
|
||||
await tester.longWait();
|
||||
});
|
||||
});
|
||||
group('Help and about', () {
|
||||
var helpDrawerButton = find.byKey(helpDrawerIcon).hitTestable();
|
||||
|
||||
appTestKeyless('Check Licenses view', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.shortWait();
|
||||
var licensesButtonText = find.byKey(licensesButton).hitTestable();
|
||||
await tester.tap(licensesButtonText);
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: do want to click all licenses and see that they show?
|
||||
});
|
||||
group('Opening of URLs', () {
|
||||
appTestKeyless('TOS link', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
if (isAndroid) {
|
||||
expect(find.byKey(tosButton).hitTestable(), findsOneWidget);
|
||||
} else {
|
||||
await tester.tap(find.byKey(tosButton).hitTestable());
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTestKeyless('Privacy link', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
if (isAndroid) {
|
||||
expect(find.byKey(privacyButton).hitTestable(), findsOneWidget);
|
||||
} else {
|
||||
await tester.tap(find.byKey(privacyButton).hitTestable());
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTestKeyless('Feedback link', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
if (isAndroid) {
|
||||
expect(find.byKey(userGuideButton).hitTestable(), findsOneWidget);
|
||||
} else {
|
||||
await tester.tap(find.byKey(userGuideButton).hitTestable());
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTestKeyless('Help link', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
if (isAndroid) {
|
||||
expect(find.byKey(helpButton).hitTestable(), findsOneWidget);
|
||||
} else {
|
||||
await tester.tap(find.byKey(helpButton).hitTestable());
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
});
|
||||
group('Troubleshooting', () {
|
||||
appTestKeyless('Diagnostics Button', skip: isAndroid,
|
||||
(WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
await tester.tap(find.byKey(diagnosticsChip).hitTestable());
|
||||
await tester.longWait();
|
||||
});
|
||||
appTestKeyless('Log button', (WidgetTester tester) async {
|
||||
await tester.tap(helpDrawerButton);
|
||||
await tester.longWait();
|
||||
await tester.tap(find.byKey(logChip).hitTestable());
|
||||
await tester.longWait();
|
||||
});
|
||||
// appTestKeyless('Allow screenshots', (WidgetTester tester) async {
|
||||
// /// Pausing test until we have Android CI.
|
||||
// await tester.tap(helpDrawerButton);
|
||||
// await tester.shortWait();
|
||||
// await tester.tap(find.byKey(screenshotChip).hitTestable());
|
||||
// await tester.longWait();
|
||||
// await tester.tap(find.byKey(screenshotChip).hitTestable());
|
||||
// await tester.shortWait();
|
||||
// });
|
||||
});
|
||||
});
|
||||
}
|
@ -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,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@Tags(['desktop', 'management'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.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'
|
||||
@ -42,8 +45,8 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('Change OTP', () {
|
||||
appTest('Disable OTP', (WidgetTester tester) async {
|
||||
group('Toggle Applications on key', () {
|
||||
appTest('Toggle OTP', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB OTP capability
|
||||
@ -53,10 +56,50 @@ void main() {
|
||||
// we expect OTP to be enabled on the Key for this test
|
||||
expect(otpChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbOtpKey));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.pump(const Duration(milliseconds: 2500));
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (otpChip != null) {
|
||||
await tester.tap(find.byKey(usbOtpKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTest('Toggle PIV', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
var usbPivKey = _getCapabilityWidgetKey(true, 'PIV');
|
||||
var pivChip = await _getCapabilityWidget(usbPivKey);
|
||||
|
||||
// find USB PIV capability
|
||||
if (pivChip != null) {
|
||||
expect(pivChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbPivKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (pivChip != null) {
|
||||
// we expect PIV to be enabled on the Key for this test
|
||||
await tester.tap(find.byKey(usbPivKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
@ -64,19 +107,90 @@ void main() {
|
||||
}
|
||||
});
|
||||
|
||||
appTest('Enable OTP', (WidgetTester tester) async {
|
||||
appTest('Toggle OATH', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB OTP capability
|
||||
var usbOtpKey = _getCapabilityWidgetKey(true, 'OTP');
|
||||
var otpChip = await _getCapabilityWidget(usbOtpKey);
|
||||
if (otpChip != null) {
|
||||
expect(otpChip.selected, equals(false));
|
||||
await tester.tap(find.byKey(usbOtpKey));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
// find USB OATH capability
|
||||
var usbOathKey = _getCapabilityWidgetKey(true, 'OATH');
|
||||
var oathChip = await _getCapabilityWidget(usbOathKey);
|
||||
if (oathChip != null) {
|
||||
// we expect OATH to be enabled on the Key for this test
|
||||
expect(oathChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbOathKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.pump(const Duration(milliseconds: 2500));
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (oathChip != null) {
|
||||
await tester.tap(find.byKey(usbOathKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTest('Toggle OpenPGP', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB OPENPGP capability
|
||||
var usbPgpKey = _getCapabilityWidgetKey(true, 'OpenPGP');
|
||||
var pgpChip = await _getCapabilityWidget(usbPgpKey);
|
||||
if (pgpChip != null) {
|
||||
// we expect OPENPGP to be enabled on the Key for this test
|
||||
expect(pgpChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbPgpKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (pgpChip != null) {
|
||||
await tester.tap(find.byKey(usbPgpKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTest('Toggle YubiHSM Auth', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB YubiHSM Auth capability
|
||||
var usbHsmKey = _getCapabilityWidgetKey(true, 'YubiHSM Auth');
|
||||
var hsmChip = await _getCapabilityWidget(usbHsmKey);
|
||||
if (hsmChip != null) {
|
||||
// we expect YubiHSM Auth to be enabled on the Key for this test
|
||||
expect(hsmChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbHsmKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (hsmChip != null) {
|
||||
await tester.tap(find.byKey(usbHsmKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
@ -84,4 +198,64 @@ void main() {
|
||||
}
|
||||
});
|
||||
});
|
||||
appTest('Toggle FIDO U2F', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB FIDO U2F capability
|
||||
var usbU2fKey = _getCapabilityWidgetKey(true, 'FIDO U2F');
|
||||
var u2fChip = await _getCapabilityWidget(usbU2fKey);
|
||||
if (u2fChip != null) {
|
||||
// we expect FIDO U2F to be enabled on the Key for this test
|
||||
expect(u2fChip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbU2fKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (u2fChip != null) {
|
||||
await tester.tap(find.byKey(usbU2fKey));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
appTest('Toggle FIDO2', (WidgetTester tester) async {
|
||||
await tester.openManagementScreen();
|
||||
|
||||
// find USB FIDO2 capability
|
||||
var usbFido2Key = _getCapabilityWidgetKey(true, 'FIDO2');
|
||||
var fido2Chip = await _getCapabilityWidget(usbFido2Key);
|
||||
if (fido2Chip != null) {
|
||||
// we expect FIDO2 to be enabled on the Key for this test
|
||||
expect(fido2Chip.selected, equals(true));
|
||||
await tester.tap(find.byKey(usbFido2Key));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.shortWait();
|
||||
}
|
||||
await tester.openManagementScreen();
|
||||
if (fido2Chip != null) {
|
||||
await tester.tap(find.byKey(usbFido2Key));
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(management_keys.saveButtonKey));
|
||||
// long wait
|
||||
await tester.ultraLongWait();
|
||||
|
||||
// no management screen visible now
|
||||
expect(find.byKey(management_keys.screenKey), findsNothing);
|
||||
await tester.longWait();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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,10 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@Tags(['android', 'desktop', 'oath'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:yubico_authenticator/app/views/keys.dart';
|
||||
import 'package:yubico_authenticator/core/state.dart';
|
||||
import 'package:yubico_authenticator/oath/keys.dart' as keys;
|
||||
import 'package:yubico_authenticator/oath/models.dart';
|
||||
import 'package:yubico_authenticator/oath/views/account_list.dart';
|
||||
|
||||
import 'utils/oath_test_util.dart';
|
||||
import 'utils/test_util.dart';
|
||||
@ -29,48 +35,210 @@ void main() {
|
||||
group('OATH UI tests', () {
|
||||
appTest('Menu items exist', (WidgetTester tester) async {
|
||||
await tester.tapActionIconButton();
|
||||
await tester.shortWait();
|
||||
expect(find.byKey(keys.addAccountAction), findsOneWidget);
|
||||
expect(find.byKey(keys.setOrManagePasswordAction), findsOneWidget);
|
||||
expect(find.byKey(keys.resetAction), findsOneWidget);
|
||||
|
||||
// close dialog
|
||||
await tester.tapTopLeftCorner();
|
||||
await tester.longWait();
|
||||
});
|
||||
});
|
||||
|
||||
group('Account tests', () {
|
||||
appTest('Create OATH account', (WidgetTester tester) async {
|
||||
// account with issuer
|
||||
var testAccount = const Account(
|
||||
issuer: 'IssuerForTests',
|
||||
name: 'NameForTests',
|
||||
secret: 'aaaaaaaaaaaaaaaa',
|
||||
);
|
||||
|
||||
await tester.deleteAccount(testAccount);
|
||||
await tester.addAccount(testAccount);
|
||||
|
||||
// account without issuer
|
||||
testAccount = const Account(
|
||||
name: 'NoIssuerName',
|
||||
secret: 'bbbbbbbbbbbbbbbb',
|
||||
);
|
||||
|
||||
await tester.deleteAccount(testAccount);
|
||||
await tester.addAccount(testAccount);
|
||||
group('Account creation', () {
|
||||
appTest('Initial reset OATH', (WidgetTester tester) async {
|
||||
/// reset OATH application
|
||||
//await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
await tester.resetOATH();
|
||||
await tester.shortWait();
|
||||
});
|
||||
appTest('Create 32 Accounts', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
|
||||
/// deletes accounts created in previous test
|
||||
appTest('Delete OATH account', (WidgetTester tester) async {
|
||||
var testAccount =
|
||||
const Account(issuer: 'IssuerForTests', name: 'NameForTests');
|
||||
for (var i = 0; i < 32; i += 1) {
|
||||
// just now merely 32 accounts
|
||||
var testAccount = Account(
|
||||
issuer: 'MaxAccount_issuer_$i',
|
||||
name: 'MaxAccount_name_$i',
|
||||
secret: 'abbaabba',
|
||||
);
|
||||
await tester.addAccount(testAccount);
|
||||
await tester.shortWait();
|
||||
|
||||
await tester.deleteAccount(testAccount);
|
||||
expect(await tester.findAccount(testAccount), isNull);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
testAccount = const Account(issuer: null, name: 'NoIssuerName');
|
||||
await tester.deleteAccount(testAccount);
|
||||
expect(await tester.findAccount(testAccount), isNull);
|
||||
await tester.shortWait();
|
||||
}
|
||||
// TODO: verify one more addAccount() is not possible
|
||||
await tester.resetOATH();
|
||||
await tester.shortWait();
|
||||
}, tags: ['slow']);
|
||||
// appTest('Create weird character-accounts and check byte count',
|
||||
// (WidgetTester tester) async {});
|
||||
group('TOTP account tests', () {
|
||||
appTest('TOTP: sha-1', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_totp_sha1',
|
||||
name: 'n__totp_sha1',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.totp,
|
||||
hashAlgorithm: HashAlgorithm.sha1);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
appTest('TOTP: sha-256', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_totp_sha256',
|
||||
name: 'n__totp_sha256',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.totp,
|
||||
hashAlgorithm: HashAlgorithm.sha256);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
appTest('TOTP: sha-512', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_totp_sha512',
|
||||
name: 'n__totp_sha512',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.totp,
|
||||
hashAlgorithm: HashAlgorithm.sha512);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
// appTest('TOTP: period-20',
|
||||
// (WidgetTester tester) async {});
|
||||
// appTest('TOTP: period-45',
|
||||
// (WidgetTester tester) async {});
|
||||
// appTest('TOTP: period-60',
|
||||
// (WidgetTester tester) async {});
|
||||
// appTest('TOTP: digits-8',
|
||||
// (WidgetTester tester) async {});
|
||||
appTest('TOTP: touch', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_totp_touch',
|
||||
name: 'n_totp_touch',
|
||||
secret: 'abbaabba',
|
||||
touch: true,
|
||||
oathType: OathType.totp,
|
||||
hashAlgorithm: HashAlgorithm.sha1);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
await tester.shortWait();
|
||||
});
|
||||
});
|
||||
// group('HOTP account tests', () {
|
||||
appTest('HOTP: sha-1', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_hotp_sha1',
|
||||
name: 'n__hotp_sha1',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.hotp,
|
||||
hashAlgorithm: HashAlgorithm.sha1);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
appTest('HOTP: sha-256', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_hotp_sha256',
|
||||
name: 'n__hotp_sha256',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.hotp,
|
||||
hashAlgorithm: HashAlgorithm.sha256);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
appTest('HOTP: sha-512', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_hotp_sha512',
|
||||
name: 'n__hotp_sha512',
|
||||
secret: 'abbaabba',
|
||||
touch: false,
|
||||
oathType: OathType.hotp,
|
||||
hashAlgorithm: HashAlgorithm.sha512);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
|
||||
await tester.shortWait();
|
||||
});
|
||||
// appTest('TOTP: digits-8',
|
||||
// (WidgetTester tester) async {});
|
||||
appTest('HOTP: touch', (WidgetTester tester) async {
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
const testAccount = Account(
|
||||
issuer: 'i_hotp_touch',
|
||||
name: 'n_hotp_touch',
|
||||
secret: 'abbaabba',
|
||||
touch: true,
|
||||
oathType: OathType.hotp,
|
||||
hashAlgorithm: HashAlgorithm.sha1);
|
||||
await tester.addAccount(testAccount);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(AccountList),
|
||||
matching: find.textContaining(testAccount.name)),
|
||||
findsOneWidget);
|
||||
await tester.shortWait();
|
||||
});
|
||||
// group('QR Code scanning', () {});
|
||||
appTest('Final reset OATH', (WidgetTester tester) async {
|
||||
/// reset OATH application
|
||||
await tester.tapAppDrawerButton(oathAppDrawer);
|
||||
await tester.resetOATH();
|
||||
await tester.longWait();
|
||||
});
|
||||
|
||||
/// adds an account, renames, verifies
|
||||
@ -82,43 +250,44 @@ void main() {
|
||||
await tester.deleteAccount(testAccount);
|
||||
await tester.deleteAccount(
|
||||
const Account(issuer: 'RenamedIssuer', name: 'RenamedName'));
|
||||
|
||||
await tester.longWait();
|
||||
await tester.addAccount(testAccount);
|
||||
await tester.longWait();
|
||||
await tester.renameAccount(testAccount, 'RenamedIssuer', 'RenamedName');
|
||||
});
|
||||
});
|
||||
|
||||
group('Password tests', () {
|
||||
/// note that the password groups should be run as whole
|
||||
|
||||
/// TODO implement test for password replacement
|
||||
/// appTest('OATH: replace oath password', (WidgetTester tester) async {
|
||||
/// await tester.replaceOathPassword('aaa111', 'bbb222');
|
||||
/// });
|
||||
|
||||
// cannot restart the app on Android to be able to unlock
|
||||
// NOTE: that the password groups should be run as whole
|
||||
// NOTE: cannot restart the app on Android to be able to unlock: skip
|
||||
group('Desktop password tests', skip: isAndroid, () {
|
||||
var testPassword = 'testPassword';
|
||||
var firstPassword = 'firstPassword';
|
||||
var secondPassword = 'secondPassword';
|
||||
var thirdPassword = 'thirdPassword';
|
||||
appTest('Reset OATH', (WidgetTester tester) async {
|
||||
await tester.resetOATH();
|
||||
});
|
||||
appTest('Set first OATH password', (WidgetTester tester) async {
|
||||
// Sets a password for OATH
|
||||
await tester.setOathPassword(firstPassword);
|
||||
});
|
||||
|
||||
appTest('Set OATH password', (WidgetTester tester) async {
|
||||
await tester.setOathPassword(testPassword);
|
||||
appTest('Set second OATH password', (WidgetTester tester) async {
|
||||
// Without removing the first, change to a second password
|
||||
await tester.unlockOathSession(firstPassword);
|
||||
await tester.replaceOathPassword(firstPassword, secondPassword);
|
||||
});
|
||||
|
||||
appTest('Set third OATH password', (WidgetTester tester) async {
|
||||
// Without removing the second, set a third password
|
||||
await tester.unlockOathSession(secondPassword);
|
||||
await tester.replaceOathPassword(secondPassword, thirdPassword);
|
||||
});
|
||||
|
||||
appTest('Remove OATH password', (WidgetTester tester) async {
|
||||
await tester.unlockOathSession(testPassword);
|
||||
await tester.removeOathPassword(testPassword);
|
||||
});
|
||||
});
|
||||
|
||||
group('All password tests', () {
|
||||
var testPassword = 'testPasswordX';
|
||||
|
||||
appTest('Set OATH password', (WidgetTester tester) async {
|
||||
await tester.setOathPassword(testPassword);
|
||||
});
|
||||
|
||||
appTest('Remove OATH password', (WidgetTester tester) async {
|
||||
await tester.removeOathPassword(testPassword);
|
||||
// restarts the app, unlocks with password, removes password req.
|
||||
await tester.unlockOathSession(thirdPassword);
|
||||
await tester.removeOathPassword(thirdPassword);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
165
integration_test/otp_test.dart
Normal file
165
integration_test/otp_test.dart
Normal file
@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
@Tags(['desktop', 'otp'])
|
||||
library;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:yubico_authenticator/app/views/keys.dart';
|
||||
import 'package:yubico_authenticator/otp/keys.dart';
|
||||
import 'package:yubico_authenticator/otp/models.dart';
|
||||
|
||||
import 'utils/otp_test_util.dart';
|
||||
import 'utils/test_util.dart';
|
||||
|
||||
void main() {
|
||||
var binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive;
|
||||
|
||||
group('OTP UI tests', () {
|
||||
appTest('Yubico OTP slot 1', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is empty"
|
||||
await tester.openSlotMenu(SlotId.one);
|
||||
|
||||
await tester.tap(find.byKey(configureYubiOtp).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
// this generates all the fields and saves yubiotp
|
||||
await tester.tap(find.byKey(useSerial).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(generatePrivateId).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(generateSecretKey).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(saveButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
});
|
||||
|
||||
appTest('Challenge-Response slot 1', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
|
||||
await tester.openSlotMenu(SlotId.one);
|
||||
|
||||
await tester.tap(find.byKey(configureChalResp).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
// this generates and saves chall-resp
|
||||
await tester.tap(find.byKey(generateSecretKey).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(saveButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
});
|
||||
|
||||
appTest('Static Password slot 2', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 2 is empty"
|
||||
|
||||
await tester.openSlotMenu(SlotId.two);
|
||||
|
||||
await tester.tap(find.byKey(configureStatic).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
// this generates and saves static password
|
||||
await tester.tap(find.byKey(generateSecretKey).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(saveButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
});
|
||||
|
||||
appTest('OATH-HOTP slot 2', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
|
||||
await tester.openSlotMenu(SlotId.two);
|
||||
|
||||
await tester.tap(find.byKey(configureHotp).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
// this writes and saves oath secret
|
||||
await tester.enterText(find.byKey(secretField), 'asdfasdf');
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(saveButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
});
|
||||
|
||||
appTest('Swap slots', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
|
||||
// taps swap
|
||||
await tester.tapSwapSlotsButton();
|
||||
await tester.tap(find.byKey(swapButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
});
|
||||
|
||||
appTest('Delete Credentials', (WidgetTester tester) async {
|
||||
await tester.tap(find.byKey(otpAppDrawer).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is configured"
|
||||
/// TODO: verify "Slot 2 is configured"
|
||||
|
||||
await tester.openSlotMenu(SlotId.one);
|
||||
await tester.tap(find.byKey(deleteAction).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(deleteButton).hitTestable());
|
||||
|
||||
/// TODO: wait for any toasts to be gone
|
||||
await tester.ultraLongWait();
|
||||
var closeFinder = find.byKey(closeButton);
|
||||
if (closeFinder.evaluate().isNotEmpty) {
|
||||
// close the view
|
||||
await tester.tap(closeFinder);
|
||||
await tester.shortWait();
|
||||
}
|
||||
|
||||
// we need to right click on slot 2
|
||||
await tester.openSlotMenu(SlotId.two);
|
||||
await tester.tap(find.byKey(deleteAction).hitTestable());
|
||||
await tester.shortWait();
|
||||
await tester.tap(find.byKey(deleteButton).hitTestable());
|
||||
await tester.shortWait();
|
||||
|
||||
/// TODO: verify "Slot 1 is empty"
|
||||
/// TODO: verify "Slot 2 is empty"
|
||||
});
|
||||
});
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user